Skip to content

Commit 0cc49cc

Browse files
Copilotgatopeich
andauthored
Pass customdata to custom marker functions (backward compatible) (#2)
* Initial plan * Add data point and trace context to custom marker functions Co-authored-by: gatopeich <7722268+gatopeich@users.noreply.github.com> * Improve comment clarity for custom marker function parameters Co-authored-by: gatopeich <7722268+gatopeich@users.noreply.github.com> * Add weather map demo and documentation for custom marker functions Co-authored-by: gatopeich <7722268+gatopeich@users.noreply.github.com> * Add backward compatibility test demo Co-authored-by: gatopeich <7722268+gatopeich@users.noreply.github.com> * Remove dist folder changes - these are build artifacts for maintainers Co-authored-by: gatopeich <7722268+gatopeich@users.noreply.github.com> * Fix coercion to allow function values for marker.symbol Allow functions to pass through enumerated coercion for custom marker symbols. Update weather map demo with proper meteorological wind barbs. * Auto-rotate custom marker functions via align() - Export align() from symbol_defs.js - Apply align() automatically to custom function results in makePointPath() - Simplify custom function signature to (r, d, trace) - Update weather demo with cleaner wind barbs and compact layout * Simplify custom marker function signature to (r, customdata) - Custom functions now receive (r, customdata) instead of (r, d, trace) - customdata is the value from trace.customdata[i] for each point - Rotation and standoff handled automatically via align() - Updated demos to use new signature * Improve weather demo with jet stream wind pattern * Add UTF-8 icons to weather demo legend * Rename pt back to d to match call sites * Remove unused trace parameter from makePointPath * Update docs and tests for (r, customdata) signature --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gatopeich <7722268+gatopeich@users.noreply.github.com>
1 parent db38309 commit 0cc49cc

File tree

7 files changed

+408
-127
lines changed

7 files changed

+408
-127
lines changed

CUSTOM_MARKER_FUNCTIONS.md

Lines changed: 90 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,60 @@ This document describes how to use custom SVG marker functions in plotly.js scat
66

77
You can now pass a custom function directly as the `marker.symbol` value to create custom marker shapes. This provides a simple, flexible way to extend the built-in marker symbols without any registration required.
88

9-
## Usage
9+
## Function Signature
10+
11+
Custom marker functions receive:
12+
13+
```javascript
14+
function customMarker(r, customdata) {
15+
// r: radius/size of the marker (half of marker.size)
16+
// customdata: the value from trace.customdata[i] for this point (optional)
17+
18+
// Return an SVG path string centered at (0,0)
19+
return 'M...Z';
20+
}
21+
```
22+
23+
**Simple markers** can use just `(r)`:
24+
```javascript
25+
function diamond(r) {
26+
return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z';
27+
}
28+
```
29+
30+
**Data-aware markers** use `(r, customdata)`:
31+
```javascript
32+
function categoryMarker(r, customdata) {
33+
if (customdata === 'high') {
34+
return 'M0,-' + r + 'L' + r + ',' + r + 'L-' + r + ',' + r + 'Z'; // up triangle
35+
}
36+
return 'M0,' + r + 'L' + r + ',-' + r + 'L-' + r + ',-' + r + 'Z'; // down triangle
37+
}
38+
```
39+
40+
Note: Rotation is handled automatically via `marker.angle` - your function just returns an unrotated path.
41+
42+
## Usage Examples
1043

1144
### Basic Example
1245

1346
```javascript
14-
// Define a custom marker function
15-
function heartMarker(r, angle, standoff) {
16-
var x = r * 0.6;
17-
var y = r * 0.8;
18-
return 'M0,' + (-y/2) +
47+
function heartMarker(r) {
48+
var x = r * 0.6, y = r * 0.8;
49+
return 'M0,' + (-y/2) +
1950
'C' + (-x) + ',' + (-y) + ' ' + (-x*2) + ',' + (-y/3) + ' ' + (-x*2) + ',0' +
2051
'C' + (-x*2) + ',' + (y/2) + ' 0,' + (y) + ' 0,' + (y*1.5) +
2152
'C0,' + (y) + ' ' + (x*2) + ',' + (y/2) + ' ' + (x*2) + ',0' +
2253
'C' + (x*2) + ',' + (-y/3) + ' ' + (x) + ',' + (-y) + ' 0,' + (-y/2) + 'Z';
2354
}
2455

25-
// Use it directly in a plot
2656
Plotly.newPlot('myDiv', [{
2757
type: 'scatter',
2858
x: [1, 2, 3, 4, 5],
2959
y: [2, 3, 4, 3, 2],
3060
mode: 'markers',
3161
marker: {
32-
symbol: heartMarker, // Pass the function directly!
62+
symbol: heartMarker,
3363
size: 15,
3464
color: 'red'
3565
}
@@ -38,158 +68,96 @@ Plotly.newPlot('myDiv', [{
3868

3969
### Multiple Custom Markers
4070

41-
You can use different custom markers for different points by passing an array:
42-
4371
```javascript
44-
function heartMarker(r) {
45-
var x = r * 0.6, y = r * 0.8;
46-
return 'M0,' + (-y/2) + 'C...Z';
47-
}
48-
49-
function starMarker(r) {
50-
var points = 5;
51-
var outerRadius = r;
52-
var innerRadius = r * 0.4;
72+
function star(r) {
5373
var path = 'M';
54-
55-
for (var i = 0; i < points * 2; i++) {
56-
var radius = i % 2 === 0 ? outerRadius : innerRadius;
57-
var ang = (i * Math.PI) / points - Math.PI / 2;
58-
var x = radius * Math.cos(ang);
59-
var y = radius * Math.sin(ang);
60-
path += (i === 0 ? '' : 'L') + x.toFixed(2) + ',' + y.toFixed(2);
74+
for (var i = 0; i < 10; i++) {
75+
var radius = i % 2 === 0 ? r : r * 0.4;
76+
var ang = (i * Math.PI) / 5 - Math.PI / 2;
77+
path += (i === 0 ? '' : 'L') + (radius * Math.cos(ang)).toFixed(2) + ',' + (radius * Math.sin(ang)).toFixed(2);
6178
}
62-
path += 'Z';
63-
return path;
79+
return path + 'Z';
6480
}
6581

6682
Plotly.newPlot('myDiv', [{
67-
type: 'scatter',
6883
x: [1, 2, 3, 4, 5],
6984
y: [2, 3, 4, 3, 2],
7085
mode: 'markers',
7186
marker: {
72-
symbol: [heartMarker, starMarker, heartMarker, starMarker, heartMarker],
87+
symbol: [heartMarker, star, 'circle', star, heartMarker],
7388
size: 18,
74-
color: ['red', 'gold', 'pink', 'orange', 'crimson']
89+
color: ['red', 'gold', 'blue', 'orange', 'crimson']
7590
}
7691
}]);
7792
```
7893

79-
### Mixing with Built-in Symbols
80-
81-
Custom functions work seamlessly with built-in symbol names:
94+
### Data-Driven Markers with customdata
8295

8396
```javascript
84-
function customDiamond(r) {
85-
var rd = r * 1.5;
86-
return 'M' + rd + ',0L0,' + rd + 'L-' + rd + ',0L0,-' + rd + 'Z';
97+
function weatherMarker(r, customdata) {
98+
var weather = customdata;
99+
100+
if (weather.type === 'sunny') {
101+
// Sun: circle with rays
102+
var cr = r * 0.5;
103+
var path = 'M' + cr + ',0A' + cr + ',' + cr + ' 0 1,1 0,-' + cr +
104+
'A' + cr + ',' + cr + ' 0 0,1 ' + cr + ',0Z';
105+
for (var i = 0; i < 8; i++) {
106+
var ang = i * Math.PI / 4;
107+
var x1 = (cr + 2) * Math.cos(ang), y1 = (cr + 2) * Math.sin(ang);
108+
var x2 = (cr + r*0.4) * Math.cos(ang), y2 = (cr + r*0.4) * Math.sin(ang);
109+
path += 'M' + x1.toFixed(2) + ',' + y1.toFixed(2) + 'L' + x2.toFixed(2) + ',' + y2.toFixed(2);
110+
}
111+
return path;
112+
}
113+
114+
if (weather.type === 'cloudy') {
115+
var cy = r * 0.2;
116+
return 'M' + (-r*0.6) + ',' + cy +
117+
'A' + (r*0.35) + ',' + (r*0.35) + ' 0 1,1 ' + (-r*0.1) + ',' + (-cy) +
118+
'A' + (r*0.4) + ',' + (r*0.4) + ' 0 1,1 ' + (r*0.5) + ',' + (-cy*0.5) +
119+
'A' + (r*0.3) + ',' + (r*0.3) + ' 0 1,1 ' + (r*0.7) + ',' + cy +
120+
'L' + (-r*0.6) + ',' + cy + 'Z';
121+
}
122+
123+
// Default: circle
124+
return 'M' + r + ',0A' + r + ',' + r + ' 0 1,1 0,-' + r + 'A' + r + ',' + r + ' 0 0,1 ' + r + ',0Z';
87125
}
88126

89127
Plotly.newPlot('myDiv', [{
90128
type: 'scatter',
91-
x: [1, 2, 3, 4],
92-
y: [1, 2, 3, 4],
129+
x: [-122.4, -118.2, -87.6],
130+
y: [37.8, 34.1, 41.9],
131+
customdata: [
132+
{ type: 'sunny' },
133+
{ type: 'cloudy' },
134+
{ type: 'sunny' }
135+
],
93136
mode: 'markers',
94137
marker: {
95-
symbol: ['circle', customDiamond, 'square', customDiamond],
96-
size: 15
138+
symbol: weatherMarker,
139+
size: 30,
140+
color: ['#FFD700', '#708090', '#FFD700']
97141
}
98142
}]);
99143
```
100144

101-
## Function Signature
102-
103-
Your custom marker function should have the following signature:
104-
105-
```javascript
106-
function customMarker(r, angle, standoff) {
107-
// r: radius/size of the marker
108-
// angle: rotation angle in degrees (for directional markers)
109-
// standoff: standoff distance from the point (for advanced use)
110-
111-
// Return an SVG path string
112-
return 'M...Z';
113-
}
114-
```
115-
116-
### Parameters
117-
118-
- **r** (number): The radius/size of the marker. Your path should scale proportionally with this value.
119-
- **angle** (number, optional): The rotation angle in degrees. Most simple markers can ignore this.
120-
- **standoff** (number, optional): The standoff distance. Most markers can ignore this.
121-
122-
### Return Value
123-
124-
The function must return a valid SVG path string. The path should:
125-
- Be centered at (0, 0)
126-
- Scale proportionally with the radius `r`
127-
- Use standard SVG path commands (M, L, C, Q, A, Z, etc.)
128-
129145
## SVG Path Commands
130146

131-
Here are the common SVG path commands you can use:
147+
Common SVG path commands:
132148

133-
- `M x,y`: Move to absolute position (x, y)
134-
- `m dx,dy`: Move to relative position (dx, dy)
135-
- `L x,y`: Line to absolute position
136-
- `l dx,dy`: Line to relative position
149+
- `M x,y`: Move to (x, y)
150+
- `L x,y`: Line to (x, y)
137151
- `H x`: Horizontal line to x
138-
- `h dx`: Horizontal line by dx
139152
- `V y`: Vertical line to y
140-
- `v dy`: Vertical line by dy
141153
- `C x1,y1 x2,y2 x,y`: Cubic Bézier curve
142154
- `Q x1,y1 x,y`: Quadratic Bézier curve
143155
- `A rx,ry rotation large-arc sweep x,y`: Elliptical arc
144156
- `Z`: Close path
145157

146-
## Examples
147-
148-
### Simple Triangle
149-
150-
```javascript
151-
function triangleMarker(r) {
152-
var h = r * 1.5;
153-
return 'M0,-' + h + 'L' + r + ',' + (h/2) + 'L-' + r + ',' + (h/2) + 'Z';
154-
}
155-
```
156-
157-
### Pentagon
158-
159-
```javascript
160-
function pentagonMarker(r) {
161-
var points = 5;
162-
var path = 'M';
163-
for (var i = 0; i < points; i++) {
164-
var angle = (i * 2 * Math.PI / points) - Math.PI / 2;
165-
var x = r * Math.cos(angle);
166-
var y = r * Math.sin(angle);
167-
path += (i === 0 ? '' : 'L') + x.toFixed(2) + ',' + y.toFixed(2);
168-
}
169-
return path + 'Z';
170-
}
171-
```
172-
173-
### Arrow
174-
175-
```javascript
176-
function arrowMarker(r) {
177-
var headWidth = r;
178-
var headLength = r * 1.5;
179-
return 'M0,-' + headLength +
180-
'L-' + headWidth + ',0' +
181-
'L' + headWidth + ',0Z';
182-
}
183-
```
184-
185158
## Notes
186159

187160
- Custom marker functions work with all marker styling options (color, size, line, etc.)
188161
- The function is called for each point that uses it
189-
- Functions are passed through as-is and not stored in any registry
190-
- This approach is simpler than the registration-based API
191-
- For best performance, define your functions once outside the plot call
192-
193-
## Browser Compatibility
194-
195-
Custom marker functions work in all browsers that support plotly.js and SVG path rendering.
162+
- Rotation is handled via `marker.angle` - your function returns an unrotated path
163+
- For best performance, define functions once outside the plot call
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Custom Markers - With and Without Customdata</title>
6+
<script src="../../dist/plotly.js"></script>
7+
<style>
8+
body { font-family: Arial, sans-serif; margin: 20px; }
9+
h2 { color: #333; }
10+
.info { background: #e7f3ff; padding: 10px; border-left: 4px solid #3b82f6; margin: 10px 0; }
11+
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; }
12+
</style>
13+
</head>
14+
<body>
15+
<h2>Custom Markers - With and Without Customdata</h2>
16+
<div class="info">
17+
<strong>Simple markers</strong> use <code>function(r)</code><br>
18+
<strong>Data-aware markers</strong> use <code>function(r, customdata)</code>
19+
</div>
20+
<div id="plot" style="width: 700px; height: 400px;"></div>
21+
22+
<script>
23+
// SIMPLE: just uses radius - diamond shape
24+
function diamondMarker(r) {
25+
return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z';
26+
}
27+
28+
// SIMPLE: star shape
29+
function starMarker(r) {
30+
var points = 5, path = 'M';
31+
for (var i = 0; i < points * 2; i++) {
32+
var radius = i % 2 === 0 ? r : r * 0.4;
33+
var ang = (i * Math.PI) / points - Math.PI / 2;
34+
path += (i === 0 ? '' : 'L') +
35+
(radius * Math.cos(ang)).toFixed(2) + ',' +
36+
(radius * Math.sin(ang)).toFixed(2);
37+
}
38+
return path + 'Z';
39+
}
40+
41+
// DATA-AWARE: shape depends on customdata[i]
42+
function dataAwareMarker(r, customdata) {
43+
var type = customdata;
44+
if (type === 'big') {
45+
// Larger diamond
46+
var r2 = r * 1.4;
47+
return 'M' + r2 + ',0L0,' + r2 + 'L-' + r2 + ',0L0,-' + r2 + 'Z';
48+
}
49+
if (type === 'star') {
50+
// Star shape
51+
var points = 5, path = 'M';
52+
for (var i = 0; i < points * 2; i++) {
53+
var radius = i % 2 === 0 ? r : r * 0.4;
54+
var ang = (i * Math.PI) / points - Math.PI / 2;
55+
path += (i === 0 ? '' : 'L') +
56+
(radius * Math.cos(ang)).toFixed(2) + ',' +
57+
(radius * Math.sin(ang)).toFixed(2);
58+
}
59+
return path + 'Z';
60+
}
61+
// Default: circle
62+
return 'M' + r + ',0A' + r + ',' + r + ' 0 1,1 0,-' + r +
63+
'A' + r + ',' + r + ' 0 0,1 ' + r + ',0Z';
64+
}
65+
66+
Plotly.newPlot('plot', [{
67+
name: 'Simple: diamond(r)',
68+
type: 'scatter',
69+
x: [1, 2, 3, 4],
70+
y: [1, 1, 1, 1],
71+
mode: 'markers',
72+
marker: { symbol: diamondMarker, size: 25, color: '#3b82f6' }
73+
}, {
74+
name: 'Simple: star(r)',
75+
type: 'scatter',
76+
x: [1, 2, 3, 4],
77+
y: [2, 2, 2, 2],
78+
mode: 'markers',
79+
marker: { symbol: starMarker, size: 25, color: '#f59e0b' }
80+
}, {
81+
name: 'Data-aware: (r, customdata)',
82+
type: 'scatter',
83+
x: [1, 2, 3, 4],
84+
y: [3, 3, 3, 3],
85+
customdata: ['circle', 'big', 'star', 'circle'],
86+
mode: 'markers',
87+
marker: { symbol: dataAwareMarker, size: 25, color: '#10b981' }
88+
}], {
89+
title: 'Custom Marker Functions',
90+
xaxis: { range: [0, 5] },
91+
yaxis: { range: [0, 4], tickvals: [1, 2, 3], ticktext: ['Diamond', 'Star', 'Data-driven'] },
92+
showlegend: true
93+
});
94+
</script>
95+
</body>
96+
</html>

0 commit comments

Comments
 (0)