Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 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
3026a1e
Optimize marker render pipeline via SVG `<symbol>`/`<use>` (#7)
Copilot Mar 22, 2026
6bbdb63
Restore dist files deleted by PR #7 (#9)
Copilot Mar 22, 2026
c74c81d
Remove coerce.js function pass-through hack; fatal errors on unknown …
Copilot Mar 22, 2026
5d2add8
Address PR #7653 review comments: restore symDef.n, rename/clean demo…
Copilot Apr 2, 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
Prev Previous commit
Next Next commit
Add addCustomMarker API for registering custom SVG markers
Co-authored-by: gatopeich <7722268+gatopeich@users.noreply.github.com>
  • Loading branch information
Copilot and gatopeich committed Nov 19, 2025
commit 831063543b2a2c155b6f52af514860c11f2e2200
68 changes: 66 additions & 2 deletions src/components/drawing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,10 +366,72 @@ Object.keys(SYMBOLDEFS).forEach(function (k) {
}
});

var MAXSYMBOL = drawing.symbolNames.length;
// add a dot in the middle of the symbol
var DOTPATH = 'M0,0.5L0.5,0L0,-0.5L-0.5,0Z';

/**
* Add a custom marker symbol
*
* @param {string} name: the name of the new marker symbol
* @param {function} drawFunc: a function(r, angle, standoff) that returns an SVG path string
* @param {object} opts: optional configuration object
* - backoff {number}: backoff distance for this symbol (default: 0)
* - needLine {boolean}: whether this symbol needs a line (default: false)
* - noDot {boolean}: whether to skip creating -dot variants (default: false)
* - noFill {boolean}: whether this symbol should not be filled (default: false)
*
* @return {number}: the symbol number assigned to the new marker, or existing number if already registered
*/
drawing.addCustomMarker = function(name, drawFunc, opts) {
opts = opts || {};

// Check if marker already exists
var existingIndex = drawing.symbolNames.indexOf(name);
if(existingIndex >= 0) {
return existingIndex;
}

// Get the next available symbol number
var n = drawing.symbolNames.length;

// Add to symbolList (base and -open variants)
drawing.symbolList.push(
n,
String(n),
name,
n + 100,
String(n + 100),
name + '-open'
);

// Register the symbol
drawing.symbolNames[n] = name;
drawing.symbolFuncs[n] = drawFunc;
drawing.symbolBackOffs[n] = opts.backoff || 0;

if(opts.needLine) {
drawing.symbolNeedLines[n] = true;
}
if(opts.noDot) {
drawing.symbolNoDot[n] = true;
} else {
// Add -dot and -open-dot variants
drawing.symbolList.push(
n + 200,
String(n + 200),
name + '-dot',
n + 300,
String(n + 300),
name + '-open-dot'
);
}
if(opts.noFill) {
drawing.symbolNoFill[n] = true;
}

return n;
};

drawing.symbolNumber = function (v) {
if (isNumeric(v)) {
v = +v;
Expand All @@ -389,7 +451,9 @@ drawing.symbolNumber = function (v) {
}
}

return v % 100 >= MAXSYMBOL || v >= 400 ? 0 : Math.floor(Math.max(v, 0));
// Use dynamic length instead of MAXSYMBOL constant
var maxSymbol = drawing.symbolNames.length;
return v % 100 >= maxSymbol || v >= 400 ? 0 : Math.floor(Math.max(v, 0));
};

function makePointPath(symbolNumber, r, t, s) {
Expand Down
6 changes: 6 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,9 @@ exports.Fx = {
};
exports.Snapshot = require('./snapshot');
exports.PlotSchema = require('./plot_api/plot_schema');

// expose Drawing methods for custom marker registration
var Drawing = require('./components/drawing');
exports.Drawing = {
addCustomMarker: Drawing.addCustomMarker
};
111 changes: 111 additions & 0 deletions test/jasmine/tests/drawing_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -573,4 +573,115 @@ describe('gradients', function() {
done();
}, done.fail);
});

describe('addCustomMarker', function() {
it('should register a new custom marker symbol', function() {
var initialLength = Drawing.symbolNames.length;

var customFunc = function(r) {
return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z';
};

var symbolNumber = Drawing.addCustomMarker('my-custom-marker', customFunc);

expect(symbolNumber).toBe(initialLength);
expect(Drawing.symbolNames[symbolNumber]).toBe('my-custom-marker');
expect(Drawing.symbolFuncs[symbolNumber]).toBe(customFunc);
expect(Drawing.symbolNames.length).toBe(initialLength + 1);
});

it('should return existing symbol number if marker already registered', function() {
var customFunc = function(r) {
return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z';
};

var firstAdd = Drawing.addCustomMarker('my-marker-2', customFunc);
var secondAdd = Drawing.addCustomMarker('my-marker-2', customFunc);

expect(firstAdd).toBe(secondAdd);
});

it('should add marker to symbolList with variants', function() {
var initialListLength = Drawing.symbolList.length;
var customFunc = function(r) {
return 'M0,0L' + r + ',0';
};

var symbolNumber = Drawing.addCustomMarker('my-marker-3', customFunc);

// Should add 6 entries: n, String(n), name, n+100, String(n+100), name-open
// Plus 6 more for dot variants if noDot is not set
expect(Drawing.symbolList.length).toBeGreaterThan(initialListLength);
expect(Drawing.symbolList).toContain('my-marker-3');
expect(Drawing.symbolList).toContain('my-marker-3-open');
expect(Drawing.symbolList).toContain('my-marker-3-dot');
expect(Drawing.symbolList).toContain('my-marker-3-open-dot');
});

it('should respect noDot option', function() {
var customFunc = function(r) {
return 'M0,0L' + r + ',0';
};

Drawing.addCustomMarker('my-marker-4', customFunc, {noDot: true});

expect(Drawing.symbolList).toContain('my-marker-4');
expect(Drawing.symbolList).toContain('my-marker-4-open');
expect(Drawing.symbolList).not.toContain('my-marker-4-dot');
expect(Drawing.symbolList).not.toContain('my-marker-4-open-dot');
});

it('should allow using custom marker in scatter plot', function(done) {
var customFunc = function(r) {
return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z';
};

Drawing.addCustomMarker('my-scatter-marker', customFunc);

Plotly.newPlot(gd, [{
type: 'scatter',
x: [1, 2, 3],
y: [2, 3, 4],
mode: 'markers',
marker: {
symbol: 'my-scatter-marker',
size: 12
}
}])
.then(function() {
var points = d3Select(gd).selectAll('.point');
expect(points.size()).toBe(3);

var firstPoint = points.node();
var path = firstPoint.getAttribute('d');
expect(path).toContain('M');
expect(path).toContain('L');
})
.then(done, done.fail);
});

it('should work with marker symbol variants', function(done) {
var customFunc = function(r) {
return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z';
};

Drawing.addCustomMarker('my-variant-marker', customFunc);

Plotly.newPlot(gd, [{
type: 'scatter',
x: [1, 2, 3],
y: [2, 3, 4],
mode: 'markers',
marker: {
symbol: ['my-variant-marker', 'my-variant-marker-open', 'my-variant-marker-dot'],
size: 12
}
}])
.then(function() {
var points = d3Select(gd).selectAll('.point');
expect(points.size()).toBe(3);
})
.then(done, done.fail);
});
});
});
7 changes: 7 additions & 0 deletions test/jasmine/tests/plot_api_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ describe('Test plot api', function () {
});
});

describe('Plotly.Drawing', function () {
it('should expose addCustomMarker method', function () {
expect(typeof Plotly.Drawing).toBe('object');
expect(typeof Plotly.Drawing.addCustomMarker).toBe('function');
});
});

describe('Plotly.newPlot', function () {
var gd;

Expand Down