Looking for a Javascript wizard to help with custom leaflet cluster

Hey! I’m working on a project that would benefit from a custom cluster the idea is to build a custom svgRing like:

I have some example code that I was able to get working with two colors which is random selection of red or green for reference:

cluster_to_layer = assign("""function(feature, latlng, index, context){
function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function ringSVG(opt) {
  function describeArc(opt) {
    const innerStart = polarToCartesian(opt.x, opt.y, opt.radius, opt.endAngle);
    const innerEnd = polarToCartesian(opt.x, opt.y, opt.radius, opt.startAngle);
    const outerStart = polarToCartesian(opt.x, opt.y, opt.radius + opt.ringThickness, opt.endAngle);
    const outerEnd = polarToCartesian(opt.x, opt.y, opt.radius + opt.ringThickness, opt.startAngle);
    const largeArcFlag = opt.endAngle - opt.startAngle <= 180 ? "0" : "1";
    return [ "M", outerStart.x, outerStart.y,
             "A", opt.radius + opt.ringThickness, opt.radius + opt.ringThickness, 0, largeArcFlag, 0, outerEnd.x, outerEnd.y,
             "L", innerEnd.x, innerEnd.y,
             "A", opt.radius, opt.radius, 0, largeArcFlag, 1, innerStart.x, innerStart.y,
             "L", outerStart.x, outerStart.y, "Z"].join(" ");
  }

  const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
      return { x: centerX + (radius * Math.cos((angleInDegrees - 90) * Math.PI / 180.0)),
               y: centerY + (radius * Math.sin((angleInDegrees - 90) * Math.PI / 180.0)) };
  }

  opt = opt || {};
  const defaults = { width: 60, height: 60, radius: 20, gapDeg: 5, goodPerc: 75, fontSize: 17, text: `test`,
                     ringThickness: 7, goodColor: 'green', badColor: 'red'};
  opt = {...defaults, ...opt};

  const badPercDeg = 360 * (100 - opt['goodPerc']) / 100;
  const stdOpt = {x: opt['width']/2, y: opt['height']/2, radius: opt['radius'], ringThickness: opt['ringThickness']};
  const dGreen = describeArc({...stdOpt, startAngle: 90, endAngle: 450 - badPercDeg - opt['gapDeg']});
  const dRed   = describeArc({...stdOpt, startAngle: 450 - badPercDeg, endAngle: 450 - opt['gapDeg']});
  const path1 = `<path class="path1" fill="${opt['goodColor']}" d="${dGreen}"></path>`
  const path2 = opt['goodPerc'] < 100 ? `<path class="path2" fill="${opt['badColor']}" d="${dRed}"></path>` : '';
  return `<svg id="svg" width="${opt['width']}" height="${opt['height']}">
            ${path1} ${path2}
            <text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle" font-size="${opt['fontSize']}"
                  fill="black"> ${opt['text'] || opt['goodPerc']}
            </text>
          </svg>`;
}
/*********************/

    const {min, max, colorscale, circleOptions, colorProp} = context.hideout;
    const csc = chroma.scale(colorscale).domain([min, max]);

    // Set color based on mean value of leaves.
    const leaves = index.getLeaves(feature.properties.cluster_id);
    let valueSum = 0;
    for (let i = 0; i < leaves.length; ++i) {
        valueSum += leaves[i].properties[colorProp]
    }
    const valueMean = valueSum / leaves.length;

    // icon background color
    const scatterIcon = L.DivIcon.extend({
        createIcon: function(oldIcon) {
               let icon = L.DivIcon.prototype.createIcon.call(this, oldIcon);
               return icon;
        }
    });

    // Render a circle with the number of leaves written in the center.
    const total = feature.properties.point_count_abbreviated;
    const numOffline = getRandomInt(0, total);
    const numOnline = total - numOffline;
    const goodPerc = numOnline / total * 100; 

    const icon = new scatterIcon({
        html: ringSVG({
                text:`${total-numOffline}/${total}`,
                goodPerc,
                width: 70,
                height: 70,
                radius: 15,
                fontSize: 12,
                ringThickness: 5,
            }),
        className: "marker-cluster",
        iconSize: L.point(40, 40),
        className: "marker-cluster",
        iconSize: L.point(40, 40),
        color: csc(valueMean)
    });
    return L.marker(latlng, {icon : icon})
}
""")

In my project I have this code I setup based on that example for trying to create a working solution (currently not working) but ideally it looks within the dataset and splits the SVG ring by the m_type and the SVG ring will represent the % and the color of each m_type:

# setup fetch data
everything_df = pd.DataFrame(r_everything(None))

# Convert the 'date_created' column to datetime
everything_df["date_created"] = pd.to_datetime(everything_df["date_created"])

# Format the 'date_created' column
everything_df["date_created"] = everything_df["date_created"].dt.strftime("%b %d, %Y")

gdf = gpd.GeoDataFrame(
    everything_df, geometry=gpd.points_from_xy(everything_df.lon, everything_df.lat))

geojson = gdf.to_json()

geojson_dict = json.loads(geojson)

cluster_to_layer = assign("""function(feature, latlng, index, context){
    function ringSVG(opt) {
        function describeArc(opt) {
            const innerStart = polarToCartesian(opt.x, opt.y, opt.radius, opt.endAngle);
            const innerEnd = polarToCartesian(opt.x, opt.y, opt.radius, opt.startAngle);
            const outerStart = polarToCartesian(opt.x, opt.y, opt.radius + opt.ringThickness, opt.endAngle);
            const outerEnd = polarToCartesian(opt.x, opt.y, opt.radius + opt.ringThickness, opt.startAngle);
            const largeArcFlag = opt.endAngle - opt.startAngle <= 180 ? "0" : "1";
            return [ "M", outerStart.x, outerStart.y,
                     "A", opt.radius + opt.ringThickness, opt.radius + opt.ringThickness, 0, largeArcFlag, 0, outerEnd.x, outerEnd.y,
                     "L", innerEnd.x, innerEnd.y,
                     "A", opt.radius, opt.radius, 0, largeArcFlag, 1, innerStart.x, innerStart.y,
                     "L", outerStart.x, outerStart.y, "Z"].join(" ");
        }

        const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
            return { x: centerX + (radius * Math.cos((angleInDegrees - 90) * Math.PI / 180.0)),
                     y: centerY + (radius * Math.sin((angleInDegrees - 90) * Math.PI / 180.0)) };
        }

        opt = opt || {};
        const defaults = { width: 60, height: 60, radius: 20, gapDeg: 5, fontSize: 17, text: `test`,
                           ringThickness: 7, colors: [] };
        opt = {...defaults, ...opt};

        let startAngle = 90;
        let paths = '';
        for (let i = 0; i < opt.colors.length; i++) {
            const endAngle = startAngle + (360 / opt.colors.length) - opt.gapDeg;
            const d = describeArc({ x: opt.width / 2, y: opt.height / 2, radius: opt.radius, ringThickness: opt.ringThickness, startAngle, endAngle });
            paths += `<path fill="${opt.colors[i]}" d="${d}"></path>`;
            startAngle = endAngle + opt.gapDeg;
        }

        console.log("SVG Paths: ", paths);

        return `<svg width="${opt.width}" height="${opt.height}">
                    ${paths}
                    <text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle" font-size="${opt.fontSize}"
                          fill="black">${opt.text || opt.goodPerc}
                    </text>
                </svg>`;
    }

    const {min, max, colorscale, circleOptions, colorProp} = context.hideout;
    const csc = chroma.scale(colorscale).domain([min, max]);

    const leaves = index.getLeaves(feature.properties.cluster_id);
    const m_types = leaves.map(leaf => leaf.properties.m_type);
    const m_type_colors = {
        "restaurants": "red",
        "fishing": "cyan",
        "digital store": "white",
        "parks": "green",
        "bait": "blue",
        "seafood market": "blue",
        "vacation rental": "yellow",
        "rv hookups": "gray",
        "events": "purple",
        "stores": "salmon",
        "nonprofit": "pink"
    };

    let colors = m_types.map(m_type => m_type_colors[m_type] || 'gray');
    console.log("Cluster m_types: ", m_types);
    console.log("Assigned colors: ", colors);

    const scatterIcon = L.DivIcon.extend({
        createIcon: function(oldIcon) {
               let icon = L.DivIcon.prototype.createIcon.call(this, oldIcon);
               return icon;
        }
    });

    const total = feature.properties.point_count_abbreviated;
    const goodPerc = 100; // assuming all are 'good' for simplicity

    const icon = new scatterIcon({
        html: ringSVG({
                text: `${total}`,
                goodPerc,
                width: 70,
                height: 70,
                radius: 15,
                fontSize: 12,
                ringThickness: 5,
                colors
            }),
        className: "marker-cluster",
        iconSize: L.point(40, 40)
    });
    return L.marker(latlng, {icon : icon});
}
""")

dl.GeoJSON(
      data=geojson_dict,
      id="locations_layer",
      cluster=True,
      zoomToBoundsOnClick=True,
      superClusterOptions=dict(radius=40),
      hideout=dict(
          m_type_colors=m_type_colors,
          circleOptions=dict(fillOpacity=1, stroke=False, radius=3),
          min=0,
      ),
      pointToLayer=point_to_layer_js,
      clusterToLayer=cluster_to_layer,
  )

However the end result of the code looks like:

Not the strongest javascript developer so I’ve been struggling on building a solution but I think this is possible from my testing and this type of cluster could be useful in a range of other developers projects if created and understood. Would appreciate some help from a javascript wizard if you are out their and able to take on the challenge :pray:

Cheers,
Pip

I’m far from a Javascript wizard, and could be fundamentally misunderstanding something here, but if you are setting up hideout like this…

hideout=dict(
          m_type_colors=m_type_colors,
          circleOptions=dict(fillOpacity=1, stroke=False, radius=3),
          min=0,
      )

… and accessing it like this…

const {min, max, colorscale, circleOptions, colorProp} = context.hideout;
const csc = chroma.scale(colorscale).domain([min, max]);

… are you trying to retrieve (and use) things that haven’t been set?

It looks very interesting, and I’d be interested in digging into it more, but I think I’d need a complete working example to get started.

Do you have an example of the data/an example cluster or two that’s supposed to be translated into this svg? I can kinda see the shape of this (no pun intended, sorry), but I’m struggling without a concrete dataset in front of me to really pull it together.

@tbonethemighty @davidharris , Thanks for showing interest but in creating a working example for the both of you I’ve actually been able to get a mostly working example! So apparently I’m the javascript wizard :mage:t2: lol


Jokes aside, still have one small problem I could use some help with if yall are able? I’d like to render the ringSVG so the colors are consecutive , for example the number 7 in corpus has 3 purple two on one side and the other purple on the far end, would be ideal if they where all together. I attempted to fix this with: everything_df = everything_df.sort_values(by="type") in an attempt to group by type before building the ringSVG.

also it seems like the ringSVG is maxed at only showing 11 not sure what the problem is but if you zoom out you can really see something is off:
Screenshot 2024-07-06 at 1.06.35 PM

I’ve also designed an example working project for you to test and play around with should run out of the box just pip install dash pandas geopandas dash-leaflet:

from dash import Dash
from dash import html
import json
import requests
import pandas as pd
import geopandas as gpd
from dash_extensions.javascript import assign
import dash_leaflet as dl


# Hit api endpoint and get map data
def fetch_data(url, fallback_data, timeout=5):
    try:
        response = requests.get(url, timeout=timeout)
        response.raise_for_status()
        return response.json()
    except (requests.RequestException, json.JSONDecodeError):
        return fallback_data


def filter_data(data, filter_d):
    if filter_d:
        return [x for x in data if x["m_type"] == filter_d]
    return data


def transform_data(data, base_url):

    return [
        {
            "name": x["name"],
            "icon_data": {
                "anchorY": int(x["anchorY"]),
                "height": int(x["height"]),
                "url": x["icon_data"],
                "width": int(x["width"]),
            },
            "image": f"{base_url}{x['picture']}",
            "lat": x["lat"],
            "lon": x["lon"],
            "tags": x["name"],
            "type": x["m_type"],
            "sendtourl": x["url"],
            "description": x.get("description", ""),  # Use get method to avoid KeyError
            "pk": x["pk"],
            "date_created": x["date_created"],
            "city": x["city"],
        }
        for x in data
        if x["active_on_map"] == True
    ]


def r_everything(filter_d=None):
    base_url = "https://pipinstallpython.pythonanywhere.com"
    fallback_data = [
        {
            "pk": i,
            "active_on_map": True,
            "name": [
                "Camp Aranzazo",
                "Chamber of Comerce",
                "JJ's Cafe",
                "Panjo's Pizza & Pasta",
            ][i],
            "m_type": ["nonprofit", "stores", "restaurants", "restaurants"][i],
            "picture": [
                "/media/map_location_pictures/2022/08/02/camp_aranzazu.jpeg",
                "/media/map_location_pictures/2022/08/02/chamber_of_comerce.jpeg",
                "/media/map_location_pictures/2022/08/02/jjscafee.jpg",
                "/media/map_location_pictures/2022/08/02/panjosRockport.jpg",
            ][i],
            "phone_number": ["", "", "", ""][i],
            "lat": [28.113, 28.02618, 28.04631, 28.05503][i],
            "lon": [-97.04, -97.05021, -97.044, -97.04046][i],
            "address": ["", "", "", ""][i],
            "city": ["Rockport", "Rockport", "Rockport", "Rockport"][i],
            "state": ["Texas", "Texas", "Texas", "Texas"][i],
            "zip_code": [78382, 78382, 78382, 78382][i],
            "url": ["", "", "https://www.yelp.com/biz/jjs-cafe-rockport", ""][i],
            "author": [1, 1, 1, 1][i],
            "QR_image": [
                "/media/QR_Location/2022/08/02/qr-_edY2l4T.png",
                "/media/QR_Location/2022/08/02/qr-_TnPDqKi.png",
                "/media/QR_Location/2022/08/02/qr-https%3A/www.yelp.com/biz/jjs-cafe-rockport.png",
                "/media/QR_Location/2022/08/02/qr-_MHLGlhc.png",
            ][i],
            "date_created": ["2022-08-02", "2022-08-02", "2022-08-02", "2022-08-02"][i],
            "icon_data": [
                "https://cdn-icons-png.flaticon.com/512/1043/1043927.png",
                "https://cdn-icons-png.flaticon.com/512/2830/2830346.png",
                "https://cdn-icons-png.flaticon.com/512/926/926292.png",
                "https://cdn-icons-png.flaticon.com/512/7836/7836772.png",
            ][i],
            "width": [242, 242, 242, 242][i],
            "height": [242, 242, 242, 242][i],
            "anchorY": [242, 242, 242, 242][i],
        }
        for i in range(4)
    ]
    r = fetch_data(
        "https://pipinstallpython.pythonanywhere.com/map/api/", fallback_data
    )

    data = [r[key] for key in r] if isinstance(r, dict) else r
    filtered_data = filter_data(data, filter_d)

    return transform_data(filtered_data, base_url)


app = Dash(
    __name__,
    assets_url_path="assets",
    external_stylesheets=[
        "https://use.fontawesome.com/releases/v6.2.1/css/all.css",
        "https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.css",
    ],
    external_scripts=[
        "https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.1.0/chroma.min.js",
        "https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.js",
    ],
    use_pages=False,
    suppress_callback_exceptions=True,
)


# prep map

# setup fetch data
everything_df = pd.DataFrame(r_everything(None))

# Convert the 'date_created' column to datetime
everything_df["date_created"] = pd.to_datetime(everything_df["date_created"])

# Format the 'date_created' column
everything_df["date_created"] = everything_df["date_created"].dt.strftime("%b %d, %Y")

# Group by 'type' and retain all fields
everything_df = everything_df.sort_values(by="type")

gdf = gpd.GeoDataFrame(
    everything_df, geometry=gpd.points_from_xy(everything_df.lon, everything_df.lat)
)

geojson = gdf.to_json()

geojson_dict = json.loads(geojson)

point_to_layer_js = assign(
    """
function(feature, latlng){
    const iconData = feature.properties.icon_data;
    const flag = L.icon({
        iconUrl: iconData.url, 
        iconSize: [35, 35],
        tooltipAnchor: [20, 0]  // Adjusts the tooltip 10px to the right
    });
    const marker = L.marker(latlng, {icon: flag});
    marker.bindTooltip('<img src="' + feature.properties.image + '" style="width: 30vw; height: 20vh; max-width: 250px; max-height: 250px;">');
    marker.on('click', function(e) {
        // Create a new custom event
        var event = new CustomEvent('marker_click', {detail: feature.properties.pk});
        // Dispatch the event
        window.dispatchEvent(event);
    });
    return marker;
}
"""
)

m_type_colors = {
    "restaurants": "red",
    "fishing": "cyan",
    "digital store": "white",
    "parks": "green",
    "bait": "blue",
    "seafood market": "blue",
    "vacation rental": "yellow",
    "rv hookups": "gray",
    "events": "purple",
    "stores": "salmon",
    "nonprofit": "pink"
}

cluster_to_layer = assign(
    """function(feature, latlng, index, context){
    function ringSVG(opt) {
        function describeArc(opt) {
            const innerStart = polarToCartesian(opt.x, opt.y, opt.radius, opt.endAngle);
            const innerEnd = polarToCartesian(opt.x, opt.y, opt.radius, opt.startAngle);
            const outerStart = polarToCartesian(opt.x, opt.y, opt.radius + opt.ringThickness, opt.endAngle);
            const outerEnd = polarToCartesian(opt.x, opt.y, opt.radius + opt.ringThickness, opt.startAngle);
            const largeArcFlag = opt.endAngle - opt.startAngle <= 180 ? "0" : "1";
            return [ "M", outerStart.x, outerStart.y,
                     "A", opt.radius + opt.ringThickness, opt.radius + opt.ringThickness, 0, largeArcFlag, 0, outerEnd.x, outerEnd.y,
                     "L", innerEnd.x, innerEnd.y,
                     "A", opt.radius, opt.radius, 0, largeArcFlag, 1, innerStart.x, innerStart.y,
                     "L", outerStart.x, outerStart.y, "Z"].join(" ");
        }

        const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
            return { x: centerX + (radius * Math.cos((angleInDegrees - 90) * Math.PI / 180.0)),
                     y: centerY + (radius * Math.sin((angleInDegrees - 90) * Math.PI / 180.0)) };
        }

        opt = opt || {};
        const defaults = { width: 60, height: 60, radius: 20, gapDeg: 5, fontSize: 17, text: `test`,
                           ringThickness: 7, colors: [] };
        opt = {...defaults, ...opt};

        let startAngle = 90;
        let paths = '';
        for (let i = 0; i < opt.colors.length; i++) {
            const endAngle = startAngle + (360 / opt.colors.length) - opt.gapDeg;
            const d = describeArc({ x: opt.width / 2, y: opt.height / 2, radius: opt.radius, ringThickness: opt.ringThickness, startAngle, endAngle });
            paths += `<path fill="${opt.colors[i]}" d="${d}"></path>`;
            startAngle = endAngle + opt.gapDeg;
        }

        console.log("SVG Paths: ", paths);

        return `<svg width="${opt.width}" height="${opt.height}">
                    ${paths}
                    <text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle" font-size="${opt.fontSize}"
                          fill="black">${opt.text || opt.goodPerc}
                    </text>
                </svg>`;
    }

    const {min, max, colorscale, circleOptions, colorProp} = context.hideout;
    const csc = chroma.scale(colorscale).domain([min, max]);

    const leaves = index.getLeaves(feature.properties.cluster_id);
    const type = leaves.map(leaf => leaf.properties.type);
    const m_type_colors = {
        "restaurants": "red",
        "fishing": "cyan",
        "digital store": "white",
        "parks": "green",
        "bait": "blue",
        "seafood market": "blue",
        "vacation rental": "yellow",
        "rv hookups": "gray",
        "events": "purple",
        "stores": "salmon",
        "nonprofit": "pink"
    };

    let colors = type.map(type => m_type_colors[type] || 'gray');
    console.log("Cluster type: ", type);
    console.log("Assigned colors: ", colors);

    const scatterIcon = L.DivIcon.extend({
        createIcon: function(oldIcon) {
               let icon = L.DivIcon.prototype.createIcon.call(this, oldIcon);
               return icon;
        }
    });

    const total = feature.properties.point_count_abbreviated;
    const goodPerc = 100; // assuming all are 'good' for simplicity

    const icon = new scatterIcon({
        html: ringSVG({
                text: `${total}`,
                goodPerc,
                width: 70,
                height: 70,
                radius: 15,
                fontSize: 12,
                ringThickness: 5,
                colors
            }),
        className: "marker-cluster",
        iconSize: L.point(40, 40)
    });
    return L.marker(latlng, {icon : icon});
}
"""
)


app.layout = html.Div(
    [
        dl.Map(
            [
                dl.TileLayer(
                    url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
                ),
                dl.GeoJSON(
                    data=geojson_dict,
                    id="locations_layer",
                    cluster=True,
                    zoomToBoundsOnClick=True,
                    superClusterOptions=dict(radius=40),
                    hideout=dict(
                        m_type_colors=m_type_colors,
                        circleOptions=dict(fillOpacity=1, stroke=False, radius=3),
                        min=0,
                    ),
                    pointToLayer=point_to_layer_js,
                    clusterToLayer=cluster_to_layer,
                ),
                dl.EasyButton(
                    icon="fa-search",
                    title="Search Map",
                    id="search_map_display_btn",
                    n_clicks=1,
                ),
            ],
            style={
                "position": "absolute",
                "width": "100%",
                "height": "92%",
                "left": "0",
                "z-index": -1,
            },
            center=[27.94093, -97.20840],
            zoom=10,
            id="map_example",
        )
    ],
    style={"height": "100vh", "overflow": "hidden"},
)


if __name__ == "__main__":
    app.run_server(debug=True, host="localhost", port="8111")

Should be able to directly run that code and it will launch up with a working example, if you have any issues let me know.

Thanks! We getting closer to a new custom cluster :crossed_fingers:

1 Like

@tbonethemighty and @davidharris actually spoke too soon, just found the solution:

cluster_to_layer = assign(
    """function(feature, latlng, index, context){
    function ringSVG(opt) {
        function describeArc(opt) {
            const innerStart = polarToCartesian(opt.x, opt.y, opt.radius, opt.endAngle);
            const innerEnd = polarToCartesian(opt.x, opt.y, opt.radius, opt.startAngle);
            const outerStart = polarToCartesian(opt.x, opt.y, opt.radius + opt.ringThickness, opt.endAngle);
            const outerEnd = polarToCartesian(opt.x, opt.y, opt.radius + opt.ringThickness, opt.startAngle);
            const largeArcFlag = opt.endAngle - opt.startAngle <= 180 ? "0" : "1";
            return [ "M", outerStart.x, outerStart.y,
                     "A", opt.radius + opt.ringThickness, opt.radius + opt.ringThickness, 0, largeArcFlag, 0, outerEnd.x, outerEnd.y,
                     "L", innerEnd.x, innerEnd.y,
                     "A", opt.radius, opt.radius, 0, largeArcFlag, 1, innerStart.x, innerStart.y,
                     "L", outerStart.x, outerStart.y, "Z"].join(" ");
        }

        const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
            return { x: centerX + (radius * Math.cos((angleInDegrees - 90) * Math.PI / 180.0)),
                     y: centerY + (radius * Math.sin((angleInDegrees - 90) * Math.PI / 180.0)) };
        }

        opt = opt || {};
        const defaults = { width: 60, height: 60, radius: 20, gapDeg: 5, fontSize: 17, text: `test`,
                           ringThickness: 7, colors: [] };
        opt = {...defaults, ...opt};

        let startAngle = 90;
        let paths = '';
        const totalPerc = opt.colors.reduce((acc, val) => acc + val.perc, 0);
        for (let i = 0; i < opt.colors.length; i++) {
            const segmentPerc = opt.colors[i].perc / totalPerc;
            const endAngle = startAngle + (segmentPerc * 360) - opt.gapDeg;
            const d = describeArc({ x: opt.width / 2, y: opt.height / 2, radius: opt.radius, ringThickness: opt.ringThickness, startAngle, endAngle });
            paths += `<path fill="${opt.colors[i].color}" d="${d}"></path>`;
            startAngle = endAngle + opt.gapDeg;
        }

        console.log("SVG Paths: ", paths);

        return `<svg width="${opt.width}" height="${opt.height}">
                    ${paths}
                    <text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle" font-size="${opt.fontSize}"
                          fill="black">${opt.text || opt.goodPerc}
                    </text>
                </svg>`;
    }

    const leaves = index.getLeaves(feature.properties.cluster_id);
    const m_types = leaves.map(leaf => leaf.properties.type);

    // Count the occurrences of each m_type
    const m_type_counts = m_types.reduce((acc, m_type) => {
        acc[m_type] = (acc[m_type] || 0) + 1;
        return acc;
    }, {});

    // Calculate the percentage for each m_type
    const colors = Object.keys(m_type_counts).map(m_type => ({
        color: context.hideout.m_type_colors[m_type] || 'gray',
        perc: m_type_counts[m_type]
    }));

    const scatterIcon = L.DivIcon.extend({
        createIcon: function(oldIcon) {
               let icon = L.DivIcon.prototype.createIcon.call(this, oldIcon);
               return icon;
        }
    });

    const total = feature.properties.point_count_abbreviated;

    const icon = new scatterIcon({
        html: ringSVG({
                text: `${total}`,
                colors
            }),
        className: "marker-cluster",
        iconSize: L.point(40, 40)
    });
    return L.marker(latlng, {icon: icon});
}
"""
)

looks about right:
Screenshot 2024-07-06 at 1.20.19 PM

Hope you all at least found this interesting and maybe even learned something cool and useful from the code and back and forth with myself lol :slight_smile:

Snap, I was literally about to post that I had the first half of it for you :laughing: But glad you were able to get where you wanted to go even without my help.

And yes, super interesting to look into!

1 Like