Skip to content

[WIP] Refactor Plotly.js marker symbols to use constant SVG paths#8

Closed
Copilot wants to merge 1 commit intomasterfrom
copilot/refactor-plotly-js-marker-symbols
Closed

[WIP] Refactor Plotly.js marker symbols to use constant SVG paths#8
Copilot wants to merge 1 commit intomasterfrom
copilot/refactor-plotly-js-marker-symbols

Conversation

Copy link
Copy Markdown

Copilot AI commented Mar 21, 2026

Thanks for asking me to work on this. I will get started on it and keep this PR's description up to date as I form a plan and make progress.

Original prompt

Summary

Refactor Plotly.js marker symbols from functions that compute SVG paths per-point to constant SVG path strings rendered via <symbol>/<use> reuse. This eliminates per-point path computation, produces smaller SVG output, and creates a unified API for default and custom markers.

Code Guidelines

  • Reduce code as much as possible without obscuring.

Current Architecture (to be replaced)

src/components/drawing/symbol_defs.js

  • 55 marker symbols, each defined as { n: <number>, f: function(r, angle, standoff) { ... return SVG path string }, needLine, noDot, noFill, backoff }.
  • Each function dynamically computes an SVG path string for arbitrary r, angle, and standoff. It calls round() extensively and uses a parseSvgPath + align() function for rotation/standoff transforms.
  • The align() function (lines 721–813) parses the SVG path string, applies rotation and standoff transforms by manipulating path coordinates, and returns a new path string. It has a single-entry cache.
  • Imports parse-svg-path npm package just for align().

src/components/drawing/index.js

  • Builds drawing.symbolFuncs[], drawing.symbolNames[], drawing.symbolBackOffs[], drawing.symbolNeedLines{}, drawing.symbolNoDot{}, drawing.symbolNoFill{}, drawing.symbolList[] from symbol_defs.js (lines 322–367).
  • makePointPath(symbolNumber, r, t, s) (line 395): calls drawing.symbolFuncs[base](r, t, s) to get a path string, appends DOTPATH if needed. Called for every single marker point in the SVG rendering pipeline.
  • drawing.singlePointStyle() (line 892): for each point, computes angle/standoff, calls makePointPath(), sets sel.attr('d', path) — meaning every point gets a unique <path d="..."> with a fully computed path string.
  • drawing.selectedPointStyle() (line 1192): same pattern — recomputes path via makePointPath() on selection size change.

src/traces/scattergl/convert.js

  • getSymbolSdf() (line 462): uses Drawing.symbolFuncs[symbolNumber % 100] to generate SVG path strings for SDF texture generation (WebGL). Calls the function with SYMBOL_SIZE (=20) and the angle.
  • SYMBOL_SVG_CIRCLE = Drawing.symbolFuncs[0](SYMBOL_SIZE * 0.05) — used for dot overlay.
  • The SDF generation already takes a viewBox parameter, so it can work with any reference size.

Current SVG output for 1000 markers of the same symbol

<path class="point" d="M10,0A10,10 0 1,1 0,-10A10,10 0 0,1 10,0Z" transform="translate(...)"/>
<path class="point" d="M10,0A10,10 0 1,1 0,-10A10,10 0 0,1 10,0Z" transform="translate(...)"/>
... (1000 identical d="..." strings)

Target Architecture

1. src/components/drawing/symbol_defs.js — Convert to constant path strings

Replace each symbol's f: function(r, angle, standoff) {...} with p: "SVG path string" — a constant string pre-computed for r=10 on a viewport from -10,-10 to +10,+10 centered on 0,0.

  • Pre-compute each path by evaluating the existing function with r=10, angle=0, standoff=0 (the default/common case).
  • Delete the align() function entirely (107 lines).
  • Delete the skipAngle() function.
  • Remove the parse-svg-path dependency (it was only used by align()).
  • Remove the round() helper function.
  • Keep metadata: n, needLine, noDot, noFill, backoff.
  • The empty path 'M0,0Z' remains for the angle === null case, handled in the rendering pipeline.

Example transformation:

// BEFORE
circle: {
    n: 0,
    f: function(r, angle, standoff) {
        if(skipAngle(angle)) return emptyPath;
        var rs = round(r, 2);
        var circle = 'M' + rs + ',0A' + rs + ',' + rs + ' 0 1,1 0,-' + rs + 'A' + rs + ',' + rs + ' 0 0,1 ' + rs + ',0Z';
        return standoff ? align(angle, standoff, circle) : circle;
    }
}

// AFTER
circle: {
    n: 0,
    p: 'M10,0A10,10 0 1,1 0,-10A10,10 0 0,1 10,0Z'
}

Do this for ALL 55 symbols. Compute each path string by mentally/manually evaluating the function at r=10. Here are some to get started (compute all of them):

  • circle (n:0): 'M10,0A10,10 0 1,1 0,-10A10,10 0 0,1 10,0Z'
  • square (n:1): 'M10,10H-10V-10H10Z'
  • diamond (n:2): 'M13,0L0,13L-13,0L0,-13Z'
  • cross (n:3): 'M12,4H4V12H-4V4H-12V-4H-4V-12H4V-4H12Z'
  • x (n:4): Evaluate round(10 * 0.8 / sqrt2, 2) = round(5.657, 2) = 5.66, then build the path with rx=5.66.
  • triangle-up (n:5): rt = round(10 * 2 / sqrt3, 2) = 11.55, r2 = 5, rs = 10'M-11.55,5H11.55L0,-10Z'
  • Continue for all 55...

For symbols with trigonometric computations (arrow, arrow-wide), evaluate at r=10:

  • arrow (n:53): headAngle = PI/2.5, x = 2*10*cos(PI/2.5) ≈ 12.36, y = 2*10*sin(PI/2.5) ≈ 19.02'M0,0L-12.36,19.02L12.36,19.02Z' (round to 2 decimal places)

2. src/components/drawing/index.js — Use <symbol>/<use> pipeline

Replace drawing.symbolFuncs[] with drawing.symbolPaths[]:

drawing.symbolPaths = [];
// ...in ...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Send tasks to Copilot coding agent from [Slack](https://gh.io/cca-slack-docs) and [Teams](https://gh.io/cca-teams-docs) to turn conversations into code. Copilot posts an update in your thread when it's finished.

@gatopeich
Copy link
Copy Markdown
Owner

Duplicate

Copilot AI requested a review from gatopeich March 21, 2026 04:28
Copilot stopped work on behalf of gatopeich due to an error March 21, 2026 04:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants