layerchart 0.60.3 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/components/Arc.svelte +83 -27
  2. package/dist/components/Area.svelte +51 -12
  3. package/dist/components/Bar.svelte +5 -1
  4. package/dist/components/Circle.svelte +54 -12
  5. package/dist/components/ComputedStyles.svelte +16 -0
  6. package/dist/components/ComputedStyles.svelte.d.ts +20 -0
  7. package/dist/components/GeoPath.svelte +36 -44
  8. package/dist/components/GeoPath.svelte.d.ts +2 -2
  9. package/dist/components/GeoPoint.svelte +27 -13
  10. package/dist/components/GeoTile.svelte +22 -7
  11. package/dist/components/Graticule.svelte.d.ts +4 -4
  12. package/dist/components/Group.svelte +46 -18
  13. package/dist/components/HitCanvas.svelte +7 -21
  14. package/dist/components/Legend.svelte.d.ts +1 -1
  15. package/dist/components/Line.svelte +70 -31
  16. package/dist/components/LinearGradient.svelte +89 -26
  17. package/dist/components/LinearGradient.svelte.d.ts +2 -2
  18. package/dist/components/Points.svelte +23 -41
  19. package/dist/components/Points.svelte.d.ts +2 -1
  20. package/dist/components/RadialGradient.svelte +86 -28
  21. package/dist/components/RadialGradient.svelte.d.ts +2 -2
  22. package/dist/components/Rect.svelte +62 -18
  23. package/dist/components/Rule.svelte +1 -1
  24. package/dist/components/Spline.svelte +95 -57
  25. package/dist/components/Text.svelte +80 -22
  26. package/dist/components/TransformControls.svelte.d.ts +1 -1
  27. package/dist/components/charts/AreaChart.svelte +14 -10
  28. package/dist/components/charts/BarChart.svelte +13 -9
  29. package/dist/components/charts/LineChart.svelte +13 -9
  30. package/dist/components/charts/PieChart.svelte +6 -2
  31. package/dist/components/charts/PieChart.svelte.d.ts +2 -1
  32. package/dist/components/charts/ScatterChart.svelte +14 -10
  33. package/dist/components/layout/Canvas.svelte +93 -11
  34. package/dist/components/layout/Canvas.svelte.d.ts +13 -0
  35. package/dist/components/layout/Svg.svelte +1 -1
  36. package/dist/components/tooltip/Tooltip.svelte.d.ts +1 -1
  37. package/dist/utils/canvas.d.ts +46 -0
  38. package/dist/utils/canvas.js +146 -0
  39. package/dist/utils/common.d.ts +10 -1
  40. package/dist/utils/common.js +13 -0
  41. package/dist/utils/index.d.ts +2 -0
  42. package/dist/utils/index.js +2 -0
  43. package/dist/utils/math.d.ts +2 -0
  44. package/dist/utils/math.js +10 -0
  45. package/dist/utils/path.d.ts +7 -0
  46. package/dist/utils/path.js +17 -3
  47. package/package.json +2 -2
@@ -7,6 +7,7 @@
7
7
 
8
8
  import Axis from '../Axis.svelte';
9
9
  import Bars from '../Bars.svelte';
10
+ import Canvas from '../layout/Canvas.svelte';
10
11
  import Chart from '../Chart.svelte';
11
12
  import Grid from '../Grid.svelte';
12
13
  import Highlight from '../Highlight.svelte';
@@ -16,7 +17,12 @@
16
17
  import Svg from '../layout/Svg.svelte';
17
18
  import * as Tooltip from '../tooltip/index.js';
18
19
 
19
- import { accessor, chartDataArray, type Accessor } from '../../utils/common.js';
20
+ import {
21
+ accessor,
22
+ chartDataArray,
23
+ defaultChartPadding,
24
+ type Accessor,
25
+ } from '../../utils/common.js';
20
26
 
21
27
  type ChartProps = ComponentProps<Chart<TData>>;
22
28
 
@@ -32,6 +38,7 @@
32
38
  rule?: typeof rule;
33
39
  series?: typeof series;
34
40
  seriesLayout?: typeof seriesLayout;
41
+ renderContext?: typeof renderContext;
35
42
  }
36
43
 
37
44
  export let data: $$Props['data'] = [];
@@ -111,6 +118,8 @@
111
118
  labels?: Partial<ComponentProps<Labels>>;
112
119
  } = {};
113
120
 
121
+ export let renderContext: 'svg' | 'canvas' = 'svg';
122
+
114
123
  $: allSeriesData = series
115
124
  .flatMap((s) =>
116
125
  s.data?.map((d) => {
@@ -195,12 +204,7 @@
195
204
  {y1Range}
196
205
  c={isVertical ? y : x}
197
206
  cRange={['hsl(var(--color-primary))']}
198
- padding={axis === false
199
- ? undefined
200
- : {
201
- left: axis === true || axis === 'y' ? 16 : 0,
202
- bottom: (axis === true || axis === 'x' ? 16 : 0) + (legend === true ? 32 : 0),
203
- }}
207
+ padding={defaultChartPadding(axis, legend)}
204
208
  tooltip={{ mode: 'band' }}
205
209
  {...$$restProps}
206
210
  let:x
@@ -229,7 +233,7 @@
229
233
  getBarsProps,
230
234
  }}
231
235
  <slot {...slotProps}>
232
- <Svg>
236
+ <svelte:component this={renderContext === 'canvas' ? Canvas : Svg}>
233
237
  <slot name="grid" {...slotProps}>
234
238
  {#if grid}
235
239
  <Grid
@@ -301,7 +305,7 @@
301
305
  {#if labels}
302
306
  <Labels {...props.labels} {...typeof labels === 'object' ? labels : null} />
303
307
  {/if}
304
- </Svg>
308
+ </svelte:component>
305
309
 
306
310
  <slot name="legend" {...slotProps}>
307
311
  {#if legend}
@@ -4,6 +4,7 @@
4
4
  import { format } from '@layerstack/utils';
5
5
 
6
6
  import Axis from '../Axis.svelte';
7
+ import Canvas from '../layout/Canvas.svelte';
7
8
  import Chart from '../Chart.svelte';
8
9
  import Grid from '../Grid.svelte';
9
10
  import Highlight from '../Highlight.svelte';
@@ -15,7 +16,12 @@
15
16
  import Svg from '../layout/Svg.svelte';
16
17
  import * as Tooltip from '../tooltip/index.js';
17
18
 
18
- import { accessor, chartDataArray, type Accessor } from '../../utils/common.js';
19
+ import {
20
+ accessor,
21
+ chartDataArray,
22
+ defaultChartPadding,
23
+ type Accessor,
24
+ } from '../../utils/common.js';
19
25
 
20
26
  interface $$Props extends ComponentProps<Chart<TData>> {
21
27
  axis?: typeof axis;
@@ -26,6 +32,7 @@
26
32
  props?: typeof props;
27
33
  rule?: typeof rule;
28
34
  series?: typeof series;
35
+ renderContext?: typeof renderContext;
29
36
  }
30
37
 
31
38
  export let data: $$Props['data'] = [];
@@ -65,6 +72,8 @@
65
72
  points?: Partial<ComponentProps<Points>>;
66
73
  } = {};
67
74
 
75
+ export let renderContext: 'svg' | 'canvas' = 'svg';
76
+
68
77
  $: allSeriesData = series
69
78
  .flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d })))
70
79
  .filter((d) => d) as Array<TData & { stackData?: any }>;
@@ -99,12 +108,7 @@
99
108
  yBaseline={0}
100
109
  yNice
101
110
  {radial}
102
- padding={radial || axis === false
103
- ? undefined
104
- : {
105
- left: axis === true || axis === 'y' ? 16 : 0,
106
- bottom: (axis === true || axis === 'x' ? 16 : 0) + (legend === true ? 32 : 0),
107
- }}
111
+ padding={radial ? undefined : defaultChartPadding(axis, legend)}
108
112
  tooltip={{ mode: 'bisect-x' }}
109
113
  {...$$restProps}
110
114
  let:x
@@ -133,7 +137,7 @@
133
137
  getSplineProps,
134
138
  }}
135
139
  <slot {...slotProps}>
136
- <Svg center={radial}>
140
+ <svelte:component this={renderContext === 'canvas' ? Canvas : Svg} center={radial}>
137
141
  <slot name="grid" {...slotProps}>
138
142
  {#if grid}
139
143
  <Grid x={radial} y {...typeof grid === 'object' ? grid : null} {...props.grid} />
@@ -203,7 +207,7 @@
203
207
  />
204
208
  {/each}
205
209
  </slot>
206
- </Svg>
210
+ </svelte:component>
207
211
 
208
212
  <slot name="legend" {...slotProps}>
209
213
  {#if legend}
@@ -4,6 +4,7 @@
4
4
  import { format } from '@layerstack/utils';
5
5
 
6
6
  import Arc from '../Arc.svelte';
7
+ import Canvas from '../layout/Canvas.svelte';
7
8
  import Chart from '../Chart.svelte';
8
9
  import Group from '../Group.svelte';
9
10
  import Legend from '../Legend.svelte';
@@ -30,6 +31,7 @@
30
31
  range?: typeof range;
31
32
  series?: typeof series;
32
33
  value?: typeof label;
34
+ renderContext?: typeof renderContext;
33
35
  }
34
36
 
35
37
  export let data: ChartProps['data'] = [];
@@ -98,6 +100,8 @@
98
100
  legend?: Partial<ComponentProps<Legend>>;
99
101
  } = {};
100
102
 
103
+ export let renderContext: 'svg' | 'canvas' = 'svg';
104
+
101
105
  $: allSeriesData = series
102
106
  .flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d })))
103
107
  .filter((d) => d) as Array<TData>;
@@ -151,7 +155,7 @@
151
155
  tooltip,
152
156
  }}
153
157
  <slot {...slotProps}>
154
- <Svg {center}>
158
+ <svelte:component this={renderContext === 'canvas' ? Canvas : Svg} {center}>
155
159
  <slot name="belowMarks" {...slotProps} />
156
160
 
157
161
  <slot name="marks" {...slotProps}>
@@ -216,7 +220,7 @@
216
220
  </slot>
217
221
 
218
222
  <slot name="aboveMarks" {...slotProps} />
219
- </Svg>
223
+ </svelte:component>
220
224
 
221
225
  <slot name="legend" {...slotProps}>
222
226
  {#if legend}
@@ -207,7 +207,7 @@ declare class __sveltets_Render<TData> {
207
207
  tickValues?: any[] | undefined | undefined;
208
208
  tickFontSize?: number | undefined;
209
209
  tickLength?: number | undefined;
210
- placement?: ("center" | "left" | "right" | "bottom" | "top" | "top-left" | "top-right" | "bottom-left" | "bottom-right") | undefined;
210
+ placement?: ("center" | "bottom" | "left" | "right" | "top" | "top-left" | "top-right" | "bottom-left" | "bottom-right") | undefined;
211
211
  orientation?: "horizontal" | "vertical" | undefined;
212
212
  onClick?: ((tick: any) => any) | undefined | undefined;
213
213
  variant?: "ramp" | "swatches" | undefined;
@@ -244,6 +244,7 @@ declare class __sveltets_Render<TData> {
244
244
  props?: Partial<ComponentProps<Arc>>;
245
245
  }[] | undefined;
246
246
  value?: Accessor<TData>;
247
+ renderContext?: "canvas" | "svg";
247
248
  };
248
249
  events(): {} & {
249
250
  [evt: string]: CustomEvent<any>;
@@ -4,6 +4,7 @@
4
4
  import { format } from '@layerstack/utils';
5
5
 
6
6
  import Axis from '../Axis.svelte';
7
+ import Canvas from '../layout/Canvas.svelte';
7
8
  import Chart from '../Chart.svelte';
8
9
  import Grid from '../Grid.svelte';
9
10
  import Highlight from '../Highlight.svelte';
@@ -14,7 +15,12 @@
14
15
  import Svg from '../layout/Svg.svelte';
15
16
  import * as Tooltip from '../tooltip/index.js';
16
17
 
17
- import { accessor, chartDataArray, type Accessor } from '../../utils/common.js';
18
+ import {
19
+ accessor,
20
+ chartDataArray,
21
+ defaultChartPadding,
22
+ type Accessor,
23
+ } from '../../utils/common.js';
18
24
 
19
25
  interface $$Props extends ComponentProps<Chart<TData>> {
20
26
  axis?: typeof axis;
@@ -23,6 +29,7 @@
23
29
  legend?: typeof legend;
24
30
  props?: typeof props;
25
31
  series?: typeof series;
32
+ renderContext?: typeof renderContext;
26
33
  }
27
34
 
28
35
  export let data: $$Props['data'] = [];
@@ -55,6 +62,8 @@
55
62
  rule?: Partial<ComponentProps<Rule>>;
56
63
  } = {};
57
64
 
65
+ export let renderContext: 'svg' | 'canvas' = 'svg';
66
+
58
67
  // Default xScale based on first data's `x` value
59
68
  $: xScale =
60
69
  $$props.xScale ??
@@ -74,7 +83,7 @@
74
83
  data: s.data,
75
84
  stroke: s.color,
76
85
  fill: s.color,
77
- 'fill-opacity': 0.3,
86
+ fillOpacity: 0.3,
78
87
  ...props.points,
79
88
  ...s.props,
80
89
  };
@@ -90,12 +99,7 @@
90
99
  {y}
91
100
  {yScale}
92
101
  yNice
93
- padding={axis === false
94
- ? undefined
95
- : {
96
- left: axis === true || axis === 'y' ? 16 : 0,
97
- bottom: (axis === true || axis === 'x' ? 16 : 0) + (legend === true ? 32 : 0),
98
- }}
102
+ padding={defaultChartPadding(axis, legend)}
99
103
  tooltip={{ mode: 'voronoi' }}
100
104
  {...$$restProps}
101
105
  let:x
@@ -117,7 +121,7 @@
117
121
  : null}
118
122
 
119
123
  <slot {...slotProps}>
120
- <Svg>
124
+ <svelte:component this={renderContext === 'canvas' ? Canvas : Svg}>
121
125
  <slot name="grid" {...slotProps}>
122
126
  {#if grid}
123
127
  <Grid x y {...typeof grid === 'object' ? grid : null} {...props.grid} />
@@ -171,7 +175,7 @@
171
175
  {...typeof labels === 'object' ? labels : null}
172
176
  />
173
177
  {/if}
174
- </Svg>
178
+ </svelte:component>
175
179
 
176
180
  <slot name="legend" {...slotProps}>
177
181
  {#if legend}
@@ -1,11 +1,36 @@
1
+ <script lang="ts" context="module">
2
+ import { getContext, onDestroy, setContext } from 'svelte';
3
+
4
+ type ComponentRender = {
5
+ name: string;
6
+ render: (ctx: CanvasRenderingContext2D) => any;
7
+ retainState?: boolean;
8
+ };
9
+
10
+ export type CanvasContext = {
11
+ /** Register component to render. Returns method to unregister on component destory */
12
+ register(component: ComponentRender): () => void;
13
+ invalidate(): void;
14
+ };
15
+
16
+ export const canvasContextKey = Symbol();
17
+
18
+ export function getCanvasContext() {
19
+ return getContext<CanvasContext>(canvasContextKey);
20
+ }
21
+
22
+ function setCanvasContext(context: CanvasContext) {
23
+ setContext(canvasContextKey, context);
24
+ }
25
+ </script>
26
+
1
27
  <script lang="ts">
2
- import { onMount, setContext } from 'svelte';
3
- import { writable } from 'svelte/store';
4
- import { scaleCanvas } from 'layercake';
28
+ import { onMount } from 'svelte';
5
29
  import { cls } from '@layerstack/tailwind';
6
30
 
7
31
  import { chartContext } from '../ChartContext.svelte';
8
32
  import { transformContext } from '../TransformContext.svelte';
33
+ import { scaleCanvas } from '../../utils/canvas.js';
9
34
 
10
35
  const { width, height, containerWidth, containerHeight, padding } = chartContext();
11
36
 
@@ -38,21 +63,44 @@
38
63
  /** A string passed to `aria-describedby` property on the `<canvas>` tag. */
39
64
  export let describedBy: string | undefined = undefined;
40
65
 
41
- const ctx = writable({});
66
+ /**
67
+ * Translate children to center (useful for radial layouts)
68
+ */
69
+ export let center: boolean | 'x' | 'y' = false;
70
+
71
+ let components = new Map<Symbol, ComponentRender>();
72
+ let pendingInvalidation = false;
73
+ let frameId: number | undefined;
74
+
75
+ const { mode, scale, translate } = transformContext();
42
76
 
43
77
  onMount(() => {
44
78
  context = element?.getContext('2d', { willReadFrequently }) as CanvasRenderingContext2D;
45
79
  });
46
80
 
47
- const { mode, scale, translate } = transformContext();
81
+ onDestroy(() => {
82
+ if (frameId) {
83
+ cancelAnimationFrame(frameId);
84
+ }
85
+ });
48
86
 
49
- $: if (context) {
87
+ function update() {
88
+ if (!context) return;
89
+ // TODO: only `scaleCanvas()` when containerWidth/Height change (not all invalidations)
90
+ // scaleCanvas in `update()` to fix `requestAnimationFrame()` timing causing flash of blank canvas
50
91
  scaleCanvas(context, $containerWidth, $containerHeight);
92
+
51
93
  context.clearRect(0, 0, $containerWidth, $containerHeight);
52
94
 
53
95
  context.translate($padding.left ?? 0, $padding.top ?? 0);
54
96
 
55
- if (mode === 'canvas') {
97
+ if (center) {
98
+ const newTranslate = {
99
+ x: center === 'x' || center === true ? $width / 2 : 0,
100
+ y: center === 'y' || center === true ? $height / 2 : 0,
101
+ };
102
+ context.translate(newTranslate.x, newTranslate.y);
103
+ } else if (mode === 'canvas') {
56
104
  const center = { x: $width / 2, y: $height / 2 };
57
105
  const newTranslate = {
58
106
  x: $translate.x * $scale + center.x - center.x * $scale,
@@ -62,12 +110,46 @@
62
110
  context.scale($scale, $scale);
63
111
  }
64
112
 
65
- // Force children to re-draw
66
- $ctx = context;
113
+ components.forEach((c) => {
114
+ if (c.retainState) {
115
+ // Do not call save/restore canvas draw state (https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/save) (ex. Group ctx.translate() affecting children)
116
+ c.render(context);
117
+ } else {
118
+ context.save();
119
+ c.render(context);
120
+ context.restore();
121
+ }
122
+ });
123
+
124
+ pendingInvalidation = false;
125
+ }
126
+
127
+ const canvasContext: CanvasContext = {
128
+ register(component) {
129
+ const key = Symbol();
130
+ components.set(key, component);
131
+ this.invalidate();
132
+
133
+ // Unregister
134
+ return () => {
135
+ components.delete(key);
136
+ this.invalidate();
137
+ };
138
+ },
139
+ invalidate() {
140
+ if (pendingInvalidation) return;
141
+ pendingInvalidation = true;
142
+ frameId = requestAnimationFrame(update);
143
+ },
144
+ };
145
+
146
+ $: {
147
+ // Redraw when resized
148
+ $containerWidth, $containerHeight;
149
+ canvasContext.invalidate();
67
150
  }
68
151
 
69
- $: ctx.set(context);
70
- setContext('canvas', { ctx });
152
+ setCanvasContext(canvasContext);
71
153
  </script>
72
154
 
73
155
  <canvas
@@ -1,4 +1,16 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
+ type ComponentRender = {
3
+ name: string;
4
+ render: (ctx: CanvasRenderingContext2D) => any;
5
+ retainState?: boolean;
6
+ };
7
+ export type CanvasContext = {
8
+ /** Register component to render. Returns method to unregister on component destory */
9
+ register(component: ComponentRender): () => void;
10
+ invalidate(): void;
11
+ };
12
+ export declare const canvasContextKey: unique symbol;
13
+ export declare function getCanvasContext(): CanvasContext;
2
14
  declare const __propDef: {
3
15
  props: {
4
16
  [x: string]: any;
@@ -11,6 +23,7 @@ declare const __propDef: {
11
23
  label?: string | undefined | undefined;
12
24
  labelledBy?: string | undefined | undefined;
13
25
  describedBy?: string | undefined | undefined;
26
+ center?: boolean | "x" | "y" | undefined;
14
27
  };
15
28
  events: {
16
29
  pointerenter: PointerEvent;
@@ -32,7 +32,7 @@
32
32
  export let title: string | undefined = undefined;
33
33
 
34
34
  /**
35
- * Translate children to center, useful for radial layouts
35
+ * Translate children to center (useful for radial layouts)
36
36
  */
37
37
  export let center: boolean | 'x' | 'y' = false;
38
38
 
@@ -6,7 +6,7 @@ declare const __propDef: {
6
6
  y?: "pointer" | "data" | number | undefined | undefined;
7
7
  xOffset?: number | undefined;
8
8
  yOffset?: number | undefined;
9
- anchor?: ("center" | "left" | "right" | "bottom" | "top" | "top-left" | "top-right" | "bottom-left" | "bottom-right") | undefined;
9
+ anchor?: ("center" | "bottom" | "left" | "right" | "top" | "top-left" | "top-right" | "bottom-left" | "bottom-right") | undefined;
10
10
  contained?: "container" | false | undefined;
11
11
  variant?: "default" | "invert" | "none" | undefined;
12
12
  motion?: boolean | undefined;
@@ -0,0 +1,46 @@
1
+ export declare const DEFAULT_FILL = "rgb(0, 0, 0)";
2
+ type ComputedStylesOptions = {
3
+ styles?: Partial<Omit<CSSStyleDeclaration, 'fillOpacity' | 'strokeWidth' | 'opacity'> & {
4
+ fillOpacity?: number | string;
5
+ strokeWidth?: number | string;
6
+ opacity?: number | string;
7
+ }>;
8
+ classes?: string;
9
+ };
10
+ /**
11
+ * Appends or reuses `<svg>` element below `<canvas>` to resolve CSS variables and classes (ex. `stroke: hsl(var(--color-primary))` => `stroke: rgb(...)` )
12
+ */
13
+ export declare function getComputedStyles(canvas: HTMLCanvasElement, { styles, classes }?: ComputedStylesOptions): CSSStyleDeclaration;
14
+ /** Render SVG path data onto canvas context. Supports CSS variables and classes by tranferring to hidden `<svg>` element before retrieval) */
15
+ export declare function renderPathData(canvasCtx: CanvasRenderingContext2D, pathData: string | null | undefined, styleOptions?: ComputedStylesOptions): void;
16
+ export declare function renderText(canvasCtx: CanvasRenderingContext2D, text: string | number | null | undefined, coords: {
17
+ x: number;
18
+ y: number;
19
+ }, styleOptions?: ComputedStylesOptions): void;
20
+ export declare function renderRect(canvasCtx: CanvasRenderingContext2D, coords: {
21
+ x: number;
22
+ y: number;
23
+ width: number;
24
+ height: number;
25
+ }, styleOptions?: ComputedStylesOptions): void;
26
+ /** Clear canvas accounting for Canvas `context.translate(...)` */
27
+ export declare function clearCanvasContext(canvasCtx: CanvasRenderingContext2D, options: {
28
+ containerWidth: number;
29
+ containerHeight: number;
30
+ padding: {
31
+ top: number;
32
+ bottom: number;
33
+ left: number;
34
+ right: number;
35
+ };
36
+ }): void;
37
+ /**
38
+ Scales a canvas for high DPI / retina displays.
39
+ @see: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#examples
40
+ @see: https://web.dev/articles/canvas-hidipi
41
+ */
42
+ export declare function scaleCanvas(ctx: CanvasRenderingContext2D, width: number, height: number): {
43
+ width: number;
44
+ height: number;
45
+ };
46
+ export {};
@@ -0,0 +1,146 @@
1
+ export const DEFAULT_FILL = 'rgb(0, 0, 0)';
2
+ const CANVAS_STYLES_ELEMENT_ID = '__layerchart_canvas_styles_id';
3
+ /**
4
+ * Appends or reuses `<svg>` element below `<canvas>` to resolve CSS variables and classes (ex. `stroke: hsl(var(--color-primary))` => `stroke: rgb(...)` )
5
+ */
6
+ export function getComputedStyles(canvas, { styles, classes } = {}) {
7
+ try {
8
+ // Get or create `<svg>` below `<canvas>`
9
+ let svg = document.getElementById(CANVAS_STYLES_ELEMENT_ID);
10
+ if (!svg) {
11
+ svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
12
+ svg.setAttribute('id', CANVAS_STYLES_ELEMENT_ID);
13
+ svg.style.display = 'none';
14
+ // Add `<svg>` next to `<canvas>` to allow same scope resolution for CSS variables
15
+ canvas.after(svg);
16
+ }
17
+ svg = svg; // guarantee SVG is set
18
+ // 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
19
+ svg.removeAttribute('style');
20
+ svg.removeAttribute('class');
21
+ // Add styles and class to svg element
22
+ if (styles) {
23
+ Object.assign(svg.style, styles);
24
+ }
25
+ if (classes) {
26
+ svg.setAttribute('class', classes);
27
+ }
28
+ const computedStyles = window.getComputedStyle(svg);
29
+ return computedStyles;
30
+ }
31
+ catch (e) {
32
+ console.error('Unable to get computed styles', e);
33
+ return {};
34
+ }
35
+ }
36
+ /** Render onto canvas context. Supports CSS variables and classes by tranferring to hidden `<svg>` element before retrieval) */
37
+ function render(canvasCtx, render, styleOptions = {}) {
38
+ // TODO: Consider memoizing? How about reactiving to CSS variable changes (light/dark mode toggle)
39
+ const computedStyles = getComputedStyles(canvasCtx.canvas, styleOptions);
40
+ // Adhere to CSS paint order: https://developer.mozilla.org/en-US/docs/Web/CSS/paint-order
41
+ const paintOrder = computedStyles?.paintOrder === 'stroke' ? ['stroke', 'fill'] : ['fill', 'stroke'];
42
+ if (computedStyles?.opacity) {
43
+ canvasCtx.globalAlpha = Number(computedStyles?.opacity);
44
+ }
45
+ // Text properties
46
+ canvasCtx.font = `${computedStyles.fontSize} ${computedStyles.fontFamily}`; // build string instead of using `computedStyles.font` to fix/workaround `tabular-nums` returning `null`
47
+ // TODO: Hack to handle `textAnchor` with canvas. Try to find a better approach
48
+ if (computedStyles.textAnchor === 'middle') {
49
+ canvasCtx.textAlign = 'center';
50
+ }
51
+ else if (computedStyles.textAnchor === 'end') {
52
+ canvasCtx.textAlign = 'right';
53
+ }
54
+ else {
55
+ canvasCtx.textAlign = computedStyles.textAlign; // TODO: Handle/map `justify` and `match-parent`?
56
+ }
57
+ // TODO: Handle `textBaseline` / `verticalAnchor` (Text)
58
+ // canvasCtx.textBaseline = 'top';
59
+ // canvasCtx.textBaseline = 'middle';
60
+ // canvasCtx.textBaseline = 'bottom';
61
+ // canvasCtx.textBaseline = 'alphabetic';
62
+ // canvasCtx.textBaseline = 'hanging';
63
+ // canvasCtx.textBaseline = 'ideographic';
64
+ // Dashed lines
65
+ if (computedStyles.strokeDasharray.includes(',')) {
66
+ const dashArray = computedStyles.strokeDasharray
67
+ .split(',')
68
+ .map((s) => Number(s.replace('px', '')));
69
+ canvasCtx.setLineDash(dashArray);
70
+ }
71
+ paintOrder.forEach((attr) => {
72
+ if (attr === 'fill') {
73
+ const fill = styleOptions.styles?.fill instanceof CanvasGradient
74
+ ? styleOptions.styles?.fill
75
+ : ['none', DEFAULT_FILL].includes(computedStyles?.fill)
76
+ ? null
77
+ : computedStyles?.fill;
78
+ if (fill) {
79
+ const currentGlobalAlpha = canvasCtx.globalAlpha;
80
+ if (computedStyles?.fillOpacity) {
81
+ canvasCtx.globalAlpha = Number(computedStyles?.fillOpacity);
82
+ }
83
+ canvasCtx.fillStyle = fill;
84
+ render.fill(canvasCtx);
85
+ // Restore in case it was modified by `fillOpacity`
86
+ canvasCtx.globalAlpha = currentGlobalAlpha;
87
+ }
88
+ }
89
+ else if (attr === 'stroke') {
90
+ const stroke = styleOptions.styles?.stroke instanceof CanvasGradient
91
+ ? styleOptions.styles?.stroke
92
+ : computedStyles?.stroke === 'none'
93
+ ? null
94
+ : computedStyles?.stroke;
95
+ if (stroke) {
96
+ canvasCtx.lineWidth =
97
+ typeof computedStyles?.strokeWidth === 'string'
98
+ ? Number(computedStyles?.strokeWidth?.replace('px', ''))
99
+ : (computedStyles?.strokeWidth ?? 1);
100
+ canvasCtx.strokeStyle = stroke;
101
+ render.stroke(canvasCtx);
102
+ }
103
+ }
104
+ });
105
+ }
106
+ /** Render SVG path data onto canvas context. Supports CSS variables and classes by tranferring to hidden `<svg>` element before retrieval) */
107
+ export function renderPathData(canvasCtx, pathData, styleOptions = {}) {
108
+ const path = new Path2D(pathData ?? '');
109
+ render(canvasCtx, {
110
+ fill: (ctx) => ctx.fill(path),
111
+ stroke: (ctx) => ctx.stroke(path),
112
+ }, styleOptions);
113
+ }
114
+ export function renderText(canvasCtx, text, coords, styleOptions = {}) {
115
+ if (text) {
116
+ render(canvasCtx, {
117
+ fill: (ctx) => ctx.fillText(text.toString(), coords.x, coords.y),
118
+ stroke: (ctx) => ctx.strokeText(text.toString(), coords.x, coords.y),
119
+ }, styleOptions);
120
+ }
121
+ }
122
+ export function renderRect(canvasCtx, coords, styleOptions = {}) {
123
+ render(canvasCtx, {
124
+ fill: (ctx) => ctx.fillRect(coords.x, coords.y, coords.width, coords.height),
125
+ stroke: (ctx) => ctx.strokeRect(coords.x, coords.y, coords.width, coords.height),
126
+ }, styleOptions);
127
+ }
128
+ /** Clear canvas accounting for Canvas `context.translate(...)` */
129
+ export function clearCanvasContext(canvasCtx, options) {
130
+ // Clear with negative offset due to Canvas `context.translate(...)`
131
+ canvasCtx.clearRect(-options.padding.left, -options.padding.top, options.containerWidth, options.containerHeight);
132
+ }
133
+ /**
134
+ Scales a canvas for high DPI / retina displays.
135
+ @see: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#examples
136
+ @see: https://web.dev/articles/canvas-hidipi
137
+ */
138
+ export function scaleCanvas(ctx, width, height) {
139
+ const devicePixelRatio = window.devicePixelRatio || 1;
140
+ ctx.canvas.width = width * devicePixelRatio;
141
+ ctx.canvas.height = height * devicePixelRatio;
142
+ ctx.canvas.style.width = `${width}px`;
143
+ ctx.canvas.style.height = `${height}px`;
144
+ ctx.scale(devicePixelRatio, devicePixelRatio);
145
+ return { width: ctx.canvas.width, height: ctx.canvas.height };
146
+ }
@@ -1,6 +1,15 @@
1
- import type Chart from '../components/Chart.svelte';
2
1
  import type { ComponentProps } from 'svelte';
2
+ import type Chart from '../components/Chart.svelte';
3
+ import type LineChart from '../components/charts/LineChart.svelte';
3
4
  export type Accessor<TData = any> = number | string | ((d: TData) => any) | undefined | null | Accessor<TData>[];
4
5
  export declare function accessor<TData = any>(prop: Accessor<TData>): (d: TData) => any;
5
6
  /** Guarantee chart data is an array */
6
7
  export declare function chartDataArray<TData = any>(data: ComponentProps<Chart<TData>>['data']): any[];
8
+ type SimplifiedChartProps = ComponentProps<LineChart<any>>;
9
+ export declare function defaultChartPadding(axis: SimplifiedChartProps['axis'], legend: SimplifiedChartProps['legend']): {
10
+ top: number;
11
+ left: number;
12
+ bottom: number;
13
+ right: number;
14
+ } | undefined;
15
+ export {};