diff --git a/draftlogs/8055_change.md b/draftlogs/8055_change.md new file mode 100644 index 00000000000..671ceabf5ed --- /dev/null +++ b/draftlogs/8055_change.md @@ -0,0 +1 @@ + - Memoize color specifier parsing, reducing the time to draw marker-heavy SVG `scatter` traces by roughly a third [[#8055](https://github.com/plotly/plotly.js/pull/8055)] \ No newline at end of file diff --git a/src/components/color/index.js b/src/components/color/index.js index 8460b966980..bd7921cc74d 100644 --- a/src/components/color/index.js +++ b/src/components/color/index.js @@ -271,6 +271,37 @@ const contrast = (cstr, lightAmount, darkAmount) => { } }; +// `stroke` and `fill` below run once per data point, and every point of a trace +// normally repeats the same specifier, so re-deriving the styles each time is +// pure overhead. Cache both values a specifier yields, keyed on the specifier +// itself, and take them from a single `parse` so that a miss costs no more than +// it has to. Only strings are cached, since they are the only specifiers that +// repeat by value rather than by identity. +const MAX_MEMO_SIZE = 1000; + +const styleCache = new Map(); + +const computeStyle = (cstr) => { + const c = parse(cstr); + // Force alpha to 1 in the color so that it gets dropped from the string. + return [formatRgb({ ...c, alpha: 1 }), c.alpha]; +}; + +const styleOf = (cstr) => { + if (typeof cstr !== 'string') return computeStyle(cstr); + + let value = styleCache.get(cstr); + if (value === undefined) { + value = computeStyle(cstr); + // Stop growing rather than evicting: a graph only ever uses a handful + // of distinct colors, so a full cache means array-valued colors, which + // repeat too little to be worth tracking. + if (styleCache.size < MAX_MEMO_SIZE) styleCache.set(cstr, value); + } + + return value; +}; + /** * Apply `stroke` and `stroke-opacity` styles to a D3 selection. * @@ -278,7 +309,8 @@ const contrast = (cstr, lightAmount, darkAmount) => { * @param {*} cstr - Color specifier */ const stroke = (s, cstr) => { - s.style({ stroke: rgb(cstr), 'stroke-opacity': parse(cstr).alpha }); + const style = styleOf(cstr); + s.style({ stroke: style[0], 'stroke-opacity': style[1] }); }; /** @@ -288,7 +320,8 @@ const stroke = (s, cstr) => { * @param {*} cstr - Color specifier */ const fill = (s, cstr) => { - s.style({ fill: rgb(cstr), 'fill-opacity': parse(cstr).alpha }); + const style = styleOf(cstr); + s.style({ fill: style[0], 'fill-opacity': style[1] }); }; /**