diff --git a/.changeset/bright-buses-buy.md b/.changeset/bright-buses-buy.md new file mode 100644 index 000000000..650013953 --- /dev/null +++ b/.changeset/bright-buses-buy.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat: Support Canvas context for most primatives (Arc, Area, Circle, Group, Line, LinearGradient, Rect, Spline, and Text). Also updates components using primatives (Axis, Bar, Grid, Rule, and more) diff --git a/.changeset/chilly-jeans-film.md b/.changeset/chilly-jeans-film.md new file mode 100644 index 000000000..e23109128 --- /dev/null +++ b/.changeset/chilly-jeans-film.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +Add `spikePath()` util diff --git a/.changeset/chilly-moles-wave.md b/.changeset/chilly-moles-wave.md new file mode 100644 index 000000000..284fdec8d --- /dev/null +++ b/.changeset/chilly-moles-wave.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(circlePath): Correctly handle sweep argument diff --git a/.changeset/cold-penguins-jump.md b/.changeset/cold-penguins-jump.md new file mode 100644 index 000000000..cd60cd2e2 --- /dev/null +++ b/.changeset/cold-penguins-jump.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat: Update all simplified charts to support `renderContext` prop to switch between Svg (default) and Canvas (AreaChart, BarChart, LineChart, PieChart, and ScatterChart) diff --git a/.changeset/dry-dodos-build.md b/.changeset/dry-dodos-build.md new file mode 100644 index 000000000..31e5d8f75 --- /dev/null +++ b/.changeset/dry-dodos-build.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Canvas): Support `center` prop (similar to `Svg`) to translate children to center (useful for radial layouts) diff --git a/.changeset/dry-masks-suffer.md b/.changeset/dry-masks-suffer.md new file mode 100644 index 000000000..9cca5abd3 --- /dev/null +++ b/.changeset/dry-masks-suffer.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat: Add new `renderPathData()` canvas util to simplify rendering SVG path data onto canvas context with CSS variable and class support diff --git a/.changeset/dry-singers-travel.md b/.changeset/dry-singers-travel.md new file mode 100644 index 000000000..7ac5505b7 --- /dev/null +++ b/.changeset/dry-singers-travel.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat: Add new `renderRect()` canvas util to simplify rendering rectangles onto canvas context with CSS variable and class support diff --git a/.changeset/early-keys-think.md b/.changeset/early-keys-think.md new file mode 100644 index 000000000..6e0d66ffb --- /dev/null +++ b/.changeset/early-keys-think.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +breaking(LinearGradient|RadialGradient): Rename `url` slot prop to `gradient`. Improves name, especially within canvas context diff --git a/.changeset/gentle-months-rest.md b/.changeset/gentle-months-rest.md new file mode 100644 index 000000000..fa9e68f91 --- /dev/null +++ b/.changeset/gentle-months-rest.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix: Reduce likihood of clipping for Canvas-rendered simplified charts by increasing default padding (and add top)) diff --git a/.changeset/ninety-numbers-fail.md b/.changeset/ninety-numbers-fail.md new file mode 100644 index 000000000..e511fb1ee --- /dev/null +++ b/.changeset/ninety-numbers-fail.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Canvas): Support multiple children (fix infinite loops, coordinating redraws, etc). Resolves issue #158 diff --git a/.changeset/orange-parrots-arrive.md b/.changeset/orange-parrots-arrive.md new file mode 100644 index 000000000..085733e0c --- /dev/null +++ b/.changeset/orange-parrots-arrive.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat: Add new `renderText()` canvas util to simplify rendering text onto canvas context with CSS variable and class support diff --git a/.changeset/popular-stingrays-change.md b/.changeset/popular-stingrays-change.md new file mode 100644 index 000000000..3528218b5 --- /dev/null +++ b/.changeset/popular-stingrays-change.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat: Improve Canvas implementation with registering render functions and common invalidation to synchronize redrawing diff --git a/.changeset/pretty-bears-report.md b/.changeset/pretty-bears-report.md new file mode 100644 index 000000000..b30cca348 --- /dev/null +++ b/.changeset/pretty-bears-report.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat: Add `scaleCanvas` util diff --git a/.changeset/shaggy-rocks-tan.md b/.changeset/shaggy-rocks-tan.md new file mode 100644 index 000000000..891b504d9 --- /dev/null +++ b/.changeset/shaggy-rocks-tan.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +Add `clearCanvasContext()` util diff --git a/.changeset/warm-cars-attack.md b/.changeset/warm-cars-attack.md new file mode 100644 index 000000000..0d16563c4 --- /dev/null +++ b/.changeset/warm-cars-attack.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat: Add `ComputedStyles` component to easily resolve classes / CSS variable values (useful when working with ) diff --git a/.changeset/wicked-mirrors-sleep.md b/.changeset/wicked-mirrors-sleep.md new file mode 100644 index 000000000..7c0f699a3 --- /dev/null +++ b/.changeset/wicked-mirrors-sleep.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +breaking(GeoPath): Simplify render prop use case by leveraging renderPathData() (ex. HitCanvas) diff --git a/packages/layerchart/package.json b/packages/layerchart/package.json index 1ba97473c..7d0e8dda5 100644 --- a/packages/layerchart/package.json +++ b/packages/layerchart/package.json @@ -81,7 +81,7 @@ "type": "module", "dependencies": { "@dagrejs/dagre": "^1.1.4", - "@layerstack/svelte-actions": "^0.0.9", + "@layerstack/svelte-actions": "^0.0.11", "@layerstack/svelte-stores": "^0.0.9", "@layerstack/tailwind": "^0.0.11", "@layerstack/utils": "^0.0.7", diff --git a/packages/layerchart/src/lib/components/Arc.svelte b/packages/layerchart/src/lib/components/Arc.svelte index f23ddd44f..5565ec2a1 100644 --- a/packages/layerchart/src/lib/components/Arc.svelte +++ b/packages/layerchart/src/lib/components/Arc.svelte @@ -17,7 +17,7 @@ // https://svelte.dev/repl/09711e43a1264ba18945d7db7cab9335?version=3.38.2 // https://codepen.io/simeydotme/pen/rrOEmO/ - import { tick } from 'svelte'; + import { onDestroy, tick } from 'svelte'; import type { SVGAttributes } from 'svelte/elements'; import type { spring as springStore, tweened as tweenedStore } from 'svelte/motion'; import { arc as d3arc } from 'd3-shape'; @@ -28,6 +28,8 @@ import { motionStore } from '$lib/stores/motionStore.js'; import { degreesToRadians } from '$lib/utils/math.js'; import type { TooltipContextValue } from './tooltip/TooltipContext.svelte'; + import { getCanvasContext } from './layout/Canvas.svelte'; + import { renderPathData } from '../utils/canvas.js'; export let spring: boolean | Parameters[1] = undefined; export let tweened: boolean | Parameters[1] = undefined; @@ -80,6 +82,11 @@ export let padAngle = 0; // export let padRadius = 0; + export let fill: string | undefined = undefined; + export let fillOpacity: number | undefined = undefined; + export let stroke: string | undefined = undefined; + export let strokeWidth: number | undefined = undefined; + export let track: boolean | SVGAttributes = false; const { yRange } = chartContext(); @@ -183,36 +190,85 @@ * Data to set when showing tooltip */ export let data: any = undefined; + + const canvasContext = getCanvasContext(); + const renderContext = canvasContext ? 'canvas' : 'svg'; + + function render(ctx: CanvasRenderingContext2D) { + ctx.translate(xOffset, yOffset); + + // Track + const trackProps = { ...(typeof track === 'object' ? track : null) }; + renderPathData(ctx, trackArc(), { + styles: { + fill: trackProps['fill'] ?? undefined, + fillOpacity: trackProps['fill-opacity'] ?? undefined, + stroke: trackProps['stroke'] ?? undefined, + strokeWidth: trackProps['stroke-width'] ?? undefined, + opacity: trackProps['opacity'] ?? undefined, + }, + classes: trackProps.class ?? undefined, + }); + + // Arc + renderPathData(ctx, arc(), { + styles: { fill, fillOpacity, stroke, strokeWidth }, + classes: $$props.class, + }); + } + + let canvasUnregister: ReturnType; + $: if (renderContext === 'canvas') { + canvasUnregister = canvasContext.register({ name: 'Arc', render }); + } + + $: if (renderContext === 'canvas') { + // Redraw when props changes (TODO: styles, class, etc) + arc && trackArc; + canvasContext.invalidate(); + } + + onDestroy(() => { + if (renderContext === 'canvas') { + canvasUnregister(); + } + }); -{#if track} +{#if renderContext === 'svg'} + {#if track} + + {/if} + + tooltip?.show(e, data)} + on:pointermove={(e) => tooltip?.show(e, data)} + on:pointerleave={(e) => tooltip?.hide()} + on:touchmove={(e) => { + if (tooltip) { + // Prevent touch to not interfer with pointer when using tooltip + e.preventDefault(); + } + }} + on:click + on:pointerenter + on:pointermove + on:pointerleave + on:touchmove /> {/if} - - tooltip?.show(e, data)} - on:pointermove={(e) => tooltip?.show(e, data)} - on:pointerleave={(e) => tooltip?.hide()} - on:touchmove={(e) => { - if (tooltip) { - // Prevent touch to not interfer with pointer when using tooltip - e.preventDefault(); - } - }} - on:click - on:pointerenter - on:pointermove - on:pointerleave - on:touchmove -/> - diff --git a/packages/layerchart/src/lib/components/Area.svelte b/packages/layerchart/src/lib/components/Area.svelte index 5758f2816..c01390ac3 100644 --- a/packages/layerchart/src/lib/components/Area.svelte +++ b/packages/layerchart/src/lib/components/Area.svelte @@ -1,5 +1,5 @@ + {#if line} {/if} - - +{#if renderContext === 'svg'} + + +{/if} diff --git a/packages/layerchart/src/lib/components/Bar.svelte b/packages/layerchart/src/lib/components/Bar.svelte index 5fb11cf49..d6fa6bed1 100644 --- a/packages/layerchart/src/lib/components/Bar.svelte +++ b/packages/layerchart/src/lib/components/Bar.svelte @@ -9,6 +9,7 @@ import { isScaleBand } from '../utils/scales.js'; import { accessor, type Accessor } from '../utils/common.js'; import { greatestAbs } from '@layerstack/utils'; + import { getCanvasContext } from './layout/Canvas.svelte'; const { x: xContext, y: yContext, xScale } = chartContext(); @@ -106,9 +107,12 @@ z` .split('\n') .join(''); + + const canvasContext = getCanvasContext(); + const renderContext = canvasContext ? 'canvas' : 'svg'; -{#if _rounded === 'all' || radius === 0} +{#if (_rounded === 'all' || radius === 0) && renderContext === 'svg'} - import { tick } from 'svelte'; + import { onDestroy, tick } from 'svelte'; import type { spring as springStore, tweened as tweenedStore } from 'svelte/motion'; import { cls } from '@layerstack/tailwind'; import { motionStore } from '$lib/stores/motionStore.js'; + import { getCanvasContext } from './layout/Canvas.svelte'; + import { circlePath } from '../utils/path.js'; + import { renderPathData } from '../utils/canvas.js'; export let cx: number = 0; export let initialCx = cx; @@ -17,6 +20,11 @@ export let spring: boolean | Parameters[1] = undefined; export let tweened: boolean | Parameters[1] = undefined; + export let fill: string | undefined = undefined; + export let fillOpacity: number | undefined = undefined; + export let stroke: string | undefined = undefined; + export let strokeWidth: number | undefined = undefined; + let tweened_cx = motionStore(initialCx, { spring, tweened }); let tweened_cy = motionStore(initialCy, { spring, tweened }); let tweened_r = motionStore(initialR, { spring, tweened }); @@ -26,16 +34,50 @@ tweened_cy.set(cy); tweened_r.set(r); }); + + const canvasContext = getCanvasContext(); + const renderContext = canvasContext ? 'canvas' : 'svg'; + + function render(ctx: CanvasRenderingContext2D) { + const pathData = circlePath({ cx: $tweened_cx, cy: $tweened_cy, r: $tweened_r }); + renderPathData(ctx, pathData, { + styles: { fill, fillOpacity, stroke, strokeWidth }, + classes: $$props.class, + }); + } + + let canvasUnregister: ReturnType; + $: if (renderContext === 'canvas') { + canvasUnregister = canvasContext.register({ name: 'Circle', render }); + } + + $: if (renderContext === 'canvas') { + // Redraw when props changes (TODO: styles, class, etc) + $tweened_cx && $tweened_cy && $tweened_r; + canvasContext.invalidate(); + } + + onDestroy(() => { + if (renderContext === 'canvas') { + canvasUnregister(); + } + }); - - +{#if renderContext === 'svg'} + + +{/if} diff --git a/packages/layerchart/src/lib/components/ComputedStyles.svelte b/packages/layerchart/src/lib/components/ComputedStyles.svelte new file mode 100644 index 000000000..2c8fb4324 --- /dev/null +++ b/packages/layerchart/src/lib/components/ComputedStyles.svelte @@ -0,0 +1,16 @@ + + +
(styles = _styles)} +>
+ + diff --git a/packages/layerchart/src/lib/components/GeoPath.svelte b/packages/layerchart/src/lib/components/GeoPath.svelte index 487d3f246..34091e478 100644 --- a/packages/layerchart/src/lib/components/GeoPath.svelte +++ b/packages/layerchart/src/lib/components/GeoPath.svelte @@ -1,6 +1,5 @@ diff --git a/packages/layerchart/src/lib/components/GeoPoint.svelte b/packages/layerchart/src/lib/components/GeoPoint.svelte index ed44c1190..c752a1542 100644 --- a/packages/layerchart/src/lib/components/GeoPoint.svelte +++ b/packages/layerchart/src/lib/components/GeoPoint.svelte @@ -1,10 +1,10 @@ {#if renderContext === 'svg'} @@ -46,3 +49,14 @@ {/if} {/if} + +{#if renderContext === 'canvas'} + {#if $$slots.default} + + + + + {:else} + + {/if} +{/if} diff --git a/packages/layerchart/src/lib/components/GeoTile.svelte b/packages/layerchart/src/lib/components/GeoTile.svelte index 5657557a9..b5ef6aeea 100644 --- a/packages/layerchart/src/lib/components/GeoTile.svelte +++ b/packages/layerchart/src/lib/components/GeoTile.svelte @@ -1,6 +1,5 @@ {#if renderContext === 'svg' && url} diff --git a/packages/layerchart/src/lib/components/Group.svelte b/packages/layerchart/src/lib/components/Group.svelte index c23b19339..6cf3ca221 100644 --- a/packages/layerchart/src/lib/components/Group.svelte +++ b/packages/layerchart/src/lib/components/Group.svelte @@ -1,9 +1,10 @@ - - { - if (preventTouchMove) { - // Prevent touch to not interfer with pointer - e.preventDefault(); + const canvasContext = getCanvasContext(); + const renderContext = canvasContext ? 'canvas' : 'svg'; + + function render(ctx: CanvasRenderingContext2D) { + ctx.translate($tweened_x ?? 0, $tweened_y ?? 0); + } + + let canvasUnregister: ReturnType; + $: if (renderContext === 'canvas') { + canvasUnregister = canvasContext.register({ name: 'Group', render, retainState: true }); + } + + $: if (renderContext === 'canvas') { + $tweened_x && $tweened_y; + canvasContext.invalidate(); + } + + onDestroy(() => { + if (renderContext === 'canvas') { + canvasUnregister(); } - }} -> + }); + + +{#if renderContext === 'canvas'} - +{:else if renderContext === 'svg'} + + { + if (preventTouchMove) { + // Prevent touch to not interfer with pointer + e.preventDefault(); + } + }} + > + + +{/if} diff --git a/packages/layerchart/src/lib/components/HitCanvas.svelte b/packages/layerchart/src/lib/components/HitCanvas.svelte index 5352fd59a..4a7145f4c 100644 --- a/packages/layerchart/src/lib/components/HitCanvas.svelte +++ b/packages/layerchart/src/lib/components/HitCanvas.svelte @@ -1,7 +1,5 @@ - - - - - {#if markerStart} +{#if renderContext === 'svg'} + + + + + {#if markerStart} + + {/if} + + + - {/if} - - - - - + +{/if} diff --git a/packages/layerchart/src/lib/components/LinearGradient.svelte b/packages/layerchart/src/lib/components/LinearGradient.svelte index 44c448d16..a925ff520 100644 --- a/packages/layerchart/src/lib/components/LinearGradient.svelte +++ b/packages/layerchart/src/lib/components/LinearGradient.svelte @@ -1,6 +1,12 @@ - - - - {#if stops} - {#each stops as stop, i} - {#if Array.isArray(stop)} - - {:else} - - {/if} - {/each} - {/if} - - - - - +{#if renderContext === 'canvas'} + +{:else if renderContext === 'svg'} + + + + {#if stops} + {#each stops as stop, i} + {#if Array.isArray(stop)} + + {:else} + + {/if} + {/each} + {/if} + + + + + +{/if} diff --git a/packages/layerchart/src/lib/components/Points.svelte b/packages/layerchart/src/lib/components/Points.svelte index 9fac7d4d8..3c5f0c95a 100644 --- a/packages/layerchart/src/lib/components/Points.svelte +++ b/packages/layerchart/src/lib/components/Points.svelte @@ -3,8 +3,7 @@ - {#if renderContext === 'svg'} + {#if renderContext === 'svg' || (renderContext === 'canvas' && !render)} {#if links} {#each _links as link} @@ -225,7 +205,9 @@ cy={$radial ? radialPoint[1] : point.y} r={point.r} fill={fill ?? ($config.c ? $cGet(point.data) : null)} + {fillOpacity} {stroke} + {strokeWidth} class={className} {...$$restProps} /> diff --git a/packages/layerchart/src/lib/components/RadialGradient.svelte b/packages/layerchart/src/lib/components/RadialGradient.svelte index 0b1bf6bd1..8a08c2c93 100644 --- a/packages/layerchart/src/lib/components/RadialGradient.svelte +++ b/packages/layerchart/src/lib/components/RadialGradient.svelte @@ -1,6 +1,12 @@ - - - - {#if stops} - {#each stops as stop, i} - {#if Array.isArray(stop)} - - {:else} - - {/if} - {/each} - {/if} - - - - - +{#if renderContext === 'canvas'} + +{:else if renderContext === 'svg'} + + + + {#if stops} + {#each stops as stop, i} + {#if Array.isArray(stop)} + + {:else} + + {/if} + {/each} + {/if} + + + + + +{/if} diff --git a/packages/layerchart/src/lib/components/Rect.svelte b/packages/layerchart/src/lib/components/Rect.svelte index 08600db1c..d05b14083 100644 --- a/packages/layerchart/src/lib/components/Rect.svelte +++ b/packages/layerchart/src/lib/components/Rect.svelte @@ -1,5 +1,5 @@ - - - +{#if renderContext === 'svg'} + + + +{/if} diff --git a/packages/layerchart/src/lib/components/Rule.svelte b/packages/layerchart/src/lib/components/Rule.svelte index 704e87f53..3c5ad1edc 100644 --- a/packages/layerchart/src/lib/components/Rule.svelte +++ b/packages/layerchart/src/lib/components/Rule.svelte @@ -67,7 +67,7 @@ {x2} {y2} {...$$restProps} - class={cls('test grid stroke-surface-content/10', $$props.class)} + class={cls('stroke-surface-content/10', $$props.class)} /> {:else} - import { tick, type ComponentProps } from 'svelte'; + import { onDestroy, tick, type ComponentProps } from 'svelte'; import { writable } from 'svelte/store'; import { tweened as tweenedStore } from 'svelte/motion'; import { draw as _drawTransition } from 'svelte/transition'; @@ -21,6 +21,8 @@ import { accessor, type Accessor } from '../utils/common.js'; import { isScaleBand } from '../utils/scales.js'; import { flattenPathData } from '../utils/path.js'; + import { renderPathData } from '../utils/canvas.js'; + import { getCanvasContext } from './layout/Canvas.svelte'; const { data: contextData, @@ -61,6 +63,11 @@ export let curve: CurveFactory | CurveFactoryLineOnly | undefined = undefined; export let defined: Parameters['defined']>[0] | undefined = undefined; + export let fill: string | undefined = undefined; + export let stroke: string | undefined = undefined; + export let strokeWidth: number | undefined = undefined; + export let opacity: number | undefined = undefined; + /** Marker to attach to start, mid, and end points of path */ export let marker: ComponentProps['type'] | ComponentProps | undefined = undefined; @@ -153,6 +160,31 @@ key = Symbol(); } + const canvasContext = getCanvasContext(); + const renderContext = canvasContext ? 'canvas' : 'svg'; + + function render(ctx: CanvasRenderingContext2D) { + renderPathData(ctx, $tweened_d, { + styles: { stroke, fill, strokeWidth, opacity }, + classes: $$props.class, + }); + } + + let canvasUnregister: ReturnType; + $: if (renderContext === 'canvas') { + canvasUnregister = canvasContext.register({ name: 'Spline', render }); + + tweened_d.subscribe(() => { + canvasContext.invalidate(); + }); + } + + onDestroy(() => { + if (renderContext === 'canvas') { + canvasUnregister(); + } + }); + let pathEl: SVGPathElement | undefined = undefined; const startPoint = writable(undefined); $: endPoint = motionStore(undefined, { @@ -187,63 +219,69 @@ } -{#key key} - - - - - {#if markerStart} +{#if renderContext === 'svg'} + {#key key} + + + + + {#if markerStart} + + {/if} + + + - {/if} - + - - - + + + - - - - - {#if $$slots.start && $startPoint} - - - - {/if} - - {#if $$slots.end && $endPoint} - - - - {/if} -{/key} + {#if $$slots.start && $startPoint} + + + + {/if} + + {#if $$slots.end && $endPoint} + + + + {/if} + {/key} +{/if} diff --git a/packages/layerchart/src/lib/components/Text.svelte b/packages/layerchart/src/lib/components/Text.svelte index 1efcd441c..e9cfbcc3d 100644 --- a/packages/layerchart/src/lib/components/Text.svelte +++ b/packages/layerchart/src/lib/components/Text.svelte @@ -1,10 +1,12 @@ - - - - {#if isValidXOrY(x) && isValidXOrY(y)} - - {#each wordsByLines as line, index} - - {line.words.join(' ')} - - {/each} - - {/if} - +{#if renderContext === 'svg'} + + + + {#if isValidXOrY(x) && isValidXOrY(y)} + + {#each wordsByLines as line, index} + + {line.words.join(' ')} + + {/each} + + {/if} + +{/if} diff --git a/packages/layerchart/src/lib/components/charts/AreaChart.svelte b/packages/layerchart/src/lib/components/charts/AreaChart.svelte index 99c9bf0d8..7681a4999 100644 --- a/packages/layerchart/src/lib/components/charts/AreaChart.svelte +++ b/packages/layerchart/src/lib/components/charts/AreaChart.svelte @@ -7,6 +7,7 @@ import Area from '../Area.svelte'; import Axis from '../Axis.svelte'; + import Canvas from '../layout/Canvas.svelte'; import Chart from '../Chart.svelte'; import Grid from '../Grid.svelte'; import Highlight from '../Highlight.svelte'; @@ -18,7 +19,12 @@ import Svg from '../layout/Svg.svelte'; import * as Tooltip from '../tooltip/index.js'; - import { accessor, chartDataArray, type Accessor } from '../../utils/common.js'; + import { + accessor, + chartDataArray, + defaultChartPadding, + type Accessor, + } from '../../utils/common.js'; interface $$Props extends ComponentProps> { axis?: typeof axis; @@ -30,6 +36,7 @@ rule?: typeof rule; series?: typeof series; seriesLayout?: typeof seriesLayout; + renderContext?: typeof renderContext; } export let data: $$Props['data'] = []; @@ -74,6 +81,8 @@ labels?: Partial>; } = {}; + export let renderContext: 'svg' | 'canvas' = 'svg'; + $: allSeriesData = series .flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d }))) .filter((d) => d) as Array; @@ -126,7 +135,7 @@ ? s.value[1] : (s.value ?? s.key), fill: s.color, - 'fill-opacity': 0.3, + fillOpacity: 0.3, ...props.area, ...s.props, line: { @@ -151,12 +160,7 @@ yBaseline={0} yNice {radial} - padding={radial || axis === false - ? undefined - : { - left: axis === true || axis === 'y' ? 16 : 0, - bottom: (axis === true || axis === 'x' ? 16 : 0) + (legend === true ? 32 : 0), - }} + padding={radial ? undefined : defaultChartPadding(axis, legend)} tooltip={{ mode: 'bisect-x' }} {...$$restProps} let:x @@ -186,7 +190,7 @@ }} - + {#if grid} @@ -261,7 +265,7 @@ {#if labels} {/if} - + {#if legend} diff --git a/packages/layerchart/src/lib/components/charts/BarChart.svelte b/packages/layerchart/src/lib/components/charts/BarChart.svelte index 8dca8ff04..2d51f4cac 100644 --- a/packages/layerchart/src/lib/components/charts/BarChart.svelte +++ b/packages/layerchart/src/lib/components/charts/BarChart.svelte @@ -7,6 +7,7 @@ import Axis from '../Axis.svelte'; import Bars from '../Bars.svelte'; + import Canvas from '../layout/Canvas.svelte'; import Chart from '../Chart.svelte'; import Grid from '../Grid.svelte'; import Highlight from '../Highlight.svelte'; @@ -16,7 +17,12 @@ import Svg from '../layout/Svg.svelte'; import * as Tooltip from '../tooltip/index.js'; - import { accessor, chartDataArray, type Accessor } from '../../utils/common.js'; + import { + accessor, + chartDataArray, + defaultChartPadding, + type Accessor, + } from '../../utils/common.js'; type ChartProps = ComponentProps>; @@ -32,6 +38,7 @@ rule?: typeof rule; series?: typeof series; seriesLayout?: typeof seriesLayout; + renderContext?: typeof renderContext; } export let data: $$Props['data'] = []; @@ -111,6 +118,8 @@ labels?: Partial>; } = {}; + export let renderContext: 'svg' | 'canvas' = 'svg'; + $: allSeriesData = series .flatMap((s) => s.data?.map((d) => { @@ -195,12 +204,7 @@ {y1Range} c={isVertical ? y : x} cRange={['hsl(var(--color-primary))']} - padding={axis === false - ? undefined - : { - left: axis === true || axis === 'y' ? 16 : 0, - bottom: (axis === true || axis === 'x' ? 16 : 0) + (legend === true ? 32 : 0), - }} + padding={defaultChartPadding(axis, legend)} tooltip={{ mode: 'band' }} {...$$restProps} let:x @@ -229,7 +233,7 @@ getBarsProps, }} - + {#if grid} {/if} - + {#if legend} diff --git a/packages/layerchart/src/lib/components/charts/LineChart.svelte b/packages/layerchart/src/lib/components/charts/LineChart.svelte index fd8b76698..d2bda138f 100644 --- a/packages/layerchart/src/lib/components/charts/LineChart.svelte +++ b/packages/layerchart/src/lib/components/charts/LineChart.svelte @@ -4,6 +4,7 @@ import { format } from '@layerstack/utils'; import Axis from '../Axis.svelte'; + import Canvas from '../layout/Canvas.svelte'; import Chart from '../Chart.svelte'; import Grid from '../Grid.svelte'; import Highlight from '../Highlight.svelte'; @@ -15,7 +16,12 @@ import Svg from '../layout/Svg.svelte'; import * as Tooltip from '../tooltip/index.js'; - import { accessor, chartDataArray, type Accessor } from '../../utils/common.js'; + import { + accessor, + chartDataArray, + defaultChartPadding, + type Accessor, + } from '../../utils/common.js'; interface $$Props extends ComponentProps> { axis?: typeof axis; @@ -26,6 +32,7 @@ props?: typeof props; rule?: typeof rule; series?: typeof series; + renderContext?: typeof renderContext; } export let data: $$Props['data'] = []; @@ -65,6 +72,8 @@ points?: Partial>; } = {}; + export let renderContext: 'svg' | 'canvas' = 'svg'; + $: allSeriesData = series .flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d }))) .filter((d) => d) as Array; @@ -99,12 +108,7 @@ yBaseline={0} yNice {radial} - padding={radial || axis === false - ? undefined - : { - left: axis === true || axis === 'y' ? 16 : 0, - bottom: (axis === true || axis === 'x' ? 16 : 0) + (legend === true ? 32 : 0), - }} + padding={radial ? undefined : defaultChartPadding(axis, legend)} tooltip={{ mode: 'bisect-x' }} {...$$restProps} let:x @@ -133,7 +137,7 @@ getSplineProps, }} - + {#if grid} @@ -203,7 +207,7 @@ /> {/each} - + {#if legend} diff --git a/packages/layerchart/src/lib/components/charts/PieChart.svelte b/packages/layerchart/src/lib/components/charts/PieChart.svelte index 6c1f8d638..53161f99c 100644 --- a/packages/layerchart/src/lib/components/charts/PieChart.svelte +++ b/packages/layerchart/src/lib/components/charts/PieChart.svelte @@ -4,6 +4,7 @@ import { format } from '@layerstack/utils'; import Arc from '../Arc.svelte'; + import Canvas from '../layout/Canvas.svelte'; import Chart from '../Chart.svelte'; import Group from '../Group.svelte'; import Legend from '../Legend.svelte'; @@ -30,6 +31,7 @@ range?: typeof range; series?: typeof series; value?: typeof label; + renderContext?: typeof renderContext; } export let data: ChartProps['data'] = []; @@ -98,6 +100,8 @@ legend?: Partial>; } = {}; + export let renderContext: 'svg' | 'canvas' = 'svg'; + $: allSeriesData = series .flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d }))) .filter((d) => d) as Array; @@ -151,7 +155,7 @@ tooltip, }} - + @@ -216,7 +220,7 @@ - + {#if legend} diff --git a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte b/packages/layerchart/src/lib/components/charts/ScatterChart.svelte index d29f630bd..9d18083e3 100644 --- a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte +++ b/packages/layerchart/src/lib/components/charts/ScatterChart.svelte @@ -4,6 +4,7 @@ import { format } from '@layerstack/utils'; import Axis from '../Axis.svelte'; + import Canvas from '../layout/Canvas.svelte'; import Chart from '../Chart.svelte'; import Grid from '../Grid.svelte'; import Highlight from '../Highlight.svelte'; @@ -14,7 +15,12 @@ import Svg from '../layout/Svg.svelte'; import * as Tooltip from '../tooltip/index.js'; - import { accessor, chartDataArray, type Accessor } from '../../utils/common.js'; + import { + accessor, + chartDataArray, + defaultChartPadding, + type Accessor, + } from '../../utils/common.js'; interface $$Props extends ComponentProps> { axis?: typeof axis; @@ -23,6 +29,7 @@ legend?: typeof legend; props?: typeof props; series?: typeof series; + renderContext?: typeof renderContext; } export let data: $$Props['data'] = []; @@ -55,6 +62,8 @@ rule?: Partial>; } = {}; + export let renderContext: 'svg' | 'canvas' = 'svg'; + // Default xScale based on first data's `x` value $: xScale = $$props.xScale ?? @@ -74,7 +83,7 @@ data: s.data, stroke: s.color, fill: s.color, - 'fill-opacity': 0.3, + fillOpacity: 0.3, ...props.points, ...s.props, }; @@ -90,12 +99,7 @@ {y} {yScale} yNice - padding={axis === false - ? undefined - : { - left: axis === true || axis === 'y' ? 16 : 0, - bottom: (axis === true || axis === 'x' ? 16 : 0) + (legend === true ? 32 : 0), - }} + padding={defaultChartPadding(axis, legend)} tooltip={{ mode: 'voronoi' }} {...$$restProps} let:x @@ -117,7 +121,7 @@ : null} - + {#if grid} @@ -171,7 +175,7 @@ {...typeof labels === 'object' ? labels : null} /> {/if} - + {#if legend} diff --git a/packages/layerchart/src/lib/components/layout/Canvas.svelte b/packages/layerchart/src/lib/components/layout/Canvas.svelte index 7df807ad5..53b10696e 100644 --- a/packages/layerchart/src/lib/components/layout/Canvas.svelte +++ b/packages/layerchart/src/lib/components/layout/Canvas.svelte @@ -1,11 +1,36 @@ + + & { + fillOpacity?: number | string; + strokeWidth?: number | string; + opacity?: number | string; + } + >; + classes?: string; +}; + +/** + * Appends or reuses `` element below `` to resolve CSS variables and classes (ex. `stroke: hsl(var(--color-primary))` => `stroke: rgb(...)` ) + */ +export function getComputedStyles( + canvas: HTMLCanvasElement, + { styles, classes }: ComputedStylesOptions = {} +) { + try { + // Get or create `` below `` + let svg = document.getElementById(CANVAS_STYLES_ELEMENT_ID) as SVGElement | null; + + if (!svg) { + svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('id', CANVAS_STYLES_ELEMENT_ID); + svg.style.display = 'none'; + // Add `` next to `` to allow same scope resolution for CSS variables + canvas.after(svg); + } + svg = svg!; // guarantee SVG is set + + // Remove any previously set styles or classes. Not able to do as part of cleanup below as `window.getComputedStyles()` appearing to be lazily read and removing `style` results in incorrect values, and copying result is very slow + svg.removeAttribute('style'); + svg.removeAttribute('class'); + + // Add styles and class to svg element + if (styles) { + Object.assign(svg.style, styles); + } + + if (classes) { + svg.setAttribute('class', classes); + } + + const computedStyles = window.getComputedStyle(svg); + return computedStyles; + } catch (e) { + console.error('Unable to get computed styles', e); + return {} as CSSStyleDeclaration; + } +} + +/** Render onto canvas context. Supports CSS variables and classes by tranferring to hidden `` element before retrieval) */ +function render( + canvasCtx: CanvasRenderingContext2D, + render: { + stroke: (canvasCtx: CanvasRenderingContext2D) => void; + fill: (canvasCtx: CanvasRenderingContext2D) => void; + }, + styleOptions: ComputedStylesOptions = {} +) { + // TODO: Consider memoizing? How about reactiving to CSS variable changes (light/dark mode toggle) + const computedStyles = getComputedStyles(canvasCtx.canvas, styleOptions); + + // 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']; + + if (computedStyles?.opacity) { + canvasCtx.globalAlpha = Number(computedStyles?.opacity); + } + + // Text properties + canvasCtx.font = `${computedStyles.fontSize} ${computedStyles.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') { + canvasCtx.textAlign = 'center'; + } else if (computedStyles.textAnchor === 'end') { + canvasCtx.textAlign = 'right'; + } else { + canvasCtx.textAlign = computedStyles.textAlign as CanvasTextAlign; // TODO: Handle/map `justify` and `match-parent`? + } + + // TODO: Handle `textBaseline` / `verticalAnchor` (Text) + // canvasCtx.textBaseline = 'top'; + // canvasCtx.textBaseline = 'middle'; + // canvasCtx.textBaseline = 'bottom'; + // canvasCtx.textBaseline = 'alphabetic'; + // canvasCtx.textBaseline = 'hanging'; + // canvasCtx.textBaseline = 'ideographic'; + + // Dashed lines + if (computedStyles.strokeDasharray.includes(',')) { + const dashArray = computedStyles.strokeDasharray + .split(',') + .map((s) => Number(s.replace('px', ''))); + canvasCtx.setLineDash(dashArray); + } + + paintOrder.forEach((attr) => { + if (attr === 'fill') { + const fill = + (styleOptions.styles?.fill as any) instanceof CanvasGradient + ? styleOptions.styles?.fill + : ['none', DEFAULT_FILL].includes(computedStyles?.fill) + ? null + : computedStyles?.fill; + + if (fill) { + const currentGlobalAlpha = canvasCtx.globalAlpha; + if (computedStyles?.fillOpacity) { + canvasCtx.globalAlpha = Number(computedStyles?.fillOpacity); + } + + canvasCtx.fillStyle = fill; + render.fill(canvasCtx); + + // Restore in case it was modified by `fillOpacity` + canvasCtx.globalAlpha = currentGlobalAlpha; + } + } else if (attr === 'stroke') { + const stroke = + (styleOptions.styles?.stroke as any) instanceof CanvasGradient + ? styleOptions.styles?.stroke + : computedStyles?.stroke === 'none' + ? null + : computedStyles?.stroke; + if (stroke) { + canvasCtx.lineWidth = + typeof computedStyles?.strokeWidth === 'string' + ? Number(computedStyles?.strokeWidth?.replace('px', '')) + : (computedStyles?.strokeWidth ?? 1); + + canvasCtx.strokeStyle = stroke; + render.stroke(canvasCtx); + } + } + }); +} + +/** Render SVG path data onto canvas context. Supports CSS variables and classes by tranferring to hidden `` element before retrieval) */ +export function renderPathData( + canvasCtx: CanvasRenderingContext2D, + pathData: string | null | undefined, + styleOptions: ComputedStylesOptions = {} +) { + const path = new Path2D(pathData ?? ''); + + render( + canvasCtx, + { + fill: (ctx) => ctx.fill(path), + stroke: (ctx) => ctx.stroke(path), + }, + styleOptions + ); +} + +export function renderText( + canvasCtx: CanvasRenderingContext2D, + text: string | number | null | undefined, + coords: { x: number; y: number }, + styleOptions: ComputedStylesOptions = {} +) { + if (text) { + render( + canvasCtx, + { + fill: (ctx) => ctx.fillText(text.toString(), coords.x, coords.y), + stroke: (ctx) => ctx.strokeText(text.toString(), coords.x, coords.y), + }, + styleOptions + ); + } +} + +export function renderRect( + canvasCtx: CanvasRenderingContext2D, + coords: { x: number; y: number; width: number; height: number }, + styleOptions: ComputedStylesOptions = {} +) { + render( + canvasCtx, + { + fill: (ctx) => ctx.fillRect(coords.x, coords.y, coords.width, coords.height), + stroke: (ctx) => ctx.strokeRect(coords.x, coords.y, coords.width, coords.height), + }, + styleOptions + ); +} + +/** Clear canvas accounting for Canvas `context.translate(...)` */ +export function clearCanvasContext( + canvasCtx: CanvasRenderingContext2D, + options: { + containerWidth: number; + containerHeight: number; + padding: { + top: number; + bottom: number; + left: number; + right: number; + }; + } +) { + // Clear with negative offset due to Canvas `context.translate(...)` + canvasCtx.clearRect( + -options.padding.left, + -options.padding.top, + options.containerWidth, + options.containerHeight + ); +} + +/** + Scales a canvas for high DPI / retina displays. + @see: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#examples + @see: https://web.dev/articles/canvas-hidipi +*/ +export function scaleCanvas(ctx: CanvasRenderingContext2D, width: number, height: number) { + const devicePixelRatio = window.devicePixelRatio || 1; + + ctx.canvas.width = width * devicePixelRatio; + ctx.canvas.height = height * devicePixelRatio; + + ctx.canvas.style.width = `${width}px`; + ctx.canvas.style.height = `${height}px`; + + ctx.scale(devicePixelRatio, devicePixelRatio); + return { width: ctx.canvas.width, height: ctx.canvas.height }; +} diff --git a/packages/layerchart/src/lib/utils/common.ts b/packages/layerchart/src/lib/utils/common.ts index 6e005ac39..359959cbd 100644 --- a/packages/layerchart/src/lib/utils/common.ts +++ b/packages/layerchart/src/lib/utils/common.ts @@ -1,6 +1,8 @@ -import type Chart from '../components/Chart.svelte'; -import { get } from 'lodash-es'; import type { ComponentProps } from 'svelte'; +import { get } from 'lodash-es'; + +import type Chart from '../components/Chart.svelte'; +import type LineChart from '../components/charts/LineChart.svelte'; export type Accessor = | number @@ -37,3 +39,22 @@ export function chartDataArray(data: ComponentProps>[' return data.descendants(); } } + +// Using LineChart but could any simplified chart +type SimplifiedChartProps = ComponentProps>; + +export function defaultChartPadding( + axis: SimplifiedChartProps['axis'], + legend: SimplifiedChartProps['legend'] +) { + if (axis === false) { + return undefined; + } else { + return { + top: axis === true || axis === 'y' ? 4 : 0, + left: axis === true || axis === 'y' ? 20 : 0, + bottom: (axis === true || axis === 'x' ? 20 : 0) + (legend === true ? 32 : 0), + right: axis === true || axis === 'x' ? 4 : 0, + }; + } +} diff --git a/packages/layerchart/src/lib/utils/index.ts b/packages/layerchart/src/lib/utils/index.ts index 749c156b1..817db2b70 100644 --- a/packages/layerchart/src/lib/utils/index.ts +++ b/packages/layerchart/src/lib/utils/index.ts @@ -1,8 +1,10 @@ +export * from './canvas.js'; export * from './common.js'; export * from './geo.js'; export * from './graph.js'; export * from './hierarchy.js'; export * from './math.js'; +export * from './path.js'; export * from './pivot.js'; export * from './stack.js'; export * from './ticks.js'; diff --git a/packages/layerchart/src/lib/utils/math.ts b/packages/layerchart/src/lib/utils/math.ts index f968bec88..120207e6d 100644 --- a/packages/layerchart/src/lib/utils/math.ts +++ b/packages/layerchart/src/lib/utils/math.ts @@ -53,3 +53,13 @@ export function celsiusToFahrenheit(temperature: number) { export function fahrenheitToCelsius(temperature: number) { return (temperature - 32) * (5 / 9); } + +/** Parse percent string (`50%`) to decimal (`0.5`) */ +export function parsePercent(percent: string | number) { + if (typeof percent === 'number') { + // Assume already decimal + return percent; + } else { + return Number(percent.replace('%', '')) / 100; + } +} diff --git a/packages/layerchart/src/lib/utils/path.ts b/packages/layerchart/src/lib/utils/path.ts index d0b77e5d6..98305a127 100644 --- a/packages/layerchart/src/lib/utils/path.ts +++ b/packages/layerchart/src/lib/utils/path.ts @@ -21,13 +21,41 @@ export function circlePath(dimensions: { r: number; sweep?: 'inside' | 'outside'; }) { - // sweep: 0 (inside), 1 (outside) + // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#arcs const { cx, cy, r, sweep = 'outside' } = dimensions; + // sweep: 0 (inside), 1 (outside) + const _sweep = sweep === 'outside' ? 1 : 0; + return ` M ${cx - r} ${cy} - a ${r},${r} 0 1,${sweep} ${r * 2},0 - a ${r},${r} 0 1,${sweep} -${r * 2},0 + a ${r},${r} 0 1,${_sweep} ${r * 2},0 + a ${r},${r} 0 1,${_sweep} -${r * 2},0 + `; +} + +/** Create spike (triangle) using path data */ +export function spikePath({ + x, + y, + width, + height, +}: { + x: number; + y: number; + width: number; + height: number; +}) { + const startPoint = { x: x - width / 2, y }; + const midPoint = { x, y: y - height }; + const endPoint = { x: x + width / 2, y }; + + const pathData = ` + M ${startPoint.x},${startPoint.y} + L ${midPoint.x},${midPoint.y} + L ${endPoint.x},${endPoint.y} `; + + return pathData; } /** Flatten all `y` coordinates to `0` */ diff --git a/packages/layerchart/src/routes/docs/components/Arc/+page.svelte b/packages/layerchart/src/routes/docs/components/Arc/+page.svelte index 3ac5928f1..8e48c9ee4 100644 --- a/packages/layerchart/src/routes/docs/components/Arc/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Arc/+page.svelte @@ -54,7 +54,7 @@ {#key spring} - + - + @@ -635,6 +681,7 @@ yAxis: { format: 'metric' }, }} legend + {renderContext} /> @@ -667,6 +714,7 @@ yAxis: { format: 'metric' }, }} legend={{ placement: 'top-right', classes: { root: 'mt-2' } }} + {renderContext} /> @@ -702,6 +750,7 @@ yAxis: { format: 'metric' }, }} legend + {renderContext} /> @@ -710,7 +759,7 @@
- +
@@ -718,7 +767,13 @@
- +
@@ -745,6 +800,7 @@ }, }} padding={{ left: 0, bottom: 16 }} + {renderContext} /> @@ -753,7 +809,15 @@
- + d.date} class="text-sm fill-surface-300 stroke-none" /> @@ -773,6 +837,7 @@ grid={false} bandPadding={0.1} props={{ bars: { radius: 1, strokeWidth: 0 } }} + {renderContext} />
@@ -801,6 +866,7 @@ xAxis: { ticks: (scale) => scaleTime(scale.domain(), scale.range()).ticks() }, rule: { y: false }, }} + {renderContext} > @@ -818,7 +884,7 @@
- +
@@ -826,7 +892,7 @@
- +
@@ -839,6 +905,7 @@ x="date" y="value" props={{ xAxis: { ticks: (scale) => scaleTime(scale.domain(), scale.range()).ticks() } }} + {renderContext} /> @@ -847,7 +914,7 @@
- +
@@ -855,7 +922,13 @@
- +
@@ -871,6 +944,7 @@ yScale={scaleLog()} yDomain={[1, 100]} props={{ yAxis: { ticks: [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100] } }} + {renderContext} /> @@ -879,7 +953,7 @@
- + {format(x(data), PeriodType.DayTime)} @@ -897,7 +971,7 @@
- + - + {format(x(data))} diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index 7a7974f3a..f8c2f6f58 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -186,8 +186,8 @@ - - + + @@ -224,8 +224,8 @@ - - + + @@ -263,8 +263,8 @@ - - + + @@ -306,8 +306,8 @@ - - + + @@ -376,8 +376,8 @@ - - + + @@ -410,10 +410,10 @@ - + @@ -478,9 +478,12 @@ - + @@ -525,8 +528,8 @@ - - + + diff --git a/packages/layerchart/src/routes/docs/components/Frame/+page.svelte b/packages/layerchart/src/routes/docs/components/Frame/+page.svelte index 136afcfbd..b12f490e0 100644 --- a/packages/layerchart/src/routes/docs/components/Frame/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Frame/+page.svelte @@ -85,8 +85,8 @@ padding={{ top: 20, bottom: 20, left: 20, right: 20 }} > - - + + diff --git a/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte b/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte index 751e252de..13d439205 100644 --- a/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte @@ -2,7 +2,7 @@ import { geoIdentity, type GeoProjection } from 'd3-geo'; import { feature } from 'topojson-client'; - import { Chart, Canvas, GeoPath, HitCanvas, Tooltip } from 'layerchart'; + import { Chart, Canvas, GeoPath, HitCanvas, Tooltip, renderPathData } from 'layerchart'; import { Field, Switch } from 'svelte-ux'; import Preview from '$lib/docs/Preview.svelte'; @@ -38,16 +38,15 @@ > - - - {#if tooltip.data} - + + + {#if tooltip.data} - - {/if} + {/if} + { + render={(ctx, { newGeoPath }) => { for (var feature of counties.features) { const color = nextColor(); - ctx.beginPath(); - geoPath(feature); - ctx.fillStyle = color; - ctx.fill(); - + const geoPath = newGeoPath(); // Stroking shape seems to help with dark border, but there is still antialising and thus gaps - ctx.strokeStyle = color; - ctx.stroke(); + renderPathData(ctx, geoPath(feature), { styles: { fill: color, stroke: color } }); setColorData(color, feature); } diff --git a/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte b/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte index 230a9392a..78f791fce 100644 --- a/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte @@ -1,6 +1,7 @@

Examples

+ + + Svg + Canvas + + +

Basic

- +
@@ -82,6 +92,7 @@ data={dateSeriesData} x="date" series={[{ key: 'value', color: 'hsl(var(--color-secondary))' }]} + {renderContext} />
@@ -95,6 +106,7 @@ x="date" y="value" props={{ spline: { curve: curveCatmullRom } }} + {renderContext} />
@@ -111,6 +123,7 @@ { key: 'bananas', color: 'hsl(var(--color-success))' }, { key: 'oranges', color: 'hsl(var(--color-warning))' }, ]} + {renderContext} /> @@ -129,6 +142,7 @@ { key: 'oranges', color: 'hsl(var(--color-warning))' }, ]} tooltip={{ mode: 'voronoi' }} + {renderContext} > {#each series as s} @@ -164,7 +178,7 @@
- +
@@ -172,7 +186,7 @@
- +
@@ -180,7 +194,14 @@
- +
@@ -199,6 +220,7 @@ points: false, }, }} + {renderContext} /> @@ -237,6 +259,7 @@ }, }} tooltip={{ mode: 'voronoi' }} + {renderContext} /> @@ -273,6 +296,7 @@ }, }} tooltip={{ mode: 'voronoi' }} + {renderContext} /> @@ -314,6 +338,7 @@ }, }} tooltip={{ mode: 'voronoi' }} + {renderContext} /> @@ -322,14 +347,14 @@
- + - + @@ -356,9 +381,9 @@
- + - {@const thresholdOffset = (yScale(50) / (height + padding.bottom)) * 100 + '%'} + {@const thresholdOffset = yScale(50) / (height + padding.bottom)} - + @@ -403,6 +428,7 @@ }; })} tooltip={{ mode: 'manual' }} + {renderContext} />
@@ -440,6 +466,7 @@ }; })} tooltip={{ mode: 'manual' }} + {renderContext} />
@@ -463,6 +490,7 @@ yBaseline={undefined} tooltip={{ mode: 'manual' }} props={{ yAxis: { tweened: true }, grid: { tweened: true } }} + {renderContext} /> @@ -471,7 +499,7 @@
- +
@@ -479,7 +507,7 @@
- + {#each series as s}
@@ -513,7 +542,7 @@
- +
@@ -521,7 +550,7 @@
- +
@@ -538,6 +567,7 @@ { key: 'oranges', color: 'hsl(var(--color-warning))' }, ]} legend + {renderContext} /> @@ -546,7 +576,7 @@
- +
- - + + - + {format(x(data), PeriodType.DayTime)} diff --git a/packages/layerchart/src/routes/docs/components/LinearGradient/+page.svelte b/packages/layerchart/src/routes/docs/components/LinearGradient/+page.svelte index 8fad7ff66..8db5482ca 100644 --- a/packages/layerchart/src/routes/docs/components/LinearGradient/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/LinearGradient/+page.svelte @@ -66,15 +66,15 @@
- + {#each { length: 6 } as _, i} - + {/each} - + {#each { length: 6 } as _, i} - + {/each} diff --git a/packages/layerchart/src/routes/docs/components/PieChart/+page.svelte b/packages/layerchart/src/routes/docs/components/PieChart/+page.svelte index dc45b80af..bf42bbbbc 100644 --- a/packages/layerchart/src/routes/docs/components/PieChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/PieChart/+page.svelte @@ -6,6 +6,7 @@ import Preview from '$lib/docs/Preview.svelte'; import { longData } from '$lib/utils/genData.js'; + import { Field, ToggleGroup, ToggleOption } from 'svelte-ux'; const dataByYear = group(longData, (d) => d.year); const data = dataByYear.get(2019) ?? []; @@ -27,15 +28,24 @@ { key: 'exercise', value: 20, maxValue: 30, color: '#a3e635' }, { key: 'stand', value: 10, maxValue: 12, color: '#22d3ee' }, ]; + + let renderContext: 'svg' | 'canvas' = 'svg';

Examples

+ + + Svg + Canvas + + +

Basic

- +
@@ -43,7 +53,7 @@
- +
@@ -51,7 +61,7 @@
- +
@@ -59,7 +69,15 @@
- +
@@ -77,6 +95,7 @@ cornerRadius={10} padAngle={0.02} props={{ group: { y: 80 } }} + {renderContext} />
@@ -93,6 +112,7 @@ outerRadius={-25} innerRadius={-20} cornerRadius={10} + {renderContext} />
@@ -101,9 +121,9 @@
- + - + @@ -148,6 +168,7 @@ props={{ group: { y: 45 }, }} + {renderContext} />
@@ -164,6 +185,7 @@ innerRadius={-20} cornerRadius={5} padAngle={0.01} + {renderContext} />
@@ -179,6 +201,7 @@ outerRadius={-25} innerRadius={-20} cornerRadius={10} + {renderContext} /> @@ -196,6 +219,7 @@ innerRadius={-20} cornerRadius={10} props={{ group: { y: 70 } }} + {renderContext} /> @@ -216,6 +240,7 @@ outerRadius={-25} innerRadius={-20} cornerRadius={10} + {renderContext} /> @@ -238,6 +263,7 @@ outerRadius={-25} innerRadius={-20} cornerRadius={10} + {renderContext} /> @@ -253,6 +279,7 @@ { key: 2019, data: dataByYear.get(2019), props: { innerRadius: -20 } }, { key: 2018, data: dataByYear.get(2018), props: { outerRadius: -30 } }, ]} + {renderContext} /> @@ -261,7 +288,13 @@
- +
@@ -269,7 +302,7 @@
- +
@@ -282,6 +315,7 @@ key="fruit" value="value" legend={{ placement: 'top-left', orientation: 'vertical' }} + {renderContext} /> @@ -307,6 +341,7 @@ }} value="value" legend + {renderContext} /> @@ -325,6 +360,7 @@ 'hsl(var(--color-danger))', 'hsl(var(--color-info))', ]} + {renderContext} /> @@ -333,7 +369,7 @@
- +
@@ -341,7 +377,13 @@
- +
@@ -355,6 +397,7 @@ value="value" c="color" cRange={dataWithColor.map((d) => d.color)} + {renderContext} /> @@ -369,6 +412,7 @@ value="value" padding={{ right: 80 }} legend={{ placement: 'right', orientation: 'vertical' }} + {renderContext} /> @@ -383,6 +427,7 @@ value="value" placement="left" legend={{ placement: 'right', orientation: 'vertical' }} + {renderContext} /> @@ -397,6 +442,7 @@ value="value" placement="right" legend={{ placement: 'left', orientation: 'vertical' }} + {renderContext} /> @@ -412,6 +458,7 @@ center={false} props={{ group: { x: 200, center: 'y' } }} legend={{ placement: 'right', orientation: 'vertical' }} + {renderContext} /> @@ -420,6 +467,6 @@
- +
--> diff --git a/packages/layerchart/src/routes/docs/components/RadialGradient/+page.svelte b/packages/layerchart/src/routes/docs/components/RadialGradient/+page.svelte index 6544dfca3..23ab5919d 100644 --- a/packages/layerchart/src/routes/docs/components/RadialGradient/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/RadialGradient/+page.svelte @@ -13,16 +13,21 @@
- - + + - - + + - - + + @@ -58,16 +63,26 @@
- - + + - - + + - - + + @@ -80,15 +95,15 @@
- + {#each { length: 6 } as _, i} - + {/each} - + {#each { length: 6 } as _, i} - + {/each} diff --git a/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte b/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte index efcbd1576..0190dca0c 100644 --- a/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte @@ -1,5 +1,5 @@

Examples

+ + + Svg + Canvas + + +

Basic

- +
@@ -51,7 +61,14 @@
- +
@@ -59,7 +76,16 @@
- +
@@ -67,7 +93,7 @@
- +
@@ -89,6 +115,7 @@ ][i], }; })} + {renderContext} />
@@ -113,6 +140,7 @@ ][i], }; })} + {renderContext} />
@@ -121,7 +149,7 @@
- +
@@ -144,6 +172,7 @@ }; })} legend + {renderContext} />
@@ -168,6 +197,7 @@ }; })} legend + {renderContext} /> @@ -187,6 +217,7 @@ grid: { tweened: { duration: 200 } }, points: { tweened: { duration: 200 } }, }} + {renderContext} >
- +
@@ -229,7 +260,7 @@
- +
@@ -244,6 +275,7 @@ axis={false} grid={false} props={{ highlight: { lines: false } }} + {renderContext} > @@ -258,7 +290,7 @@
- +
- - + + - + {format(x(data), 'integer')} diff --git a/packages/layerchart/src/routes/docs/components/Spline/+page.svelte b/packages/layerchart/src/routes/docs/components/Spline/+page.svelte index b0e1efdcb..41ed18c5e 100644 --- a/packages/layerchart/src/routes/docs/components/Spline/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Spline/+page.svelte @@ -1,7 +1,7 @@