Display HTML clusters with custom properties using react-map-gl (Mapbox) - reactjs

I am trying to adapt the example Display HTML clusters with custom properties for react-map-gl.
I got basic clusters without custom styling working (adapted from Create and style clusters):
<ReactMapGL ref={mapRef}>
<Source id="poi-modal-geojson" type="geojson" data={pointsToGeoJSONFeatureCollection(points)}
cluster={true}
clusterMaxZoom={14}
clusterRadius={50}
>
<Layer {...{
id: 'clusters',
type: 'circle',
source: 'poi-modal-geojson',
filter: ['has', 'point_count'],
paint: {
'circle-color': [
'step',
['get', 'point_count'],
'#51bbd6',
100,
'#f1f075',
750,
'#f28cb1'
],
'circle-radius': [
'step',
['get', 'point_count'],
20,
100,
30,
750,
40
]
}
}} />
<Layer {...{
id: 'unclustered-point',
type: 'circle',
source: 'poi-modal-geojson',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 4,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff'
}
}} />
</Source>
</ReactMapGL>
Here, pointsToGeoJSONFeatureCollection(points: any[]): GeoJSON.FeatureCollection<GeoJSON.Geometry> is a function returning a GeoJSON (adapted from here).
However, I need more complex styling of markers and I am trying to adapt Display HTML clusters with custom properties without success so far. I mainly tried to adapt updateMarkers() and to call it inside useEffect():
const mapRef: React.Ref<MapRef> = React.createRef();
const markers: any = {};
let markersOnScreen: any = {};
useEffect(() => {
const map = mapRef.current.getMap();
function updateMarkers() {
const newMarkers: any = {};
const features = map.querySourceFeatures('poi-modal-geojson');
// for every cluster on the screen, create an HTML marker for it (if we didn't yet),
// and add it to the map if it's not there already
for (const feature of features) {
const coords = feature.geometry.coordinates;
const props = feature.properties;
if (!props.cluster) continue;
const id = props.cluster_id;
let marker = markers[id];
if (!marker) {
let markerProps = {
key: 'marker' + id,
longitude: coords[0],
latitude: coords[1],
className: 'mapboxgl-marker-start'
}
const el = React.createElement(Marker, markerProps, null),
marker = markers[id] = el;
}
newMarkers[id] = marker;
if (!markersOnScreen[id]) {
// TODO re-add
// marker.addTo(map);
}
}
// for every marker we've added previously, remove those that are no longer visible
for (const id in markersOnScreen) {
if (!newMarkers[id]) delete markersOnScreen[id];
}
markersOnScreen = newMarkers;
}
// after the GeoJSON data is loaded, update markers on the screen on every frame
map.on('render', () => {
if (!map.isSourceLoaded('poi-modal-geojson')) return;
updateMarkers();
});
}, [points]);
Unfortunately, the Marker created using React.createElement() isn't displayed I am not sure what is the right approach to create Marker elements in updateMarkers() or if my approach is completely wrong.

There is a great article on marker clustering which uses the supercluster and use-supercluster libraries and it makes clustering really easy not only for map box but for other map libraries as well, you can find it here.
You just have to convert your points into GeoJSON Feature objects in order to pass them to the useSupercluster hook and for the calculations to work. It will return an array of points and clusters depending on your current viewport, and you can map through it and display the elements accordingly based on the element.properties.cluster flag.
The properties property of the GeoJSON Feature object can be custom so you can pass whatever you need to display the markers later on when you get the final cluster array.

Related

Is it bad practice to access HTML elements by ID in React TypeScript?

I was told at a previous job that I should never access HTML elements directly through means like getElementById in React TypeScript. I'm currently implementing Chart.js. For setting up the chart, I was initially using a useRef hook instead of accessing context, but now it seems like I need to grab the canvas by ID in order to instantiate it properly. I want to know if this is kosher.
I suspect something is wrong with me not using a context, because my chart data doesn't load and throws a console error: "Failed to create chart: can't acquire context from the given item"
useEffect(() => {
chart = new Chart(chartRef.current, {
type: "bar",
data: {
labels: labelsArray.map((label) => {
const date = new Date(label);
// add one because month is 0-indexed
return date.getUTCMonth() + 1 + "/" + date.getUTCDate();
}),
datasets: [
{
data: valuesArray,
backgroundColor: "#1565C0",
borderRadius: 6,
},
],
},
options: {
interaction: { mode: "index" },
onHover: (event, chartElement) => {
const target = event.native.target;
(target as HTMLInputElement).style.cursor = chartElement[0]
? "pointer"
: "default";
},
plugins: {
tooltip: {
mode: "index",
enabled: true,
},
title: {
display: true,
text: "Daily Usage Past 30 Days",
align: "start",
fullSize: true,
font: {
size: 24,
},
padding: {
bottom: 36,
},
},
},
scales: {
x: {
display: false,
},
},
elements: {
line: {
borderJoinStyle: "round",
},
},
},
});
return () => {
chart.destroy();
};
}, [labelsArray, valuesArray]);
and HTML:
<div className="mt-80 ml-12 p-8 shadow-lg border-2 border-slate-100 rounded-xl items-center">
<canvas id="chart" ref={chartRef}></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</div>
Also, per the Chart.js documentation: "To create a chart, we need to instantiate the Chart class. To do this, we need to pass in the node, jQuery instance, or 2d context of the canvas of where we want to draw the chart." Not sure how we would do this with a useRef
Yes,
It is not good practice to access dom elements directly through document API.
Because in react
virtual dom is responsible for painting/ re-rendering the UI.
State updation is the proper way to tell react to trigger re-render.
The flow is state updation -> calculate differences -> find who over is using that state -> grab those components -> re-render only those components.
virtual dom is the source of truth for react to render and update actual DOM.
Now, If you directly access some dom elements and do some operation on it, like updating, react will never know that some change has happened and it will basically break the flow of the react, in which case there will be no reason to use react.js
The flow would be accessing some dom element -> updating -> displaying.
The problem with this approach if react encounters that later what i have in virtual dom is not actual presentation in the actual dom, which will create mess.
That is the reason there is useRef hook to manipulate dom.

Mapbox layer not updating after source update

I'm using Redux state to update an array of coordinates in a Mapbox source. I initially check if there is a source with the id, if yes, I set the data of the source, if not I add the source to the map. When the redux state is changed, it triggers an effect which updates the coordinates of the features in the geojson object and uses setData to change the source. I've tried removing the layer, changing source and adding the layer, which just gave me the old layer (even though the source had indeed been updated). I also tried just updating the source alone and seeing if the layer would update dynamically, it did not.
Here is the code for the effect, which is triggered when the redux state is changed.
useEffect(() => {
const geoJsonObj = {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: []
}
};
for (let i = 0; i < (props.mapRoutes.length); i++) {
geoJsonObj.data.features.push({
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: props.mapRoutes[i].geometry.coordinates
}
});
};
const routeLayer = {
id: 'route',
type: 'line',
source: 'route',
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': '#ff3814',
'line-width': 5,
'line-opacity': 0.75
}
};
const jsonString = JSON.stringify(geoJsonObj);
const jsonObj = JSON.parse(jsonString);
if (props.mapRoutes.length) {
if (map.current.getSource('route')) {
map.current.getSource('route').setData(jsonObj);
} else {
map.current.addSource('route', jsonObj);
map.current.addLayer(routeLayer);
};
};
}, [props.mapRoutes]);
Neither of these worked and I am having trouble finding how to update a layer based on an updated source. Everything seems right when I inspect the source in the console, I just can't manage to update the layer on the map.
Any help would be appreciated.
I found the problem, I was using the original geoJson object for the setData method instead of the data entry, which was one level too high in the object. Just a simple error which was overlooked.

How to animate dynamic updates in CanvasJS using React functional components?

Using React functional components, I have not been able to find a way to animate my chart with dynamic data received asynchronously. The sandbox below illustrates the problem with a timer simulating the asynchronous read.
https://codesandbox.io/s/basic-column-chart-in-react-canvasjs-0gfv6?fontsize=14&hidenavigation=1&theme=dark
When running the example code, you should see 5 vertical bars of increasing heights animate. Then, after 5 seconds, it switches immediately to 4 bars of descending heights. I am looking to have that update animate.
Here is some reference information I've reviewed:
CanvasJS React Demos: many of which animate on initial draw, but I couldn't find one that animates with dynamic data loaded after the initial render.
Chart using JSON Data is an demo that has dynamic data, but doesn't animate.
Reviewing the CanvasJS forum, I found a couple links, but none that address React functional components
Vishwas from Team Canvas said:
To update dataPoints dynamically and to animate chart, you can instantiate the chart, update dataPoints via chart-options and then call chart.render as shown in this updated JSFiddle.
var chart = new CanvasJS.Chart("chartContainer", {
title: {
text: "Animation test"
},
animationEnabled: true,
data: [{
type: "column",
dataPoints: []
}]
});
chart.options.data[0].dataPoints = [{ label: "Apple", y: 658 },
{ label: "Orange", y: 200 },
{ label: "Banana", y: 900 }];
chart.render();
This sample is pure JS, but I tried to adapt the principle to my React functional component. To better comport with React best practices, I incorporated the useState hook for storing the data and the useEffect hook to handle the fetch. But, alas, I couldn't get my sandbox to animate with the dynamic data.
I think the problem is that CanvasJS expects to animate only on the first render, as stated by Sanjoy in the CanvasJS forum on 7/19/2016.
I found this SO question from Jan 2015 that suggests:
My current ugly workaround is to reinstantiate the chart every time I
update just to achieve that animation effect.
I'm hopeful that the situation has improved in the last four years, but if this hack is still the best/only way to go, I need some guidance on how to reinstantiate the chart every time using a React functional component.
To force a remount of a component pass a different key when you want to remount the component
<CanvasJSChart
key={dataPoints.toString()} // force a remount when `dataPoints` change
containerProps={containerProps}
options={options}
onRef={ref => (chart.current = ref)}
/>
Working example
I found a partial answer. Full executing code is in this code sandbox, but the critical bit is to delay the initial render of the chart until a state variable indicates that the data is available:
return (
<div className="App">
{!initialized ? (
<h1> Loading...</h1>
) : (
<CanvasJSChart containerProps={containerProps} options={options} />
)}
</div>
);
This is only a partial solution because subsequent data updates still do not animate.
Both examples works fine.
You can always animate chars with some kind of calling. I use in this case setInterval.
<script src="https://canvasjs.com/assets/script/canvasjs.min.js"></script>
<script>
var chart;
window.onload = function () {
chart = new CanvasJS.Chart("chartContainer", {
title: {
text: "Animation test"
},
animationEnabled: true,
data: [{
type: "column",
dataPoints: []
}]
});
chart.options.data[0].dataPoints = [{ label: "Apple", y: 0 },
{ label: "Orange", y: 0 },
{ label: "Banana", y: 0 }];
chart.render();
}
var max = 0;
var s = {c: 0, i: 0};
function ANIMATE() {
if (typeof chart === 'undefined') return;
chart.options.data[0].dataPoints.forEach(function(item, index, array) {
if (index == s.i) {
array[index].y += 3;
s.c++;
}
if (s.c > 12) {
s.i++;
s.c = 0;
if (s.i == 15) { s.i = 0}
}
});
if (max < 12) {
chart.options.data[0].dataPoints.push({label: "apple" + Math.random(), y: 1 + Math.random() * 10});
max++;
}
chart.render()
}
setInterval(function(){
ANIMATE()
}, 1)
</script>
<div id="chartContainer" style="height: 370px; width: 100%;"></div>

GeoJSON marker is not showing on my leafletjs map

I'm writing my first larger project in react and I need to set up markers in my map component. I've set everythin up as it is shown in the tutorial however it is not working correctly with my code and the markers are not shown on map.
const dummyGeoJson = {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: {},
geometry: {
type: "Point",
coordinates: [16.959285736083984, 52.40472293138462]
}
}
]
};
class EventMap extends React.Component {
componentDidMount() {
this.map = L.map("map", {
center: [51.9194, 19.1451],
zoom: 6
});
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 20
}).addTo(this.map);
var geoJsonLayer = L.geoJSON().addTo(this.map);
geoJsonLayer.addData(dummyGeoJson);
}
render() {
return <Wrapper width="100%" height="800px" id="map" />;
}
}
From what i've read in official leaflet tutorial this code should create a new geojson layer and create a marker in a position referenced in geojson but actually the only thing that is shown is my tile layer.
You need to use a pointToLayer function in a GeoJSON options object when creating the GeoJSON layer like this:
componentDidMount() {
const map = L.map("map", {
center: [51.9194, 19.1451],
zoom: 6
});
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 20
}).addTo(map);
L.geoJSON(dummyGeoJson, {
pointToLayer: (feature, latlng) => {
return L.marker(latlng, { icon: customMarker });
}
}).addTo(map);
}
You can then pass a customMarker variable to define some options in order to make your marker be displayed on the UI
Demo
Welcome to SO!
The most probable reason is that you bundle your app (typically with webpack), but the build misses Leaflet default icon images.
So your Marker is there, but you cannot see it because its icon image is missing.
An easy way to debug it is to use another icon instead, as suggested in kboul's answer, or even more simply by using a CircleMarker.
Then to solve the issue of the build engine missing to process the default icon images, see Leaflet #4968:
explicitly import / require the Leaflet default icon images and modify the L.Icon.Default options to use the new imported paths
or use the leaflet-defaulticon-compatibility plugin (I am the author).

vis.js network: Edge colors not displaying in react.js app

I am having a problem with the edge colors when rendering a network visualisation through a react app.
I have a MindMapComponent which contains a network.
I receive the data for the network through the props for the component:
class MindMapComponent extends React.Component {
//React component to display a single submission Item.
//Displays the text and author of a Perspective Item
constructor(props) {
super(props);
this.state = {map: props.mindMap, node_options: props.node_options, edge_options: props.edge_options}
}
I then go ahead and create the network in my componentDidMount function:
componentDidMount(){
var edge_dataset = new vis.DataSet(JSON.parse(this.state.map.conceptmap_edges));
var nodes_dataset = new vis.DataSet(JSON.parse(this.state.map.conceptmap_nodes));
var data = {
nodes:nodes_dataset ,
edges: edge_dataset
};
var edge_options =JSON.parse(this.state.edge_options);
edge_options.color = {inherit:false};
var options = {
edges: edge_options,
nodes: JSON.parse(this.state.node_options),
physics: false,
locale: 'en',
interaction: {
navigationButtons: true,
keyboard: true,
hover: true
}
};
var network = new vis.Network(this.refs.network, data,options)
console.log(network);
}
And finally render the whole thing in my render function
render() {
const liStyle = {
borderStyle: 'outset',
backgroundColor: 'lightgrey',
marginBottom: '10px',
listStyleType: 'none'
};
const metaStyle = {
paddingLeft: '15px'
}
const networkStyle = {
height:'250px',
borderRight:'dashed 2px'
}
var dateString = new Date(this.state.map.date_added);
dateString = dateString.getDate() + "/" + (dateString.getMonth() +1) + "/" + dateString.getFullYear();
return <li key={this.state.map.id} style={liStyle}>
<div className = 'row'>
<div className = 'col-lg-8' ref = "network" style = {networkStyle}></div>
<div className = 'col-lg-4' style = {metaStyle}><br/><p>Submitted on: {dateString}</p></div>
</div>
</li>
}
The final network should look like this (rendered in normal html+js app).
However in my react app the colors of the edges do not display
I took a look inside of the network data structure (through the console.log(network) at the end of component did mount).
The body.data.edges part of the structure contains the correct color value. However the body.edge.[id].options.color part is empty
I believe this is the source of my problem but am not sure whether my analysis is correct or how to go about fixing it.
I think I have a solution (I ran into the same problem as you, but then in Angular2).
Try setting the color as an Object (see http://visjs.org/docs/network/edges.html) and specify the inherit property to false. E.g.(using typescript)
const myEdge: Edge = {
id: 'myEdge',
from: 'NODE1',
to: 'NODE2',
arrows: 'to',
color: {color: '#ff0000', inherit: false};
dashes: true
}
You might also want to set the highlight and hover colors

Resources