1

TL;DR: In @react-google-maps/api, I want to be able to make dynamic cluster icons/symbol in the style of pie charts, based on the markers in the cluster, but it seems I can only make icons from a static array, and cannot pass the the markers as parameters.

Full Description: I am using typescript react with the package @react-google-maps/api, and I'm trying to find a way with the ClustererComponent/MarkerClusterer to take a callback or similar in order to be able to be able to create an svg for each cluster based on the markers in the given cluster.

The current issue is that the way I understand it, I am limited to a static array of urls to icons, and thought I can make an svg in those, I have no way to pass parameters into those svgs, as the only way the package allows me to chose a style is thought index in the style array.

I have read thought the following material, but have not been able to get find a way to make an icon dynamically based on the markers:

I have found libraries like this: https://github.com/hassanlatif/google-map-chart-marker-clusterer, that should be able to be used as a solution, but they don't seem to work with the @react-google-maps/api, only with earlier versions of google map. If this is not the case, and these can be used directly, then I would be more then happy with an answer describing how to use libraries like the one above with @react-google-maps/api, as that should allow be to make clusters in the same way as the picture below.

EDIT: as I got reminded in the comments, here is the code I have so far:

What I've tried: I have tried to find any way to set in an svg element instead of a url, but have since just decided to make a url with the svg data, as shown below. I have tried to edit the url of the clusters under the MarkerClusterer thought the callback for onClusteringBegin, onClusteringEnd and onLoad, but so far, no luck.

How I make the svg into url-data, so it can be used for img src

/*
 * Pie Chart SVG Icon in URL form
 *
 * Inspiration taken from: https://medium.com/hackernoon/a-simple-pie-chart-in-svg-dbdd653b6936
 *
 * Note: As of right now, I am identifying the difference in marker types by setting the type-number I use in the title of the marker
 */
    const serializeXmlNode = (xmlNode: any) => {
        if (typeof window.XMLSerializer != "undefined") {
            return (new window.XMLSerializer()).serializeToString(xmlNode);
        } else if (typeof xmlNode.xml != "undefined") {
            return xmlNode.xml;
        }
        return "";
    }

    function getCoordinatesForPercent(percent: number) {
        const x = Math.cos(2 * Math.PI * percent);
        const y = Math.sin(2 * Math.PI * percent);
        return [x, y];
    }

    const makePieChartIcon = (slices: any[]) => {
        const svgNS = 'http://www.w3.org/2000/svg';

        var svg = document.createElementNS(svgNS, 'svg')
        svg.setAttribute('viewBox', '-1.1 -1.1 2.2 2.2')
        svg.setAttribute('style', 'transform: rotate(-90deg)')
        svg.setAttribute('height', '60')

        var circle = document.createElementNS(svgNS, 'circle')
        circle.setAttribute('r', '1.1')
        circle.setAttribute('fill', 'white')
        svg.appendChild(circle);

        let cumulativePercent = 0;

        slices.map((slice: any) => {
            const [startX, startY] = getCoordinatesForPercent(cumulativePercent);
            cumulativePercent += slice.percent;
            const [endX, endY] = getCoordinatesForPercent(cumulativePercent);
            const largeArcFlag = slice.percent > .5 ? 1 : 0;
            const pathData = [
                `M ${startX} ${startY}`, // Move
                `A 1 1 0 ${largeArcFlag} 1 ${endX} ${endY}`, // Arc
                `L 0 0`, // Line
            ].join(' ');
            const path = document.createElementNS(svgNS, 'path');
            path.setAttribute('d', pathData);
            path.setAttribute('fill', slice.color);
            svg.appendChild(path);
        })

        var svgUrl = 'data:image/svg+xml;charset=UTF-8,' + serializeXmlNode(svg)
        return svgUrl
    }

    const makeDynamicClusterIcon = (markers: any[]) => {
        var numMarkers = markers.length;
        var slices = markers.reduce((acc: any, marker: any) => {
            acc[parseInt(marker.title)].percent += 1 / numMarkers;
            return acc;
        }, [
            { percent: 0, color: 'Green' },
            { percent: 0, color: 'Blue' },
            { percent: 0, color: 'Red' },
        ])
        var newIconURL = makePieChartIcon(slices)
        return newIconURL;
    }

How I use the MarkerClusterer Component

<MarkerClusterer
    options={{
        averageCenter: true,
        styles: clusterStyles,
    }}
>
    {(clusterer) =>
        markerData.map((marker: any) => (
            <Marker
                key={marker.key}
                title={String(marker.type)}
                position={{ lat: marker.lat, lng: marker.lng }}
                clusterer={clusterer}
            />
        ))
    }
</MarkerClusterer>

Right now, I can only use some static styles, but I have them as the following for testing:

const clusterStyles = [
        {
            height: 50, textColor: '#ffffff', width: 50,
            url: 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns="http://www.w3.org/2000/svg" height="50" width="100"%3E%3Ccircle cx="25" cy="25" r="20" stroke="black" stroke-width="3" fill="green" /%3E%3C/svg%3E',
        },
        {
            height: 50, textColor: '#ffffff', width: 50,
            url: 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns="http://www.w3.org/2000/svg" height="50" width="100"%3E%3Ccircle cx="25" cy="25" r="20" stroke="black" stroke-width="3" fill="red" /%3E%3C/svg%3E',
        }
    ];
Micniks
  • 61
  • 6
  • Can you add what you have tried so far? With some code. – James Feb 13 '21 at 15:45
  • @James I edited it to show the code I had and tried to give some better explanation, but forgot to submit it, and then I found a solution today, so I have uploaded both as part of the edit, but thanks for reminding me to be more concrete about the code question I submitted. – Micniks Feb 15 '21 at 14:47
  • Glad to hear it. Could you post your solution as an answer to this question so it helps others? You are allowed and encouraged to answer your own questions if possible on stack overflow – James Feb 15 '21 at 15:08
  • 1
    @James Done, I have moved it from the last edit-update down to an answer. Thanks for helping me thought both my first question on StackOverflow, and my first Answer as well – Micniks Feb 15 '21 at 15:12

1 Answers1

3

I found a solution, by finding out that the style array for each cluster (ClusterStyles) can be changed, and then I have change it with the data from the specific markers in the given cluster. I ended up doing this in the callback onClusteringEnd, as here:

{/*  Added to the MarkerClusterer  */}
onClusteringEnd={(clusterer) => {
    clusterer.clusters.map((cluster) => {
        cluster.clusterIcon.styles = makeDynamicClusterIcon(cluster.markers)
    })
}}

And I changed the last line with return of the makeDynamicClusterIcon function I showed above to instead say:

return [{ url: newIconURL, height: 60, width: 60, textColor: '#FFFFFF', textSize: 22 }];
Micniks
  • 61
  • 6
  • 1
    It didn't work for me, no matter how many different ways I tried to set multiple custom styles (each cluster would have it's own icon). Maybe the solution proposed is missing some details? – ivan Dec 14 '21 at 09:36
  • Same here. Looking for a solution for the last 2 weeks. No luck. If you find a solution for this, please post it here. – Abhijith Sasikumar Mar 02 '22 at 02:15