Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3e2e9f6
Initial plan
Copilot Mar 21, 2026
cabef1a
Initial plan: replace symbolFuncs with symbolPaths and use SVG symbol…
Copilot Mar 21, 2026
6002603
Replace symbolFuncs/makePointPath with symbolPaths + SVG symbol/use r…
Copilot Mar 21, 2026
d193f68
Address review feedback: revert whitespace changes, remove fallback, …
Copilot Mar 21, 2026
8093f41
Clarify rotatePath() is only for scattergl SDF pipeline; SVG markers …
Copilot Mar 21, 2026
9e9a2a2
Update weather map demos to use new SVG symbol/use API with static pa…
Copilot Mar 21, 2026
c08b62f
Clean up decimal precision in SVG path strings in demo files
Copilot Mar 21, 2026
be9ce95
Add weather map SVG output file to repo
Copilot Mar 21, 2026
000a080
Remove temporary weather demo capture files from repo
Copilot Mar 21, 2026
e760332
Scope _symMap to defs DOM node (per-SVG, not per-layout)
Copilot Mar 21, 2026
c74e2ed
Remove temp verify_ids.html demo file
Copilot Mar 21, 2026
22c12d6
Pre-build dot-variant paths at init; ensureSymbolDef reduced to singl…
Copilot Mar 21, 2026
19debe7
ensureSymbolDef: per-SVG map on defs node; custom paths get c0,c1,… i…
Copilot Mar 21, 2026
008a312
ensureSymbolDef: uniform path→id map; single lookup for all symbol types
Copilot Mar 21, 2026
ea201d4
plan: give n=100 its own <symbol> via open-encoded sym.n
Copilot Mar 21, 2026
bb36f77
Document why Z cannot replace CSS for open/closed markers; pre-build …
Copilot Mar 21, 2026
919b610
gitignore: stop tracking dist build artifacts (JS bundles, plot-schem…
Copilot Mar 22, 2026
acbe796
drawing: remove expanded Z-command reasoning comment
Copilot Mar 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ build/*

dist/*.LICENSE.txt
dist/*.css
dist/*.js
dist/*.min.js
dist/plot-schema.json
dist/translation-keys.txt

npm-debug.log*
*.sublime*
Expand Down
261 changes: 110 additions & 151 deletions devtools/demos/all_demos.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8">
<title>Custom Marker Functions - All Demos</title>
<title>Custom Markers - SVG path strings (New API)</title>
<script src="../../dist/plotly.js"></script>
<style>
body { font-family: Arial, sans-serif; margin: 20px; max-width: 900px; margin: 0 auto; padding: 20px; }
Expand All @@ -15,195 +15,135 @@
</style>
</head>
<body>
<h1>Custom Marker Functions</h1>
<h1>Custom Markers — SVG path strings</h1>
<div class="info">
Pass functions as <code>marker.symbol</code> to create custom shapes.<br>
<strong>Simple:</strong> <code>function(r)</code> — r is marker radius<br>
<strong>Data-aware:</strong> <code>function(r, customdata)</code> — access per-point data
Pass SVG path strings as <code>marker.symbol</code> to create custom shapes.<br>
Paths are precomputed at <strong>r=20</strong>; Plotly scales them by <code>size/20</code>.<br>
Use an <strong>array</strong> for per-point shapes. Rotation uses <code>marker.angle</code>
(applied as <code>transform="rotate()"</code> via SVG <code>&lt;use&gt;</code>).
</div>

<!-- Demo 1: Basic custom markers -->
<h2>1. Basic Custom Markers</h2>
<p>Define functions returning SVG path strings. Mix with built-in symbols.</p>
<p>Precomputed SVG paths at r=20. Mix with built-in symbol names.</p>
<div id="plot1" class="plot"></div>
<pre>
// Heart shape
function heart(r) {
var x = r * 0.6, y = r * 0.8;
return 'M0,' + (-y/2) +
'C' + (-x) + ',' + (-y) + ' ' + (-x*2) + ',' + (-y/3) + ' ' + (-x*2) + ',0' +
'C' + (-x*2) + ',' + (y/2) + ' 0,' + y + ' 0,' + (y*1.5) +
'C0,' + y + ' ' + (x*2) + ',' + (y/2) + ' ' + (x*2) + ',0' +
'C' + (x*2) + ',' + (-y/3) + ' ' + x + ',' + (-y) + ' 0,' + (-y/2) + 'Z';
}
// Heart shape at r=20
var HEART = 'M0,-8C-12,-16 -24,-5.33 -24,0C-24,8 0,16 0,24C0,16 24,8 24,0C24,-5.33 12,-16 0,-8Z';

// 5-point star
function star(r) {
var path = 'M';
for (var i = 0; i < 10; i++) {
var rad = i % 2 === 0 ? r : r * 0.4;
var ang = i * Math.PI / 5 - Math.PI / 2;
path += (i ? 'L' : '') + (rad * Math.cos(ang)).toFixed(2) + ',' + (rad * Math.sin(ang)).toFixed(2);
}
return path + 'Z';
}
// 5-point star at r=20
var STAR = 'M0,-20L4.70,-6.47L19.02,-6.18L7.61,2.47L11.76,16.18' +
'L0,8L-11.76,16.18L-7.61,2.47L-19.02,-6.18L-4.70,-6.47Z';

Plotly.newPlot('plot1', [{
x: [1, 2, 3, 4, 5],
y: [2, 3, 4, 3, 2],
mode: 'markers+lines',
marker: {
symbol: [heart, star, 'circle', star, heart], // mix functions and strings
// mix path strings and built-in symbol names
symbol: [HEART, STAR, 'circle', STAR, HEART],
size: 25,
color: ['red', 'gold', 'blue', 'gold', 'red']
}
}]);</pre>

<!-- Demo 2: Data-aware markers -->
<h2>2. Data-Aware Markers</h2>
<p>Access <code>customdata[i]</code> to vary shape per point.</p>
<!-- Demo 2: Per-point shapes driven by data -->
<h2>2. Per-Point Shapes Driven by Data</h2>
<p>Build a per-point symbol array from your data instead of using a function.</p>
<div id="plot2" class="plot"></div>
<pre>
function shapeByData(r, customdata) {
if (customdata === 'star') {
// Star shape
var path = 'M';
for (var i = 0; i < 10; i++) {
var rad = i % 2 === 0 ? r : r * 0.4;
var ang = i * Math.PI / 5 - Math.PI / 2;
path += (i ? 'L' : '') + (rad * Math.cos(ang)).toFixed(2) + ',' + (rad * Math.sin(ang)).toFixed(2);
}
return path + 'Z';
}
if (customdata === 'big') {
r *= 1.4; // Larger diamond
}
// Default: diamond
return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z';
}
var DIAMOND = 'M20,0L0,20L-20,0L0,-20Z';
var BIG_DIAMOND = 'M28,0L0,28L-28,0L0,-28Z'; // 1.4× size
var STAR = '...'; // (same as Demo 1)

var types = ['normal', 'big', 'star', 'normal'];
var symbols = types.map(function(t) {
if (t === 'big') return BIG_DIAMOND;
if (t === 'star') return STAR;
return DIAMOND;
});

Plotly.newPlot('plot2', [{
x: [1, 2, 3, 4],
y: [1, 1, 1, 1],
customdata: ['normal', 'big', 'star', 'normal'],
mode: 'markers',
marker: { symbol: shapeByData, size: 25, color: '#10b981' }
marker: { symbol: symbols, size: 25, color: '#10b981' }
}]);</pre>

<!-- Demo 3: Weather map -->
<h2>3. Weather Map</h2>
<p>Complex example: sun, cloud, and wind barbs based on weather data.</p>
<h2>3. Weather Map with Rotation</h2>
<p>Per-point symbols + <code>marker.angle</code> for wind direction rotation via SVG.</p>
<div id="plot3" class="plot"></div>
<pre>
function weatherMarker(r, data) {
if (data.type === 'sunny') {
// Sun: circle with rays
var cr = r * 0.5, path = 'M' + cr + ',0A' + cr + ',' + cr + ' 0 1,1 -' + cr + ',0A' + cr + ',' + cr + ' 0 1,1 ' + cr + ',0';
for (var i = 0; i < 8; i++) {
var ang = i * Math.PI / 4;
path += 'M' + ((cr+2) * Math.cos(ang)).toFixed(1) + ',' + ((cr+2) * Math.sin(ang)).toFixed(1) +
'L' + ((cr+r*0.4) * Math.cos(ang)).toFixed(1) + ',' + ((cr+r*0.4) * Math.sin(ang)).toFixed(1);
}
return path;
}
if (data.type === 'cloudy') {
return 'M-8,3 A6,6 0 1,1 -2,-4 A7,7 0 1,1 8,-2 A5,5 0 1,1 10,3 Z';
}
if (data.type === 'wind') {
// Wind barb: staff + barbs based on speed
var path = 'M0,' + r + 'L0,-' + r, y = -r;
for (var i = 0; i < Math.min(data.speed, 3); i++) {
path += 'M0,' + y + 'L' + (r*0.6) + ',' + (y + r*0.3);
y += r * 0.3;
}
return path;
}
return 'M' + r + ',0A' + r + ',' + r + ' 0 1,0 -' + r + ',0A' + r + ',' + r + ' 0 1,0 ' + r + ',0';
}
// Paths at r=20 for each weather type
var SUN_PATH = 'M10,0A10,10 0 1,1 0,-10A10,10 0 0,1 10,0Z' + /* rays... */;
var CLOUD_PATH = 'M-12,4A7,7 0 1,1 -2,-4A8,8 0 1,1 10,-2A6,6 0 1,1 14,4L-12,4Z';
var WIND_PATHS = {
1: 'M0,24L0,-24M0,-24L12,-18',
2: 'M0,24L0,-24M0,-24L12,-18M0,-18L12,-12',
3: 'M0,24L0,-24M0,-24L12,-18M0,-18L12,-12M0,-12L12,-6'
};

var locations = [
{ name: 'Seattle', lon: -122, lat: 47, weather: { type: 'cloudy' } },
{ name: 'SF', lon: -122, lat: 38, weather: { type: 'sunny' } },
{ name: 'Denver', lon: -105, lat: 40, weather: { type: 'sunny' } },
{ name: 'Chicago', lon: -88, lat: 42, weather: { type: 'cloudy' } },
{ name: 'NYC', lon: -74, lat: 41, weather: { type: 'cloudy' } },
{ name: 'Miami', lon: -80, lat: 26, weather: { type: 'sunny' } },
// Wind arrows (jet stream)
{ lon: -115, lat: 46, weather: { type: 'wind', direction: 100, speed: 3 } },
{ lon: -100, lat: 42, weather: { type: 'wind', direction: 120, speed: 2 } },
{ lon: -85, lat: 36, weather: { type: 'wind', direction: 150, speed: 1 } }
];
var symbols = locations.map(function(l) {
if (l.weather.type === 'sunny') return SUN_PATH;
if (l.weather.type === 'cloudy') return CLOUD_PATH;
return WIND_PATHS[l.weather.speed];
});

Plotly.newPlot('plot3', [{
x: locations.map(l => l.lon),
y: locations.map(l => l.lat),
customdata: locations.map(l => l.weather),
text: locations.map(l => l.name || ''),
mode: 'markers+text',
textposition: 'bottom center',
marker: {
symbol: weatherMarker,
size: 30,
color: locations.map(l => ({ sunny: '#FFD700', cloudy: '#708090', wind: '#4169E1' }[l.weather.type])),
angle: locations.map(l => l.weather.direction || 0)
symbol: symbols,
angle: locations.map(l => l.weather.direction || 0), // SVG rotate()
size: 30 // scale = 30/20 = 1.5×
}
}], { xaxis: { range: [-130, -70] }, yaxis: { range: [20, 52], scaleanchor: 'x' } });</pre>
}]);</pre>

<script>
// Precomputed path strings at r=20
var HEART = 'M0,-8C-12,-16 -24,-5.33 -24,0C-24,8 0,16 0,24C0,16 24,8 24,0C24,-5.33 12,-16 0,-8Z';
var STAR = 'M0.00,-20.00L4.70,-6.47L19.02,-6.18L7.61,2.47L11.76,16.18L0.00,8.00L-11.76,16.18L-7.61,2.47L-19.02,-6.18L-4.70,-6.47Z';
var DIAMOND = 'M20,0L0,20L-20,0L0,-20Z';
var BIG_DIAMOND = 'M28,0L0,28L-28,0L0,-28Z';
var SUN_PATH = 'M10,0A10,10 0 1,1 0,-10A10,10 0 0,1 10,0Z' +
'M12,0L18,0M8.49,8.49L12.73,12.73' +
'M0,12L0,18M-8.49,8.49L-12.73,12.73' +
'M-12,0L-18,0M-8.49,-8.49L-12.73,-12.73' +
'M0,-12L0,-18M8.49,-8.49L12.73,-12.73';
var CLOUD_PATH = 'M-12,4A7,7 0 1,1 -2,-4A8,8 0 1,1 10,-2A6,6 0 1,1 14,4L-12,4Z';
var WIND_PATHS = {
1: 'M0,24L0,-24M0,-24L12,-18',
2: 'M0,24L0,-24M0,-24L12,-18M0,-18L12,-12',
3: 'M0,24L0,-24M0,-24L12,-18M0,-18L12,-12M0,-12L12,-6'
};

// === Demo 1: Basic markers ===
function heart(r) {
var x = r * 0.6, y = r * 0.8;
return 'M0,' + (-y/2) + 'C' + (-x) + ',' + (-y) + ' ' + (-x*2) + ',' + (-y/3) + ' ' + (-x*2) + ',0C' + (-x*2) + ',' + (y/2) + ' 0,' + y + ' 0,' + (y*1.5) + 'C0,' + y + ' ' + (x*2) + ',' + (y/2) + ' ' + (x*2) + ',0C' + (x*2) + ',' + (-y/3) + ' ' + x + ',' + (-y) + ' 0,' + (-y/2) + 'Z';
}
function star(r) {
var path = 'M';
for (var i = 0; i < 10; i++) {
var rad = i % 2 === 0 ? r : r * 0.4, ang = i * Math.PI / 5 - Math.PI / 2;
path += (i ? 'L' : '') + (rad * Math.cos(ang)).toFixed(2) + ',' + (rad * Math.sin(ang)).toFixed(2);
}
return path + 'Z';
}
Plotly.newPlot('plot1', [{
x: [1, 2, 3, 4, 5], y: [2, 3, 4, 3, 2], mode: 'markers+lines',
marker: { symbol: [heart, star, 'circle', star, heart], size: 25, color: ['red', 'gold', 'blue', 'gold', 'red'] }
}], { title: 'Mix custom functions with built-in symbols' });

// === Demo 2: Data-aware ===
function shapeByData(r, customdata) {
if (customdata === 'star') {
var path = 'M';
for (var i = 0; i < 10; i++) {
var rad = i % 2 === 0 ? r : r * 0.4, ang = i * Math.PI / 5 - Math.PI / 2;
path += (i ? 'L' : '') + (rad * Math.cos(ang)).toFixed(2) + ',' + (rad * Math.sin(ang)).toFixed(2);
}
return path + 'Z';
x: [1, 2, 3, 4, 5],
y: [2, 3, 4, 3, 2],
mode: 'markers+lines',
marker: {
symbol: [HEART, STAR, 'circle', STAR, HEART],
size: 25,
color: ['red', 'gold', 'blue', 'gold', 'red']
}
if (customdata === 'big') r *= 1.4;
return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z';
}
}], { title: 'Mix custom path strings with built-in symbols' });

// === Demo 2: Per-point shapes from data ===
var types = ['normal', 'big', 'star', 'normal'];
var symbols2 = types.map(function(t) {
if (t === 'big') return BIG_DIAMOND;
if (t === 'star') return STAR;
return DIAMOND;
});
Plotly.newPlot('plot2', [{
x: [1, 2, 3, 4], y: [1, 1, 1, 1], customdata: ['normal', 'big', 'star', 'normal'],
mode: 'markers', marker: { symbol: shapeByData, size: 25, color: '#10b981' }
}], { title: 'Shape varies by customdata value', xaxis: { range: [0, 5] } });
x: [1, 2, 3, 4],
y: [1, 1, 1, 1],
mode: 'markers',
marker: { symbol: symbols2, size: 25, color: '#10b981' }
}], { title: 'Per-point shapes mapped from data', xaxis: { range: [0, 5] } });

// === Demo 3: Weather map ===
function weatherMarker(r, data) {
if (data.type === 'sunny') {
var cr = r * 0.5, path = 'M' + cr + ',0A' + cr + ',' + cr + ' 0 1,1 -' + cr + ',0A' + cr + ',' + cr + ' 0 1,1 ' + cr + ',0';
for (var i = 0; i < 8; i++) {
var ang = i * Math.PI / 4;
path += 'M' + ((cr+2) * Math.cos(ang)).toFixed(1) + ',' + ((cr+2) * Math.sin(ang)).toFixed(1) + 'L' + ((cr+r*0.4) * Math.cos(ang)).toFixed(1) + ',' + ((cr+r*0.4) * Math.sin(ang)).toFixed(1);
}
return path;
}
if (data.type === 'cloudy') return 'M-8,3 A6,6 0 1,1 -2,-4 A7,7 0 1,1 8,-2 A5,5 0 1,1 10,3 Z';
if (data.type === 'wind') {
var path = 'M0,' + r + 'L0,-' + r, y = -r;
for (var i = 0; i < Math.min(data.speed, 3); i++) { path += 'M0,' + y + 'L' + (r*0.6) + ',' + (y + r*0.3); y += r * 0.3; }
return path;
}
return 'M' + r + ',0A' + r + ',' + r + ' 0 1,0 -' + r + ',0A' + r + ',' + r + ' 0 1,0 ' + r + ',0';
}
var locations = [
{ name: 'Seattle', lon: -122, lat: 47, weather: { type: 'cloudy' } },
{ name: 'SF', lon: -122, lat: 38, weather: { type: 'sunny' } },
Expand All @@ -213,14 +153,33 @@ <h2>3. Weather Map</h2>
{ name: 'Miami', lon: -80, lat: 26, weather: { type: 'sunny' } },
{ lon: -115, lat: 46, weather: { type: 'wind', direction: 100, speed: 3 } },
{ lon: -100, lat: 42, weather: { type: 'wind', direction: 120, speed: 2 } },
{ lon: -85, lat: 36, weather: { type: 'wind', direction: 150, speed: 1 } }
{ lon: -85, lat: 36, weather: { type: 'wind', direction: 150, speed: 1 } }
];
var symbols3 = locations.map(function(l) {
if (l.weather.type === 'sunny') return SUN_PATH;
if (l.weather.type === 'cloudy') return CLOUD_PATH;
return WIND_PATHS[Math.min(l.weather.speed || 1, 3)];
});
Plotly.newPlot('plot3', [{
x: locations.map(l => l.lon), y: locations.map(l => l.lat),
customdata: locations.map(l => l.weather), text: locations.map(l => l.name || ''),
mode: 'markers+text', textposition: 'bottom center',
marker: { symbol: weatherMarker, size: 30, color: locations.map(l => ({ sunny: '#FFD700', cloudy: '#708090', wind: '#4169E1' }[l.weather.type])), angle: locations.map(l => l.weather.direction || 0) }
}], { title: 'Weather icons with wind direction via marker.angle', xaxis: { range: [-130, -70] }, yaxis: { range: [20, 52], scaleanchor: 'x' } });
x: locations.map(function(l) { return l.lon; }),
y: locations.map(function(l) { return l.lat; }),
text: locations.map(function(l) { return l.name || ''; }),
mode: 'markers+text',
textposition: 'bottom center',
marker: {
symbol: symbols3,
size: 30,
color: locations.map(function(l) {
return { sunny: '#FFD700', cloudy: '#708090', wind: '#4169E1' }[l.weather.type];
}),
angle: locations.map(function(l) { return l.weather.direction || 0; }),
line: { width: 1.5, color: '#333' }
}
}], {
title: 'Weather icons with wind direction via marker.angle (SVG rotate)',
xaxis: { range: [-130, -70] },
yaxis: { range: [20, 52], scaleanchor: 'x' }
});
</script>
</body>
</html>
Loading