From efacef63b1f1a80c2a2061b47c8c939fd652a317 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 12 Jun 2025 08:42:44 -0400 Subject: [PATCH 01/12] docs(SubmarineCablesGlobe): Improve perf by disabling hit canvas while spinning --- .../src/routes/docs/examples/SubmarineCablesGlobe/+page.svelte | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.svelte index eac0b8e49..6d900cbb8 100644 --- a/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.svelte @@ -8,7 +8,6 @@ import { Chart, - Circle, GeoPath, GeoPoint, GeoVisible, @@ -80,7 +79,7 @@ bind:context > {#snippet children({ context })} - + Date: Thu, 12 Jun 2025 08:46:27 -0400 Subject: [PATCH 02/12] fix(Canvas): Improve performance by skipping unnecessary work when hit canvas is unneeded --- .changeset/violet-gifts-fail.md | 5 ++ .../src/lib/components/layout/Canvas.svelte | 72 ++++++++++--------- 2 files changed, 45 insertions(+), 32 deletions(-) create mode 100644 .changeset/violet-gifts-fail.md diff --git a/.changeset/violet-gifts-fail.md b/.changeset/violet-gifts-fail.md new file mode 100644 index 000000000..4482e0cb8 --- /dev/null +++ b/.changeset/violet-gifts-fail.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Canvas): Improve performance by skipping unnecessary work when hit canvas is unneeded diff --git a/packages/layerchart/src/lib/components/layout/Canvas.svelte b/packages/layerchart/src/lib/components/layout/Canvas.svelte index a8e0b54a3..36852ea15 100644 --- a/packages/layerchart/src/lib/components/layout/Canvas.svelte +++ b/packages/layerchart/src/lib/components/layout/Canvas.svelte @@ -346,45 +346,53 @@ context.restore(); } - // sync hit canvas with main canvas + /* + * Sync hit canvas with main canvas + */ if (hitCanvasContext) { - // scale hit canvas to match main canvas - scaleCanvas(hitCanvasContext, ctx.containerWidth, ctx.containerHeight); - hitCanvasContext.clearRect(0, 0, ctx.containerWidth, ctx.containerHeight); - - // reset and sync transform to the state after retainState components - hitCanvasContext.resetTransform(); - hitCanvasContext.setTransform(mainTransformAfterRetain); - - // reset color generator - colorGenerator = rgbColorGenerator(); - const inactiveMoving = !activeCanvas && transformCtx.moving; - - // render retainState components on hit canvas (e.g., Group) - for (const c of retainStateComponents) { - const componentHasEvents = c.events && Object.values(c.events).filter((d) => d).length > 0; - - if (componentHasEvents && !inactiveMoving && !transformCtx.dragging) { - // since the transform was already applied via setTransform, skip rendering - // the retainState component's transform again; proceed to its children - continue; + if (disableHitCanvas || transformCtx.dragging || inactiveMoving) { + // Skip rendering hit canvas + hitCanvasContext.clearRect(0, 0, ctx.containerWidth, ctx.containerHeight); + } else { + // scale hit canvas to match main canvas + scaleCanvas(hitCanvasContext, ctx.containerWidth, ctx.containerHeight); + hitCanvasContext.clearRect(0, 0, ctx.containerWidth, ctx.containerHeight); + + // reset and sync transform to the state after retainState components + hitCanvasContext.resetTransform(); + hitCanvasContext.setTransform(mainTransformAfterRetain); + + // reset color generator + colorGenerator = rgbColorGenerator(); + + // render retainState components on hit canvas (e.g., Group) + for (const c of retainStateComponents) { + const componentHasEvents = + c.events && Object.values(c.events).filter((d) => d).length > 0; + + if (componentHasEvents) { + // since the transform was already applied via setTransform, skip rendering + // the retainState component's transform again; proceed to its children + continue; + } } - } - // render non-retainState components on hit canvas - for (const c of nonRetainStateComponents) { - const componentHasEvents = c.events && Object.values(c.events).filter((d) => d).length > 0; + // render non-retainState components on hit canvas + for (const c of nonRetainStateComponents) { + const componentHasEvents = + c.events && Object.values(c.events).filter((d) => d).length > 0; - if (componentHasEvents && !inactiveMoving && !transformCtx.dragging && !disableHitCanvas) { - const color = getColorStr(colorGenerator.next().value); - const styleOverrides = { styles: { fill: color, stroke: color, _fillOpacity: 0.1 } }; + if (componentHasEvents) { + const color = getColorStr(colorGenerator.next().value); + const styleOverrides = { styles: { fill: color, stroke: color, _fillOpacity: 0.1 } }; - hitCanvasContext.save(); - c.render(hitCanvasContext, styleOverrides); - hitCanvasContext.restore(); + hitCanvasContext.save(); + c.render(hitCanvasContext, styleOverrides); + hitCanvasContext.restore(); - componentByColor.set(color, c); + componentByColor.set(color, c); + } } } } From 700f976f9c269af236372ee476d0f463d665dabe Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 12 Jun 2025 09:41:13 -0400 Subject: [PATCH 03/12] Do not unnessarily call `getComputedStyles()` if component does not use classes --- packages/layerchart/src/lib/utils/canvas.ts | 57 ++++++++++++--------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts index 151372d1c..3c4bdacee 100644 --- a/packages/layerchart/src/lib/utils/canvas.ts +++ b/packages/layerchart/src/lib/utils/canvas.ts @@ -7,14 +7,16 @@ export const DEFAULT_FILL = 'rgb(0, 0, 0)'; const CANVAS_STYLES_ELEMENT_ID = '__layerchart_canvas_styles_id'; +type StyleOptions = Partial< + Omit & { + fillOpacity?: number | string; + strokeWidth?: number | string; + opacity?: number | string; + } +>; + export type ComputedStylesOptions = { - styles?: Partial< - Omit & { - fillOpacity?: number | string; - strokeWidth?: number | string; - opacity?: number | string; - } - >; + styles?: StyleOptions; classes?: ClassValue | null; }; @@ -79,26 +81,33 @@ function render( // console.count('render'); // TODO: Consider memoizing? How about reactiving to CSS variable changes (light/dark mode toggle) - const computedStyles = getComputedStyles(ctx.canvas, styleOptions); + let resolvedStyles: StyleOptions; + if (styleOptions.classes == null) { + // Skip resolving styles if no classes are provided + resolvedStyles = styleOptions.styles ?? {}; + } else { + const computedStyles = getComputedStyles(ctx.canvas, styleOptions); + resolvedStyles = computedStyles; + } // Adhere to CSS paint order: https://developer.mozilla.org/en-US/docs/Web/CSS/paint-order const paintOrder = - computedStyles?.paintOrder === 'stroke' ? ['stroke', 'fill'] : ['fill', 'stroke']; + resolvedStyles?.paintOrder === 'stroke' ? ['stroke', 'fill'] : ['fill', 'stroke']; - if (computedStyles?.opacity) { - ctx.globalAlpha = Number(computedStyles?.opacity); + if (resolvedStyles?.opacity) { + ctx.globalAlpha = Number(resolvedStyles?.opacity); } // Text properties - ctx.font = `${computedStyles.fontWeight} ${computedStyles.fontSize} ${computedStyles.fontFamily}`; // build string instead of using `computedStyles.font` to fix/workaround `tabular-nums` returning `null` + ctx.font = `${resolvedStyles.fontWeight} ${resolvedStyles.fontSize} ${resolvedStyles.fontFamily}`; // build string instead of using `computedStyles.font` to fix/workaround `tabular-nums` returning `null` // TODO: Hack to handle `textAnchor` with canvas. Try to find a better approach - if (computedStyles.textAnchor === 'middle') { + if (resolvedStyles.textAnchor === 'middle') { ctx.textAlign = 'center'; - } else if (computedStyles.textAnchor === 'end') { + } else if (resolvedStyles.textAnchor === 'end') { ctx.textAlign = 'right'; } else { - ctx.textAlign = computedStyles.textAlign as CanvasTextAlign; // TODO: Handle/map `justify` and `match-parent`? + ctx.textAlign = resolvedStyles.textAlign as CanvasTextAlign; // TODO: Handle/map `justify` and `match-parent`? } // TODO: Handle `textBaseline` / `verticalAnchor` (Text) @@ -110,8 +119,8 @@ function render( // ctx.textBaseline = 'ideographic'; // Dashed lines - if (computedStyles.strokeDasharray.includes(',')) { - const dashArray = computedStyles.strokeDasharray + if (resolvedStyles.strokeDasharray?.includes(',')) { + const dashArray = resolvedStyles.strokeDasharray .split(',') .map((s) => Number(s.replace('px', ''))); ctx.setLineDash(dashArray); @@ -125,13 +134,13 @@ function render( (styleOptions.styles?.fill as any) instanceof CanvasPattern || !styleOptions.styles?.fill?.includes('var')) ? styleOptions.styles.fill - : computedStyles?.fill; + : resolvedStyles?.fill; if (fill && !['none', DEFAULT_FILL].includes(fill)) { const currentGlobalAlpha = ctx.globalAlpha; - const fillOpacity = Number(computedStyles?.fillOpacity); - const opacity = Number(computedStyles?.opacity); + const fillOpacity = Number(resolvedStyles?.fillOpacity); + const opacity = Number(resolvedStyles?.opacity); ctx.globalAlpha = fillOpacity * opacity; ctx.fillStyle = fill; @@ -146,13 +155,13 @@ function render( ((styleOptions.styles?.stroke as any) instanceof CanvasGradient || !styleOptions.styles?.stroke?.includes('var')) ? styleOptions.styles?.stroke - : computedStyles?.stroke; + : resolvedStyles?.stroke; if (stroke && !['none'].includes(stroke)) { ctx.lineWidth = - typeof computedStyles?.strokeWidth === 'string' - ? Number(computedStyles?.strokeWidth?.replace('px', '')) - : (computedStyles?.strokeWidth ?? 1); + typeof resolvedStyles?.strokeWidth === 'string' + ? Number(resolvedStyles?.strokeWidth?.replace('px', '')) + : (resolvedStyles?.strokeWidth ?? 1); ctx.strokeStyle = stroke; render.stroke(ctx); From adc69ea34644df8a1d4c8eacb5faa905471e6f2e Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 12 Jun 2025 10:54:07 -0400 Subject: [PATCH 04/12] refactor: Use LayerStack endOfInterval() instead of internal --- .../layerchart/src/lib/components/MonthPath.svelte | 4 ++-- packages/layerchart/src/lib/utils/date.ts | 12 ------------ .../src/routes/docs/components/Calendar/+page.svelte | 6 +++--- 3 files changed, 5 insertions(+), 17 deletions(-) delete mode 100644 packages/layerchart/src/lib/utils/date.ts diff --git a/packages/layerchart/src/lib/components/MonthPath.svelte b/packages/layerchart/src/lib/components/MonthPath.svelte index 4dbc09a93..e0737a042 100644 --- a/packages/layerchart/src/lib/components/MonthPath.svelte +++ b/packages/layerchart/src/lib/components/MonthPath.svelte @@ -31,8 +31,8 @@