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
Cheers,
Pip