Skip to content
Draft
Prev Previous commit
Next Next commit
Add addCustomMarker API for registering custom SVG markers
Co-authored-by: gatopeich <[email protected]>
  • 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