IBMStreams / streamsx.visualization

Real-Time Visual Analytics
http://169.44.122.120:3000/#/developer/home
Apache License 2.0
5 stars 4 forks source link

Leaflet help #143

Closed jchailloux closed 7 years ago

jchailloux commented 7 years ago

I have few sample that use leaflet and I'm not able to create them using the toolkit.

Is anyone knows how to ?

areas.html uses geofences.txt cluster.html uses cluster.txt

Sample.zip

data.zip

This is one of the x transform function I tried on the dataset for areas

x =>{ return { center: { lat: 55.618881, lng: 12.079211, zoom: 1

    },
    defaults: {
        scrollWheelZoom: false

    },
    geojson: {
        data:x.tuples[0].tuple,
        style: {
            fillColor: '#ccfa12',
            weight: 2,
            opacity: 1,
            color: 'white',
            dashArray: '3',
            fillOpacity: 0.7
        }
    }
}

}

sriumcp commented 7 years ago

Could you please export your READ app -- make a gist out of it -- and give me a link to the gist? This will help me understand what your app is doing... You can export an app by going to the Apps tab and clicking on the up arrow in the sidebar -- which copies your READ app as a JSON object for others to import.

jchailloux commented 7 years ago

Here is the json file.

MyApp.json.zip

sriumcp commented 7 years ago

I don't have the full picture since I don't have access to data produced by ws://localhost:56652 and ws://localhost:56651.

But after importing your app, one thing I noticed right away was that in your Geofenses dataset, you have a transform function that uses state (parent dataset = Raw; latest value from Raw = x; current state = s), but state is not enabled and initialized.

Could you please enable the state and set to some valid initial value (is s an array? If so, perhaps an empty array [] is a valid initial value in your situation)? This is briefly described in the usage info tab of the transform dataset.

Alternately, your Geofenses transform could also look like this without any state: x => x.tuples[0].tuple.geofences

sriumcp commented 7 years ago

I also noticed that you have Pie, Multibar Horizontal, and Leaflet Map visualizations defined for Geofences dataset. While Pie and Multibar Horizontal require the data to be an array, Leaflet Map requires the data to be an object. So, the three of them are not going to work on the same dataset. You can know more about the data formats by looking into the usage info for each visualization, and also by examining the sample app bundled with READ which includes a dataset for every supported READ visualization.

jchailloux commented 7 years ago

The leaflet map should use the GeoJSON dataset.

I sent yo the copy of the data part on the dataset page.

x =>{ return { center: { lat: 55.618881, lng: 12.079211, zoom: 1

    },
    defaults: {
        scrollWheelZoom: false

    },
    geojson: {
        data:x.tuples[0].tuple,
        style: {
            fillColor: '#ccfa12',
            weight: 2,
            opacity: 1,
            color: 'white',
            dashArray: '3',
            fillOpacity: 0.7
        }
    }
}

}

geoJSON.dataset.zip

sriumcp commented 7 years ago

When I paste your dataset into a raw READ dataset, I see stuff like this: "features": "{\"type\": \"FeatureCollection\",\"features\": ... which is invalid json. Could you please verify / ensure that your READ dataset is producing valid json -- in particular, there should be quotes here and not escaped quotes. In fact, the value for features key seems to be a string and not a JSON object as one would expect...

jchailloux commented 7 years ago

JSON.parse(x.tuples[0].tuple.features) does the trick.

How can have the same capability such as changing the color according to the number of entities inside like I did in the areas.html ? function getColor(d) { return d > 6000 ? '#800000' : d > 5000 ? '#800026' : d > 4000 ? '#BD0026' : d > 3000 ? '#E31A1C' : d > 2000 ? '#FC4E2A' : d > 1000 ? '#FD8D3C' : d > 500 ? '#FEB24C' : d > 100 ? '#FED976' : '#FFEDA0'; }

What about a popup onmouseover ? image

image

sriumcp commented 7 years ago

For styling... here is an idea. This assumes that you want to style each feature based on feature.properties.number -- if you want to use a different property, e.g., feature.properties.density modify this function appropriately (and also modify the properties object appropriately so that it contains the data you need for the style function).

Transform your geojson using the following logic...

/* x is the geojson */
x => {
  /* We are setting a style function for the geojson -- this is something angular-leaflet-directive understands */
    x.geojson.style = function style(feature) {
      /* We want to style each feature based on feature.properties.number */
        let d = feature.properties.number;
        return {
            /* set fillColor based on feature.properties.number */
            fillColor:  d > 6000 ? '#800000' :
                        d > 5000 ? '#800026' :
                        d > 4000 ? '#BD0026' :
                        d > 3000 ? '#E31A1C' :
                        d > 2000 ? '#FC4E2A' :
                        d > 1000 ? '#FD8D3C' :
                        d > 500 ? '#FEB24C' :
                        d > 100 ? '#FED976' :
                        '#FFEDA0',
            weight: 2,
            opacity: 1,
            color: 'white',
            dashArray: '3',
            fillOpacity: 0.7
        }
    };
    return x;
}

Here's the relevant leaflet documentation: http://leafletjs.com/reference.html#geojson

Could you please also try using the onEachFeature call back (in addition to the style callback) to bind a popup? Not 100% sure this will do the trick -- but hopefully angular-leaflet-directive (which READ uses) supports this callback as well (I know per-feature styling is supported)

jchailloux commented 7 years ago

I miss the point. In the same transform or do I have to transform again x from the previous transform ?

sriumcp commented 7 years ago

Either way is fine... ultimately, we want the data used by the visualization to contain the style callback. So you can do it in the same function or in a new function.

jchailloux commented 7 years ago

Great

this works x =>{ return { center: { lat: 55.618881, lng: 12.079211, zoom: 14

    },
    defaults: {
        scrollWheelZoom: false

    },
    geojson: {
        data:JSON.parse(x.tuples[0].tuple.features),
        style: function style(feature) {
  /* We want to style each feature based on feature.properties.number */
    let d = feature.properties.number;
    return {
        /* set fillColor based on feature.properties.number */
        fillColor:  d > 6 ? '#800000' :
                    d > 5 ? '#800026' :
                    d > 4 ? '#BD0026' :
                    d > 3 ? '#E31A1C' :
                    d > 2 ? '#FC4E2A' :
                    d > 1 ? '#FD8D3C' :
                    d > 500 ? '#FEB24C' :
                    d > 100 ? '#FED976' :
                    '#FFEDA0',
        weight: 2,
        opacity: 1,
        color: 'white',
        dashArray: '3',
        fillOpacity: 0.7
    }
}
    }
}

}

sriumcp commented 7 years ago

Perfect. Just out of curiosity... can you show me your new map also?

jchailloux commented 7 years ago

What about the mouse over could we have this function available to update ? Could we display the legend for colors ?

image

sriumcp commented 7 years ago

legends are already supported... please look into the maps dashboard that is part of the sample app (included when you download READ).

sriumcp commented 7 years ago

For binding a popup on mouseover... could you please experiment with the onEachFeature callback as I mentioned earlier. Leaflet documentation is here: http://leafletjs.com/reference.html#geojson (again I'm not 100% certain this solution works since I'm yet to test this).

jchailloux commented 7 years ago

In the areas.html I did that. The question is more about where to write the code I want.

function style(feature) { return { weight: 1, opacity: 1, color: 'white', dashArray: '1', fillOpacity: feature.properties.density+0.5, // fillOpacity: 0.6, fillColor: getColor(feature.properties.number) }; }

function highlightFeature(e) { var layer = e.target;

layer.setStyle({
    weight: 1,
    color: '#000',
    dashArray: '',
    fillOpacity: 0.9
});

if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
    layer.bringToFront();
}

info.update(layer.feature.properties);

}

function resetHighlight(e) { geojsonLayer.resetStyle(e.target); info.update(); }

function zoomToFeature(e) { map.fitBounds(e.target.getBounds()); }

function onEachFeature(feature, layer) { layer.on({ mouseover: highlightFeature, mouseout: resetHighlight, click: zoomToFeature }); }

var geojsonLayer; var legend = L.control({position: 'bottomright'});

sriumcp commented 7 years ago

onEachFeature would be a assigned to geojson just like style... geojson will now have data, style, and onEachFeature, with the last two being functions. You can try this in the same transform where you are adding the style function.

jchailloux commented 7 years ago

onEachFeature looks with this code

x =>{ return { center: { lat: 55.618881, lng: 12.079211, zoom: 14

    },
    defaults: {
        scrollWheelZoom: false

    },
    geojson: {
        data:JSON.parse(x.tuples[0].tuple.features),
        style: function style(feature) {
            /* We want to style each feature based on feature.properties.number */
            let d = feature.properties.number;
            return {
                /* set fillColor based on feature.properties.number */
                fillColor:  d > 6 ? '#800000' :
                            d > 5 ? '#800026' :
                            d > 4 ? '#BD0026' :
                            d > 3 ? '#E31A1C' :
                            d > 2 ? '#FC4E2A' :
                            d > 1 ? '#FD8D3C' :
                            d > 500 ? '#FEB24C' :
                        d > 100 ? '#FED976' :
                        '#FFEDA0',
                weight: 2,
                opacity: 1,
                color: 'white',
                dashArray: '3',
                fillOpacity: 0.7
            }
        },
        onEachFeature: function onEachFeature(feature, layer) {
            layer.on({
                mouseover: function highlightFeature(e) {
                        var layer = e.target;

                        layer.setStyle({
                            weight: 1,
                            color: '#000',
                            dashArray: '',
                            fillOpacity: 0.9
                        });

                        if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
                            layer.bringToFront();
                        }

                        info.update(layer.feature.properties);
                    }

, mouseout: function resetHighlight(e) { geojsonLayer.resetStyle(e.target); info.update(); } , click: function zoomToFeature(e) { map.fitBounds(e.target.getBounds()); } }); } } } }

    return {
    center: {
        lat: 55.618881,
        lng: 12.079211,
        zoom: 14

    },
    defaults: {
        scrollWheelZoom: false

    },
    geojson: {
        data:JSON.parse(x.tuples[0].tuple.features),
        style: function style(feature) {
            /* We want to style each feature based on feature.properties.number */
            let d = feature.properties.number;
            return {
                /* set fillColor based on feature.properties.number */
                fillColor:  d > 6 ? '#800000' :
                            d > 5 ? '#800026' :
                            d > 4 ? '#BD0026' :
                            d > 3 ? '#E31A1C' :
                            d > 2 ? '#FC4E2A' :
                            d > 1 ? '#FD8D3C' :
                            d > 500 ? '#FEB24C' :
                        d > 100 ? '#FED976' :
                        '#FFEDA0',
                weight: 2,
                opacity: 1,
                color: 'white',
                dashArray: '3',
                fillOpacity: 0.7
            }
        },
        onEachFeature: function onEachFeature(feature, layer) {
            layer.on({
                mouseover: function highlightFeature(e) {
                        var layer = e.target;

                        layer.setStyle({
                            weight: 1,
                            color: '#000',
                            dashArray: '',
                            fillOpacity: 0.9
                        });

                        if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
                            layer.bringToFront();
                        }

                        info.update(layer.feature.properties);
                    }

, mouseout: function resetHighlight(e) { geojsonLayer.resetStyle(e.target); info.update(); } , click: function zoomToFeature(e) { map.fitBounds(e.target.getBounds()); } }); } } } }

image

jchailloux commented 7 years ago

The info is not display. Do you have an other great idea ?

var info = L.control();

info.onAdd = function (map) { this._div = L.DomUtil.create('div', 'info'); this.update(); return this._div; };

info.update = function (props) { this._div.innerHTML = '

Areas Density

' + (props ? ''+new Date(props.ts).toGMTString()+'
'+'' + props.name + '('+ props.OBJECTID+') : '+parseInt( props.area ).toLocaleString()+' m2
' + props.number + ' people
' + props.density.toLocaleString(undefined, { minimumFractionDigits: 4 }) + ' people / m2' : ' over a zones'); }; info.addTo(map);

jchailloux commented 7 years ago

For legend I have some issues

image

jchailloux commented 7 years ago

I also notice that the mouseout and click don't work.

sriumcp commented 7 years ago

I see... Please try importing this new sample app: https://gist.github.com/sriumcp/9b9ecc058b1e9b3f505d7bbf335b4cf5

I will fix the app bundled with READ soon.

sriumcp commented 7 years ago
screen shot 2016-12-07 at 12 32 38 pm
jchailloux commented 7 years ago

Legend is ok now thanks

image

jchailloux commented 7 years ago

Could we avoid to set center: { lat: 55.618881, lng: 12.079211, zoom: 14

    },

at all time and then let the user zomm in or out and move ? At all time it goes back to the "default"

sriumcp commented 7 years ago

Two ideas.

  1. Move the center object within the defaults object (instead of keeping them at the same level; this will create an initial centering with user control for zooming)
  2. Remove the center object altogether (there will no initial centering)
sriumcp commented 7 years ago

Could you please export your current READ app and give me a link... I can try experimenting with mouseover and mouseout and click.

jchailloux commented 7 years ago

Demo_READ.json.zip I sent you the streams application on email

jchailloux commented 7 years ago

StreamsUI.zip This is the node application I would like to move on the toolkit (in case of)

sriumcp commented 7 years ago

Could you please replace your onEachFeature function with the below function? This is the angular leaflet way of manipulating the map object (sorry -- its not yet fully documented in READ; I'm planning to switch from angular-leaflet to the regular Leaflet library in the next release to make map making in READ simpler)...

            onEachFeature: function onEachFeature(feature, layer) {
                layer.on({
                    mouseover: function highlightFeature(e) {
                            var layer = e.target;

                            layer.setStyle({
                                weight: 1,
                                color: '#000',
                                dashArray: '',
                                fillOpacity: 0.9
                            });

                            if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
                                layer.bringToFront();
                            }
                        },
                    mouseout: function mouseOut(e) {
                            leafletData.getGeoJSON().then(geojson => {
                                geojson.resetStyle(e.target);
                            })                        
                    },
                    click: function zoomToFeature(e) {
                            leafletData.getMap().then(map => {
                                map.fitBounds(e.target.getBounds());
                            });
                        }
                });
            }
jchailloux commented 7 years ago

This solved the mouse issues. The pending issues are info not display on mouseover and removed on mouseout and zoom/center always reset.

Don't be sorry READ looks so promises.

jchailloux commented 7 years ago

I will go thru the cluster html and see how to convert. I will go back for questions and/or solutions

sriumcp commented 7 years ago

Re: the zoom/center, did you move the center into the defaults property, or is it still at the top level?

defaults: {
            scrollWheelZoom: false,
            center: {
                lat: 55.618881,
                lng: 12.079211,
                zoom: 14                
            },
        },
jchailloux commented 7 years ago

still in the x transform

jchailloux commented 7 years ago

switching to default solve the issue thanks

sriumcp commented 7 years ago

How about this function? Of course, you will need to change the popup content and latlng based on feature properties...

            onEachFeature: function onEachFeature(feature, layer) {
                layer.on({
                    mouseover: function highlightFeature(e) {
                            var layer = e.target;

                            layer.setStyle({
                                weight: 1,
                                color: '#000',
                                dashArray: '',
                                fillOpacity: 0.9
                            });

                            if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
                                layer.bringToFront();
                            }

                            leafletData.getMap().then(map => {
                                var popup = L.popup()
                                .setContent('<p>Hello world!<br />This is a nice popup.</p>')
                                .setLatLng(L.latLng(55.618881, 12.079211))
                                .openOn(map);                           
                            });

                        },
                    mouseout: function mouseOut(e) {
                            leafletData.getGeoJSON().then(geojson => {
                                geojson.resetStyle(e.target);
                            });
                            leafletData.getMap().then(map => {
                                console.log(map);
                                map.closePopup();
                            });

                    },
                    click: function zoomToFeature(e) {
                            leafletData.getMap().then(map => {
                                map.fitBounds(e.target.getBounds());
                            });
                        }
                });
            }
jchailloux commented 7 years ago

I updated the popup as follow. I have some issues (some blink popup) but I guess it's because I have fences inside fences.

                                    leafletData.getMap().then(map => {
                                        var popup = L.popup()
                                        .setContent('<h4>Areas Density</h4>'+'<i>'+new Date(feature.properties.ts).toGMTString()+'</i><br/>'+'<b>' + feature.properties.name + '('+ feature.properties.OBJECTID+') : '+parseInt( feature.properties.area ).toLocaleString()+' m<sup>2</sup></b><br/>' + feature.properties.number + ' people<br />' + feature.properties.density.toLocaleString(undefined, { minimumFractionDigits: 4 }) + ' people / m<sup>2</sup>' )
                                        .setLatLng(L.latLng(feature.geometry.coordinates[0][0][1], feature.geometry.coordinates[0][0][0]))
                                        .openOn(map);                           
                                    });

thanks

jchailloux commented 7 years ago

could I get the upper right latlon to have the popup at the same place ?

jchailloux commented 7 years ago

Demo_READ.json.zip

The last READ app

sriumcp commented 7 years ago

Yes, I notice the flickering as well. Could you try L.control instead of pop up -- now that you know how to get a reference to the map object? Going back to your original idea (of adding an info div) instead of a popup might resolve this issue.

jchailloux commented 7 years ago

I'm trying to migrate the Cluster.html on visualization. I'm having issues because it looks like the marker definition is not valid.

using this transform x => { / taipei: { layer: "northTaiwan", lat: 25.0391667, lng: 121.525, } / var markers='{'; var first=1; for (var value of x.tuples[0].tuple.locations) { markers = markers+(first == 0?',':'')+value.id+':{lat:'+value.latitude+',lng:' +value.longitude+'}'; first=0; } markers = markers+'}'; // var markers = leafletData.getMap().markerClusterGroup(); // L.markers.clearLayers(); return{ defaults: { scrollWheelZoom: false, center: { lat: 55.618881, lng: 12.079211, zoom: 14

                }
    },
    markers

}

}

This is the output { "defaults": { "scrollWheelZoom": false, "center": { "lat": 55.618881, "lng": 12.079211, "zoom": 14 } }, "markers": "{142:{lat:55.61476346106971,lng:12.06946614470684},14:{lat:55.615240079731045,lng:12.08811528552074},161:{lat:55.61174290104943,lng:12.077519594403434},20:{lat:55.62208372595761,lng:12.097116836162703},146:{lat:55.62137107740439,lng:12.08397103090601},134:{lat:55.621323562892904,lng:12.077009278061105},196:{lat:55.62424874263861,lng:12.090774580333495},166:{lat:55.61450581129024,lng:12.075062586655413},24:{lat:55.61044152035306,lng:12.092445946702231},3:{lat:55.61921674935665,lng:12.096485266871463}}" }

Demo_READ.json.zip

jchailloux commented 7 years ago

I tried this without success leafletData.getMap().then(map => { var info = L.control({position: 'bottomleft'}); var _div = L.DomUtil.create('div', 'info'); _div.innerHTML = '

Areas Density

'+''+new Date(feature.properties.ts).toGMTString()+'
'+'' + feature.properties.name + '('+ feature.properties.OBJECTID+') : '+parseInt( feature.properties.area ).toLocaleString()+' m2
' + feature.properties.number + ' people
' + feature.properties.density.toLocaleString(undefined, { minimumFractionDigits: 4 }) + ' people / m2'; info.addTo(map);
});

sriumcp commented 7 years ago

Ok... two points:

  1. The markers object is not an object at all but is a string right now which is a problem... markers are supported with proper objects not strings.... below is an example of properly defined markers...

    {
    center: {
        lat: 52.52,
        lng: 13.40,
        zoom: 4
    },
    markers: {
        m1: {
            lat: 52.52,
            lng: 13.40
        }
    }
    }
  2. When you share your READ app next time -- could you please replace the ws://... (Websocket) datasets with Raw datasets with the appropriate JSON data pasted in them? I know this kills all the dynamics and makes the app static -- but this is useful for sharing and debugging (I am not set up to run your Streams app properly; this makes it easier to debug the READ app).

jchailloux commented 7 years ago

Demo_READ.json.zip

This should contains raw dataset for the RawClusterStack

sriumcp commented 7 years ago

With the following transform function for ClusterStackTrans...

x => {
    var markerObject = {};
    x.tuples[0].tuple.locations.map(y => {
        markerObject[y.id] = {
            lat: y.latitude,
            lng: y.longitude,
            message: "this place is on earth",
            group: "Europe"
        };
    });

    return{
        defaults: {
            scrollWheelZoom: false,
             center: {
                        lat: 55.618881,
                        lng: 12.079211,
                        zoom: 14

                    }
        },
        markers: markerObject

    }
}

This map is the result....

screen shot 2016-12-08 at 8 16 13 am

Here is the documentation for angular-leaflet-directive which READ uses, and especially the documentation for the markers attribute: https://github.com/tombatossals/angular-leaflet-directive/blob/master/doc/markers-attribute.md (please also click on the Usage Info tab of the READ visualization to see how READ utilizes the various angular-leaflet-directive attributes).

I need to update and commit a new version of READ in order to support marker clustering (essentially include this leaflet plugin as a READ dependency). I can let you know once I have this up (you can upgrade to the latest version of READ once I do this).

jchailloux commented 7 years ago

Demo_READ.json.zip

I did the same for the GeoJSONRaw regarding the popup and div info

jchailloux commented 7 years ago

For the Cluster I have MarkerCluster plugin not loaded

sriumcp commented 7 years ago

Yes, I am aware of the MarkerCluster plugin issue... fix is on the way!

jchailloux commented 7 years ago

When it will work it could be nice to create a "migration" guide base on the 2 samples. I will work on and let you know to improve the doc ans samples.

sriumcp commented 7 years ago

Marker clusters are now supported in the latest version of READ (0.7.1)...

Here's the updated transform function.

x => {
    var markerObject = {};
    x.tuples[0].tuple.locations.map(y => {
        markerObject[y.id] = {
            lat: y.latitude,
            lng: y.longitude,
            group: "Europe"
        };
    });

    return {
        defaults: {
            scrollWheelZoom: false,
             center: {
                        lat: 55.618881,
                        lng: 12.079211,
                        zoom: 14

                    }
        },
        markers: markerObject
    }
}

Notice that there is only a single change -- a new group property which got added to the markers (you can set the group as anything you want in this example).

Here's the result...

screen shot 2016-12-08 at 12 40 02 pm

I will provide a quick list of steps for migration here (if you wish to help improve the documentation by creating a wiki page which properly documents these steps, you are very welcome to do so. Please issue a PR for the same).