Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2d44177
Initial plan
Copilot Nov 19, 2025
8310635
Add addCustomMarker API for registering custom SVG markers
Copilot Nov 19, 2025
343dd6c
Add demo file for custom marker feature
Copilot Nov 19, 2025
90c697a
Add comprehensive documentation for custom marker API
Copilot Nov 19, 2025
547b0b7
Add implementation summary document
Copilot Nov 19, 2025
86086e0
Simplify API: allow passing custom functions directly as marker.symbol
Copilot Nov 21, 2025
099b0d6
Check if symbol is function before calling symbolNumber() in backoff …
Copilot Nov 22, 2025
42ab66d
Simplify function check in backoff calculation
Copilot Nov 22, 2025
d8739bd
Merge pull request #1 from gatopeich/copilot/add-custom-svg-markers
gatopeich Nov 22, 2025
398d1d4
Merge branch 'plotly:master' into master
gatopeich Nov 22, 2025
db38309
Add change draft log entry
gatopeich Nov 22, 2025
0cc49cc
Pass customdata to custom marker functions (backward compatible) (#2)
Copilot Jan 18, 2026
c055d26
Fix test assertions for custom marker function call counts
gatopeich Jan 19, 2026
ce14c7e
Tighten test assertions: expect exactly 3 or 6 marker function calls
ap-viavi Jan 20, 2026
fd70889
Add consolidated demo: all_demos.html
ap-viavi Jan 20, 2026
2c1110b
Fix CI: rename docs to lowercase, skip non-symbol exports in schema
gatopeich Jan 23, 2026
71ca7a9
Merge branch 'plotly:master' into master
gatopeich Feb 12, 2026
e073716
Fix brittle test assertions in custom marker function tests (#6)
Claude Feb 13, 2026
cef92be
Merge branch 'plotly:master' into master
gatopeich Feb 24, 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
163 changes: 163 additions & 0 deletions custom_marker_functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Custom Marker Functions

This document describes how to use custom SVG marker functions in plotly.js scatter plots.

## Overview

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.

## Function Signature

Custom marker functions receive:

```javascript
function customMarker(r, customdata) {
// r: radius/size of the marker (half of marker.size)
// customdata: the value from trace.customdata[i] for this point (optional)

// Return an SVG path string centered at (0,0)
return 'M...Z';
}
```

**Simple markers** can use just `(r)`:
```javascript
function diamond(r) {
return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z';
}
```

**Data-aware markers** use `(r, customdata)`:
```javascript
function categoryMarker(r, customdata) {
if (customdata === 'high') {
return 'M0,-' + r + 'L' + r + ',' + r + 'L-' + r + ',' + r + 'Z'; // up triangle
}
return 'M0,' + r + 'L' + r + ',-' + r + 'L-' + r + ',-' + r + 'Z'; // down triangle
}
```

Note: Rotation is handled automatically via `marker.angle` - your function just returns an unrotated path.

## Usage Examples

### Basic Example

```javascript
function heartMarker(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';
}

Plotly.newPlot('myDiv', [{
type: 'scatter',
x: [1, 2, 3, 4, 5],
y: [2, 3, 4, 3, 2],
mode: 'markers',
marker: {
symbol: heartMarker,
size: 15,
color: 'red'
}
}]);
```

### Multiple Custom Markers

```javascript
function star(r) {
var path = 'M';
for (var i = 0; i < 10; i++) {
var radius = i % 2 === 0 ? r : r * 0.4;
var ang = (i * Math.PI) / 5 - Math.PI / 2;
path += (i === 0 ? '' : 'L') + (radius * Math.cos(ang)).toFixed(2) + ',' + (radius * Math.sin(ang)).toFixed(2);
}
return path + 'Z';
}

Plotly.newPlot('myDiv', [{
x: [1, 2, 3, 4, 5],
y: [2, 3, 4, 3, 2],
mode: 'markers',
marker: {
symbol: [heartMarker, star, 'circle', star, heartMarker],
size: 18,
color: ['red', 'gold', 'blue', 'orange', 'crimson']
}
}]);
```

### Data-Driven Markers with customdata

```javascript
function weatherMarker(r, customdata) {
var weather = customdata;

if (weather.type === 'sunny') {
// Sun: circle with rays
var cr = r * 0.5;
var path = 'M' + cr + ',0A' + cr + ',' + cr + ' 0 1,1 0,-' + cr +
'A' + cr + ',' + cr + ' 0 0,1 ' + cr + ',0Z';
for (var i = 0; i < 8; i++) {
var ang = i * Math.PI / 4;
var x1 = (cr + 2) * Math.cos(ang), y1 = (cr + 2) * Math.sin(ang);
var x2 = (cr + r*0.4) * Math.cos(ang), y2 = (cr + r*0.4) * Math.sin(ang);
path += 'M' + x1.toFixed(2) + ',' + y1.toFixed(2) + 'L' + x2.toFixed(2) + ',' + y2.toFixed(2);
}
return path;
}

if (weather.type === 'cloudy') {
var cy = r * 0.2;
return 'M' + (-r*0.6) + ',' + cy +
'A' + (r*0.35) + ',' + (r*0.35) + ' 0 1,1 ' + (-r*0.1) + ',' + (-cy) +
'A' + (r*0.4) + ',' + (r*0.4) + ' 0 1,1 ' + (r*0.5) + ',' + (-cy*0.5) +
'A' + (r*0.3) + ',' + (r*0.3) + ' 0 1,1 ' + (r*0.7) + ',' + cy +
'L' + (-r*0.6) + ',' + cy + 'Z';
}

// Default: circle
return 'M' + r + ',0A' + r + ',' + r + ' 0 1,1 0,-' + r + 'A' + r + ',' + r + ' 0 0,1 ' + r + ',0Z';
}

Plotly.newPlot('myDiv', [{
type: 'scatter',
x: [-122.4, -118.2, -87.6],
y: [37.8, 34.1, 41.9],
customdata: [
{ type: 'sunny' },
{ type: 'cloudy' },
{ type: 'sunny' }
],
mode: 'markers',
marker: {
symbol: weatherMarker,
size: 30,
color: ['#FFD700', '#708090', '#FFD700']
}
}]);
```

## SVG Path Commands

Common SVG path commands:

- `M x,y`: Move to (x, y)
- `L x,y`: Line to (x, y)
- `H x`: Horizontal line to x
- `V y`: Vertical line to y
- `C x1,y1 x2,y2 x,y`: Cubic Bézier curve
- `Q x1,y1 x,y`: Quadratic Bézier curve
- `A rx,ry rotation large-arc sweep x,y`: Elliptical arc
- `Z`: Close path

## Notes

- Custom marker functions work with all marker styling options (color, size, line, etc.)
- The function is called for each point that uses it
- Rotation is handled via `marker.angle` - your function returns an unrotated path
- For best performance, define functions once outside the plot call
172 changes: 172 additions & 0 deletions devtools/custom_marker_demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Custom Marker Functions Demo</title>
<script src="dist/plotly.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #333;
border-bottom: 2px solid #3b82f6;
padding-bottom: 10px;
}
.plot {
width: 100%;
height: 500px;
margin: 20px 0;
}
.code-block {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 15px;
margin: 15px 0;
overflow-x: auto;
}
pre {
margin: 0;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.info {
background-color: #e7f3ff;
border-left: 4px solid #3b82f6;
padding: 15px;
margin: 15px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>Custom Marker Functions Demo</h1>

<div class="info">
<strong>New Feature:</strong> You can now pass custom functions directly as
<code>marker.symbol</code> values to create custom marker shapes!
</div>

<h2>Example: Custom Marker Functions</h2>
<div id="plot1" class="plot"></div>

<h3>Code:</h3>
<div class="code-block">
<pre>// Define custom marker functions
function heartMarker(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';
}

function star5Marker(r) {
var points = 5, path = 'M';
for (var i = 0; i < points * 2; i++) {
var radius = i % 2 === 0 ? r : r * 0.4;
var ang = (i * Math.PI) / points - Math.PI / 2;
path += (i === 0 ? '' : 'L') +
(radius * Math.cos(ang)).toFixed(2) + ',' +
(radius * Math.sin(ang)).toFixed(2);
}
return path + 'Z';
}

// Use them directly in a plot
Plotly.newPlot('plot1', [{
x: [1, 2, 3, 4, 5],
y: [2, 3, 4, 3, 2],
mode: 'markers+lines',
marker: {
symbol: [heartMarker, star5Marker, 'circle', star5Marker, heartMarker],
size: 20,
color: ['red', 'gold', 'blue', 'orange', 'crimson']
}
}]);</pre>
</div>
</div>

<script>
// Define custom heart marker
function heartMarker(r) {
var x = r * 0.6;
var 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';
}

// Define custom 5-point star marker
function star5Marker(r) {
var points = 5;
var outerRadius = r;
var innerRadius = r * 0.4;
var path = 'M';

for (var i = 0; i < points * 2; i++) {
var radius = i % 2 === 0 ? outerRadius : innerRadius;
var ang = (i * Math.PI) / points - Math.PI / 2;
var x = radius * Math.cos(ang);
var y = radius * Math.sin(ang);
path += (i === 0 ? '' : 'L') + x.toFixed(2) + ',' + y.toFixed(2);
}
path += 'Z';
return path;
}

// Create the plot
var trace = {
x: [1, 2, 3, 4, 5],
y: [2, 3, 4, 3, 2],
mode: 'markers+lines',
name: 'Custom Markers',
marker: {
symbol: [heartMarker, star5Marker, 'circle', star5Marker, heartMarker],
size: 20,
color: ['#e74c3c', '#f39c12', '#3498db', '#ff9800', '#c0392b'],
line: {
color: '#34495e',
width: 2
}
},
line: {
color: '#95a5a6',
width: 2,
dash: 'dot'
}
};

var layout = {
title: 'Custom Marker Functions Demo',
xaxis: {
title: 'X Axis',
gridcolor: '#ecf0f1'
},
yaxis: {
title: 'Y Axis',
gridcolor: '#ecf0f1'
},
plot_bgcolor: '#fafafa',
showlegend: true
};

Plotly.newPlot('plot1', [trace], layout);
console.log('Plot created successfully!');
</script>
</body>
</html>
Loading