Skip to content

Commit 8310635

Browse files
Copilotgatopeich
andcommitted
Add addCustomMarker API for registering custom SVG markers
Co-authored-by: gatopeich <7722268+gatopeich@users.noreply.github.com>
1 parent 2d44177 commit 8310635

4 files changed

Lines changed: 190 additions & 2 deletions

File tree

src/components/drawing/index.js

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,10 +366,72 @@ Object.keys(SYMBOLDEFS).forEach(function (k) {
366366
}
367367
});
368368

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

372+
/**
373+
* Add a custom marker symbol
374+
*
375+
* @param {string} name: the name of the new marker symbol
376+
* @param {function} drawFunc: a function(r, angle, standoff) that returns an SVG path string
377+
* @param {object} opts: optional configuration object
378+
* - backoff {number}: backoff distance for this symbol (default: 0)
379+
* - needLine {boolean}: whether this symbol needs a line (default: false)
380+
* - noDot {boolean}: whether to skip creating -dot variants (default: false)
381+
* - noFill {boolean}: whether this symbol should not be filled (default: false)
382+
*
383+
* @return {number}: the symbol number assigned to the new marker, or existing number if already registered
384+
*/
385+
drawing.addCustomMarker = function(name, drawFunc, opts) {
386+
opts = opts || {};
387+
388+
// Check if marker already exists
389+
var existingIndex = drawing.symbolNames.indexOf(name);
390+
if(existingIndex >= 0) {
391+
return existingIndex;
392+
}
393+
394+
// Get the next available symbol number
395+
var n = drawing.symbolNames.length;
396+
397+
// Add to symbolList (base and -open variants)
398+
drawing.symbolList.push(
399+
n,
400+
String(n),
401+
name,
402+
n + 100,
403+
String(n + 100),
404+
name + '-open'
405+
);
406+
407+
// Register the symbol
408+
drawing.symbolNames[n] = name;
409+
drawing.symbolFuncs[n] = drawFunc;
410+
drawing.symbolBackOffs[n] = opts.backoff || 0;
411+
412+
if(opts.needLine) {
413+
drawing.symbolNeedLines[n] = true;
414+
}
415+
if(opts.noDot) {
416+
drawing.symbolNoDot[n] = true;
417+
} else {
418+
// Add -dot and -open-dot variants
419+
drawing.symbolList.push(
420+
n + 200,
421+
String(n + 200),
422+
name + '-dot',
423+
n + 300,
424+
String(n + 300),
425+
name + '-open-dot'
426+
);
427+
}
428+
if(opts.noFill) {
429+
drawing.symbolNoFill[n] = true;
430+
}
431+
432+
return n;
433+
};
434+
373435
drawing.symbolNumber = function (v) {
374436
if (isNumeric(v)) {
375437
v = +v;
@@ -389,7 +451,9 @@ drawing.symbolNumber = function (v) {
389451
}
390452
}
391453

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

395459
function makePointPath(symbolNumber, r, t, s) {

src/core.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,9 @@ exports.Fx = {
8181
};
8282
exports.Snapshot = require('./snapshot');
8383
exports.PlotSchema = require('./plot_api/plot_schema');
84+
85+
// expose Drawing methods for custom marker registration
86+
var Drawing = require('./components/drawing');
87+
exports.Drawing = {
88+
addCustomMarker: Drawing.addCustomMarker
89+
};

test/jasmine/tests/drawing_test.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,4 +573,115 @@ describe('gradients', function() {
573573
done();
574574
}, done.fail);
575575
});
576+
577+
describe('addCustomMarker', function() {
578+
it('should register a new custom marker symbol', function() {
579+
var initialLength = Drawing.symbolNames.length;
580+
581+
var customFunc = function(r) {
582+
return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z';
583+
};
584+
585+
var symbolNumber = Drawing.addCustomMarker('my-custom-marker', customFunc);
586+
587+
expect(symbolNumber).toBe(initialLength);
588+
expect(Drawing.symbolNames[symbolNumber]).toBe('my-custom-marker');
589+
expect(Drawing.symbolFuncs[symbolNumber]).toBe(customFunc);
590+
expect(Drawing.symbolNames.length).toBe(initialLength + 1);
591+
});
592+
593+
it('should return existing symbol number if marker already registered', function() {
594+
var customFunc = function(r) {
595+
return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z';
596+
};
597+
598+
var firstAdd = Drawing.addCustomMarker('my-marker-2', customFunc);
599+
var secondAdd = Drawing.addCustomMarker('my-marker-2', customFunc);
600+
601+
expect(firstAdd).toBe(secondAdd);
602+
});
603+
604+
it('should add marker to symbolList with variants', function() {
605+
var initialListLength = Drawing.symbolList.length;
606+
var customFunc = function(r) {
607+
return 'M0,0L' + r + ',0';
608+
};
609+
610+
var symbolNumber = Drawing.addCustomMarker('my-marker-3', customFunc);
611+
612+
// Should add 6 entries: n, String(n), name, n+100, String(n+100), name-open
613+
// Plus 6 more for dot variants if noDot is not set
614+
expect(Drawing.symbolList.length).toBeGreaterThan(initialListLength);
615+
expect(Drawing.symbolList).toContain('my-marker-3');
616+
expect(Drawing.symbolList).toContain('my-marker-3-open');
617+
expect(Drawing.symbolList).toContain('my-marker-3-dot');
618+
expect(Drawing.symbolList).toContain('my-marker-3-open-dot');
619+
});
620+
621+
it('should respect noDot option', function() {
622+
var customFunc = function(r) {
623+
return 'M0,0L' + r + ',0';
624+
};
625+
626+
Drawing.addCustomMarker('my-marker-4', customFunc, {noDot: true});
627+
628+
expect(Drawing.symbolList).toContain('my-marker-4');
629+
expect(Drawing.symbolList).toContain('my-marker-4-open');
630+
expect(Drawing.symbolList).not.toContain('my-marker-4-dot');
631+
expect(Drawing.symbolList).not.toContain('my-marker-4-open-dot');
632+
});
633+
634+
it('should allow using custom marker in scatter plot', function(done) {
635+
var customFunc = function(r) {
636+
return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z';
637+
};
638+
639+
Drawing.addCustomMarker('my-scatter-marker', customFunc);
640+
641+
Plotly.newPlot(gd, [{
642+
type: 'scatter',
643+
x: [1, 2, 3],
644+
y: [2, 3, 4],
645+
mode: 'markers',
646+
marker: {
647+
symbol: 'my-scatter-marker',
648+
size: 12
649+
}
650+
}])
651+
.then(function() {
652+
var points = d3Select(gd).selectAll('.point');
653+
expect(points.size()).toBe(3);
654+
655+
var firstPoint = points.node();
656+
var path = firstPoint.getAttribute('d');
657+
expect(path).toContain('M');
658+
expect(path).toContain('L');
659+
})
660+
.then(done, done.fail);
661+
});
662+
663+
it('should work with marker symbol variants', function(done) {
664+
var customFunc = function(r) {
665+
return 'M' + r + ',0L0,' + r + 'L-' + r + ',0L0,-' + r + 'Z';
666+
};
667+
668+
Drawing.addCustomMarker('my-variant-marker', customFunc);
669+
670+
Plotly.newPlot(gd, [{
671+
type: 'scatter',
672+
x: [1, 2, 3],
673+
y: [2, 3, 4],
674+
mode: 'markers',
675+
marker: {
676+
symbol: ['my-variant-marker', 'my-variant-marker-open', 'my-variant-marker-dot'],
677+
size: 12
678+
}
679+
}])
680+
.then(function() {
681+
var points = d3Select(gd).selectAll('.point');
682+
expect(points.size()).toBe(3);
683+
})
684+
.then(done, done.fail);
685+
});
686+
});
576687
});

test/jasmine/tests/plot_api_test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ describe('Test plot api', function () {
3131
});
3232
});
3333

34+
describe('Plotly.Drawing', function () {
35+
it('should expose addCustomMarker method', function () {
36+
expect(typeof Plotly.Drawing).toBe('object');
37+
expect(typeof Plotly.Drawing.addCustomMarker).toBe('function');
38+
});
39+
});
40+
3441
describe('Plotly.newPlot', function () {
3542
var gd;
3643

0 commit comments

Comments
 (0)