From 423e1bde23b1c66b1987f353212131ea8f1fe8f4 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 27 Apr 2026 14:54:50 -0400 Subject: [PATCH 01/36] split Circle.svelte into 3 layer-specific components (Circle.svg.svelte, etc) along with CircleState (Circle.shared.svelte.ts). Keep Circle.svelte and delegate to underlying type --- bundle-analyzer/bundle-analyzer.ts | 5 +- bundle-analyzer/bundle-reports/latest.json | 122 +++-- bundle-analyzer/define-scenarios.ts | 40 ++ docs/src/content/guides/bundle-size.md | 122 +++++ packages/layerchart/package.json | 15 + packages/layerchart/src/lib/canvas.ts | 11 + .../src/lib/components/Circle.canvas.svelte | 126 +++++ .../src/lib/components/Circle.html.svelte | 113 ++++ .../lib/components/Circle.shared.svelte.ts | 288 +++++++++++ .../src/lib/components/Circle.svelte | 489 +----------------- .../src/lib/components/Circle.svg.svelte | 91 ++++ ...olve-data-driven-fill-through-cScale-1.png | Bin 2371 -> 2332 bytes ...olve-data-driven-fill-through-cScale-2.png | Bin 2371 -> 2332 bytes packages/layerchart/src/lib/html.ts | 11 + packages/layerchart/src/lib/svg.ts | 13 + 15 files changed, 930 insertions(+), 516 deletions(-) create mode 100644 docs/src/content/guides/bundle-size.md create mode 100644 packages/layerchart/src/lib/canvas.ts create mode 100644 packages/layerchart/src/lib/components/Circle.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Circle.html.svelte create mode 100644 packages/layerchart/src/lib/components/Circle.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Circle.svg.svelte create mode 100644 packages/layerchart/src/lib/html.ts create mode 100644 packages/layerchart/src/lib/svg.ts diff --git a/bundle-analyzer/bundle-analyzer.ts b/bundle-analyzer/bundle-analyzer.ts index a6324253a..976ba0c9e 100644 --- a/bundle-analyzer/bundle-analyzer.ts +++ b/bundle-analyzer/bundle-analyzer.ts @@ -190,9 +190,12 @@ import * as LayerChartGraph from "layerchart/graph"; `; } else { // Group imports by source module (root vs each sub-path). + // Per-scenario `subpathOverrides` win over the default mapping — + // used to test layer-specific variants like `layerchart/svg`. const groups = new Map([["layerchart", []]]); for (const name of scenario.imports) { - const sub = SUBPATH_FOR_COMPONENT[name]; + const overrideSub = scenario.subpathOverrides?.[name]; + const sub = overrideSub ?? SUBPATH_FOR_COMPONENT[name]; const mod = sub ? `layerchart/${sub}` : "layerchart"; const list = groups.get(mod) ?? []; list.push(name); diff --git a/bundle-analyzer/bundle-reports/latest.json b/bundle-analyzer/bundle-reports/latest.json index ec404b5a3..d9757ba09 100644 --- a/bundle-analyzer/bundle-reports/latest.json +++ b/bundle-analyzer/bundle-reports/latest.json @@ -1,12 +1,12 @@ { - "timestamp": "2026-04-27T15:20:04.413Z", + "timestamp": "2026-04-27T18:45:12.060Z", "results": [ { "scenario": "core", "description": "Bare minimum: Chart context + Svg layer", "group": "Foundation", - "size": 442098, - "gzipSize": 107778, + "size": 445128, + "gzipSize": 108465, "imports": [ "Chart", "Svg" @@ -16,8 +16,8 @@ "scenario": "canvas", "description": "Canvas-based rendering", "group": "Foundation", - "size": 442098, - "gzipSize": 107781, + "size": 445128, + "gzipSize": 108462, "imports": [ "Chart", "Canvas" @@ -27,8 +27,8 @@ "scenario": "line-chart", "description": "Basic line chart with axes and grid", "group": "Cartesian charts", - "size": 442122, - "gzipSize": 107791, + "size": 445152, + "gzipSize": 108476, "imports": [ "Chart", "Svg", @@ -41,8 +41,8 @@ "scenario": "line-chart-interactive", "description": "Line chart with tooltip and highlight", "group": "Cartesian charts", - "size": 456691, - "gzipSize": 110228, + "size": 459721, + "gzipSize": 111180, "imports": [ "Chart", "Svg", @@ -57,8 +57,8 @@ "scenario": "area-chart", "description": "Area chart with axes", "group": "Cartesian charts", - "size": 466669, - "gzipSize": 113523, + "size": 469700, + "gzipSize": 114550, "imports": [ "Chart", "Svg", @@ -71,8 +71,8 @@ "scenario": "bar-chart", "description": "Bar chart with axes", "group": "Cartesian charts", - "size": 450954, - "gzipSize": 109885, + "size": 453984, + "gzipSize": 110796, "imports": [ "Chart", "Svg", @@ -85,8 +85,8 @@ "scenario": "scatter-chart", "description": "Scatter plot with points", "group": "Cartesian charts", - "size": 446347, - "gzipSize": 108930, + "size": 449378, + "gzipSize": 109622, "imports": [ "Chart", "Svg", @@ -100,8 +100,8 @@ "scenario": "pie-chart", "description": "Pie/donut chart with arcs", "group": "Cartesian charts", - "size": 449147, - "gzipSize": 109598, + "size": 452177, + "gzipSize": 110265, "imports": [ "Chart", "Svg", @@ -114,8 +114,8 @@ "scenario": "high-level-charts", "description": "All high-level chart components (LineChart, BarChart, etc.)", "group": "Cartesian charts", - "size": 539736, - "gzipSize": 129920, + "size": 542767, + "gzipSize": 130577, "imports": [ "LineChart", "AreaChart", @@ -129,8 +129,8 @@ "scenario": "geo", "description": "Geographic map with paths", "group": "Geo", - "size": 457050, - "gzipSize": 111245, + "size": 460080, + "gzipSize": 111946, "imports": [ "Chart", "Svg", @@ -143,8 +143,8 @@ "scenario": "geo-tiles", "description": "Geographic map with tile layer", "group": "Geo", - "size": 461497, - "gzipSize": 112807, + "size": 464527, + "gzipSize": 113459, "imports": [ "Chart", "Svg", @@ -158,8 +158,8 @@ "scenario": "geo-full", "description": "Full geo setup with all geo components", "group": "Geo", - "size": 509302, - "gzipSize": 126141, + "size": 512332, + "gzipSize": 126809, "imports": [ "Chart", "Svg", @@ -181,8 +181,8 @@ "scenario": "hierarchy-tree", "description": "Tree layout with links", "group": "Hierarchy", - "size": 468353, - "gzipSize": 114736, + "size": 471383, + "gzipSize": 115397, "imports": [ "Chart", "Svg", @@ -196,8 +196,8 @@ "scenario": "hierarchy-treemap", "description": "Treemap layout", "group": "Hierarchy", - "size": 448537, - "gzipSize": 109814, + "size": 451567, + "gzipSize": 110477, "imports": [ "Chart", "Svg", @@ -211,8 +211,8 @@ "scenario": "hierarchy-pack", "description": "Circle packing layout", "group": "Hierarchy", - "size": 448357, - "gzipSize": 109912, + "size": 451387, + "gzipSize": 110577, "imports": [ "Chart", "Svg", @@ -225,8 +225,8 @@ "scenario": "force", "description": "Force-directed graph layout", "group": "Graph / network", - "size": 470805, - "gzipSize": 115559, + "size": 473835, + "gzipSize": 116221, "imports": [ "Chart", "Svg", @@ -240,8 +240,8 @@ "scenario": "dagre", "description": "Dagre directed graph layout", "group": "Graph / network", - "size": 528235, - "gzipSize": 131007, + "size": 531265, + "gzipSize": 131636, "imports": [ "Chart", "Svg", @@ -255,8 +255,8 @@ "scenario": "sankey", "description": "Sankey flow diagram", "group": "Graph / network", - "size": 470462, - "gzipSize": 114901, + "size": 473493, + "gzipSize": 115556, "imports": [ "Chart", "Svg", @@ -270,8 +270,8 @@ "scenario": "chord", "description": "Chord diagram", "group": "Graph / network", - "size": 451080, - "gzipSize": 109952, + "size": 454110, + "gzipSize": 110645, "imports": [ "Chart", "Svg", @@ -279,12 +279,52 @@ "Ribbon" ] }, + { + "scenario": "circle-agnostic", + "description": "Standalone Circle (agnostic) — baseline", + "group": "Layer-specific", + "size": 69174, + "gzipSize": 17535, + "imports": [ + "Circle" + ] + }, + { + "scenario": "circle-svg", + "description": "Standalone Circle from `layerchart/svg`", + "group": "Layer-specific", + "size": 55226, + "gzipSize": 13543, + "imports": [ + "Circle" + ] + }, + { + "scenario": "circle-canvas", + "description": "Standalone Circle from `layerchart/canvas`", + "group": "Layer-specific", + "size": 64018, + "gzipSize": 16324, + "imports": [ + "Circle" + ] + }, + { + "scenario": "circle-html", + "description": "Standalone Circle from `layerchart/html`", + "group": "Layer-specific", + "size": 55863, + "gzipSize": 13680, + "imports": [ + "Circle" + ] + }, { "scenario": "all", "description": "Everything from layerchart (worst case)", "group": "Worst case", - "size": 986243, - "gzipSize": 243851, + "size": 989273, + "gzipSize": 244522, "imports": [ "*" ] diff --git a/bundle-analyzer/define-scenarios.ts b/bundle-analyzer/define-scenarios.ts index 2d1d50918..a2b89b21e 100644 --- a/bundle-analyzer/define-scenarios.ts +++ b/bundle-analyzer/define-scenarios.ts @@ -16,6 +16,14 @@ export interface Scenario { extraImports?: string[]; /** Optional grouping label used to organize scenarios in the PR comment */ group?: string; + /** + * Per-import sub-path overrides. Maps a component name to a sub-path + * suffix (e.g. `"svg"` → `"layerchart/svg"`) for scenarios that test + * per-layer variants of agnostic components. + * + * Overrides take precedence over the analyzer's default subpath map. + */ + subpathOverrides?: Record; } export interface ComponentInfo { @@ -166,6 +174,38 @@ export const scenarios: Scenario[] = [ imports: ["Chart", "Svg", "Chord", "Ribbon"], }, + // --- Layer-specific (opt-in per-layer variants) --- + // Standalone Circle measurements (in isolation — no Chart context, no + // Highlight pulling in the agnostic version transitively). These are the + // cleanest way to see what `layerchart/svg` etc. actually save. + { + name: "circle-agnostic", + group: "Layer-specific", + description: "Standalone Circle (agnostic) — baseline", + imports: ["Circle"], + }, + { + name: "circle-svg", + group: "Layer-specific", + description: "Standalone Circle from `layerchart/svg`", + imports: ["Circle"], + subpathOverrides: { Circle: "svg" }, + }, + { + name: "circle-canvas", + group: "Layer-specific", + description: "Standalone Circle from `layerchart/canvas`", + imports: ["Circle"], + subpathOverrides: { Circle: "canvas" }, + }, + { + name: "circle-html", + group: "Layer-specific", + description: "Standalone Circle from `layerchart/html`", + imports: ["Circle"], + subpathOverrides: { Circle: "html" }, + }, + // --- Worst case --- { name: "all", diff --git a/docs/src/content/guides/bundle-size.md b/docs/src/content/guides/bundle-size.md new file mode 100644 index 000000000..49df091bb --- /dev/null +++ b/docs/src/content/guides/bundle-size.md @@ -0,0 +1,122 @@ +--- +title: Bundle Size +description: How to reduce LayerChart's footprint in your application bundle +category: advanced +--- + +LayerChart ships a layer-agnostic, batteries-included API by default — `` works with ``, ``, and `` rendering, primitives like `` and `` auto-detect the surrounding layer, and chart-level features like brushing, tooltips, and tile maps activate when you pass the right prop. + +That flexibility has a cost: every consumer of `import { Chart } from 'layerchart'` would otherwise pay for code paths they may never reach. To keep the default bundle small, LayerChart uses three layered strategies: + +1. **Lazy-loaded opt-in features** — Heavy features are dynamically imported only when activated +2. **Sub-path exports for heavy dependencies** — Components that pull in big external deps live behind opt-in sub-paths +3. **Per-layer primitive variants** — Layer-agnostic primitives have SVG/Canvas/HTML-specific variants for users who commit to one layer + +The first two cost you nothing — they're transparent. The third is opt-in: you swap an import to get a smaller bundle in exchange for losing layer flexibility on that import. + +## What you get for free + +The following heavy features are loaded only when you use them, with no code change required from you: + +| Feature | When it loads | +| --- | --- | +| `` (and brush state) | When `` is set | +| `` | When `tooltipContext` is set and you don't provide a custom `tooltip` snippet | +| `Voronoi` hit-detection | When `` is used | +| `Arc` (radial tooltip rects) | When `` or `mode="band"` is used inside a radial chart | +| `d3-quadtree` | When `` (or `quadtree-x` / `quadtree-y`) is used | +| `Spline` (radial linear grid) | When `` with `` is used | +| `Bar` highlight overlay | When `` is set | +| ``, ``, ``, `` | When the corresponding prop is set on `` | + +These additions to your chart cause an extra HTTP fetch the first time the corresponding feature is used. On a fast network this is unnoticeable; on slow networks the chart paints first and the optional feature appears as it loads. + +## Sub-path exports for heavy dependencies + +Components that bring in large d3 modules or framework-specific libraries are not re-exported from the root `'layerchart'` entry. They live behind opt-in sub-paths so the default barrel doesn't drag those deps into bundlers that don't tree-shake aggressively. + +| Sub-path | Components | Heavy dep saved | +| --- | --- | --- | +| `layerchart/geo` | `Geo*` (12), `Graticule`, `TileImage` | `d3-geo` (~15 KB), `d3-tile` | +| `layerchart/hierarchy` | `Tree`, `Treemap`, `Pack`, `Partition` | `d3-hierarchy` (~6 KB) | +| `layerchart/force` | `ForceSimulation` | `d3-force` (~7 KB) | +| `layerchart/graph` | `Dagre`, `Sankey`, `Chord`, `Ribbon` | `@dagrejs/dagre` (~22 KB), `d3-sankey`, `d3-chord` | + +If you use these components, just import from the sub-path: + +```ts +import { Tree, Treemap } from 'layerchart/hierarchy'; +import { GeoPath, GeoProjection } from 'layerchart/geo'; +import { ForceSimulation } from 'layerchart/force'; +import { Sankey, Dagre } from 'layerchart/graph'; +``` + +If you don't use them, you don't pay for them — the agnostic root export simply doesn't expose them, so even bundlers that mishandle tree-shaking can't accidentally include them. + +## Per-layer primitive variants (opt-in) + +Layer-agnostic primitives — components like ``, ``, ``, ``, `` — auto-detect the surrounding layer (``, ``, or ``) and render appropriately. To do this they bundle all three rendering paths. + +If you know your chart only renders to one layer, you can opt into a layer-specific variant: + +```ts +// Default: agnostic, ~17 KB gz, works with Svg/Canvas/Html +import { Circle } from 'layerchart'; + +// SVG-only, ~13 KB gz (~25% smaller) +import { Circle } from 'layerchart/svg'; + +// Canvas-only +import { Circle } from 'layerchart/canvas'; + +// HTML-only, ~13 KB gz +import { Circle } from 'layerchart/html'; +``` + +The agnostic version (`Circle.svelte`) dispatches to the appropriate per-layer variant under the hood at runtime, so you can mix per-layer and agnostic imports in the same chart — the resolved code path is identical. + +### When per-layer is worth it + +- ✅ You're building many charts in a single layer (most likely SVG) +- ✅ You're shipping to a bandwidth-sensitive context (mobile, embedded views, AMP-style pages) +- ✅ You want to sketch out the absolute minimum bundle for a specific use case + +### When to stay on the agnostic API + +- 🤷 You mix layers in the same project (some charts SVG, some Canvas) +- 🤷 Your bundle savings would be small relative to the rest of your app +- 🤷 You value the flexibility to swap a chart's rendering layer later without touching imports + +### Components currently split + +| Primitive | SVG variant | Canvas variant | HTML variant | +| --- | --- | --- | --- | +| `Circle` | `layerchart/svg` | `layerchart/canvas` | `layerchart/html` | + +More primitives (`Rect`, `Line`, `Path`, `Text`, `Bar`) are planned. The pattern follows the same shape — we'll add to the table as they land. + +## Worst case: importing everything + +If you `import * as LayerChart from 'layerchart'` (or your bundler can't tree-shake at all), you'll pay for the entire surface area of the root barrel — currently around 240 KB gz across all components. The strategies above exist precisely to keep this from happening for typical consumers. + +If you're not sure what your bundle looks like in practice, run a tool like [`rollup-plugin-visualizer`](https://github.com/btd/rollup-plugin-visualizer) or `vite build --mode=production` with source maps and inspect the output. + +## Reference: scenario sizes + +The numbers below are gzipped totals from LayerChart's own bundle analyzer. They represent the cost of importing the listed components from `'layerchart'` (or the sub-path when noted), measured against a minimal Svelte app with Svelte's own runtime treated as external. + +| Scenario | Imports | Gzipped | +| --- | --- | --- | +| `core` | `Chart`, `Svg` | ~106 KB | +| `line-chart` | `Chart`, `Svg`, `Line`, `Axis`, `Grid` | ~106 KB | +| `geo` (sub-path) | `Chart`, `Svg`, `GeoProjection`, `GeoPath`, `GeoPoint` | ~109 KB | +| `force` (sub-path) | `Chart`, `Svg`, `ForceSimulation`, `Link`, `Circle`, `Text` | ~114 KB | +| `dagre` (sub-path) | `Chart`, `Svg`, `Dagre`, `Link`, `Circle`, `Text` | ~129 KB | +| `circle-svg` (per-layer) | `Circle` from `layerchart/svg` | ~13 KB | +| `circle-agnostic` | `Circle` from `layerchart` | ~17 KB | + +`core` is what every chart pays. The other rows show what specific feature additions cost on top. + +## Background: how LayerChart minimizes baseline cost + +If you want to dig deeper, every release of LayerChart runs an automated bundle analyzer ([`bundle-analyzer/`](https://github.com/techniq/layerchart/tree/main/bundle-analyzer) in the repo) across ~25 representative scenarios and posts the per-scenario size to PR comments. The CI guards against unintended bundle regressions and lets us continue to land lazy-loading and code-split improvements without slowing the default chart down. diff --git a/packages/layerchart/package.json b/packages/layerchart/package.json index 508ce4afe..6421723c6 100644 --- a/packages/layerchart/package.json +++ b/packages/layerchart/package.json @@ -139,6 +139,21 @@ "svelte": "./dist/graph.js", "default": "./dist/graph.js" }, + "./svg": { + "types": "./dist/svg.d.ts", + "svelte": "./dist/svg.js", + "default": "./dist/svg.js" + }, + "./canvas": { + "types": "./dist/canvas.d.ts", + "svelte": "./dist/canvas.js", + "default": "./dist/canvas.js" + }, + "./html": { + "types": "./dist/html.d.ts", + "svelte": "./dist/html.js", + "default": "./dist/html.js" + }, "./utils/*": { "types": "./dist/utils/*.d.ts", "svelte": "./dist/utils/*.js", diff --git a/packages/layerchart/src/lib/canvas.ts b/packages/layerchart/src/lib/canvas.ts new file mode 100644 index 000000000..e92830e43 --- /dev/null +++ b/packages/layerchart/src/lib/canvas.ts @@ -0,0 +1,11 @@ +/** + * Canvas-only variants of layer-agnostic components. + * + * Each export here is a Canvas-specific implementation. Use these when you + * know your chart only renders to Canvas and want a smaller bundle. + * + * The agnostic versions (e.g. `import { Circle } from 'layerchart'`) still + * work and dispatch to these per-layer variants under the hood. + */ +export { default as Circle } from './components/Circle.canvas.svelte'; +export type { CircleProps, CirclePropsWithoutHTML } from './components/Circle.shared.svelte.js'; diff --git a/packages/layerchart/src/lib/components/Circle.canvas.svelte b/packages/layerchart/src/lib/components/Circle.canvas.svelte new file mode 100644 index 000000000..6ccb8ae25 --- /dev/null +++ b/packages/layerchart/src/lib/components/Circle.canvas.svelte @@ -0,0 +1,126 @@ + + + diff --git a/packages/layerchart/src/lib/components/Circle.html.svelte b/packages/layerchart/src/lib/components/Circle.html.svelte new file mode 100644 index 000000000..cc3b01628 --- /dev/null +++ b/packages/layerchart/src/lib/components/Circle.html.svelte @@ -0,0 +1,113 @@ + + + + +{#if c.dataMode} + {#each c.resolvedItems as item (item.key)} + {@const resolvedFill = resolveColorProp(rest.fill, item.d, c.chartCtx.cScale)} + {@const resolvedStroke = resolveColorProp(rest.stroke, item.d, c.chartCtx.cScale)} + {@const resolvedStrokeWidth = resolveStyleProp(rest.strokeWidth, item.d)} + {@const resolvedOpacity = resolveStyleProp(rest.opacity, item.d)} + {@const resolvedClass = resolveStyleProp(rest.class, item.d)} + {@const resolvedBorderWidth = + resolvedStrokeWidth != null + ? `${resolvedStrokeWidth}px` + : resolvedStroke != null + ? '1px' + : undefined} +
+ {/each} +{:else} +
+ {@render children?.()} +
+{/if} + + diff --git a/packages/layerchart/src/lib/components/Circle.shared.svelte.ts b/packages/layerchart/src/lib/components/Circle.shared.svelte.ts new file mode 100644 index 000000000..4b1bf9029 --- /dev/null +++ b/packages/layerchart/src/lib/components/Circle.shared.svelte.ts @@ -0,0 +1,288 @@ +import type { Snippet } from 'svelte'; +import { untrack } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; + +import type { Without } from '$lib/utils/types.js'; +import type { DataProp, DataDrivenStyleProps } from '$lib/utils/dataProp.js'; +import { + hasAnyDataProp, + resolveDataProp, + resolveGeoDataPair, +} from '$lib/utils/dataProp.js'; +import { chartDataArray } from '$lib/utils/common.js'; +import { parseDashArray } from '$lib/utils/path.js'; +import { createMotion, createDataMotionMap, type MotionProp } from '$lib/utils/motion.svelte.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import { getGeoContext } from '$lib/contexts/geo.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import type { GeoState } from '$lib/states/geo.svelte.js'; + +export type CirclePropsWithoutHTML = { + /** + * The center x position of the circle. + * - `number`: pixel value (direct) + * - `string`: data property name, resolved via xScale + * - `function(d)`: accessor called per data item, result passed through xScale + * + * @default 0 + */ + cx?: DataProp; + + /** + * The initial center x position of the circle (pixel mode only). + * + * @default cx + */ + initialCx?: number; + + /** + * The center y position of the circle. + * - `number`: pixel value (direct) + * - `string`: data property name, resolved via yScale + * - `function(d)`: accessor called per data item, result passed through yScale + * + * @default 0 + */ + cy?: DataProp; + + /** + * The initial center y position of the circle (pixel mode only). + * + * @default cy + */ + initialCy?: number; + + /** + * The radius of the circle. + * - `number`: pixel value (direct) + * - `string`: data property name, resolved via rScale + * - `function(d)`: accessor called per data item, result passed through rScale + * + * @default 1 + */ + r?: DataProp; + + /** + * The initial radius of the circle (pixel mode only). + * + * @default r + */ + initialR?: number; + + /** + * Data array to iterate over in data mode. + * Falls back to chart context data when not provided. + */ + data?: any[]; + + /** + * Key function for keyed {#each} rendering in data mode. + * + * @default (d, i) => i + */ + key?: (d: any, index: number) => any; + + /** + * A bindable reference to the `` element (pixel mode only). + * + * @bindable + */ + ref?: SVGCircleElement; + + /** Motion configuration (pixel mode only). */ + motion?: MotionProp; + + /** + * Dashed-border pattern. Accepts a number (single dash length), a + * `[dash, gap, ...]` array, or a string (same syntax as SVG + * `stroke-dasharray`). HTML layer approximates via `border-style: dashed`. + */ + dashArray?: number | number[] | string; + + /** Children content to render. Note: Only works for Html layers */ + children?: Snippet; +} & DataDrivenStyleProps; + +export type CircleProps = CirclePropsWithoutHTML & + Without, CirclePropsWithoutHTML>; + +const defaultKey = (_: any, i: number) => i; + +/** + * Resolve the cx/cy/r values for a single data item, going through + * either the geo projection or the chart's x/y/r scales. + */ +export function resolveCircle( + d: any, + props: { cx?: DataProp; cy?: DataProp; r?: DataProp }, + chartCtx: ChartState, + geo: GeoState +): { cx: number; cy: number; r: number } { + if (geo.projection) { + const [projX, projY] = resolveGeoDataPair(props.cx, props.cy, d, geo.projection); + return { + cx: projX, + cy: projY, + r: resolveDataProp(props.r, d, chartCtx.rScale, typeof props.r === 'number' ? props.r : 1), + }; + } + return { + cx: resolveDataProp(props.cx, d, chartCtx.xScale, 0), + cy: resolveDataProp(props.cy, d, chartCtx.yScale, 0), + r: resolveDataProp(props.r, d, chartCtx.rScale, typeof props.r === 'number' ? props.r : 1), + }; +} + +/** + * Reactive state shared by every per-layer Circle variant. Instantiate from + * each `Circle.svg.svelte` / `Circle.canvas.svelte` / `Circle.html.svelte` + * component setup, passing a getter for the props. Exposes the derived + * computations + motion sources every layer needs. + * + * Per-layer specific bits (e.g. SVG's `bind:this` ref, HTML's + * `staticBorderWidth`, canvas's `render` function and canvas registration) + * stay in their respective `.svelte` files. + */ +export class CircleState { + // Default initializer so derived class fields below can safely call this + // before the constructor body sets the real getter. + #getProps: () => CircleProps = () => ({}) as CircleProps; + + // Contexts (must be read inside a Svelte component setup, which is where + // CircleState is instantiated). + chartCtx = getChartContext(); + geo = getGeoContext(); + + // Reactive derivations + dashArrayResolved = $derived(parseDashArray(this.#getProps().dashArray)); + dashArrayAttr = $derived( + this.dashArrayResolved ? this.dashArrayResolved.join(' ') : undefined + ); + dataMode = $derived( + hasAnyDataProp(this.#getProps().cx, this.#getProps().cy, this.#getProps().r) + ); + #resolvedData: any[] = $derived( + this.dataMode ? (this.#getProps().data ?? chartDataArray(this.chartCtx.data)) : [] + ); + + // Per-key motion tracking (only created when motion is configured) + #dataMotionMap: ReturnType = null; + + resolvedItems = $derived.by(() => { + if (!this.dataMode) return []; + const props = this.#getProps(); + const keyFn = props.key ?? defaultKey; + return this.#resolvedData.map((d, i) => { + const key = keyFn(d, i); + const resolved = resolveCircle(d, props, this.chartCtx, this.geo); + const animated = this.#dataMotionMap?.get(key); + return { + d, + key, + cx: animated?.cx ?? resolved.cx, + cy: animated?.cy ?? resolved.cy, + r: animated?.r ?? resolved.r, + }; + }); + }); + + // Pixel-mode motion sources. Initial values are captured at construction; + // subsequent updates come from the prop getters. + #motionCx!: ReturnType>; + #motionCy!: ReturnType>; + #motionR!: ReturnType>; + + // Static (non-data-driven) values used by SVG/HTML branches in pixel mode. + staticFill = $derived( + typeof this.#getProps().fill === 'string' ? (this.#getProps().fill as string) : undefined + ); + staticFillOpacity = $derived( + typeof this.#getProps().fillOpacity === 'number' + ? (this.#getProps().fillOpacity as number) + : undefined + ); + staticStroke = $derived( + typeof this.#getProps().stroke === 'string' ? (this.#getProps().stroke as string) : undefined + ); + staticStrokeWidth = $derived( + typeof this.#getProps().strokeWidth === 'number' + ? (this.#getProps().strokeWidth as number) + : undefined + ); + staticOpacity = $derived( + typeof this.#getProps().opacity === 'number' + ? (this.#getProps().opacity as number) + : undefined + ); + staticClassName = $derived( + typeof this.#getProps().class === 'string' ? (this.#getProps().class as string) : undefined + ); + + constructor(getProps: () => CircleProps) { + this.#getProps = getProps; + + const initial = getProps(); + const initialCx = + initial.initialCx ?? (typeof initial.cx === 'number' ? initial.cx : 0); + const initialCy = + initial.initialCy ?? (typeof initial.cy === 'number' ? initial.cy : 0); + const initialR = + initial.initialR ?? (typeof initial.r === 'number' ? initial.r : 1); + + this.#motionCx = createMotion( + initialCx, + () => (typeof getProps().cx === 'number' ? (getProps().cx as number) : 0), + initial.motion + ); + this.#motionCy = createMotion( + initialCy, + () => (typeof getProps().cy === 'number' ? (getProps().cy as number) : 0), + initial.motion + ); + this.#motionR = createMotion( + initialR, + () => (typeof getProps().r === 'number' ? (getProps().r as number) : 1), + initial.motion + ); + + this.#dataMotionMap = createDataMotionMap(initial.motion); + if (this.#dataMotionMap) { + const motionMap = this.#dataMotionMap; + $effect(() => { + if (!this.dataMode) return; + const props = getProps(); + const keyFn = props.key ?? defaultKey; + const activeKeys = new Set(); + for (let i = 0; i < this.#resolvedData.length; i++) { + const d = this.#resolvedData[i]; + const key = keyFn(d, i); + activeKeys.add(key); + const resolved = resolveCircle(d, props, this.chartCtx, this.geo); + untrack(() => motionMap.update(key, resolved)); + } + untrack(() => motionMap.cleanup(activeKeys)); + }); + } + } + + get motionCx() { + return this.#motionCx.current; + } + get motionCy() { + return this.#motionCy.current; + } + get motionR() { + return this.#motionR.current; + } +} + +/** Build the standard `markInfo` payload used by every Circle variant. */ +export function circleMarkInfo(props: CircleProps, dataMode: boolean) { + if (!dataMode) return {}; + return { + data: props.data, + x: typeof props.cx === 'string' ? props.cx : undefined, + y: typeof props.cy === 'string' ? props.cy : undefined, + color: typeof props.fill === 'string' ? props.fill : undefined, + }; +} diff --git a/packages/layerchart/src/lib/components/Circle.svelte b/packages/layerchart/src/lib/components/Circle.svelte index 7b99eabda..b613d3e33 100644 --- a/packages/layerchart/src/lib/components/Circle.svelte +++ b/packages/layerchart/src/lib/components/Circle.svelte @@ -1,487 +1,28 @@ {#if layerCtx === 'svg'} - {#if dataMode} - {#each resolvedItems as item (item.key)} - {@const resolvedFill = resolveColorProp(fill, item.d, chartCtx.cScale)} - {@const resolvedStroke = resolveColorProp(stroke, item.d, chartCtx.cScale)} - {@const resolvedFillOpacity = resolveStyleProp(fillOpacity, item.d)} - {@const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d)} - {@const resolvedOpacity = resolveStyleProp(opacity, item.d)} - {@const resolvedClass = resolveStyleProp(className, item.d)} - - {/each} - {:else} - - {/if} + +{:else if layerCtx === 'canvas'} + {:else if layerCtx === 'html'} - {#if dataMode} - {#each resolvedItems as item (item.key)} - {@const resolvedFill = resolveColorProp(fill, item.d, chartCtx.cScale)} - {@const resolvedStroke = resolveColorProp(stroke, item.d, chartCtx.cScale)} - {@const resolvedFillOpacity = resolveStyleProp(fillOpacity, item.d)} - {@const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d)} - {@const resolvedOpacity = resolveStyleProp(opacity, item.d)} - {@const resolvedClass = resolveStyleProp(className, item.d)} - {@const resolvedBorderWidth = - resolvedStrokeWidth != null - ? `${resolvedStrokeWidth}px` - : resolvedStroke != null - ? '1px' - : undefined} -
- {/each} - {:else} -
- {@render children?.()} -
- {/if} + {/if} - - diff --git a/packages/layerchart/src/lib/components/Circle.svg.svelte b/packages/layerchart/src/lib/components/Circle.svg.svelte new file mode 100644 index 000000000..b0bbb6c3c --- /dev/null +++ b/packages/layerchart/src/lib/components/Circle.svg.svelte @@ -0,0 +1,91 @@ + + + + +{#if c.dataMode} + {#each c.resolvedItems as item (item.key)} + {@const resolvedFill = resolveColorProp(rest.fill, item.d, c.chartCtx.cScale)} + {@const resolvedStroke = resolveColorProp(rest.stroke, item.d, c.chartCtx.cScale)} + {@const resolvedFillOpacity = resolveStyleProp(rest.fillOpacity, item.d)} + {@const resolvedStrokeWidth = resolveStyleProp(rest.strokeWidth, item.d)} + {@const resolvedOpacity = resolveStyleProp(rest.opacity, item.d)} + {@const resolvedClass = resolveStyleProp(rest.class, item.d)} + + {/each} +{:else} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/__screenshots__/Circle.svelte.test.ts/Circle-data-mode-should-resolve-data-driven-fill-through-cScale-1.png b/packages/layerchart/src/lib/components/__screenshots__/Circle.svelte.test.ts/Circle-data-mode-should-resolve-data-driven-fill-through-cScale-1.png index eaf18fbbcec0c603ab11b46e547f2e1fcfe3e853..e9e2ea9568883e5278ee29707ae49de0111a32a6 100644 GIT binary patch delta 284 zcmV+%0ptF|5}Xo{BuCLnL_t(|ob1`LO&dS}M&V1u1{6$yL`%;EAU1#oiAC6f#*!;r z@&=y3jmL0n*)uqlQQ`^SeP8+aPp7-Pxm+#-Nc%m2I%%R?2LYQm#PR+PDPe zg6!OZrIoU?vY{!95ff2nd`8SMSxts+NL!eII}CDwz2u)%lDJVj$FBLVs&MCVhsD{d7h}-p6A8P`Nrjy-OJ*k19z@pJh0)GT3zW@LL|NnkSdw>7{00v1!K~w_(=}S=P Tb+9nw00000NkvXXu0mjf>|m>U diff --git a/packages/layerchart/src/lib/components/__screenshots__/Circle.svelte.test.ts/Circle-data-mode-should-resolve-data-driven-fill-through-cScale-2.png b/packages/layerchart/src/lib/components/__screenshots__/Circle.svelte.test.ts/Circle-data-mode-should-resolve-data-driven-fill-through-cScale-2.png index eaf18fbbcec0c603ab11b46e547f2e1fcfe3e853..e9e2ea9568883e5278ee29707ae49de0111a32a6 100644 GIT binary patch delta 284 zcmV+%0ptF|5}Xo{BuCLnL_t(|ob1`LO&dS}M&V1u1{6$yL`%;EAU1#oiAC6f#*!;r z@&=y3jmL0n*)uqlQQ`^SeP8+aPp7-Pxm+#-Nc%m2I%%R?2LYQm#PR+PDPe zg6!OZrIoU?vY{!95ff2nd`8SMSxts+NL!eII}CDwz2u)%lDJVj$FBLVs&MCVhsD{d7h}-p6A8P`Nrjy-OJ*k19z@pJh0)GT3zW@LL|NnkSdw>7{00v1!K~w_(=}S=P Tb+9nw00000NkvXXu0mjf>|m>U diff --git a/packages/layerchart/src/lib/html.ts b/packages/layerchart/src/lib/html.ts new file mode 100644 index 000000000..b0c89ffc7 --- /dev/null +++ b/packages/layerchart/src/lib/html.ts @@ -0,0 +1,11 @@ +/** + * HTML-only variants of layer-agnostic components. + * + * Each export here is an HTML-specific implementation. Use these when you + * know your chart only renders to the HTML layer and want a smaller bundle. + * + * The agnostic versions (e.g. `import { Circle } from 'layerchart'`) still + * work and dispatch to these per-layer variants under the hood. + */ +export { default as Circle } from './components/Circle.html.svelte'; +export type { CircleProps, CirclePropsWithoutHTML } from './components/Circle.shared.svelte.js'; diff --git a/packages/layerchart/src/lib/svg.ts b/packages/layerchart/src/lib/svg.ts new file mode 100644 index 000000000..f9d9f8c23 --- /dev/null +++ b/packages/layerchart/src/lib/svg.ts @@ -0,0 +1,13 @@ +/** + * SVG-only variants of layer-agnostic components. + * + * Each export here is a SVG-specific implementation that skips the runtime + * layer-detection branches in the agnostic version (e.g. `Circle.svelte`). + * Use these when you know your chart only renders to SVG and want a smaller + * bundle. + * + * The agnostic versions (e.g. `import { Circle } from 'layerchart'`) still + * work and dispatch to these per-layer variants under the hood. + */ +export { default as Circle } from './components/Circle.svg.svelte'; +export type { CircleProps, CirclePropsWithoutHTML } from './components/Circle.shared.svelte.js'; From fb41f7543fafa2a8306d916af454887ab4464717 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 27 Apr 2026 16:02:25 -0400 Subject: [PATCH 02/36] split Text.svelte into 3 layer-specific components --- bundle-analyzer/bundle-reports/latest.json | 122 ++- bundle-analyzer/define-scenarios.ts | 27 + docs/src/content/guides/bundle-size.md | 9 +- packages/layerchart/src/lib/canvas.ts | 6 + .../{ => Circle}/Circle.canvas.svelte | 0 .../{ => Circle}/Circle.html.svelte | 0 .../{ => Circle}/Circle.shared.svelte.ts | 0 .../lib/components/{ => Circle}/Circle.svelte | 0 .../{ => Circle}/Circle.svelte.test.ts | 0 .../components/{ => Circle}/Circle.svg.svelte | 0 .../layerchart/src/lib/components/Text.svelte | 959 ------------------ .../lib/components/Text/Text.canvas.svelte | 202 ++++ .../src/lib/components/Text/Text.html.svelte | 106 ++ .../lib/components/Text/Text.shared.svelte.ts | 599 +++++++++++ .../src/lib/components/Text/Text.svelte | 29 + .../components/{ => Text}/Text.svelte.test.ts | 0 .../src/lib/components/Text/Text.svg.svelte | 190 ++++ packages/layerchart/src/lib/html.ts | 6 + packages/layerchart/src/lib/svg.ts | 6 + 19 files changed, 1258 insertions(+), 1003 deletions(-) rename packages/layerchart/src/lib/components/{ => Circle}/Circle.canvas.svelte (100%) rename packages/layerchart/src/lib/components/{ => Circle}/Circle.html.svelte (100%) rename packages/layerchart/src/lib/components/{ => Circle}/Circle.shared.svelte.ts (100%) rename packages/layerchart/src/lib/components/{ => Circle}/Circle.svelte (100%) rename packages/layerchart/src/lib/components/{ => Circle}/Circle.svelte.test.ts (100%) rename packages/layerchart/src/lib/components/{ => Circle}/Circle.svg.svelte (100%) delete mode 100644 packages/layerchart/src/lib/components/Text.svelte create mode 100644 packages/layerchart/src/lib/components/Text/Text.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Text/Text.html.svelte create mode 100644 packages/layerchart/src/lib/components/Text/Text.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Text/Text.svelte rename packages/layerchart/src/lib/components/{ => Text}/Text.svelte.test.ts (100%) create mode 100644 packages/layerchart/src/lib/components/Text/Text.svg.svelte diff --git a/bundle-analyzer/bundle-reports/latest.json b/bundle-analyzer/bundle-reports/latest.json index d9757ba09..9efdecbf6 100644 --- a/bundle-analyzer/bundle-reports/latest.json +++ b/bundle-analyzer/bundle-reports/latest.json @@ -1,12 +1,12 @@ { - "timestamp": "2026-04-27T18:45:12.060Z", + "timestamp": "2026-04-27T19:01:35.829Z", "results": [ { "scenario": "core", "description": "Bare minimum: Chart context + Svg layer", "group": "Foundation", - "size": 445128, - "gzipSize": 108465, + "size": 449088, + "gzipSize": 109291, "imports": [ "Chart", "Svg" @@ -16,8 +16,8 @@ "scenario": "canvas", "description": "Canvas-based rendering", "group": "Foundation", - "size": 445128, - "gzipSize": 108462, + "size": 449088, + "gzipSize": 109289, "imports": [ "Chart", "Canvas" @@ -27,8 +27,8 @@ "scenario": "line-chart", "description": "Basic line chart with axes and grid", "group": "Cartesian charts", - "size": 445152, - "gzipSize": 108476, + "size": 449112, + "gzipSize": 109301, "imports": [ "Chart", "Svg", @@ -41,8 +41,8 @@ "scenario": "line-chart-interactive", "description": "Line chart with tooltip and highlight", "group": "Cartesian charts", - "size": 459721, - "gzipSize": 111180, + "size": 463681, + "gzipSize": 111985, "imports": [ "Chart", "Svg", @@ -57,8 +57,8 @@ "scenario": "area-chart", "description": "Area chart with axes", "group": "Cartesian charts", - "size": 469700, - "gzipSize": 114550, + "size": 473660, + "gzipSize": 115339, "imports": [ "Chart", "Svg", @@ -71,8 +71,8 @@ "scenario": "bar-chart", "description": "Bar chart with axes", "group": "Cartesian charts", - "size": 453984, - "gzipSize": 110796, + "size": 457944, + "gzipSize": 111320, "imports": [ "Chart", "Svg", @@ -85,8 +85,8 @@ "scenario": "scatter-chart", "description": "Scatter plot with points", "group": "Cartesian charts", - "size": 449378, - "gzipSize": 109622, + "size": 453338, + "gzipSize": 110446, "imports": [ "Chart", "Svg", @@ -100,8 +100,8 @@ "scenario": "pie-chart", "description": "Pie/donut chart with arcs", "group": "Cartesian charts", - "size": 452177, - "gzipSize": 110265, + "size": 456137, + "gzipSize": 111083, "imports": [ "Chart", "Svg", @@ -114,8 +114,8 @@ "scenario": "high-level-charts", "description": "All high-level chart components (LineChart, BarChart, etc.)", "group": "Cartesian charts", - "size": 542767, - "gzipSize": 130577, + "size": 546727, + "gzipSize": 131366, "imports": [ "LineChart", "AreaChart", @@ -129,8 +129,8 @@ "scenario": "geo", "description": "Geographic map with paths", "group": "Geo", - "size": 460080, - "gzipSize": 111946, + "size": 464040, + "gzipSize": 112768, "imports": [ "Chart", "Svg", @@ -143,8 +143,8 @@ "scenario": "geo-tiles", "description": "Geographic map with tile layer", "group": "Geo", - "size": 464527, - "gzipSize": 113459, + "size": 468487, + "gzipSize": 114275, "imports": [ "Chart", "Svg", @@ -158,8 +158,8 @@ "scenario": "geo-full", "description": "Full geo setup with all geo components", "group": "Geo", - "size": 512332, - "gzipSize": 126809, + "size": 516292, + "gzipSize": 127644, "imports": [ "Chart", "Svg", @@ -181,8 +181,8 @@ "scenario": "hierarchy-tree", "description": "Tree layout with links", "group": "Hierarchy", - "size": 471383, - "gzipSize": 115397, + "size": 475343, + "gzipSize": 116273, "imports": [ "Chart", "Svg", @@ -196,8 +196,8 @@ "scenario": "hierarchy-treemap", "description": "Treemap layout", "group": "Hierarchy", - "size": 451567, - "gzipSize": 110477, + "size": 455527, + "gzipSize": 111294, "imports": [ "Chart", "Svg", @@ -211,8 +211,8 @@ "scenario": "hierarchy-pack", "description": "Circle packing layout", "group": "Hierarchy", - "size": 451387, - "gzipSize": 110577, + "size": 455347, + "gzipSize": 111395, "imports": [ "Chart", "Svg", @@ -225,8 +225,8 @@ "scenario": "force", "description": "Force-directed graph layout", "group": "Graph / network", - "size": 473835, - "gzipSize": 116221, + "size": 477795, + "gzipSize": 117095, "imports": [ "Chart", "Svg", @@ -240,8 +240,8 @@ "scenario": "dagre", "description": "Dagre directed graph layout", "group": "Graph / network", - "size": 531265, - "gzipSize": 131636, + "size": 535225, + "gzipSize": 132517, "imports": [ "Chart", "Svg", @@ -255,8 +255,8 @@ "scenario": "sankey", "description": "Sankey flow diagram", "group": "Graph / network", - "size": 473493, - "gzipSize": 115556, + "size": 477453, + "gzipSize": 116424, "imports": [ "Chart", "Svg", @@ -270,8 +270,8 @@ "scenario": "chord", "description": "Chord diagram", "group": "Graph / network", - "size": 454110, - "gzipSize": 110645, + "size": 458070, + "gzipSize": 111462, "imports": [ "Chart", "Svg", @@ -319,12 +319,52 @@ "Circle" ] }, + { + "scenario": "text-agnostic", + "description": "Standalone Text (agnostic) — baseline", + "group": "Layer-specific", + "size": 119881, + "gzipSize": 29879, + "imports": [ + "Text" + ] + }, + { + "scenario": "text-svg", + "description": "Standalone Text from `layerchart/svg`", + "group": "Layer-specific", + "size": 64758, + "gzipSize": 16574, + "imports": [ + "Text" + ] + }, + { + "scenario": "text-canvas", + "description": "Standalone Text from `layerchart/canvas`", + "group": "Layer-specific", + "size": 109397, + "gzipSize": 27464, + "imports": [ + "Text" + ] + }, + { + "scenario": "text-html", + "description": "Standalone Text from `layerchart/html`", + "group": "Layer-specific", + "size": 62401, + "gzipSize": 16047, + "imports": [ + "Text" + ] + }, { "scenario": "all", "description": "Everything from layerchart (worst case)", "group": "Worst case", - "size": 989273, - "gzipSize": 244522, + "size": 993232, + "gzipSize": 245308, "imports": [ "*" ] diff --git a/bundle-analyzer/define-scenarios.ts b/bundle-analyzer/define-scenarios.ts index a2b89b21e..0a60e0517 100644 --- a/bundle-analyzer/define-scenarios.ts +++ b/bundle-analyzer/define-scenarios.ts @@ -205,6 +205,33 @@ export const scenarios: Scenario[] = [ imports: ["Circle"], subpathOverrides: { Circle: "html" }, }, + { + name: "text-agnostic", + group: "Layer-specific", + description: "Standalone Text (agnostic) — baseline", + imports: ["Text"], + }, + { + name: "text-svg", + group: "Layer-specific", + description: "Standalone Text from `layerchart/svg`", + imports: ["Text"], + subpathOverrides: { Text: "svg" }, + }, + { + name: "text-canvas", + group: "Layer-specific", + description: "Standalone Text from `layerchart/canvas`", + imports: ["Text"], + subpathOverrides: { Text: "canvas" }, + }, + { + name: "text-html", + group: "Layer-specific", + description: "Standalone Text from `layerchart/html`", + imports: ["Text"], + subpathOverrides: { Text: "html" }, + }, // --- Worst case --- { diff --git a/docs/src/content/guides/bundle-size.md b/docs/src/content/guides/bundle-size.md index 49df091bb..edd8fcab2 100644 --- a/docs/src/content/guides/bundle-size.md +++ b/docs/src/content/guides/bundle-size.md @@ -89,11 +89,12 @@ The agnostic version (`Circle.svelte`) dispatches to the appropriate per-layer v ### Components currently split -| Primitive | SVG variant | Canvas variant | HTML variant | +| Primitive | Svg-only saves | Canvas-only saves | Html-only saves | | --- | --- | --- | --- | -| `Circle` | `layerchart/svg` | `layerchart/canvas` | `layerchart/html` | +| `Circle` | ~4 KB gz (~25%) | ~1 KB gz (~7%) | ~4 KB gz (~22%) | +| `Text` | ~13 KB gz (~45%) | ~2 KB gz (~8%) | ~13 KB gz (~46%) | -More primitives (`Rect`, `Line`, `Path`, `Text`, `Bar`) are planned. The pattern follows the same shape — we'll add to the table as they land. +More primitives (`Rect`, `Line`, `Path`, `Bar`) are planned. The pattern follows the same shape — we'll add to the table as they land. ## Worst case: importing everything @@ -114,6 +115,8 @@ The numbers below are gzipped totals from LayerChart's own bundle analyzer. They | `dagre` (sub-path) | `Chart`, `Svg`, `Dagre`, `Link`, `Circle`, `Text` | ~129 KB | | `circle-svg` (per-layer) | `Circle` from `layerchart/svg` | ~13 KB | | `circle-agnostic` | `Circle` from `layerchart` | ~17 KB | +| `text-svg` (per-layer) | `Text` from `layerchart/svg` | ~16 KB | +| `text-agnostic` | `Text` from `layerchart` | ~29 KB | `core` is what every chart pays. The other rows show what specific feature additions cost on top. diff --git a/packages/layerchart/src/lib/canvas.ts b/packages/layerchart/src/lib/canvas.ts index e92830e43..19701246e 100644 --- a/packages/layerchart/src/lib/canvas.ts +++ b/packages/layerchart/src/lib/canvas.ts @@ -9,3 +9,9 @@ */ export { default as Circle } from './components/Circle.canvas.svelte'; export type { CircleProps, CirclePropsWithoutHTML } from './components/Circle.shared.svelte.js'; +export { default as Text } from './components/Text.canvas.svelte'; +export type { + TextProps, + TextPropsWithoutHTML, + TextSegment, +} from './components/Text.shared.svelte.js'; diff --git a/packages/layerchart/src/lib/components/Circle.canvas.svelte b/packages/layerchart/src/lib/components/Circle/Circle.canvas.svelte similarity index 100% rename from packages/layerchart/src/lib/components/Circle.canvas.svelte rename to packages/layerchart/src/lib/components/Circle/Circle.canvas.svelte diff --git a/packages/layerchart/src/lib/components/Circle.html.svelte b/packages/layerchart/src/lib/components/Circle/Circle.html.svelte similarity index 100% rename from packages/layerchart/src/lib/components/Circle.html.svelte rename to packages/layerchart/src/lib/components/Circle/Circle.html.svelte diff --git a/packages/layerchart/src/lib/components/Circle.shared.svelte.ts b/packages/layerchart/src/lib/components/Circle/Circle.shared.svelte.ts similarity index 100% rename from packages/layerchart/src/lib/components/Circle.shared.svelte.ts rename to packages/layerchart/src/lib/components/Circle/Circle.shared.svelte.ts diff --git a/packages/layerchart/src/lib/components/Circle.svelte b/packages/layerchart/src/lib/components/Circle/Circle.svelte similarity index 100% rename from packages/layerchart/src/lib/components/Circle.svelte rename to packages/layerchart/src/lib/components/Circle/Circle.svelte diff --git a/packages/layerchart/src/lib/components/Circle.svelte.test.ts b/packages/layerchart/src/lib/components/Circle/Circle.svelte.test.ts similarity index 100% rename from packages/layerchart/src/lib/components/Circle.svelte.test.ts rename to packages/layerchart/src/lib/components/Circle/Circle.svelte.test.ts diff --git a/packages/layerchart/src/lib/components/Circle.svg.svelte b/packages/layerchart/src/lib/components/Circle/Circle.svg.svelte similarity index 100% rename from packages/layerchart/src/lib/components/Circle.svg.svelte rename to packages/layerchart/src/lib/components/Circle/Circle.svg.svelte diff --git a/packages/layerchart/src/lib/components/Text.svelte b/packages/layerchart/src/lib/components/Text.svelte deleted file mode 100644 index bde410c13..000000000 --- a/packages/layerchart/src/lib/components/Text.svelte +++ /dev/null @@ -1,959 +0,0 @@ - - - - -{#if layerCtx === 'svg'} - {#if dataMode} - {#each resolvedItems as item (item.key)} - {@const text = resolveTextValue(item.d)} - {@const resolvedFill = resolveColorProp(fill, item.d, chartCtx.cScale)} - {@const resolvedStroke = resolveColorProp(stroke, item.d, chartCtx.cScale)} - {@const resolvedFillOpacity = resolveStyleProp(fillOpacity, item.d)} - {@const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d)} - {@const resolvedOpacity = resolveStyleProp(opacity, item.d)} - {@const resolvedClass = resolveStyleProp(className, item.d)} - {@const dataRotateTransform = rotate ? `rotate(${rotate}, ${item.x}, ${item.y})` : ''} - - - - {text} - - - - {/each} - {:else} - - - - {#if path} - - {#key path} - - {/key} - - - - {wordsByLines.map((line) => line.words.join(' ')).join()} - - - {:else if isValidXOrY(typeof x === 'function' ? undefined : x) && isValidXOrY(typeof y === 'function' ? undefined : y)} - - {#if segments} - {#each segments as segment, index (index)} - - {segment.value} - - {/each} - {:else} - {#each wordsByLines as line, index (index)} - - {line.words.join(' ')} - - {/each} - {/if} - - {/if} - - {/if} -{:else if layerCtx === 'html'} - {#if dataMode} - {#each resolvedItems as item (item.key)} - {@const text = resolveTextValue(item.d)} - {@const resolvedFill = resolveColorProp(fill, item.d, chartCtx.cScale)} - {@const resolvedFillOpacity = resolveStyleProp(fillOpacity, item.d)} - {@const resolvedOpacity = resolveStyleProp(opacity, item.d)} - {@const resolvedClass = resolveStyleProp(className, item.d)} - {@const translateX = textAnchor === 'middle' ? '-50%' : textAnchor === 'end' ? '-100%' : '0%'} - {@const translateY = - verticalAnchor === 'middle' ? '-50%' : verticalAnchor === 'end' ? '-100%' : '0%'} -
- {text} -
- {/each} - {:else} - {@const translateX = textAnchor === 'middle' ? '-50%' : textAnchor === 'end' ? '-100%' : '0%'} - {@const translateY = - verticalAnchor === 'middle' ? '-50%' : verticalAnchor === 'end' ? '-100%' : '0%'} -
- {#if segments} - {#each segments as segment} - {segment.value} - {/each} - {:else} - {textValue} - {/if} -
- {/if} -{/if} - - diff --git a/packages/layerchart/src/lib/components/Text/Text.canvas.svelte b/packages/layerchart/src/lib/components/Text/Text.canvas.svelte new file mode 100644 index 000000000..4e90a2ade --- /dev/null +++ b/packages/layerchart/src/lib/components/Text/Text.canvas.svelte @@ -0,0 +1,202 @@ + + + diff --git a/packages/layerchart/src/lib/components/Text/Text.html.svelte b/packages/layerchart/src/lib/components/Text/Text.html.svelte new file mode 100644 index 000000000..adf3c9570 --- /dev/null +++ b/packages/layerchart/src/lib/components/Text/Text.html.svelte @@ -0,0 +1,106 @@ + + + + +{#if c.dataMode} + {#each c.resolvedItems as item (item.key)} + {@const text = c.resolveTextValue(item.d)} + {@const resolvedFill = resolveColorProp(rest.fill, item.d, c.chartCtx.cScale)} + {@const resolvedFillOpacity = resolveStyleProp(rest.fillOpacity, item.d)} + {@const resolvedOpacity = resolveStyleProp(rest.opacity, item.d)} + {@const resolvedClass = resolveStyleProp(rest.class, item.d)} + {@const textAnchor = rest.textAnchor ?? 'start'} + {@const verticalAnchor = rest.verticalAnchor ?? 'end'} + {@const translateX = textAnchor === 'middle' ? '-50%' : textAnchor === 'end' ? '-100%' : '0%'} + {@const translateY = + verticalAnchor === 'middle' ? '-50%' : verticalAnchor === 'end' ? '-100%' : '0%'} +
+ {text} +
+ {/each} +{:else} + {@const textAnchor = rest.textAnchor ?? 'start'} + {@const verticalAnchor = rest.verticalAnchor ?? 'end'} + {@const translateX = textAnchor === 'middle' ? '-50%' : textAnchor === 'end' ? '-100%' : '0%'} + {@const translateY = + verticalAnchor === 'middle' ? '-50%' : verticalAnchor === 'end' ? '-100%' : '0%'} +
+ {#if rest.segments} + {#each rest.segments as segment} + {segment.value} + {/each} + {:else} + {c.textValue} + {/if} +
+{/if} + + diff --git a/packages/layerchart/src/lib/components/Text/Text.shared.svelte.ts b/packages/layerchart/src/lib/components/Text/Text.shared.svelte.ts new file mode 100644 index 000000000..081b726f5 --- /dev/null +++ b/packages/layerchart/src/lib/components/Text/Text.shared.svelte.ts @@ -0,0 +1,599 @@ +import { untrack } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; + +import type { Without } from '$lib/utils/types.js'; +import type { DataDrivenStyleProps } from '$lib/utils/dataProp.js'; +import { + resolveDataProp, + resolveGeoDataPair, +} from '$lib/utils/dataProp.js'; +import { chartDataArray } from '$lib/utils/common.js'; +import { createMotion, createDataMotionMap, type MotionProp } from '$lib/utils/motion.svelte.js'; +import { get } from '@layerstack/utils'; +import { format as formatValue, type FormatType, type FormatConfig } from '@layerstack/utils'; +import { getStringWidth, truncateText, type TruncateTextOptions } from '$lib/utils/string.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import { getGeoContext } from '$lib/contexts/geo.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import type { GeoState } from '$lib/states/geo.svelte.js'; + +/** + * Check if a string looks like a CSS/SVG value (percentage, em, px, etc.) + * rather than a data property accessor. + */ +function isCSSValue(value: string): boolean { + return /^-?[\d.]+(%|em|rem|px|pt|cm|mm|in)?$/.test(value); +} + +/** + * Check if a Text prop value is a data-space prop. + * Functions are always data props. + * Strings are data props unless they look like CSS values (e.g. "50%", "1em"). + */ +export function isTextDataProp(value: any): boolean { + if (typeof value === 'function') return true; + if (typeof value === 'string' && !isCSSValue(value)) return true; + return false; +} + +export type TextSegment = { + value: string | number; + class?: string; +}; + +export type TextPropsWithoutHTML = { + /** + * Text value to render. + * - `number`: direct value + * - `string`: in data mode, treated as a data property name (e.g. `"label"` → `d.label`); + * in pixel mode, used as literal text + * - `function(d)`: accessor called per data item to get the text value + * + * @default 0 + */ + value?: string | number | ((d: any) => string | number); + + /** + * Array of styled text segments for inline mixed styling. + * Each segment has its own value and optional class. + * Mutually exclusive with `value`. + */ + segments?: TextSegment[]; + + /** + * Maximum width to occupy (approximate as words are not split) + */ + width?: number; + + /** + * x position of the text. + * - `number`: pixel value (direct) + * - `string` (CSS value like "50%"): SVG position value + * - `string` (property name like "date"): data property, resolved via xScale + * - `function(d)`: accessor called per data item, result passed through xScale + * + * @default 0 + */ + x?: string | number | ((d: any) => any); + + /** + * Initial x position of the text (pixel mode only). + * + * @default x + */ + initialX?: string | number; + + /** + * y position of the text. + * - `number`: pixel value (direct) + * - `string` (CSS value like "50%"): SVG position value + * - `string` (property name like "value"): data property, resolved via yScale + * - `function(d)`: accessor called per data item, result passed through yScale + * + * @default 0 + */ + y?: string | number | ((d: any) => any); + + /** + * Initial y position of the text (pixel mode only). + * + * @default y + */ + initialY?: string | number; + + /** + * dx offset of the text + * + * @default 0 + */ + dx?: string | number; + + /** + * dy offset of the text + * + * @default 0 + */ + dy?: string | number; + + /** + * Desired "line height" of the text, implemented as y offsets + * + * @default "1em" + */ + lineHeight?: string; + + /** + * Cap height of the text + * @default '0.71em' + */ + capHeight?: string; + + /** + * Whether to scale the fontSize to accommodate the specified width + * + * @default false + */ + scaleToFit?: boolean; + + /** + * Horizontal text anchor + * + * @default 'start' + */ + textAnchor?: 'start' | 'middle' | 'end' | 'inherit'; + + /** + * Vertical text anchor + * + * @default 'end' + */ + verticalAnchor?: 'start' | 'middle' | 'end' | 'inherit'; + + /** + * The dominant baseline of the text. Useful for aligning text to the baseline of the axis. + * + * @default 'auto' + */ + dominantBaseline?: + | 'auto' + | 'text-before-edge' + | 'text-after-edge' + | 'middle' + | 'hanging' + | 'ideographic' + | 'mathematical'; + + /** + * Rotational angle of the text + */ + rotate?: number; + + /** + * A bindable reference to the wrapping `` element. + * + * @bindable + */ + svgRef?: SVGElement; + + /** + * Props to pass to the wrapping `` element. + */ + svgProps?: Omit, 'children'>; + + /** + * A bindable reference to the inner `` element + * + * @bindable + */ + ref?: SVGTextElement; + + /** + * Format the displayed value. When set with `motion` and a numeric `value`, + * the number will tween smoothly and be formatted for display. + */ + format?: FormatType | FormatConfig; + + /** Motion configuration (pixel mode only). */ + motion?: MotionProp; + + /** + * Whether to enable text truncation + */ + truncate?: boolean | TruncateTextOptions; + + /** + * A unique identifier for the SVG path element. + * One is generated by default if not provided. + * + */ + pathId?: string; + + /** + * The path to render the text along. + */ + path?: string | null; + + /** + * Specify the offset for the start of the text along the path. + * Can be a percentage ('50%') or a length value. + * + * @default '0%' + */ + startOffset?: string | number; + + /** + * Data array to iterate over in data mode. + * Falls back to chart context data when not provided. + */ + data?: any[]; + + /** + * Key function for keyed {#each} rendering in data mode. + * + * @default (d, i) => i + */ + key?: (d: any, index: number) => any; +} & DataDrivenStyleProps; + +export type TextProps = TextPropsWithoutHTML & + Without, TextPropsWithoutHTML>; + +const defaultKey = (_: any, i: number) => i; + +export function getPathLength(pathRef: SVGPathElement | undefined) { + if (pathRef && typeof pathRef.getTotalLength === 'function') { + try { + return pathRef.getTotalLength(); + } catch (e) { + console.error('Error getting path length:', e); + return 0; + } + } + return 0; +} + +/** + * Convert css value to pixel value (ex. 0.71em => 11.36) + */ +export function getPixelValue(cssValue: number | string) { + // TODO: Properly measure pixel values using DOM (handle inherited font size, zoom, etc) + if (typeof cssValue === 'number') return cssValue; + const result = cssValue.match(/([\d.]+)(\D+)/); + const number = Number(result?.[1]); + switch (result?.[2]) { + case 'px': + return number; + case 'em': + case 'rem': + return number * 16; + default: + return 0; + } +} + +export function isValidXOrY(xOrY: string | number | undefined) { + return ( + (typeof xOrY === 'number' && Number.isFinite(xOrY)) || + typeof xOrY === 'string' + ); +} + +/** Build the standard `markInfo` payload used by every Text variant. */ +export function textMarkInfo(props: TextProps, dataMode: boolean) { + if (!dataMode) return {}; + return { + data: props.data, + x: typeof props.x === 'string' ? props.x : undefined, + y: typeof props.y === 'string' ? props.y : undefined, + color: typeof props.fill === 'string' ? props.fill : undefined, + }; +} + +/** + * Reactive state shared by every per-layer Text variant. Instantiate from + * each `Text.svg.svelte` / `Text.canvas.svelte` / `Text.html.svelte` + * component setup, passing a getter for the props. + * + * Per-layer specific bits (SVG `bind:this` refs, canvas's `render` function, + * canvas-specific style measurement) stay in their respective `.svelte` files. + */ +export class TextState { + #getProps: () => TextProps = () => ({}) as TextProps; + + // Contexts + chartCtx: ChartState = getChartContext(); + geo: GeoState = getGeoContext(); + + // Path measurement (only meaningful for SVG layer where the textPath element exists) + pathRef = $state(); + + // Data mode detection + dataMode = $derived(isTextDataProp(this.#getProps().x) || isTextDataProp(this.#getProps().y)); + + // Data resolution + #resolvedData: any[] = $derived( + this.dataMode ? (this.#getProps().data ?? chartDataArray(this.chartCtx.data)) : [] + ); + + resolvedItems = $derived.by(() => { + if (!this.dataMode) return []; + const props = this.#getProps(); + const keyFn = props.key ?? defaultKey; + return this.#resolvedData.map((d, i) => { + const key = keyFn(d, i); + const resolved = this.resolveTextPosition(d); + const animated = this.#dataMotionMap?.get(key); + return { + d, + key, + x: animated?.x ?? resolved.x, + y: animated?.y ?? resolved.y, + }; + }); + }); + + resolveTextPosition(d: any): { x: number; y: number } { + const props = this.#getProps(); + if (this.geo.projection) { + const [projX, projY] = resolveGeoDataPair( + props.x as any, + props.y as any, + d, + this.geo.projection + ); + return { x: projX, y: projY }; + } + return { + x: resolveDataProp(props.x as any, d, this.chartCtx.xScale, 0), + y: resolveDataProp(props.y as any, d, this.chartCtx.yScale, 0), + }; + } + + resolveTextValue(d: any): string { + const value = this.#getProps().value; + if (typeof value === 'function') { + const v = value(d); + return v != null ? String(v) : ''; + } + if (typeof value === 'string') { + const v = get(d, value); + return v != null ? String(v) : ''; + } + return value != null ? String(value) : ''; + } + + #dataMotionMap: ReturnType = null; + + // Pixel-mode motion sources + #motionX!: ReturnType>; + #motionY!: ReturnType>; + #motionValue!: ReturnType>; + + get motionX() { + return this.#motionX.current; + } + get motionY() { + return this.#motionY.current; + } + + // Resolved width: for path text, defer to the (SVG-bound) pathRef length + resolvedWidth = $derived(this.#getProps().path ? getPathLength(this.pathRef) : this.#getProps().width); + + #defaultTruncateOptions: TruncateTextOptions = $derived({ + maxChars: undefined, + position: 'end', + maxWidth: this.resolvedWidth, + }); + + truncateConfig: TruncateTextOptions | boolean = $derived.by(() => { + const truncate = this.#getProps().truncate; + if (typeof truncate === 'boolean') { + if (truncate) return this.#defaultTruncateOptions; + return false; + } + return { ...this.#defaultTruncateOptions, ...(truncate ?? {}) }; + }); + + // Numeric value tweening + rawText = $derived.by(() => { + const value = this.#getProps().value; + const motion = this.#getProps().motion; + const format = this.#getProps().format; + if (typeof value === 'function' || value == null) return ''; + if (typeof value === 'number' && motion) { + const v = this.#motionValue.current; + // @ts-expect-error - improve format types + return format ? formatValue(v, format) : String(v); + } + // @ts-expect-error - improve format types + const text = format ? formatValue(value, format) : value.toString(); + return text.replace(/\\n/g, '\n'); + }); + + textValue = $derived.by(() => { + const cfg = this.truncateConfig; + if (!cfg || cfg === true) return this.rawText; + return truncateText(this.rawText, cfg); + }); + + // Word-wrapping (depends on style measurement; SVG/canvas-only) + // Note: `style` is a placeholder — never assigned in the original component + // either, so spaceWidth always falls back to 0. Preserved for behavior parity. + #spaceWidth = $derived(getStringWidth(' ', undefined as any) || 0); + + wordsByLines = $derived.by(() => { + const props = this.#getProps(); + const width = props.width; + const scaleToFit = props.scaleToFit ?? false; + const lines = this.textValue.split('\n'); + + return lines.flatMap((line) => { + const words = line.split(/(?:(?! +)\s+)/); + if (width == null) { + return [{ words }]; + } + return words.reduce((result: { words: string[]; width?: number }[], item) => { + const currentLine = result[result.length - 1]; + const itemWidth = getStringWidth(item, undefined as any) || 0; + + if ( + currentLine && + (width == null || + scaleToFit || + (currentLine.width || 0) + itemWidth + this.#spaceWidth < width) + ) { + currentLine.words.push(item); + currentLine.width = currentLine.width || 0; + currentLine.width += itemWidth + this.#spaceWidth; + } else { + const newLine = { words: [item], width: itemWidth }; + result.push(newLine); + } + return result; + }, []); + }); + }); + + lineCount = $derived(this.wordsByLines.length); + + // Vertical positioning + startDy = $derived.by(() => { + const props = this.#getProps(); + const verticalAnchor = props.verticalAnchor ?? 'end'; + const lineHeight = props.lineHeight ?? '1em'; + const capHeight = props.capHeight ?? '0.71em'; + if (verticalAnchor === 'start') { + return getPixelValue(lineHeight); + } else if (verticalAnchor === 'middle') { + return ((this.lineCount - 1) / 2) * -getPixelValue(lineHeight) + getPixelValue(capHeight) / 2; + } + return (this.lineCount - 1) * -getPixelValue(lineHeight) - getPixelValue(capHeight) / 2; + }); + + dataModeStartDy = $derived.by(() => { + const props = this.#getProps(); + const verticalAnchor = props.verticalAnchor ?? 'end'; + const lineHeight = props.lineHeight ?? '1em'; + const capHeight = props.capHeight ?? '0.71em'; + if (verticalAnchor === 'start') return getPixelValue(lineHeight); + if (verticalAnchor === 'middle') return getPixelValue(capHeight) / 2; + return -getPixelValue(capHeight) / 2; + }); + + scaleTransform = $derived.by(() => { + const props = this.#getProps(); + const x = props.x; + const y = props.y; + const width = props.width; + const scaleToFit = props.scaleToFit ?? false; + if ( + scaleToFit && + this.lineCount > 0 && + typeof x === 'number' && + typeof y === 'number' && + typeof width === 'number' + ) { + const lineWidth = this.wordsByLines[0].width || 1; + const sx = width / lineWidth; + const sy = sx; + const originX = x - sx * x; + const originY = y - sy * y; + return `matrix(${sx}, 0, 0, ${sy}, ${originX}, ${originY})`; + } + return ''; + }); + + rotateTransform = $derived.by(() => { + const props = this.#getProps(); + return props.rotate ? `rotate(${props.rotate}, ${props.x}, ${props.y})` : ''; + }); + + transform = $derived( + (this.#getProps().transform as string | undefined) ?? + `${this.scaleTransform} ${this.rotateTransform}` + ); + + // Static (non-data-driven) values + staticFill = $derived( + typeof this.#getProps().fill === 'string' ? (this.#getProps().fill as string) : undefined + ); + staticFillOpacity = $derived( + typeof this.#getProps().fillOpacity === 'number' + ? (this.#getProps().fillOpacity as number) + : undefined + ); + staticStroke = $derived( + typeof this.#getProps().stroke === 'string' ? (this.#getProps().stroke as string) : undefined + ); + staticStrokeWidth = $derived( + typeof this.#getProps().strokeWidth === 'number' + ? (this.#getProps().strokeWidth as number) + : undefined + ); + staticOpacity = $derived( + typeof this.#getProps().opacity === 'number' ? (this.#getProps().opacity as number) : undefined + ); + staticClassName = $derived( + typeof this.#getProps().class === 'string' ? (this.#getProps().class as string) : undefined + ); + + constructor(getProps: () => TextProps) { + this.#getProps = getProps; + + const initial = getProps(); + const _initialX: string | number = + initial.initialX ?? (typeof initial.x === 'function' ? 0 : (initial.x ?? 0)); + const _initialY: string | number = + initial.initialY ?? (typeof initial.y === 'function' ? 0 : (initial.y ?? 0)); + + this.#motionX = createMotion( + _initialX, + () => { + const x = getProps().x; + return typeof x === 'number' || typeof x === 'string' ? x : 0; + }, + initial.motion + ); + this.#motionY = createMotion( + _initialY, + () => { + const y = getProps().y; + return typeof y === 'number' || typeof y === 'string' ? y : 0; + }, + initial.motion + ); + + // Tween numeric values when motion is configured + this.#motionValue = createMotion( + typeof initial.value === 'number' ? initial.value : 0, + () => (typeof getProps().value === 'number' ? (getProps().value as number) : 0), + typeof initial.value === 'number' && initial.motion + ? typeof initial.motion === 'object' && 'type' in initial.motion + ? initial.motion + : undefined + : undefined + ); + + this.#dataMotionMap = createDataMotionMap(initial.motion); + if (this.#dataMotionMap) { + const motionMap = this.#dataMotionMap; + $effect(() => { + if (!this.dataMode) return; + const props = getProps(); + const keyFn = props.key ?? defaultKey; + const activeKeys = new Set(); + for (let i = 0; i < this.#resolvedData.length; i++) { + const d = this.#resolvedData[i]; + const key = keyFn(d, i); + activeKeys.add(key); + const resolved = this.resolveTextPosition(d); + untrack(() => motionMap.update(key, resolved)); + } + untrack(() => motionMap.cleanup(activeKeys)); + }); + } + } +} + +// Re-export for per-layer files that need it +export type { TruncateTextOptions } from '$lib/utils/string.js'; diff --git a/packages/layerchart/src/lib/components/Text/Text.svelte b/packages/layerchart/src/lib/components/Text/Text.svelte new file mode 100644 index 000000000..8b81f96ec --- /dev/null +++ b/packages/layerchart/src/lib/components/Text/Text.svelte @@ -0,0 +1,29 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Text.svelte.test.ts b/packages/layerchart/src/lib/components/Text/Text.svelte.test.ts similarity index 100% rename from packages/layerchart/src/lib/components/Text.svelte.test.ts rename to packages/layerchart/src/lib/components/Text/Text.svelte.test.ts diff --git a/packages/layerchart/src/lib/components/Text/Text.svg.svelte b/packages/layerchart/src/lib/components/Text/Text.svg.svelte new file mode 100644 index 000000000..405c3d986 --- /dev/null +++ b/packages/layerchart/src/lib/components/Text/Text.svg.svelte @@ -0,0 +1,190 @@ + + + + +{#if c.dataMode} + {#each c.resolvedItems as item (item.key)} + {@const text = c.resolveTextValue(item.d)} + {@const resolvedFill = resolveColorProp(rest.fill, item.d, c.chartCtx.cScale)} + {@const resolvedStroke = resolveColorProp(rest.stroke, item.d, c.chartCtx.cScale)} + {@const resolvedFillOpacity = resolveStyleProp(rest.fillOpacity, item.d)} + {@const resolvedStrokeWidth = resolveStyleProp(rest.strokeWidth, item.d)} + {@const resolvedOpacity = resolveStyleProp(rest.opacity, item.d)} + {@const resolvedClass = resolveStyleProp(rest.class, item.d)} + {@const dataRotateTransform = rotate + ? `rotate(${rotate}, ${item.x}, ${item.y})` + : ''} + + + + {text} + + + + {/each} +{:else} + + {#if rest.path} + + {#key rest.path} + + {/key} + + + + {c.wordsByLines.map((line) => line.words.join(' ')).join()} + + + {:else if isValidXOrY(typeof rest.x === 'function' ? undefined : rest.x) && isValidXOrY(typeof rest.y === 'function' ? undefined : rest.y)} + + {#if rest.segments} + {#each rest.segments as segment, index (index)} + + {segment.value} + + {/each} + {:else} + {#each c.wordsByLines as line, index (index)} + + {line.words.join(' ')} + + {/each} + {/if} + + {/if} + +{/if} + + diff --git a/packages/layerchart/src/lib/html.ts b/packages/layerchart/src/lib/html.ts index b0c89ffc7..c00e851b7 100644 --- a/packages/layerchart/src/lib/html.ts +++ b/packages/layerchart/src/lib/html.ts @@ -9,3 +9,9 @@ */ export { default as Circle } from './components/Circle.html.svelte'; export type { CircleProps, CirclePropsWithoutHTML } from './components/Circle.shared.svelte.js'; +export { default as Text } from './components/Text.html.svelte'; +export type { + TextProps, + TextPropsWithoutHTML, + TextSegment, +} from './components/Text.shared.svelte.js'; diff --git a/packages/layerchart/src/lib/svg.ts b/packages/layerchart/src/lib/svg.ts index f9d9f8c23..38d931395 100644 --- a/packages/layerchart/src/lib/svg.ts +++ b/packages/layerchart/src/lib/svg.ts @@ -11,3 +11,9 @@ */ export { default as Circle } from './components/Circle.svg.svelte'; export type { CircleProps, CirclePropsWithoutHTML } from './components/Circle.shared.svelte.js'; +export { default as Text } from './components/Text.svg.svelte'; +export type { + TextProps, + TextPropsWithoutHTML, + TextSegment, +} from './components/Text.shared.svelte.js'; From 70d58ebab93a6f741c54103c74e34156807d4ff2 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 27 Apr 2026 16:02:50 -0400 Subject: [PATCH 03/36] Organize into component directories --- bundle-analyzer/bundle-reports/latest.json | 90 +++++++++---------- .../src/lib/bench/PrimitiveBench.svelte | 4 +- packages/layerchart/src/lib/canvas.ts | 8 +- .../src/lib/components/AnnotationLine.svelte | 2 +- .../src/lib/components/AnnotationPoint.svelte | 4 +- .../src/lib/components/AnnotationRange.svelte | 2 +- .../src/lib/components/Arc.svelte.test.ts | 2 +- .../src/lib/components/ArcLabel.svelte | 4 +- .../layerchart/src/lib/components/Axis.svelte | 2 +- .../src/lib/components/BoxPlot.svelte | 2 +- .../src/lib/components/Calendar.svelte | 2 +- .../layerchart/src/lib/components/Cell.svelte | 2 +- .../components/Circle/Circle.svelte.test.ts | 2 +- .../layerchart/src/lib/components/Grid.svelte | 2 +- .../src/lib/components/Highlight.svelte | 2 +- .../src/lib/components/Labels.svelte | 2 +- .../src/lib/components/Month.svelte | 2 +- .../src/lib/components/Points.svelte | 2 +- .../layerchart/src/lib/components/Rule.svelte | 2 +- .../lib/components/Text/Text.svelte.test.ts | 2 +- .../src/lib/components/geo/GeoPoint.svelte | 2 +- .../src/lib/components/geo/TileImage.svelte | 2 +- .../layerchart/src/lib/components/index.ts | 8 +- packages/layerchart/src/lib/html.ts | 8 +- packages/layerchart/src/lib/svg.ts | 8 +- .../src/lib/utils/arcText.svelte.ts | 2 +- 26 files changed, 85 insertions(+), 85 deletions(-) diff --git a/bundle-analyzer/bundle-reports/latest.json b/bundle-analyzer/bundle-reports/latest.json index 9efdecbf6..f2c6f1545 100644 --- a/bundle-analyzer/bundle-reports/latest.json +++ b/bundle-analyzer/bundle-reports/latest.json @@ -1,12 +1,12 @@ { - "timestamp": "2026-04-27T19:01:35.829Z", + "timestamp": "2026-04-27T19:59:42.427Z", "results": [ { "scenario": "core", "description": "Bare minimum: Chart context + Svg layer", "group": "Foundation", - "size": 449088, - "gzipSize": 109291, + "size": 449187, + "gzipSize": 109327, "imports": [ "Chart", "Svg" @@ -16,8 +16,8 @@ "scenario": "canvas", "description": "Canvas-based rendering", "group": "Foundation", - "size": 449088, - "gzipSize": 109289, + "size": 449187, + "gzipSize": 109327, "imports": [ "Chart", "Canvas" @@ -27,8 +27,8 @@ "scenario": "line-chart", "description": "Basic line chart with axes and grid", "group": "Cartesian charts", - "size": 449112, - "gzipSize": 109301, + "size": 449211, + "gzipSize": 109338, "imports": [ "Chart", "Svg", @@ -41,8 +41,8 @@ "scenario": "line-chart-interactive", "description": "Line chart with tooltip and highlight", "group": "Cartesian charts", - "size": 463681, - "gzipSize": 111985, + "size": 463780, + "gzipSize": 112022, "imports": [ "Chart", "Svg", @@ -57,8 +57,8 @@ "scenario": "area-chart", "description": "Area chart with axes", "group": "Cartesian charts", - "size": 473660, - "gzipSize": 115339, + "size": 473759, + "gzipSize": 115381, "imports": [ "Chart", "Svg", @@ -71,8 +71,8 @@ "scenario": "bar-chart", "description": "Bar chart with axes", "group": "Cartesian charts", - "size": 457944, - "gzipSize": 111320, + "size": 458043, + "gzipSize": 111359, "imports": [ "Chart", "Svg", @@ -85,8 +85,8 @@ "scenario": "scatter-chart", "description": "Scatter plot with points", "group": "Cartesian charts", - "size": 453338, - "gzipSize": 110446, + "size": 453437, + "gzipSize": 110484, "imports": [ "Chart", "Svg", @@ -100,8 +100,8 @@ "scenario": "pie-chart", "description": "Pie/donut chart with arcs", "group": "Cartesian charts", - "size": 456137, - "gzipSize": 111083, + "size": 456236, + "gzipSize": 111120, "imports": [ "Chart", "Svg", @@ -114,8 +114,8 @@ "scenario": "high-level-charts", "description": "All high-level chart components (LineChart, BarChart, etc.)", "group": "Cartesian charts", - "size": 546727, - "gzipSize": 131366, + "size": 546826, + "gzipSize": 131405, "imports": [ "LineChart", "AreaChart", @@ -129,8 +129,8 @@ "scenario": "geo", "description": "Geographic map with paths", "group": "Geo", - "size": 464040, - "gzipSize": 112768, + "size": 464139, + "gzipSize": 112804, "imports": [ "Chart", "Svg", @@ -143,8 +143,8 @@ "scenario": "geo-tiles", "description": "Geographic map with tile layer", "group": "Geo", - "size": 468487, - "gzipSize": 114275, + "size": 468586, + "gzipSize": 114313, "imports": [ "Chart", "Svg", @@ -158,8 +158,8 @@ "scenario": "geo-full", "description": "Full geo setup with all geo components", "group": "Geo", - "size": 516292, - "gzipSize": 127644, + "size": 516391, + "gzipSize": 127683, "imports": [ "Chart", "Svg", @@ -181,8 +181,8 @@ "scenario": "hierarchy-tree", "description": "Tree layout with links", "group": "Hierarchy", - "size": 475343, - "gzipSize": 116273, + "size": 475442, + "gzipSize": 116317, "imports": [ "Chart", "Svg", @@ -196,8 +196,8 @@ "scenario": "hierarchy-treemap", "description": "Treemap layout", "group": "Hierarchy", - "size": 455527, - "gzipSize": 111294, + "size": 455626, + "gzipSize": 111332, "imports": [ "Chart", "Svg", @@ -211,8 +211,8 @@ "scenario": "hierarchy-pack", "description": "Circle packing layout", "group": "Hierarchy", - "size": 455347, - "gzipSize": 111395, + "size": 455446, + "gzipSize": 111435, "imports": [ "Chart", "Svg", @@ -225,8 +225,8 @@ "scenario": "force", "description": "Force-directed graph layout", "group": "Graph / network", - "size": 477795, - "gzipSize": 117095, + "size": 477894, + "gzipSize": 117137, "imports": [ "Chart", "Svg", @@ -240,8 +240,8 @@ "scenario": "dagre", "description": "Dagre directed graph layout", "group": "Graph / network", - "size": 535225, - "gzipSize": 132517, + "size": 535324, + "gzipSize": 132559, "imports": [ "Chart", "Svg", @@ -255,8 +255,8 @@ "scenario": "sankey", "description": "Sankey flow diagram", "group": "Graph / network", - "size": 477453, - "gzipSize": 116424, + "size": 477552, + "gzipSize": 116466, "imports": [ "Chart", "Svg", @@ -270,8 +270,8 @@ "scenario": "chord", "description": "Chord diagram", "group": "Graph / network", - "size": 458070, - "gzipSize": 111462, + "size": 458169, + "gzipSize": 111499, "imports": [ "Chart", "Svg", @@ -323,8 +323,8 @@ "scenario": "text-agnostic", "description": "Standalone Text (agnostic) — baseline", "group": "Layer-specific", - "size": 119881, - "gzipSize": 29879, + "size": 119980, + "gzipSize": 29916, "imports": [ "Text" ] @@ -333,8 +333,8 @@ "scenario": "text-svg", "description": "Standalone Text from `layerchart/svg`", "group": "Layer-specific", - "size": 64758, - "gzipSize": 16574, + "size": 64857, + "gzipSize": 16608, "imports": [ "Text" ] @@ -363,8 +363,8 @@ "scenario": "all", "description": "Everything from layerchart (worst case)", "group": "Worst case", - "size": 993232, - "gzipSize": 245308, + "size": 993331, + "gzipSize": 245354, "imports": [ "*" ] diff --git a/packages/layerchart/src/lib/bench/PrimitiveBench.svelte b/packages/layerchart/src/lib/bench/PrimitiveBench.svelte index 8239d3efd..7353e885f 100644 --- a/packages/layerchart/src/lib/bench/PrimitiveBench.svelte +++ b/packages/layerchart/src/lib/bench/PrimitiveBench.svelte @@ -2,11 +2,11 @@ import Chart from '../components/Chart.svelte'; import Layer from '../components/layers/Layer.svelte'; import Rect from '../components/Rect.svelte'; - import Circle from '../components/Circle.svelte'; + import Circle from '../components/Circle/Circle.svelte'; import Ellipse from '../components/Ellipse.svelte'; import Line from '../components/Line.svelte'; import Group from '../components/Group.svelte'; - import Text from '../components/Text.svelte'; + import Text from '../components/Text/Text.svelte'; import Path from '../components/Path.svelte'; type Primitive = 'rect' | 'circle' | 'ellipse' | 'line' | 'group' | 'text' | 'path'; diff --git a/packages/layerchart/src/lib/canvas.ts b/packages/layerchart/src/lib/canvas.ts index 19701246e..cde6e126e 100644 --- a/packages/layerchart/src/lib/canvas.ts +++ b/packages/layerchart/src/lib/canvas.ts @@ -7,11 +7,11 @@ * The agnostic versions (e.g. `import { Circle } from 'layerchart'`) still * work and dispatch to these per-layer variants under the hood. */ -export { default as Circle } from './components/Circle.canvas.svelte'; -export type { CircleProps, CirclePropsWithoutHTML } from './components/Circle.shared.svelte.js'; -export { default as Text } from './components/Text.canvas.svelte'; +export { default as Circle } from './components/Circle/Circle.canvas.svelte'; +export type { CircleProps, CirclePropsWithoutHTML } from './components/Circle/Circle.shared.svelte.js'; +export { default as Text } from './components/Text/Text.canvas.svelte'; export type { TextProps, TextPropsWithoutHTML, TextSegment, -} from './components/Text.shared.svelte.js'; +} from './components/Text/Text.shared.svelte.js'; diff --git a/packages/layerchart/src/lib/components/AnnotationLine.svelte b/packages/layerchart/src/lib/components/AnnotationLine.svelte index 6ff6a9f67..2f3bae6fa 100644 --- a/packages/layerchart/src/lib/components/AnnotationLine.svelte +++ b/packages/layerchart/src/lib/components/AnnotationLine.svelte @@ -49,7 +49,7 @@
- {/each} - {:else} - {@const { angle, length } = pointsToAngleAndLength( - { x: motionX1.current, y: motionY1.current }, - { x: motionX2.current, y: motionY2.current } - )} - -
- {/if} -{/if} - - diff --git a/packages/layerchart/src/lib/components/Line/Line.canvas.svelte b/packages/layerchart/src/lib/components/Line/Line.canvas.svelte new file mode 100644 index 000000000..af0d3dd28 --- /dev/null +++ b/packages/layerchart/src/lib/components/Line/Line.canvas.svelte @@ -0,0 +1,118 @@ + + + diff --git a/packages/layerchart/src/lib/components/Line/Line.html.svelte b/packages/layerchart/src/lib/components/Line/Line.html.svelte new file mode 100644 index 000000000..d72a986e4 --- /dev/null +++ b/packages/layerchart/src/lib/components/Line/Line.html.svelte @@ -0,0 +1,88 @@ + + + + +{#if c.dataMode} + {#each c.resolvedItems as item (item.key)} + {@const resolvedStroke = resolveColorProp(rest.stroke, item.d, c.chartCtx.cScale)} + {@const resolvedStrokeWidth = resolveStyleProp(rest.strokeWidth, item.d)} + {@const resolvedOpacity = resolveStyleProp(rest.opacity, item.d)} + {@const resolvedClass = resolveStyleProp(rest.class, item.d)} + {@const { angle, length } = pointsToAngleAndLength( + { x: item.x1, y: item.y1 }, + { x: item.x2, y: item.y2 } + )} +
+ {/each} +{:else} + {@const { angle, length } = pointsToAngleAndLength( + { x: c.motionX1, y: c.motionY1 }, + { x: c.motionX2, y: c.motionY2 } + )} + +
+{/if} + + diff --git a/packages/layerchart/src/lib/components/Line/Line.shared.svelte.ts b/packages/layerchart/src/lib/components/Line/Line.shared.svelte.ts new file mode 100644 index 000000000..cf1b6d9c5 --- /dev/null +++ b/packages/layerchart/src/lib/components/Line/Line.shared.svelte.ts @@ -0,0 +1,304 @@ +import { untrack } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; + +import type { Without } from '$lib/utils/types.js'; +import type { DataProp, DataDrivenStyleProps } from '$lib/utils/dataProp.js'; +import { + hasAnyDataProp, + resolveDataProp, + resolveGeoDataPair, +} from '$lib/utils/dataProp.js'; +import { chartDataArray } from '$lib/utils/common.js'; +import { parseDashArray } from '$lib/utils/path.js'; +import { createMotion, createDataMotionMap, type MotionProp } from '$lib/utils/motion.svelte.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import { getGeoContext } from '$lib/contexts/geo.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import type { GeoState } from '$lib/states/geo.svelte.js'; +import type { MarkerOptions } from '../MarkerWrapper.svelte'; + +export type LinePropsWithoutHTML = { + /** + * The x-coordinate of the line's starting point. + * - `number`: pixel value (direct) + * - `string`: data property name, resolved via xScale + * - `function(d)`: accessor called per data item, result passed through xScale + * + * @required + */ + x1: DataProp; + + /** The initial x-coordinate of the line's starting point (pixel mode only). @default x1 */ + initialX1?: number; + + /** + * The y-coordinate of the line's starting point. + * - `number`: pixel value (direct) + * - `string`: data property name, resolved via yScale + * - `function(d)`: accessor called per data item, result passed through yScale + * + * @required + */ + y1: DataProp; + + /** The initial y-coordinate of the line's starting point (pixel mode only). @default y1 */ + initialY1?: number; + + /** + * The x-coordinate of the line's ending point. + * - `number`: pixel value (direct) + * - `string`: data property name, resolved via xScale + * - `function(d)`: accessor called per data item, result passed through xScale + * + * @required + */ + x2: DataProp; + + /** The initial x-coordinate of the line's ending point (pixel mode only). @default x2 */ + initialX2?: number; + + /** + * The y-coordinate of the line's ending point. + * - `number`: pixel value (direct) + * - `string`: data property name, resolved via yScale + * - `function(d)`: accessor called per data item, result passed through yScale + * + * @default y2 + */ + y2: DataProp; + + /** The initial y-coordinate of the line's ending point (pixel mode only). @default y2 */ + initialY2?: number; + + /** + * Data array to iterate over in data mode. + * Falls back to chart context data when not provided. + */ + data?: any[]; + + /** + * Key function for keyed {#each} rendering in data mode. + * + * @default (d, i) => i + */ + key?: (d: any, index: number) => any; + + /** Marker to attach to both start and end points of the line */ + marker?: MarkerOptions; + + /** Marker to attach to the start point of the line */ + markerStart?: MarkerOptions; + + /** Marker to attach to the mid point of the line */ + markerMid?: MarkerOptions; + + /** Marker to attach to the end point of the line */ + markerEnd?: MarkerOptions; + + /** Motion configuration (pixel mode only). */ + motion?: MotionProp; + + /** + * Dashed-line pattern. Accepts a number (single dash length), a + * `[dash, gap, ...]` array, or a string (same syntax as SVG + * `stroke-dasharray`). Works across ``, ``, and `` + * layers — HTML approximates the pattern via `repeating-linear-gradient`. + */ + dashArray?: number | number[] | string; +} & DataDrivenStyleProps; + +export type LineProps = LinePropsWithoutHTML & + Without, LinePropsWithoutHTML>; + +const defaultKey = (_: any, i: number) => i; + +/** Build the standard `markInfo` payload used by every Line variant. */ +export function lineMarkInfo(props: LineProps, dataMode: boolean) { + if (!dataMode) return {}; + return { + data: props.data, + x: + typeof props.x1 === 'string' + ? props.x1 + : typeof props.x2 === 'string' + ? props.x2 + : undefined, + y: + typeof props.y1 === 'string' + ? props.y1 + : typeof props.y2 === 'string' + ? props.y2 + : undefined, + color: + typeof props.stroke === 'string' + ? props.stroke + : typeof props.fill === 'string' + ? props.fill + : undefined, + }; +} + +/** + * Reactive state shared by every per-layer Line variant. + */ +export class LineState { + #getProps: () => LineProps = () => ({}) as LineProps; + + // Contexts + chartCtx: ChartState = getChartContext(); + geo: GeoState = getGeoContext(); + + // Data mode detection + dataMode = $derived( + hasAnyDataProp( + this.#getProps().x1, + this.#getProps().y1, + this.#getProps().x2, + this.#getProps().y2 + ) + ); + + #resolvedData: any[] = $derived( + this.dataMode ? (this.#getProps().data ?? chartDataArray(this.chartCtx.data)) : [] + ); + + resolvedItems = $derived.by(() => { + if (!this.dataMode) return []; + const props = this.#getProps(); + const keyFn = props.key ?? defaultKey; + return this.#resolvedData.map((d, i) => { + const key = keyFn(d, i); + const resolved = this.#resolveLine(d); + const animated = this.#dataMotionMap?.get(key); + return { + d, + key, + x1: animated?.x1 ?? resolved.x1, + y1: animated?.y1 ?? resolved.y1, + x2: animated?.x2 ?? resolved.x2, + y2: animated?.y2 ?? resolved.y2, + }; + }); + }); + + #resolveLine(d: any): { x1: number; y1: number; x2: number; y2: number } { + const props = this.#getProps(); + if (this.geo.projection) { + const [projX1, projY1] = resolveGeoDataPair(props.x1, props.y1, d, this.geo.projection); + const [projX2, projY2] = resolveGeoDataPair(props.x2, props.y2, d, this.geo.projection); + return { x1: projX1, y1: projY1, x2: projX2, y2: projY2 }; + } + return { + x1: resolveDataProp(props.x1, d, this.chartCtx.xScale, 0), + y1: resolveDataProp(props.y1, d, this.chartCtx.yScale, 0), + x2: resolveDataProp(props.x2, d, this.chartCtx.xScale, 0), + y2: resolveDataProp(props.y2, d, this.chartCtx.yScale, 0), + }; + } + + // Dash array + dashArrayResolved = $derived(parseDashArray(this.#getProps().dashArray)); + dashArrayAttr = $derived(this.dashArrayResolved ? this.dashArrayResolved.join(' ') : undefined); + + // Pixel-mode motion sources + #dataMotionMap: ReturnType = null; + #motionX1!: ReturnType>; + #motionY1!: ReturnType>; + #motionX2!: ReturnType>; + #motionY2!: ReturnType>; + + get motionX1() { + return this.#motionX1.current; + } + get motionY1() { + return this.#motionY1.current; + } + get motionX2() { + return this.#motionX2.current; + } + get motionY2() { + return this.#motionY2.current; + } + + // Static (non-data-driven) values for SVG/HTML pixel mode + staticFill = $derived( + typeof this.#getProps().fill === 'string' ? (this.#getProps().fill as string) : undefined + ); + staticFillOpacity = $derived( + typeof this.#getProps().fillOpacity === 'number' + ? (this.#getProps().fillOpacity as number) + : undefined + ); + staticStroke = $derived( + typeof this.#getProps().stroke === 'string' ? (this.#getProps().stroke as string) : undefined + ); + staticStrokeWidth = $derived( + typeof this.#getProps().strokeWidth === 'number' + ? (this.#getProps().strokeWidth as number) + : undefined + ); + staticOpacity = $derived( + typeof this.#getProps().opacity === 'number' + ? (this.#getProps().opacity as number) + : undefined + ); + staticClassName = $derived( + typeof this.#getProps().class === 'string' ? (this.#getProps().class as string) : undefined + ); + // For HTML rendering: stroke-width fallback as div height + staticHeight = $derived( + typeof this.#getProps().strokeWidth === 'number' + ? `${this.#getProps().strokeWidth}px` + : '1px' + ); + + constructor(getProps: () => LineProps) { + this.#getProps = getProps; + + const initial = getProps(); + const initialX1 = initial.initialX1 ?? (typeof initial.x1 === 'number' ? initial.x1 : 0); + const initialY1 = initial.initialY1 ?? (typeof initial.y1 === 'number' ? initial.y1 : 0); + const initialX2 = initial.initialX2 ?? (typeof initial.x2 === 'number' ? initial.x2 : 0); + const initialY2 = initial.initialY2 ?? (typeof initial.y2 === 'number' ? initial.y2 : 0); + + this.#motionX1 = createMotion( + initialX1, + () => (typeof getProps().x1 === 'number' ? (getProps().x1 as number) : 0), + initial.motion + ); + this.#motionY1 = createMotion( + initialY1, + () => (typeof getProps().y1 === 'number' ? (getProps().y1 as number) : 0), + initial.motion + ); + this.#motionX2 = createMotion( + initialX2, + () => (typeof getProps().x2 === 'number' ? (getProps().x2 as number) : 0), + initial.motion + ); + this.#motionY2 = createMotion( + initialY2, + () => (typeof getProps().y2 === 'number' ? (getProps().y2 as number) : 0), + initial.motion + ); + + this.#dataMotionMap = createDataMotionMap(initial.motion); + if (this.#dataMotionMap) { + const motionMap = this.#dataMotionMap; + $effect(() => { + if (!this.dataMode) return; + const props = getProps(); + const keyFn = props.key ?? defaultKey; + const activeKeys = new Set(); + for (let i = 0; i < this.#resolvedData.length; i++) { + const d = this.#resolvedData[i]; + const key = keyFn(d, i); + activeKeys.add(key); + const resolved = this.#resolveLine(d); + untrack(() => motionMap.update(key, resolved)); + } + untrack(() => motionMap.cleanup(activeKeys)); + }); + } + } +} diff --git a/packages/layerchart/src/lib/components/Line/Line.svelte b/packages/layerchart/src/lib/components/Line/Line.svelte new file mode 100644 index 000000000..0b06ec61e --- /dev/null +++ b/packages/layerchart/src/lib/components/Line/Line.svelte @@ -0,0 +1,27 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Line.svelte.test.ts b/packages/layerchart/src/lib/components/Line/Line.svelte.test.ts similarity index 98% rename from packages/layerchart/src/lib/components/Line.svelte.test.ts rename to packages/layerchart/src/lib/components/Line/Line.svelte.test.ts index 3b13a26f3..67f0e9d7e 100644 --- a/packages/layerchart/src/lib/components/Line.svelte.test.ts +++ b/packages/layerchart/src/lib/components/Line/Line.svelte.test.ts @@ -3,7 +3,7 @@ import { render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import type { Component } from 'svelte'; -import TestHarness, { componentTestId } from './tests/TestHarness.svelte'; +import TestHarness, { componentTestId } from '../tests/TestHarness.svelte'; import Line from './Line.svelte'; describe('Line', () => { diff --git a/packages/layerchart/src/lib/components/Line/Line.svg.svelte b/packages/layerchart/src/lib/components/Line/Line.svg.svelte new file mode 100644 index 000000000..5e38add01 --- /dev/null +++ b/packages/layerchart/src/lib/components/Line/Line.svg.svelte @@ -0,0 +1,122 @@ + + + + +{#if c.dataMode} + + + + + {#each c.resolvedItems as item (item.key)} + {@const resolvedFill = resolveColorProp(rest.fill, item.d, c.chartCtx.cScale)} + {@const resolvedStroke = resolveColorProp(rest.stroke, item.d, c.chartCtx.cScale)} + {@const resolvedFillOpacity = resolveStyleProp(rest.fillOpacity, item.d)} + {@const resolvedStrokeWidth = resolveStyleProp(rest.strokeWidth, item.d)} + {@const resolvedOpacity = resolveStyleProp(rest.opacity, item.d)} + {@const resolvedClass = resolveStyleProp(rest.class, item.d)} + + {/each} +{:else} + + + + +{/if} + + diff --git a/packages/layerchart/src/lib/components/Link.svelte b/packages/layerchart/src/lib/components/Link.svelte index 683cb4986..8713f0080 100644 --- a/packages/layerchart/src/lib/components/Link.svelte +++ b/packages/layerchart/src/lib/components/Link.svelte @@ -4,7 +4,7 @@ import type { MotionNoneOption, MotionTweenOption } from '$lib/utils/motion.svelte.js'; import { curveBumpX, curveBumpY, type CurveFactory } from 'd3-shape'; import type { LinkSweep, LinkType } from '$lib/utils/linkUtils.js'; - import type { PathProps, PathPropsWithoutHTML } from './Path.svelte'; + import type { PathProps, PathPropsWithoutHTML } from './Path/Path.svelte'; import type { Accessor } from '$lib/utils/common.js'; export type LinkPropsWithoutHTML = { @@ -123,7 +123,7 @@ getLinkRadialPresetPath, } from '$lib/utils/linkUtils.js'; import { getChartContext } from '$lib/contexts/chart.js'; - import Path from './Path.svelte'; + import Path from './Path/Path.svelte'; import { extractLayerProps } from '$lib/utils/attributes.js'; import { accessor } from '$lib/utils/common.js'; import { cls } from '@layerstack/tailwind'; diff --git a/packages/layerchart/src/lib/components/Month.svelte b/packages/layerchart/src/lib/components/Month.svelte index 026ccdf16..47a5a442e 100644 --- a/packages/layerchart/src/lib/components/Month.svelte +++ b/packages/layerchart/src/lib/components/Month.svelte @@ -82,7 +82,7 @@ import { index } from 'd3-array'; import { format } from '@layerstack/utils'; - import Rect, { type RectPropsWithoutHTML } from './Rect.svelte'; + import Rect, { type RectPropsWithoutHTML } from './Rect/Rect.svelte'; import Group from './Group.svelte'; import Text from './Text/Text.svelte'; import { chartDataArray } from '../utils/common.js'; diff --git a/packages/layerchart/src/lib/components/MonthPath.svelte b/packages/layerchart/src/lib/components/MonthPath.svelte index 769cc1f55..0f899d7d7 100644 --- a/packages/layerchart/src/lib/components/MonthPath.svelte +++ b/packages/layerchart/src/lib/components/MonthPath.svelte @@ -38,7 +38,7 @@ import { timeWeek } from 'd3-time'; import { cls } from '@layerstack/tailwind'; import { endOfInterval } from '@layerstack/utils'; - import Path, { type PathPropsWithoutHTML } from './Path.svelte'; + import Path, { type PathPropsWithoutHTML } from './Path/Path.svelte'; let { date, diff --git a/packages/layerchart/src/lib/components/Path.svelte b/packages/layerchart/src/lib/components/Path.svelte deleted file mode 100644 index ff8ecb0de..000000000 --- a/packages/layerchart/src/lib/components/Path.svelte +++ /dev/null @@ -1,381 +0,0 @@ - - - - -{#if layerCtx === 'svg'} - {#key key} - - - - - - {#if startContent && startPoint} - - {@render startContent({ - point: startPoint, - value: { - x: ctx.xScale?.invert?.(startPoint.x), - y: ctx.yScale?.invert?.(startPoint.y), - }, - })} - - {/if} - - {#if endContent && endPoint.current} - - {@render endContent({ - point: endPoint.current, - value: { - x: ctx.xScale?.invert?.(endPoint.current.x), - y: ctx.yScale?.invert?.(endPoint.current.y), - }, - })} - - {/if} - {/key} -{/if} - - diff --git a/packages/layerchart/src/lib/components/Path/Path.canvas.svelte b/packages/layerchart/src/lib/components/Path/Path.canvas.svelte new file mode 100644 index 000000000..d958a6e50 --- /dev/null +++ b/packages/layerchart/src/lib/components/Path/Path.canvas.svelte @@ -0,0 +1,88 @@ + + + diff --git a/packages/layerchart/src/lib/components/Path/Path.shared.svelte.ts b/packages/layerchart/src/lib/components/Path/Path.shared.svelte.ts new file mode 100644 index 000000000..7e1113978 --- /dev/null +++ b/packages/layerchart/src/lib/components/Path/Path.shared.svelte.ts @@ -0,0 +1,127 @@ +import type { Snippet } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import type { CommonStyleProps, Without } from '$lib/utils/types.js'; +import type { MarkerOptions } from '../MarkerWrapper.svelte'; + +import { interpolatePath } from 'd3-interpolate-path'; +import { flattenPathData } from '$lib/utils/path.js'; +import { + createMotion, + extractTweenConfig, + type MotionProp, + type ResolvedMotion, +} from '$lib/utils/motion.svelte.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; + +import type { draw as _drawTransition } from 'svelte/transition'; + +export type PathPropsWithoutHTML = { + /** + * Pass `` explicitly instead of calculating + * from data / context + */ + pathData?: string | undefined | null; + + /** + * Whether to animate the drawing of the path over time. + * Pass either `true` or an object with transition options to + * enable the transition. + * + * Works best with `tweened` disabled. + */ + draw?: boolean | Parameters[1]; + + /** Marker to attach to both start and end points of the line */ + marker?: MarkerOptions; + + /** Marker to attach to the middle point of the line */ + markerMid?: MarkerOptions; + + /** Marker to attach to the start point of the line */ + markerStart?: MarkerOptions; + + /** Marker to attach to the end point of the line */ + markerEnd?: MarkerOptions; + + /** + * Add additional content at the start of the line. + * Receives `{ point: DOMPoint; value: { x: number; y: number } }` as a snippet prop. + */ + startContent?: Snippet<[{ point: DOMPoint; value: { x: number; y: number } }]>; + + /** + * Add additional content at the end of the line. + * Receives `{ point: DOMPoint; value: { x: number; y: number } }` as a snippet prop. + */ + endContent?: Snippet<[{ point: DOMPoint; value: { x: number; y: number } }]>; + + /** + * A reference to the `` element. + * + * @bindable + */ + pathRef?: SVGPathElement; + + motion?: MotionProp; +} & CommonStyleProps; + +export type PathProps = PathPropsWithoutHTML & + Without, PathPropsWithoutHTML>; + +/** + * Reactive state shared by every per-layer Path variant. + */ +export class PathState { + #getProps: () => PathProps = () => ({}) as PathProps; + + // Contexts + chartCtx: ChartState = getChartContext(); + + // Path data tween source — the actual `d` attribute / canvas render input + #tweenedState!: ReturnType>; + + get tweenedPathData() { + return this.#tweenedState.current; + } + + // Re-key trigger for draw transitions + drawKey = $state(Symbol()); + + constructor(getProps: () => PathProps) { + this.#getProps = getProps; + + const initial = getProps(); + const extractedTween = extractTweenConfig(initial.motion); + const tweenedOptions: ResolvedMotion | undefined = extractedTween + ? { + type: extractedTween.type, + options: { interpolate: interpolatePath, ...extractedTween.options }, + } + : undefined; + + // Provide initial `0` baseline; only set on initial mount + const defaultPathData = (() => { + if (!tweenedOptions) { + // Fast initial render when not tweened + return ''; + } else if (initial.pathData) { + return flattenPathData( + initial.pathData, + Math.min(this.chartCtx.yScale(0) ?? this.chartCtx.yRange[0], this.chartCtx.yRange[0]) + ); + } + return ''; + })(); + + this.#tweenedState = createMotion(defaultPathData, () => getProps().pathData, tweenedOptions); + + // Re-trigger draw transition when path data changes + $effect(() => { + if (!getProps().draw) return; + // Touch dependency + void getProps().pathData; + this.drawKey = Symbol(); + }); + } +} diff --git a/packages/layerchart/src/lib/components/Path/Path.svelte b/packages/layerchart/src/lib/components/Path/Path.svelte new file mode 100644 index 000000000..efcec068f --- /dev/null +++ b/packages/layerchart/src/lib/components/Path/Path.svelte @@ -0,0 +1,21 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Path/Path.svg.svelte b/packages/layerchart/src/lib/components/Path/Path.svg.svelte new file mode 100644 index 000000000..be008f995 --- /dev/null +++ b/packages/layerchart/src/lib/components/Path/Path.svg.svelte @@ -0,0 +1,167 @@ + + + + +{#key c.drawKey} + + + + + + {#if startContent && startPoint} + + {@render startContent({ + point: startPoint, + value: { + x: c.chartCtx.xScale?.invert?.(startPoint.x), + y: c.chartCtx.yScale?.invert?.(startPoint.y), + }, + })} + + {/if} + + {#if endContent && endPoint.current} + + {@render endContent({ + point: endPoint.current, + value: { + x: c.chartCtx.xScale?.invert?.(endPoint.current.x), + y: c.chartCtx.yScale?.invert?.(endPoint.current.y), + }, + })} + + {/if} +{/key} + + diff --git a/packages/layerchart/src/lib/components/Rect.svelte b/packages/layerchart/src/lib/components/Rect.svelte deleted file mode 100644 index 0060d28a1..000000000 --- a/packages/layerchart/src/lib/components/Rect.svelte +++ /dev/null @@ -1,739 +0,0 @@ - - - - -{#if layerCtx === 'svg'} - {#if dataMode} - {#each resolvedItems as item (item.key)} - {@const resolvedFill = resolveColorProp(fill, item.d, chartCtx.cScale)} - {@const resolvedStroke = resolveColorProp(stroke, item.d, chartCtx.cScale)} - {@const resolvedFillOpacity = resolveStyleProp(fillOpacity, item.d)} - {@const resolvedStrokeOpacity = resolveStyleProp(strokeOpacity, item.d)} - {@const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d)} - {@const resolvedOpacity = resolveStyleProp(opacity, item.d)} - {@const resolvedClass = resolveStyleProp(className, item.d)} - - {/each} - {:else if pixelPathData} - - - } - {onclick} - {ondblclick} - {onpointerenter} - {onpointermove} - {onpointerleave} - {onpointerover} - {onpointerout} - /> - {:else} - - {/if} -{:else if layerCtx === 'html'} - {#if dataMode} - {#each resolvedItems as item (item.key)} - {@const resolvedFill = resolveColorProp(fill, item.d, chartCtx.cScale)} - {@const resolvedStroke = resolveColorProp(stroke, item.d, chartCtx.cScale)} - {@const resolvedFillOpacity = resolveStyleProp(fillOpacity, item.d)} - {@const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d)} - {@const resolvedOpacity = resolveStyleProp(opacity, item.d)} - {@const resolvedClass = resolveStyleProp(className, item.d)} - {@const resolvedBorderWidth = - resolvedStrokeWidth != null - ? `${resolvedStrokeWidth}px` - : resolvedStroke != null - ? '1px' - : undefined} - - -
- {/each} - {:else} - - -
- {@render children?.()} -
- {/if} -{/if} - - diff --git a/packages/layerchart/src/lib/components/Rect/Rect.canvas.svelte b/packages/layerchart/src/lib/components/Rect/Rect.canvas.svelte new file mode 100644 index 000000000..508ebb699 --- /dev/null +++ b/packages/layerchart/src/lib/components/Rect/Rect.canvas.svelte @@ -0,0 +1,152 @@ + + + diff --git a/packages/layerchart/src/lib/components/Rect/Rect.html.svelte b/packages/layerchart/src/lib/components/Rect/Rect.html.svelte new file mode 100644 index 000000000..a9751a1ad --- /dev/null +++ b/packages/layerchart/src/lib/components/Rect/Rect.html.svelte @@ -0,0 +1,103 @@ + + + + +{#if c.dataMode} + {#each c.resolvedItems as item (item.key)} + {@const resolvedFill = resolveColorProp(rest.fill, item.d, c.chartCtx.cScale)} + {@const resolvedStroke = resolveColorProp(rest.stroke, item.d, c.chartCtx.cScale)} + {@const resolvedStrokeWidth = resolveStyleProp(rest.strokeWidth, item.d)} + {@const resolvedOpacity = resolveStyleProp(rest.opacity, item.d)} + {@const resolvedClass = resolveStyleProp(rest.class, item.d)} + {@const resolvedBorderWidth = + resolvedStrokeWidth != null + ? `${resolvedStrokeWidth}px` + : resolvedStroke != null + ? '1px' + : undefined} + + +
+ {/each} +{:else} + + +
+ {@render children?.()} +
+{/if} + + diff --git a/packages/layerchart/src/lib/components/Rect/Rect.shared.svelte.ts b/packages/layerchart/src/lib/components/Rect/Rect.shared.svelte.ts new file mode 100644 index 000000000..915c5fd72 --- /dev/null +++ b/packages/layerchart/src/lib/components/Rect/Rect.shared.svelte.ts @@ -0,0 +1,436 @@ +import type { Snippet } from 'svelte'; +import { untrack } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; + +import type { CommonEvents, Without } from '$lib/utils/types.js'; +import type { DataProp, DataDrivenStyleProps } from '$lib/utils/dataProp.js'; +import { + hasAnyDataProp, + resolveDataProp, + resolveGeoDataPair, +} from '$lib/utils/dataProp.js'; +import { chartDataArray } from '$lib/utils/common.js'; +import { + resolveCorners, + cornersUniform, + resolveInsets, + type Corners, + type Insets, +} from '$lib/utils/rect.svelte.js'; +import { roundedRectPath, parseDashArray } from '$lib/utils/path.js'; +import { + createMotion, + createDataMotionMap, + parseMotionProp, + type MotionProp, + type MotionOptions, +} from '$lib/utils/motion.svelte.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import { getGeoContext } from '$lib/contexts/geo.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import type { GeoState } from '$lib/states/geo.svelte.js'; + +export type RectPropsWithoutHTML = { + /** + * The x position of the rectangle. + * - `number`: pixel value (direct) + * - `string`: data property name, resolved via xScale + * - `function(d)`: accessor called per data item, result passed through xScale + * + * @default 0 + */ + x?: DataProp; + + /** + * The initial x position (pixel mode only). + * + * @default x + */ + initialX?: number; + + /** + * The y position of the rectangle. + * - `number`: pixel value (direct) + * - `string`: data property name, resolved via yScale + * - `function(d)`: accessor called per data item, result passed through yScale + * + * @default 0 + */ + y?: DataProp; + + /** + * The initial y position (pixel mode only). + * + * @default y + */ + initialY?: number; + + /** + * The width of the rectangle (pixels). + * + * @default 0 + */ + width?: number; + initialWidth?: number; + + /** + * The height of the rectangle (pixels). + * + * @default 0 + */ + height?: number; + initialHeight?: number; + + /** + * Left/start x edge (data mode). + * - `string`: data property name, resolved via xScale + * - `function(d)`: accessor called per data item, result passed through xScale + * - `number`: pixel value + */ + x0?: DataProp; + + /** + * Right/end x edge (data mode). + * - `string`: data property name, resolved via xScale + * - `function(d)`: accessor called per data item, result passed through xScale + * - `number`: pixel value + */ + x1?: DataProp; + + /** + * Top/start y edge (data mode). + * - `string`: data property name, resolved via yScale + * - `function(d)`: accessor called per data item, result passed through yScale + * - `number`: pixel value + */ + y0?: DataProp; + + /** + * Bottom/end y edge (data mode). + * - `string`: data property name, resolved via yScale + * - `function(d)`: accessor called per data item, result passed through yScale + * - `number`: pixel value + */ + y1?: DataProp; + + /** + * Insets to shrink the rendered rectangle. + * Supports `all`, `x`, `y`, `left`, `right`, `top`, `bottom`. + */ + insets?: Insets; + + /** + * Data array to iterate over in data mode. + * Falls back to chart context data when not provided. + */ + data?: any[]; + + /** + * Key function for keyed {#each} rendering in data mode. + * + * @default (d, i) => i + */ + key?: (d: any, index: number) => any; + + /** + * Underlying `` tag when using . Useful for bindings (pixel mode only). + * + * @bindable + */ + ref?: SVGRectElement; + + /** Motion configuration (pixel mode only). */ + motion?: MotionProp<'x' | 'y' | 'width' | 'height'>; + + /** + * Dashed-border pattern. Accepts a number (single dash length), a + * `[dash, gap, ...]` array, or a string (same syntax as SVG + * `stroke-dasharray`). HTML layer approximates via `border-style: dashed`. + */ + dashArray?: number | number[] | string; + + /** + * Per-corner radii. Accepts a number (all corners equal — same as `rx`), + * a `[tl, tr, br, bl]` tuple, or `{ topLeft, topRight, bottomRight, bottomLeft }`. + * Takes precedence over `rx`/`ry` when corners differ. + */ + corners?: Corners; + + /** Children content to render. Note: Only works for Html layers */ + children?: Snippet; +} & DataDrivenStyleProps; + +export type RectProps = RectPropsWithoutHTML & + Without, RectPropsWithoutHTML> & + CommonEvents; + +const defaultKey = (_: any, i: number) => i; + +/** Build the standard `markInfo` payload used by every Rect variant. */ +export function rectMarkInfo(props: RectProps, dataMode: boolean) { + if (!dataMode) return {}; + return { + data: props.data, + x: typeof props.x === 'string' ? props.x : undefined, + y: typeof props.y === 'string' ? props.y : undefined, + color: + typeof props.fill === 'string' + ? props.fill + : typeof props.stroke === 'string' + ? props.stroke + : undefined, + }; +} + +/** + * Reactive state shared by every per-layer Rect variant. + */ +export class RectState { + #getProps: () => RectProps = () => ({}) as RectProps; + + // Contexts + chartCtx: ChartState = getChartContext(); + geo: GeoState = getGeoContext(); + + // Data mode detection + hasEdgeProps = $derived( + hasAnyDataProp( + this.#getProps().x0, + this.#getProps().y0, + this.#getProps().x1, + this.#getProps().y1 + ) + ); + dataMode = $derived( + hasAnyDataProp(this.#getProps().x, this.#getProps().y) || this.hasEdgeProps + ); + + // Data resolution + #resolvedData: any[] = $derived( + this.dataMode ? (this.#getProps().data ?? chartDataArray(this.chartCtx.data)) : [] + ); + + resolvedItems = $derived.by(() => { + if (!this.dataMode) return []; + const props = this.#getProps(); + const keyFn = props.key ?? defaultKey; + return this.#resolvedData.map((d, i) => { + const key = keyFn(d, i); + const resolved = this.#resolveRect(d); + const animated = this.#dataMotionMap?.get(key); + return { + d, + key, + x: animated?.x ?? resolved.x, + y: animated?.y ?? resolved.y, + width: animated?.width ?? resolved.width, + height: animated?.height ?? resolved.height, + }; + }); + }); + + #resolveRect(d: any): { x: number; y: number; width: number; height: number } { + const props = this.#getProps(); + const resolvedInsets = resolveInsets(props.insets); + + if (this.hasEdgeProps) { + let rx0: number, rx1p: number, ry0: number, ry1p: number; + if (this.geo.projection) { + [rx0, ry0] = resolveGeoDataPair(props.x0, props.y0, d, this.geo.projection); + [rx1p, ry1p] = resolveGeoDataPair(props.x1, props.y1, d, this.geo.projection); + } else { + rx0 = resolveDataProp(props.x0, d, this.chartCtx.xScale, 0); + rx1p = resolveDataProp(props.x1, d, this.chartCtx.xScale, 0); + ry0 = resolveDataProp(props.y0, d, this.chartCtx.yScale, 0); + ry1p = resolveDataProp(props.y1, d, this.chartCtx.yScale, 0); + } + + const left = Math.min(rx0, rx1p) + resolvedInsets.left; + const right = Math.max(rx0, rx1p) - resolvedInsets.right; + const top = Math.min(ry0, ry1p) + resolvedInsets.top; + const bottom = Math.max(ry0, ry1p) - resolvedInsets.bottom; + + return { + x: left, + y: top, + width: Math.max(0, right - left), + height: Math.max(0, bottom - top), + }; + } else { + let resolvedX: number, resolvedY: number; + if (this.geo.projection) { + [resolvedX, resolvedY] = resolveGeoDataPair(props.x, props.y, d, this.geo.projection); + } else { + resolvedX = resolveDataProp(props.x, d, this.chartCtx.xScale, 0); + resolvedY = resolveDataProp(props.y, d, this.chartCtx.yScale, 0); + } + return { + x: resolvedX + resolvedInsets.left, + y: resolvedY + resolvedInsets.top, + width: Math.max(0, (props.width ?? 0) - resolvedInsets.left - resolvedInsets.right), + height: Math.max(0, (props.height ?? 0) - resolvedInsets.top - resolvedInsets.bottom), + }; + } + } + + // Dash array + dashArrayResolved = $derived(parseDashArray(this.#getProps().dashArray)); + dashArrayAttr = $derived(this.dashArrayResolved ? this.dashArrayResolved.join(' ') : undefined); + + // Corners + cornersUniformValue = $derived.by(() => { + const corners = this.#getProps().corners; + if (corners === undefined) return undefined; + if (typeof corners === 'number') return corners; + const resolved = resolveCorners(corners, Infinity, Infinity); + return cornersUniform(resolved) ? resolved[0] : undefined; + }); + cornersNonUniform = $derived( + this.#getProps().corners !== undefined && this.cornersUniformValue === undefined + ); + + // Normalize rx/ry: if only one provided, use for both (SVG behavior) + rx = $derived( + Number( + (this.#getProps() as any).rx ?? (this.#getProps() as any).ry ?? this.cornersUniformValue + ) || 0 + ); + ry = $derived( + Number( + (this.#getProps() as any).ry ?? (this.#getProps() as any).rx ?? this.cornersUniformValue + ) || 0 + ); + + // Pixel-mode motion sources + #dataMotionMap: ReturnType = null; + #motionX!: ReturnType>; + #motionY!: ReturnType>; + #motionWidth!: ReturnType>; + #motionHeight!: ReturnType>; + + get motionX() { + return this.#motionX.current; + } + get motionY() { + return this.#motionY.current; + } + get motionWidth() { + return this.#motionWidth.current; + } + get motionHeight() { + return this.#motionHeight.current; + } + + // Resolved per-corner radii (clamped to current bounds) + resolvedCorners = $derived.by(() => { + const corners = this.#getProps().corners; + if (corners === undefined) return undefined; + return resolveCorners(corners, this.motionWidth, this.motionHeight); + }); + + borderRadiusStyle = $derived( + this.resolvedCorners ? this.resolvedCorners.map((c) => `${c}px`).join(' ') : undefined + ); + + pixelPathData = $derived.by(() => { + if (this.resolvedCorners && this.cornersNonUniform) { + return roundedRectPath( + this.motionX, + this.motionY, + this.motionWidth, + this.motionHeight, + this.resolvedCorners + ); + } + return undefined; + }); + + // Static (non-data-driven) values for SVG/HTML pixel mode + staticFill = $derived( + typeof this.#getProps().fill === 'string' ? (this.#getProps().fill as string) : undefined + ); + staticFillOpacity = $derived( + typeof this.#getProps().fillOpacity === 'number' + ? (this.#getProps().fillOpacity as number) + : undefined + ); + staticStroke = $derived( + typeof this.#getProps().stroke === 'string' ? (this.#getProps().stroke as string) : undefined + ); + staticStrokeOpacity = $derived( + typeof this.#getProps().strokeOpacity === 'number' + ? (this.#getProps().strokeOpacity as number) + : undefined + ); + staticStrokeWidth = $derived( + typeof this.#getProps().strokeWidth === 'number' + ? (this.#getProps().strokeWidth as number) + : undefined + ); + staticOpacity = $derived( + typeof this.#getProps().opacity === 'number' + ? (this.#getProps().opacity as number) + : undefined + ); + staticClassName = $derived( + typeof this.#getProps().class === 'string' ? (this.#getProps().class as string) : undefined + ); + // Match SVG's implicit `stroke-width: 1` default + staticBorderWidth = $derived.by(() => { + const props = this.#getProps(); + if (typeof props.strokeWidth === 'number') return `${props.strokeWidth}px`; + if (typeof props.stroke === 'string') return '1px'; + return undefined; + }); + + constructor(getProps: () => RectProps) { + this.#getProps = getProps; + + const initial = getProps(); + const initialX = initial.initialX ?? (typeof initial.x === 'number' ? initial.x : 0); + const initialY = initial.initialY ?? (typeof initial.y === 'number' ? initial.y : 0); + const initialWidth = initial.initialWidth ?? initial.width ?? 0; + const initialHeight = initial.initialHeight ?? initial.height ?? 0; + const motion = initial.motion; + + this.#motionX = createMotion( + initialX, + () => (typeof getProps().x === 'number' ? (getProps().x as number) : 0), + motion === undefined ? undefined : parseMotionProp(motion, 'x') + ); + this.#motionY = createMotion( + initialY, + () => (typeof getProps().y === 'number' ? (getProps().y as number) : 0), + motion === undefined ? undefined : parseMotionProp(motion, 'y') + ); + this.#motionWidth = createMotion( + initialWidth, + () => getProps().width ?? 0, + motion === undefined ? undefined : parseMotionProp(motion, 'width') + ); + this.#motionHeight = createMotion( + initialHeight, + () => getProps().height ?? 0, + motion === undefined ? undefined : parseMotionProp(motion, 'height') + ); + + this.#dataMotionMap = createDataMotionMap(motion as MotionOptions | undefined); + if (this.#dataMotionMap) { + const motionMap = this.#dataMotionMap; + $effect(() => { + if (!this.dataMode) return; + const props = getProps(); + const keyFn = props.key ?? defaultKey; + const activeKeys = new Set(); + for (let i = 0; i < this.#resolvedData.length; i++) { + const d = this.#resolvedData[i]; + const key = keyFn(d, i); + activeKeys.add(key); + const resolved = this.#resolveRect(d); + untrack(() => motionMap.update(key, resolved)); + } + untrack(() => motionMap.cleanup(activeKeys)); + }); + } + } +} diff --git a/packages/layerchart/src/lib/components/Rect/Rect.svelte b/packages/layerchart/src/lib/components/Rect/Rect.svelte new file mode 100644 index 000000000..1c6df75ff --- /dev/null +++ b/packages/layerchart/src/lib/components/Rect/Rect.svelte @@ -0,0 +1,27 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Rect.svelte.test.ts b/packages/layerchart/src/lib/components/Rect/Rect.svelte.test.ts similarity index 98% rename from packages/layerchart/src/lib/components/Rect.svelte.test.ts rename to packages/layerchart/src/lib/components/Rect/Rect.svelte.test.ts index 9d3b1ad9f..e8a3ff5ac 100644 --- a/packages/layerchart/src/lib/components/Rect.svelte.test.ts +++ b/packages/layerchart/src/lib/components/Rect/Rect.svelte.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; -import TestHarness, { componentTestId } from './tests/TestHarness.svelte'; +import TestHarness, { componentTestId } from '../tests/TestHarness.svelte'; import Rect from './Rect.svelte'; describe('Rect', () => { diff --git a/packages/layerchart/src/lib/components/Rect/Rect.svg.svelte b/packages/layerchart/src/lib/components/Rect/Rect.svg.svelte new file mode 100644 index 000000000..61ccf3f32 --- /dev/null +++ b/packages/layerchart/src/lib/components/Rect/Rect.svg.svelte @@ -0,0 +1,136 @@ + + + + +{#if c.dataMode} + {#each c.resolvedItems as item (item.key)} + {@const resolvedFill = resolveColorProp(rest.fill, item.d, c.chartCtx.cScale)} + {@const resolvedStroke = resolveColorProp(rest.stroke, item.d, c.chartCtx.cScale)} + {@const resolvedFillOpacity = resolveStyleProp(rest.fillOpacity, item.d)} + {@const resolvedStrokeOpacity = resolveStyleProp(rest.strokeOpacity, item.d)} + {@const resolvedStrokeWidth = resolveStyleProp(rest.strokeWidth, item.d)} + {@const resolvedOpacity = resolveStyleProp(rest.opacity, item.d)} + {@const resolvedClass = resolveStyleProp(rest.class, item.d)} + + {/each} +{:else if c.pixelPathData} + + + } + d={c.pixelPathData} + fill={c.staticFill} + fill-opacity={c.staticFillOpacity} + stroke={c.staticStroke} + stroke-opacity={c.staticStrokeOpacity} + stroke-width={c.staticStrokeWidth} + opacity={c.staticOpacity} + stroke-dasharray={c.dashArrayAttr} + class={cls('lc-rect', c.staticClassName)} + /> +{:else} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/RectClipPath.svelte b/packages/layerchart/src/lib/components/RectClipPath.svelte index f789b5a33..09d87add6 100644 --- a/packages/layerchart/src/lib/components/RectClipPath.svelte +++ b/packages/layerchart/src/lib/components/RectClipPath.svelte @@ -1,5 +1,5 @@
` - * - * For shapes that can't be expressed as an SVG path (or for advanced - * per-layer customization), use the `clip` snippet (SVG) alongside `path`. - */ - path?: string; - - /** - * A snippet to insert custom SVG content into the ``. When - * omitted and `path` is set, a `` is rendered automatically. - */ - clip?: Snippet<[{ id: string }]>; - - /** - * Children to render in the `` element that links to the clipPath (if not disabled). - * Provides the id, url, and useId for the clipPath as snippet props. - */ - children?: Snippet<[{ id: string; url: string; useId?: string }]>; - }; - - export type ClipPathProps = ClipPathPropsWithoutHTML & - Without, ClipPathPropsWithoutHTML>; - - - - -{#if layerCtx === 'svg'} - - - {#if clip} - {@render clip({ id })} - {:else if effectivePath} - - {/if} - - {#if useId} - - {/if} - - -{/if} - -{#if children} - {#if disabled} - {@render children({ id, url, useId })} - {:else if layerCtx === 'svg'} - - {@render children({ id, url, useId })} - - {:else if layerCtx === 'html' && effectivePath} -
- {@render children({ id, url, useId })} -
- {:else} - {@render children({ id, url, useId })} - {/if} -{/if} diff --git a/packages/layerchart/src/lib/components/ClipPath/ClipPath.canvas.svelte b/packages/layerchart/src/lib/components/ClipPath/ClipPath.canvas.svelte new file mode 100644 index 000000000..859d53d18 --- /dev/null +++ b/packages/layerchart/src/lib/components/ClipPath/ClipPath.canvas.svelte @@ -0,0 +1,45 @@ + + + + +{#if children} + {@render children({ id, url, useId })} +{/if} diff --git a/packages/layerchart/src/lib/components/ClipPath/ClipPath.html.svelte b/packages/layerchart/src/lib/components/ClipPath/ClipPath.html.svelte new file mode 100644 index 000000000..9be21f902 --- /dev/null +++ b/packages/layerchart/src/lib/components/ClipPath/ClipPath.html.svelte @@ -0,0 +1,42 @@ + + + + +{#if children} + {#if disabled || !c.effectivePath} + {@render children({ id, url, useId })} + {:else} +
+ {@render children({ id, url, useId })} +
+ {/if} +{/if} diff --git a/packages/layerchart/src/lib/components/ClipPath/ClipPath.shared.svelte.ts b/packages/layerchart/src/lib/components/ClipPath/ClipPath.shared.svelte.ts new file mode 100644 index 000000000..825015370 --- /dev/null +++ b/packages/layerchart/src/lib/components/ClipPath/ClipPath.shared.svelte.ts @@ -0,0 +1,59 @@ +import type { Snippet } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import type { Without } from '$lib/utils/types.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; + +export type ClipPathPropsWithoutHTML = { + /** A unique id for the clipPath. */ + id?: string; + + /** Use existing path or shape (by id) for clipPath */ + useId?: string; + + /** Whether to disable clipping (show all). @default false */ + disabled?: boolean; + + /** + * Invert the clip — content renders *outside* the shape instead of inside. + * @default false + */ + invert?: boolean; + + /** + * SVG path `d` string describing the clip shape. Drives all three layers. + */ + path?: string; + + /** A snippet to insert custom SVG content into the ``. */ + clip?: Snippet<[{ id: string }]>; + + /** Children to render. */ + children?: Snippet<[{ id: string; url: string; useId?: string }]>; +}; + +export type ClipPathProps = ClipPathPropsWithoutHTML & + Without, ClipPathPropsWithoutHTML>; + +/** + * Reactive state shared by every per-layer ClipPath variant. + */ +export class ClipPathState { + #getProps: () => ClipPathProps = () => ({}) as ClipPathProps; + + chartCtx: ChartState = getChartContext(); + + // Outer rect covering the chart bounds — combined with the clip shape under + // even-odd fill rule to invert the clip. + outerRect = $derived(`M0,0 H${this.chartCtx.width} V${this.chartCtx.height} H0 Z`); + + /** Effective path used for canvas + html layers when inverting. */ + effectivePath = $derived.by(() => { + const props = this.#getProps(); + return props.invert && props.path ? `${this.outerRect} ${props.path}` : props.path; + }); + + constructor(getProps: () => ClipPathProps) { + this.#getProps = getProps; + } +} diff --git a/packages/layerchart/src/lib/components/ClipPath/ClipPath.svelte b/packages/layerchart/src/lib/components/ClipPath/ClipPath.svelte new file mode 100644 index 000000000..eb454c24b --- /dev/null +++ b/packages/layerchart/src/lib/components/ClipPath/ClipPath.svelte @@ -0,0 +1,23 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/ClipPath/ClipPath.svg.svelte b/packages/layerchart/src/lib/components/ClipPath/ClipPath.svg.svelte new file mode 100644 index 000000000..d7dd05e52 --- /dev/null +++ b/packages/layerchart/src/lib/components/ClipPath/ClipPath.svg.svelte @@ -0,0 +1,49 @@ + + + + + + + {#if clip} + {@render clip({ id })} + {:else if c.effectivePath} + + {/if} + + {#if useId} + + {/if} + + + +{#if children} + {#if disabled} + {@render children({ id, url, useId })} + {:else} + + {@render children({ id, url, useId })} + + {/if} +{/if} diff --git a/packages/layerchart/src/lib/components/Contour.svelte b/packages/layerchart/src/lib/components/Contour.svelte index 35d22f634..f343e5b65 100644 --- a/packages/layerchart/src/lib/components/Contour.svelte +++ b/packages/layerchart/src/lib/components/Contour.svelte @@ -62,7 +62,7 @@ import { interpolateYlGnBu } from 'd3-scale-chromatic'; import { max, min } from 'd3-array'; - import Group from './Group.svelte'; + import Group from './Group/Group.svelte'; import Path from './Path/Path.svelte'; import { accessor as resolveAccessor, chartDataArray } from '$lib/utils/common.js'; import { getChartContext } from '$lib/contexts/chart.js'; diff --git a/packages/layerchart/src/lib/components/Density.svelte b/packages/layerchart/src/lib/components/Density.svelte index bd2a72c2c..51c639b6e 100644 --- a/packages/layerchart/src/lib/components/Density.svelte +++ b/packages/layerchart/src/lib/components/Density.svelte @@ -30,7 +30,7 @@ import { interpolateYlGnBu } from 'd3-scale-chromatic'; import { max } from 'd3-array'; - import Group from './Group.svelte'; + import Group from './Group/Group.svelte'; import Path from './Path/Path.svelte'; import { accessor as resolveAccessor, chartDataArray } from '$lib/utils/common.js'; import { getChartContext } from '$lib/contexts/chart.js'; diff --git a/packages/layerchart/src/lib/components/Ellipse.svelte b/packages/layerchart/src/lib/components/Ellipse.svelte deleted file mode 100644 index 1295cf3a4..000000000 --- a/packages/layerchart/src/lib/components/Ellipse.svelte +++ /dev/null @@ -1,485 +0,0 @@ - - - - -{#if layerCtx === 'svg'} - {#if dataMode} - {#each resolvedItems as item (item.key)} - {@const resolvedFill = resolveColorProp(fill, item.d, chartCtx.cScale)} - {@const resolvedStroke = resolveColorProp(stroke, item.d, chartCtx.cScale)} - {@const resolvedFillOpacity = resolveStyleProp(fillOpacity, item.d)} - {@const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d)} - {@const resolvedOpacity = resolveStyleProp(opacity, item.d)} - {@const resolvedClass = resolveStyleProp(className, item.d)} - - {/each} - {:else} - - {/if} -{:else if layerCtx === 'html'} - {#if dataMode} - {#each resolvedItems as item (item.key)} - {@const resolvedFill = resolveColorProp(fill, item.d, chartCtx.cScale)} - {@const resolvedStroke = resolveColorProp(stroke, item.d, chartCtx.cScale)} - {@const resolvedFillOpacity = resolveStyleProp(fillOpacity, item.d)} - {@const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d)} - {@const resolvedOpacity = resolveStyleProp(opacity, item.d)} - {@const resolvedClass = resolveStyleProp(className, item.d)} - {@const resolvedBorderWidth = - resolvedStrokeWidth != null - ? `${resolvedStrokeWidth}px` - : resolvedStroke != null - ? '1px' - : undefined} -
- {/each} - {:else} -
- {/if} -{/if} - - diff --git a/packages/layerchart/src/lib/components/Ellipse/Ellipse.canvas.svelte b/packages/layerchart/src/lib/components/Ellipse/Ellipse.canvas.svelte new file mode 100644 index 000000000..a79caf2ff --- /dev/null +++ b/packages/layerchart/src/lib/components/Ellipse/Ellipse.canvas.svelte @@ -0,0 +1,111 @@ + + + diff --git a/packages/layerchart/src/lib/components/Ellipse/Ellipse.html.svelte b/packages/layerchart/src/lib/components/Ellipse/Ellipse.html.svelte new file mode 100644 index 000000000..474e0e072 --- /dev/null +++ b/packages/layerchart/src/lib/components/Ellipse/Ellipse.html.svelte @@ -0,0 +1,92 @@ + + + + +{#if c.dataMode} + {#each c.resolvedItems as item (item.key)} + {@const resolvedFill = resolveColorProp(rest.fill, item.d, c.chartCtx.cScale)} + {@const resolvedStroke = resolveColorProp(rest.stroke, item.d, c.chartCtx.cScale)} + {@const resolvedStrokeWidth = resolveStyleProp(rest.strokeWidth, item.d)} + {@const resolvedOpacity = resolveStyleProp(rest.opacity, item.d)} + {@const resolvedClass = resolveStyleProp(rest.class, item.d)} + {@const resolvedBorderWidth = + resolvedStrokeWidth != null + ? `${resolvedStrokeWidth}px` + : resolvedStroke != null + ? '1px' + : undefined} +
+ {/each} +{:else} +
+{/if} + + diff --git a/packages/layerchart/src/lib/components/Ellipse/Ellipse.shared.svelte.ts b/packages/layerchart/src/lib/components/Ellipse/Ellipse.shared.svelte.ts new file mode 100644 index 000000000..ed394dde6 --- /dev/null +++ b/packages/layerchart/src/lib/components/Ellipse/Ellipse.shared.svelte.ts @@ -0,0 +1,221 @@ +import { untrack } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; + +import type { Without } from '$lib/utils/types.js'; +import type { DataProp, DataDrivenStyleProps } from '$lib/utils/dataProp.js'; +import { + hasAnyDataProp, + resolveDataProp, + resolveGeoDataPair, +} from '$lib/utils/dataProp.js'; +import { chartDataArray } from '$lib/utils/common.js'; +import { createMotion, createDataMotionMap, type MotionProp } from '$lib/utils/motion.svelte.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import { getGeoContext } from '$lib/contexts/geo.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import type { GeoState } from '$lib/states/geo.svelte.js'; + +export type EllipsePropsWithoutHTML = { + /** The center x position of the ellipse. @default 0 */ + cx?: DataProp; + /** The initial center x position of the ellipse (pixel mode only). @default cx */ + initialCx?: number; + /** The center y position of the ellipse. @default 0 */ + cy?: DataProp; + /** The initial center y position of the ellipse (pixel mode only). @default cy */ + initialCy?: number; + /** The radius of the ellipse on the x-axis. @default 1 */ + rx?: DataProp; + /** The initial radius of the ellipse on the x-axis (pixel mode only). @default rx */ + initialRx?: number; + /** The radius of the ellipse on the y-axis. @default 1 */ + ry?: DataProp; + /** The initial radius of the ellipse on the y-axis (pixel mode only). @default ry */ + initialRy?: number; + /** Data array to iterate over in data mode. */ + data?: any[]; + /** Key function for keyed {#each} rendering in data mode. @default (d, i) => i */ + key?: (d: any, index: number) => any; + /** A bindable reference to the `` element (pixel mode only). @bindable */ + ref?: SVGEllipseElement; + /** Motion configuration (pixel mode only). */ + motion?: MotionProp; +} & DataDrivenStyleProps; + +export type EllipseProps = EllipsePropsWithoutHTML & + Without, EllipsePropsWithoutHTML>; + +const defaultKey = (_: any, i: number) => i; + +export function ellipseMarkInfo(props: EllipseProps, dataMode: boolean) { + if (!dataMode) return {}; + return { + data: props.data, + x: typeof props.cx === 'string' ? props.cx : undefined, + y: typeof props.cy === 'string' ? props.cy : undefined, + color: + typeof props.fill === 'string' + ? props.fill + : typeof props.stroke === 'string' + ? props.stroke + : undefined, + }; +} + +export class EllipseState { + #getProps: () => EllipseProps = () => ({}) as EllipseProps; + + chartCtx: ChartState = getChartContext(); + geo: GeoState = getGeoContext(); + + dataMode = $derived( + hasAnyDataProp( + this.#getProps().cx, + this.#getProps().cy, + this.#getProps().rx, + this.#getProps().ry + ) + ); + + #resolvedData: any[] = $derived( + this.dataMode ? (this.#getProps().data ?? chartDataArray(this.chartCtx.data)) : [] + ); + + resolvedItems = $derived.by(() => { + if (!this.dataMode) return []; + const props = this.#getProps(); + const keyFn = props.key ?? defaultKey; + return this.#resolvedData.map((d, i) => { + const key = keyFn(d, i); + const resolved = this.#resolveEllipse(d); + const animated = this.#dataMotionMap?.get(key); + return { + d, + key, + cx: animated?.cx ?? resolved.cx, + cy: animated?.cy ?? resolved.cy, + rx: animated?.rx ?? resolved.rx, + ry: animated?.ry ?? resolved.ry, + }; + }); + }); + + #resolveEllipse(d: any) { + const props = this.#getProps(); + if (this.geo.projection) { + const [projX, projY] = resolveGeoDataPair(props.cx, props.cy, d, this.geo.projection); + return { + cx: projX, + cy: projY, + rx: resolveDataProp(props.rx, d, this.chartCtx.rScale, typeof props.rx === 'number' ? props.rx : 1), + ry: resolveDataProp(props.ry, d, this.chartCtx.rScale, typeof props.ry === 'number' ? props.ry : 1), + }; + } + return { + cx: resolveDataProp(props.cx, d, this.chartCtx.xScale, 0), + cy: resolveDataProp(props.cy, d, this.chartCtx.yScale, 0), + rx: resolveDataProp(props.rx, d, this.chartCtx.rScale, typeof props.rx === 'number' ? props.rx : 1), + ry: resolveDataProp(props.ry, d, this.chartCtx.rScale, typeof props.ry === 'number' ? props.ry : 1), + }; + } + + #dataMotionMap: ReturnType = null; + #motionCx!: ReturnType>; + #motionCy!: ReturnType>; + #motionRx!: ReturnType>; + #motionRy!: ReturnType>; + + get motionCx() { + return this.#motionCx.current; + } + get motionCy() { + return this.#motionCy.current; + } + get motionRx() { + return this.#motionRx.current; + } + get motionRy() { + return this.#motionRy.current; + } + + staticFill = $derived( + typeof this.#getProps().fill === 'string' ? (this.#getProps().fill as string) : undefined + ); + staticFillOpacity = $derived( + typeof this.#getProps().fillOpacity === 'number' + ? (this.#getProps().fillOpacity as number) + : undefined + ); + staticStroke = $derived( + typeof this.#getProps().stroke === 'string' ? (this.#getProps().stroke as string) : undefined + ); + staticStrokeWidth = $derived( + typeof this.#getProps().strokeWidth === 'number' + ? (this.#getProps().strokeWidth as number) + : undefined + ); + staticOpacity = $derived( + typeof this.#getProps().opacity === 'number' + ? (this.#getProps().opacity as number) + : undefined + ); + staticClassName = $derived( + typeof this.#getProps().class === 'string' ? (this.#getProps().class as string) : undefined + ); + staticBorderWidth = $derived.by(() => { + const props = this.#getProps(); + if (typeof props.strokeWidth === 'number') return `${props.strokeWidth}px`; + if (typeof props.stroke === 'string') return '1px'; + return undefined; + }); + + constructor(getProps: () => EllipseProps) { + this.#getProps = getProps; + + const initial = getProps(); + const initialCx = initial.initialCx ?? (typeof initial.cx === 'number' ? initial.cx : 0); + const initialCy = initial.initialCy ?? (typeof initial.cy === 'number' ? initial.cy : 0); + const initialRx = initial.initialRx ?? (typeof initial.rx === 'number' ? initial.rx : 1); + const initialRy = initial.initialRy ?? (typeof initial.ry === 'number' ? initial.ry : 1); + + this.#motionCx = createMotion( + initialCx, + () => (typeof getProps().cx === 'number' ? (getProps().cx as number) : 0), + initial.motion + ); + this.#motionCy = createMotion( + initialCy, + () => (typeof getProps().cy === 'number' ? (getProps().cy as number) : 0), + initial.motion + ); + this.#motionRx = createMotion( + initialRx, + () => (typeof getProps().rx === 'number' ? (getProps().rx as number) : 1), + initial.motion + ); + this.#motionRy = createMotion( + initialRy, + () => (typeof getProps().ry === 'number' ? (getProps().ry as number) : 1), + initial.motion + ); + + this.#dataMotionMap = createDataMotionMap(initial.motion); + if (this.#dataMotionMap) { + const motionMap = this.#dataMotionMap; + $effect(() => { + if (!this.dataMode) return; + const props = getProps(); + const keyFn = props.key ?? defaultKey; + const activeKeys = new Set(); + for (let i = 0; i < this.#resolvedData.length; i++) { + const d = this.#resolvedData[i]; + const key = keyFn(d, i); + activeKeys.add(key); + const resolved = this.#resolveEllipse(d); + untrack(() => motionMap.update(key, resolved)); + } + untrack(() => motionMap.cleanup(activeKeys)); + }); + } + } +} diff --git a/packages/layerchart/src/lib/components/Ellipse/Ellipse.svelte b/packages/layerchart/src/lib/components/Ellipse/Ellipse.svelte new file mode 100644 index 000000000..609bacb60 --- /dev/null +++ b/packages/layerchart/src/lib/components/Ellipse/Ellipse.svelte @@ -0,0 +1,26 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Ellipse.svelte.test.ts b/packages/layerchart/src/lib/components/Ellipse/Ellipse.svelte.test.ts similarity index 98% rename from packages/layerchart/src/lib/components/Ellipse.svelte.test.ts rename to packages/layerchart/src/lib/components/Ellipse/Ellipse.svelte.test.ts index 9377502af..cb5a1eac9 100644 --- a/packages/layerchart/src/lib/components/Ellipse.svelte.test.ts +++ b/packages/layerchart/src/lib/components/Ellipse/Ellipse.svelte.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; -import TestHarness, { componentTestId } from './tests/TestHarness.svelte'; +import TestHarness, { componentTestId } from '../tests/TestHarness.svelte'; import Ellipse from './Ellipse.svelte'; describe('Ellipse', () => { diff --git a/packages/layerchart/src/lib/components/Ellipse/Ellipse.svg.svelte b/packages/layerchart/src/lib/components/Ellipse/Ellipse.svg.svelte new file mode 100644 index 000000000..c6401e166 --- /dev/null +++ b/packages/layerchart/src/lib/components/Ellipse/Ellipse.svg.svelte @@ -0,0 +1,95 @@ + + + + +{#if c.dataMode} + {#each c.resolvedItems as item (item.key)} + {@const resolvedFill = resolveColorProp(rest.fill, item.d, c.chartCtx.cScale)} + {@const resolvedStroke = resolveColorProp(rest.stroke, item.d, c.chartCtx.cScale)} + {@const resolvedFillOpacity = resolveStyleProp(rest.fillOpacity, item.d)} + {@const resolvedStrokeWidth = resolveStyleProp(rest.strokeWidth, item.d)} + {@const resolvedOpacity = resolveStyleProp(rest.opacity, item.d)} + {@const resolvedClass = resolveStyleProp(rest.class, item.d)} + + {/each} +{:else} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/Grid.svelte b/packages/layerchart/src/lib/components/Grid.svelte index a899da27a..22117ec45 100644 --- a/packages/layerchart/src/lib/components/Grid.svelte +++ b/packages/layerchart/src/lib/components/Grid.svelte @@ -110,7 +110,7 @@ import { isScaleBand } from '$lib/utils/scales.svelte.js'; import Circle from './Circle/Circle.svelte'; - import Group, { type GroupProps } from './Group.svelte'; + import Group, { type GroupProps } from './Group/Group.svelte'; import Line from './Line/Line.svelte'; import Rule from './Rule.svelte'; // Spline (used only for radial linear grid lines) is lazy-loaded inline diff --git a/packages/layerchart/src/lib/components/Group.svelte b/packages/layerchart/src/lib/components/Group.svelte deleted file mode 100644 index f4595cf11..000000000 --- a/packages/layerchart/src/lib/components/Group.svelte +++ /dev/null @@ -1,325 +0,0 @@ - - - - -{#if layerCtx === 'canvas'} - {@render children?.()} -{:else if layerCtx === 'svg'} - {#if dataMode} - {#each resolvedItems as item (item.key)} - - {@render children?.()} - - {/each} - {:else} - - {@render children?.()} - - {/if} -{:else if layerCtx === 'html'} - {#if dataMode} - {#each resolvedItems as item (item.key)} -
- {@render children?.()} -
- {/each} - {:else} -
- {@render children?.()} -
- {/if} -{/if} - - diff --git a/packages/layerchart/src/lib/components/Group/Group.canvas.svelte b/packages/layerchart/src/lib/components/Group/Group.canvas.svelte new file mode 100644 index 000000000..575730c2e --- /dev/null +++ b/packages/layerchart/src/lib/components/Group/Group.canvas.svelte @@ -0,0 +1,38 @@ + + + + +{@render children?.()} diff --git a/packages/layerchart/src/lib/components/Group/Group.html.svelte b/packages/layerchart/src/lib/components/Group/Group.html.svelte new file mode 100644 index 000000000..50b0b00b1 --- /dev/null +++ b/packages/layerchart/src/lib/components/Group/Group.html.svelte @@ -0,0 +1,100 @@ + + + + +{#if c.dataMode} + {#each c.resolvedItems as item (item.key)} +
+ {@render children?.()} +
+ {/each} +{:else} +
+ {@render children?.()} +
+{/if} + + diff --git a/packages/layerchart/src/lib/components/Group/Group.shared.svelte.ts b/packages/layerchart/src/lib/components/Group/Group.shared.svelte.ts new file mode 100644 index 000000000..9a5143e4b --- /dev/null +++ b/packages/layerchart/src/lib/components/Group/Group.shared.svelte.ts @@ -0,0 +1,217 @@ +import type { Snippet } from 'svelte'; +import { untrack } from 'svelte'; +import type { HTMLAttributes } from 'svelte/elements'; + +import type { Transition, TransitionParams, Without } from '$lib/utils/types.js'; +import type { DataProp } from '$lib/utils/dataProp.js'; +import { + hasAnyDataProp, + resolveDataProp, + resolveGeoDataPair, +} from '$lib/utils/dataProp.js'; +import { chartDataArray } from '$lib/utils/common.js'; +import { + createMotion, + createDataMotionMap, + extractTweenConfig, + type MotionProp, +} from '$lib/utils/motion.svelte.js'; +import { fade } from 'svelte/transition'; +import { cubicIn } from 'svelte/easing'; +import { getChartContext } from '$lib/contexts/chart.js'; +import { getGeoContext } from '$lib/contexts/geo.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import type { GeoState } from '$lib/states/geo.svelte.js'; + +export type GroupPropsWithoutHTML = { + /** + * Translate x position of the group. + * - `number`: pixel value (direct) + * - `string`: data property name, resolved via xScale + * - `function(d)`: accessor called per data item, result passed through xScale + */ + x?: DataProp; + + /** Initial translate x (pixel mode only). @default x */ + initialX?: number; + + /** + * Translate y position of the group. + * - `number`: pixel value (direct) + * - `string`: data property name, resolved via yScale + * - `function(d)`: accessor called per data item, result passed through yScale + */ + y?: DataProp; + + /** Initial translate y (pixel mode only). @default y */ + initialY?: number; + + /** + * Data array to iterate over in data mode. + * Falls back to chart context data when not provided. + */ + data?: any[]; + + /** + * Key function for keyed {#each} rendering in data mode. + * + * @default (d, i) => i + */ + key?: (d: any, index: number) => any; + + /** Center within chart. @default false */ + center?: boolean | 'x' | 'y'; + + /** + * Prevent `touchmove` default, which can interfere with `pointermove` when + * used with `Tooltip`, for example. + * + * @default false + */ + preventTouchMove?: boolean; + + /** The opacity of the element. (0 to 1) */ + opacity?: number; + + children?: Snippet; + + /** + * A reference to the rendered DOM element, which could be + * either nothing, a `` element (when using ``), or a `
` element + * (when using ``). + * + * @bindable + */ + ref?: Element; + + motion?: MotionProp; + + /** Transition function for entering elements @default fade if motion is tweened */ + transitionIn?: In; + + /** Parameters for the transitionIn function @default { easing: cubicIn } */ + transitionInParams?: TransitionParams; +}; + +export type GroupProps = GroupPropsWithoutHTML & + Without, GroupPropsWithoutHTML>; + +const defaultKey = (_: any, i: number) => i; + +/** + * Reactive state shared by every per-layer Group variant. + */ +export class GroupState { + #getProps: () => GroupProps = () => ({}) as GroupProps; + + chartCtx: ChartState = getChartContext(); + geo: GeoState = getGeoContext(); + + // Data mode detection + dataMode = $derived(hasAnyDataProp(this.#getProps().x, this.#getProps().y)); + + #resolvedData: any[] = $derived( + this.dataMode ? (this.#getProps().data ?? chartDataArray(this.chartCtx.data)) : [] + ); + + resolvedItems = $derived.by(() => { + if (!this.dataMode) return []; + const props = this.#getProps(); + const keyFn = props.key ?? defaultKey; + return this.#resolvedData.map((d, i) => { + const key = keyFn(d, i); + const resolved = this.#resolveGroup(d); + const animated = this.#dataMotionMap?.get(key); + return { + d, + key, + x: animated?.x ?? resolved.x, + y: animated?.y ?? resolved.y, + }; + }); + }); + + #resolveGroup(d: any): { x: number; y: number } { + const props = this.#getProps(); + if (this.geo.projection) { + const [projX, projY] = resolveGeoDataPair(props.x, props.y, d, this.geo.projection); + return { x: projX, y: projY }; + } + return { + x: resolveDataProp(props.x, d, this.chartCtx.xScale, 0), + y: resolveDataProp(props.y, d, this.chartCtx.yScale, 0), + }; + } + + // Pixel-mode position (with center support) + trueX = $derived.by(() => { + const props = this.#getProps(); + if (typeof props.x === 'number') return props.x; + if (props.x == null && (props.center === 'x' || props.center === true)) + return this.chartCtx.width / 2; + return 0; + }); + trueY = $derived.by(() => { + const props = this.#getProps(); + if (typeof props.y === 'number') return props.y; + if (props.y == null && (props.center === 'y' || props.center === true)) + return this.chartCtx.height / 2; + return 0; + }); + + #dataMotionMap: ReturnType = null; + #motionX!: ReturnType>; + #motionY!: ReturnType>; + + get motionX() { + return this.#motionX.current; + } + get motionY() { + return this.#motionY.current; + } + + // Transform string for SVG/HTML pixel mode + transform = $derived.by(() => { + const props = this.#getProps(); + if (props.center || props.x != null || props.y != null) { + return `translate(${this.motionX}px, ${this.motionY}px)`; + } + return undefined; + }); + + // Default transition (fade when motion is tweened) + defaultTransitionIn = $derived( + extractTweenConfig(this.#getProps().motion)?.options ? fade : () => ({}) + ); + defaultTransitionInParams = { easing: cubicIn }; + + constructor(getProps: () => GroupProps) { + this.#getProps = getProps; + + const initial = getProps(); + const initialX = initial.initialX ?? (typeof initial.x === 'number' ? initial.x : undefined); + const initialY = initial.initialY ?? (typeof initial.y === 'number' ? initial.y : undefined); + + this.#motionX = createMotion(initialX, () => this.trueX, initial.motion); + this.#motionY = createMotion(initialY, () => this.trueY, initial.motion); + + this.#dataMotionMap = createDataMotionMap(initial.motion); + if (this.#dataMotionMap) { + const motionMap = this.#dataMotionMap; + $effect(() => { + if (!this.dataMode) return; + const props = getProps(); + const keyFn = props.key ?? defaultKey; + const activeKeys = new Set(); + for (let i = 0; i < this.#resolvedData.length; i++) { + const d = this.#resolvedData[i]; + const key = keyFn(d, i); + activeKeys.add(key); + const resolved = this.#resolveGroup(d); + untrack(() => motionMap.update(key, resolved)); + } + untrack(() => motionMap.cleanup(activeKeys)); + }); + } + } +} diff --git a/packages/layerchart/src/lib/components/Group/Group.svelte b/packages/layerchart/src/lib/components/Group/Group.svelte new file mode 100644 index 000000000..aafd4bd6c --- /dev/null +++ b/packages/layerchart/src/lib/components/Group/Group.svelte @@ -0,0 +1,26 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Group.svelte.test.ts b/packages/layerchart/src/lib/components/Group/Group.svelte.test.ts similarity index 96% rename from packages/layerchart/src/lib/components/Group.svelte.test.ts rename to packages/layerchart/src/lib/components/Group/Group.svelte.test.ts index 3960bbff2..3092f1844 100644 --- a/packages/layerchart/src/lib/components/Group.svelte.test.ts +++ b/packages/layerchart/src/lib/components/Group/Group.svelte.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; -import TestHarness, { componentTestId } from './tests/TestHarness.svelte'; +import TestHarness, { componentTestId } from '../tests/TestHarness.svelte'; import Group from './Group.svelte'; describe('Group', () => { diff --git a/packages/layerchart/src/lib/components/Group/Group.svg.svelte b/packages/layerchart/src/lib/components/Group/Group.svg.svelte new file mode 100644 index 000000000..c3475ca8f --- /dev/null +++ b/packages/layerchart/src/lib/components/Group/Group.svg.svelte @@ -0,0 +1,93 @@ + + + + +{#if c.dataMode} + {#each c.resolvedItems as item (item.key)} + + {@render children?.()} + + {/each} +{:else} + + {@render children?.()} + +{/if} diff --git a/packages/layerchart/src/lib/components/Hull.svelte b/packages/layerchart/src/lib/components/Hull.svelte index aabb52cde..e2d66d5c7 100644 --- a/packages/layerchart/src/lib/components/Hull.svelte +++ b/packages/layerchart/src/lib/components/Hull.svelte @@ -61,7 +61,7 @@ import { cls } from '@layerstack/tailwind'; import GeoPath from './geo/GeoPath.svelte'; - import Group, { type GroupProps } from './Group.svelte'; + import Group, { type GroupProps } from './Group/Group.svelte'; import Spline from './Spline.svelte'; import { getChartContext } from '$lib/contexts/chart.js'; import { getGeoContext } from '$lib/contexts/geo.js'; diff --git a/packages/layerchart/src/lib/components/Image.svelte b/packages/layerchart/src/lib/components/Image.svelte deleted file mode 100644 index 00bc45ef4..000000000 --- a/packages/layerchart/src/lib/components/Image.svelte +++ /dev/null @@ -1,558 +0,0 @@ - - - - -{#if layerCtx === 'svg'} - {#if dataMode} - {#each resolvedItems as item, i (item.key)} - {@const resolvedHrefValue = resolveHref(item.d)} - {@const renderX = item.x - item.width / 2} - {@const renderY = item.y - item.height / 2} - {#if item.r !== undefined} - - - - - - {/if} - - {/each} - {:else} - {#if pixelR !== undefined} - - - - - - {/if} - - {/if} -{:else if layerCtx === 'html'} - {#if dataMode} - {#each resolvedItems as item (item.key)} - {@const resolvedHrefValue = resolveHref(item.d)} - - {/each} - {:else} - - {/if} -{/if} - - diff --git a/packages/layerchart/src/lib/components/Image/Image.canvas.svelte b/packages/layerchart/src/lib/components/Image/Image.canvas.svelte new file mode 100644 index 000000000..a02594c80 --- /dev/null +++ b/packages/layerchart/src/lib/components/Image/Image.canvas.svelte @@ -0,0 +1,132 @@ + + + diff --git a/packages/layerchart/src/lib/components/Image/Image.html.svelte b/packages/layerchart/src/lib/components/Image/Image.html.svelte new file mode 100644 index 000000000..171b27a05 --- /dev/null +++ b/packages/layerchart/src/lib/components/Image/Image.html.svelte @@ -0,0 +1,114 @@ + + + + +{#if c.dataMode} + {#each c.resolvedItems as item (item.key)} + {@const resolvedHrefValue = c.resolveHref(item.d)} + + {/each} +{:else} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/Image/Image.shared.svelte.ts b/packages/layerchart/src/lib/components/Image/Image.shared.svelte.ts new file mode 100644 index 000000000..49cb3784c --- /dev/null +++ b/packages/layerchart/src/lib/components/Image/Image.shared.svelte.ts @@ -0,0 +1,265 @@ +import { untrack } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import { get } from '@layerstack/utils'; + +import type { Without } from '$lib/utils/types.js'; +import type { DataProp } from '$lib/utils/dataProp.js'; +import { + hasAnyDataProp, + resolveDataProp, + resolveGeoDataPair, +} from '$lib/utils/dataProp.js'; +import { chartDataArray } from '$lib/utils/common.js'; +import { + createMotion, + createDataMotionMap, + parseMotionProp, + type MotionProp, + type MotionOptions, +} from '$lib/utils/motion.svelte.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import { getGeoContext } from '$lib/contexts/geo.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import type { GeoState } from '$lib/states/geo.svelte.js'; + +export type ImagePropsWithoutHTML = { + /** + * Image URL. In data mode, resolved per item. + */ + href?: string | ((d: any) => string); + x?: DataProp; + initialX?: number; + y?: DataProp; + initialY?: number; + width?: DataProp; + initialWidth?: number; + height?: DataProp; + initialHeight?: number; + /** Circular clip radius. When set, overrides width/height to 2*r. */ + r?: DataProp; + /** Rotation in degrees clockwise. */ + rotate?: DataProp; + /** SVG preserveAspectRatio attribute. @default 'xMidYMid meet' */ + preserveAspectRatio?: string; + /** CORS attribute for the image. */ + crossOrigin?: '' | 'anonymous' | 'use-credentials'; + /** Image rendering quality. */ + imageRendering?: string; + /** The opacity of the image. (0 to 1) */ + opacity?: number; + /** Data array to iterate over in data mode. */ + data?: any[]; + /** Key function for keyed {#each} rendering in data mode. @default (d, i) => i */ + key?: (d: any, index: number) => any; + /** A bindable reference to the `` element (pixel mode only). @bindable */ + ref?: SVGImageElement; + /** Motion configuration (pixel mode only). */ + motion?: MotionProp<'x' | 'y' | 'width' | 'height'>; +}; + +export type ImageProps = ImagePropsWithoutHTML & + Without, ImagePropsWithoutHTML>; + +const defaultKey = (_: any, i: number) => i; + +export function imageMarkInfo(props: ImageProps, dataMode: boolean) { + if (!dataMode) return {}; + return { + data: props.data, + x: typeof props.x === 'string' ? props.x : undefined, + y: typeof props.y === 'string' ? props.y : undefined, + }; +} + +export class ImageState { + #getProps: () => ImageProps = () => ({}) as ImageProps; + + chartCtx: ChartState = getChartContext(); + geo: GeoState = getGeoContext(); + + dataMode = $derived( + hasAnyDataProp( + this.#getProps().x, + this.#getProps().y, + this.#getProps().width, + this.#getProps().height, + this.#getProps().r + ) || typeof this.#getProps().href === 'function' + ); + + #resolvedData: any[] = $derived( + this.dataMode ? (this.#getProps().data ?? chartDataArray(this.chartCtx.data)) : [] + ); + + resolveImage(d: any) { + const props = this.#getProps(); + const resolvedR = + props.r !== undefined ? resolveDataProp(props.r, d, null, 0) : undefined; + const defaultSize = resolvedR !== undefined ? resolvedR * 2 : 16; + const resolvedWidth = + props.width !== undefined + ? resolveDataProp(props.width, d, null, defaultSize) + : defaultSize; + const resolvedHeight = + props.height !== undefined + ? resolveDataProp(props.height, d, null, defaultSize) + : defaultSize; + + let resolvedX: number, resolvedY: number; + if (this.geo.projection) { + [resolvedX, resolvedY] = resolveGeoDataPair(props.x, props.y, d, this.geo.projection); + } else { + resolvedX = resolveDataProp(props.x, d, this.chartCtx.xScale, 0); + resolvedY = resolveDataProp(props.y, d, this.chartCtx.yScale, 0); + } + + return { + x: resolvedX, + y: resolvedY, + width: resolvedWidth, + height: resolvedHeight, + r: resolvedR, + rotate: props.rotate !== undefined ? resolveDataProp(props.rotate, d, null, 0) : undefined, + }; + } + + resolveHref(d: any): string | undefined { + const href = this.#getProps().href; + if (!href) return undefined; + if (typeof href === 'function') return href(d); + const dataValue = get(d, href); + if (dataValue !== undefined) return String(dataValue); + return href; + } + + resolvedItems = $derived.by(() => { + if (!this.dataMode) return []; + const props = this.#getProps(); + const keyFn = props.key ?? defaultKey; + return this.#resolvedData.map((d, i) => { + const key = keyFn(d, i); + const resolved = this.resolveImage(d); + const animated = this.#dataMotionMap?.get(key); + return { + d, + key, + x: animated?.x ?? resolved.x, + y: animated?.y ?? resolved.y, + width: animated?.width ?? resolved.width, + height: animated?.height ?? resolved.height, + r: resolved.r, + rotate: resolved.rotate, + }; + }); + }); + + // Pixel-mode helpers + defaultSize = $derived( + typeof this.#getProps().r === 'number' ? (this.#getProps().r as number) * 2 : 16 + ); + resolvedPixelWidth = $derived( + typeof this.#getProps().width === 'number' + ? (this.#getProps().width as number) + : this.defaultSize + ); + resolvedPixelHeight = $derived( + typeof this.#getProps().height === 'number' + ? (this.#getProps().height as number) + : this.defaultSize + ); + pixelR = $derived( + typeof this.#getProps().r === 'number' ? (this.#getProps().r as number) : undefined + ); + pixelRotate = $derived( + typeof this.#getProps().rotate === 'number' ? (this.#getProps().rotate as number) : undefined + ); + + #dataMotionMap: ReturnType = null; + #motionX!: ReturnType>; + #motionY!: ReturnType>; + #motionWidth!: ReturnType>; + #motionHeight!: ReturnType>; + + get motionX() { + return this.#motionX.current; + } + get motionY() { + return this.#motionY.current; + } + get motionWidth() { + return this.#motionWidth.current; + } + get motionHeight() { + return this.#motionHeight.current; + } + + constructor(getProps: () => ImageProps) { + this.#getProps = getProps; + + const initial = getProps(); + const initialX = initial.initialX ?? (typeof initial.x === 'number' ? initial.x : 0); + const initialY = initial.initialY ?? (typeof initial.y === 'number' ? initial.y : 0); + const initialWidth = + initial.initialWidth ?? + (typeof initial.width === 'number' + ? initial.width + : typeof initial.r === 'number' + ? initial.r * 2 + : 16); + const initialHeight = + initial.initialHeight ?? + (typeof initial.height === 'number' + ? initial.height + : typeof initial.r === 'number' + ? initial.r * 2 + : 16); + const motion = initial.motion; + + this.#motionX = createMotion( + initialX, + () => (typeof getProps().x === 'number' ? (getProps().x as number) : 0), + motion === undefined ? undefined : parseMotionProp(motion, 'x') + ); + this.#motionY = createMotion( + initialY, + () => (typeof getProps().y === 'number' ? (getProps().y as number) : 0), + motion === undefined ? undefined : parseMotionProp(motion, 'y') + ); + this.#motionWidth = createMotion( + initialWidth, + () => this.resolvedPixelWidth, + motion === undefined ? undefined : parseMotionProp(motion, 'width') + ); + this.#motionHeight = createMotion( + initialHeight, + () => this.resolvedPixelHeight, + motion === undefined ? undefined : parseMotionProp(motion, 'height') + ); + + this.#dataMotionMap = createDataMotionMap(motion as MotionOptions | undefined); + if (this.#dataMotionMap) { + const motionMap = this.#dataMotionMap; + $effect(() => { + if (!this.dataMode) return; + const props = getProps(); + const keyFn = props.key ?? defaultKey; + const activeKeys = new Set(); + for (let i = 0; i < this.#resolvedData.length; i++) { + const d = this.#resolvedData[i]; + const key = keyFn(d, i); + activeKeys.add(key); + const resolved = this.resolveImage(d); + untrack(() => + motionMap.update(key, { + x: resolved.x, + y: resolved.y, + width: resolved.width, + height: resolved.height, + }) + ); + } + untrack(() => motionMap.cleanup(activeKeys)); + }); + } + } +} diff --git a/packages/layerchart/src/lib/components/Image/Image.svelte b/packages/layerchart/src/lib/components/Image/Image.svelte new file mode 100644 index 000000000..76aba8b02 --- /dev/null +++ b/packages/layerchart/src/lib/components/Image/Image.svelte @@ -0,0 +1,26 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Image.svelte.test.ts b/packages/layerchart/src/lib/components/Image/Image.svelte.test.ts similarity index 98% rename from packages/layerchart/src/lib/components/Image.svelte.test.ts rename to packages/layerchart/src/lib/components/Image/Image.svelte.test.ts index 138e74bb6..12d97ce59 100644 --- a/packages/layerchart/src/lib/components/Image.svelte.test.ts +++ b/packages/layerchart/src/lib/components/Image/Image.svelte.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; -import TestHarness, { componentTestId } from './tests/TestHarness.svelte'; +import TestHarness, { componentTestId } from '../tests/TestHarness.svelte'; import Image from './Image.svelte'; describe('Image', () => { diff --git a/packages/layerchart/src/lib/components/Image/Image.svg.svelte b/packages/layerchart/src/lib/components/Image/Image.svg.svelte new file mode 100644 index 000000000..a9d247ec1 --- /dev/null +++ b/packages/layerchart/src/lib/components/Image/Image.svg.svelte @@ -0,0 +1,138 @@ + + + + +{#if c.dataMode} + {#each c.resolvedItems as item, i (item.key)} + {@const resolvedHrefValue = c.resolveHref(item.d)} + {@const renderX = item.x - item.width / 2} + {@const renderY = item.y - item.height / 2} + {#if item.r !== undefined} + + + + + + {/if} + + {/each} +{:else} + {#if c.pixelR !== undefined} + + + + + + {/if} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/Labels.svelte b/packages/layerchart/src/lib/components/Labels.svelte index f3266d624..64cf5b7a8 100644 --- a/packages/layerchart/src/lib/components/Labels.svelte +++ b/packages/layerchart/src/lib/components/Labels.svelte @@ -83,7 +83,7 @@ import { isScaleBand } from '$lib/utils/scales.svelte.js'; import { getChartContext } from '$lib/contexts/chart.js'; - import Group from './Group.svelte'; + import Group from './Group/Group.svelte'; import { extractLayerProps } from '$lib/utils/attributes.js'; import { createDimensionGetter } from '$lib/utils/rect.svelte.js'; diff --git a/packages/layerchart/src/lib/components/LinearGradient.svelte b/packages/layerchart/src/lib/components/LinearGradient.svelte deleted file mode 100644 index 57d3bcb08..000000000 --- a/packages/layerchart/src/lib/components/LinearGradient.svelte +++ /dev/null @@ -1,232 +0,0 @@ - - - - -{#if layerCtx === 'canvas'} - - {@render children?.({ id, gradient: asAny(canvasGradient) })} -{:else if layerCtx === 'svg'} - - - {#if stopsContent} - {@render stopsContent?.()} - {:else if stops} - {#each stops as stop, i} - {#if Array.isArray(stop)} - - {:else} - - {/if} - {/each} - {/if} - - - - {@render children?.({ id, gradient: `url(#${id})` })} -{:else if layerCtx === 'html'} - {@render children?.({ id, gradient: createCSSGradient() })} -{/if} diff --git a/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.canvas.svelte b/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.canvas.svelte new file mode 100644 index 000000000..9c0a95d57 --- /dev/null +++ b/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.canvas.svelte @@ -0,0 +1,75 @@ + + + + +{@render children?.({ id, gradient: asAny(canvasGradient) })} diff --git a/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.html.svelte b/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.html.svelte new file mode 100644 index 000000000..38f6f88ab --- /dev/null +++ b/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.html.svelte @@ -0,0 +1,51 @@ + + + + +{@render children?.({ id, gradient: createCSSGradient() })} diff --git a/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.shared.svelte.ts b/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.shared.svelte.ts new file mode 100644 index 000000000..17d36a7ae --- /dev/null +++ b/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.shared.svelte.ts @@ -0,0 +1,56 @@ +import type { Snippet } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import type { Without } from '$lib/utils/types.js'; + +export type LinearGradientPropsWithoutHTML = { + /** Unique id for linearGradient */ + id?: string; + + /** + * Array of strings (colors) distributed equally from 0-100%, or + * `[offset, color]` tuples. + * + * @default `['var(--tw-gradient-from)', 'var(--tw-gradient-to)']` + */ + stops?: string[] | [string | number, string][]; + + /** Apply color stops top-to-bottom (true) or left-to-right (false). @default false */ + vertical?: boolean; + + /** @default '0%' */ + x1?: string; + + /** @default '0%' */ + y1?: string; + + /** @default vertical ? '0%' : '100%' */ + x2?: string; + + /** @default vertical ? '100%' : '0%' */ + y2?: string; + + /** Rotate the gradient by a given angle in degrees */ + rotate?: number; + + /** + * Define the coordinate system for attributes (i.e. gradientUnits) + * + * @default 'objectBoundingBox' + */ + units?: 'objectBoundingBox' | 'userSpaceOnUse'; + + /** + * A bindable reference to the underlying `` element + * + * @bindable + */ + ref?: SVGLinearGradientElement; + + /** Render as a child of the gradient and will opt out of the default stops being rendered. */ + stopsContent?: Snippet; + + children?: Snippet<[{ id: string; gradient: string }]>; +}; + +export type LinearGradientProps = LinearGradientPropsWithoutHTML & + Without, LinearGradientPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.svelte b/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.svelte new file mode 100644 index 000000000..9073f8da2 --- /dev/null +++ b/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.svelte @@ -0,0 +1,26 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.svg.svelte b/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.svg.svelte new file mode 100644 index 000000000..f87a395c4 --- /dev/null +++ b/packages/layerchart/src/lib/components/LinearGradient/LinearGradient.svg.svelte @@ -0,0 +1,73 @@ + + + + + + + {#if stopsContent} + {@render stopsContent?.()} + {:else if stops} + {#each stops as stop, i} + {#if Array.isArray(stop)} + + {:else} + + {/if} + {/each} + {/if} + + + +{@render children?.({ id, gradient: `url(#${id})` })} diff --git a/packages/layerchart/src/lib/components/Month.svelte b/packages/layerchart/src/lib/components/Month.svelte index 47a5a442e..a0a4a15f9 100644 --- a/packages/layerchart/src/lib/components/Month.svelte +++ b/packages/layerchart/src/lib/components/Month.svelte @@ -83,7 +83,7 @@ import { format } from '@layerstack/utils'; import Rect, { type RectPropsWithoutHTML } from './Rect/Rect.svelte'; - import Group from './Group.svelte'; + import Group from './Group/Group.svelte'; import Text from './Text/Text.svelte'; import { chartDataArray } from '../utils/common.js'; import { getChartContext } from '$lib/contexts/chart.js'; diff --git a/packages/layerchart/src/lib/components/Path/Path.svg.svelte b/packages/layerchart/src/lib/components/Path/Path.svg.svelte index be008f995..2f0cbb233 100644 --- a/packages/layerchart/src/lib/components/Path/Path.svg.svelte +++ b/packages/layerchart/src/lib/components/Path/Path.svg.svelte @@ -9,7 +9,7 @@ import { cls } from '@layerstack/tailwind'; import { createControlledMotion } from '$lib/utils/motion.svelte.js'; import { createId } from '$lib/utils/createId.js'; - import Group from '../Group.svelte'; + import Group from '../Group/Group.svelte'; import MarkerWrapper from '../MarkerWrapper.svelte'; import { PathState, type PathProps } from './Path.shared.svelte.js'; diff --git a/packages/layerchart/src/lib/components/Pattern.svelte b/packages/layerchart/src/lib/components/Pattern.svelte deleted file mode 100644 index fea7beb07..000000000 --- a/packages/layerchart/src/lib/components/Pattern.svelte +++ /dev/null @@ -1,398 +0,0 @@ - - - - -{#if layerCtx === 'canvas'} - {@render children?.({ id, pattern: asAny(canvasPattern) })} -{:else if layerCtx === 'svg'} - - - {#if patternContent} - {@render patternContent?.()} - {:else} - {#if background} - - {/if} - - {#each shapes.filter((shape) => shape.type === 'line') as line} - - {/each} - - {#each shapes.filter((shape) => shape.type === 'circle') as circle} - - {/each} - {/if} - - - - {@render children?.({ id, pattern: `url(#${id})` })} -{:else if layerCtx === 'html'} - {@render children?.({ id, pattern: createCSSPattern() })} -{/if} diff --git a/packages/layerchart/src/lib/components/Pattern/Pattern.canvas.svelte b/packages/layerchart/src/lib/components/Pattern/Pattern.canvas.svelte new file mode 100644 index 000000000..b689b8e31 --- /dev/null +++ b/packages/layerchart/src/lib/components/Pattern/Pattern.canvas.svelte @@ -0,0 +1,48 @@ + + + + +{@render children?.({ id, pattern: asAny(canvasPattern) })} diff --git a/packages/layerchart/src/lib/components/Pattern/Pattern.html.svelte b/packages/layerchart/src/lib/components/Pattern/Pattern.html.svelte new file mode 100644 index 000000000..500aa4701 --- /dev/null +++ b/packages/layerchart/src/lib/components/Pattern/Pattern.html.svelte @@ -0,0 +1,107 @@ + + + + +{@render children?.({ id, pattern: createCSSPattern() })} diff --git a/packages/layerchart/src/lib/components/Pattern/Pattern.shared.svelte.ts b/packages/layerchart/src/lib/components/Pattern/Pattern.shared.svelte.ts new file mode 100644 index 000000000..0939d025a --- /dev/null +++ b/packages/layerchart/src/lib/components/Pattern/Pattern.shared.svelte.ts @@ -0,0 +1,152 @@ +import type { Snippet } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import type { Without } from '$lib/utils/types.js'; + +export type PatternLineDef = { + /** The width of the line @default 1 */ + width?: string; + /** The rotation of the line @default 0 */ + rotate?: number; + /** The color of the line @default 'var(--color-surface-content)' */ + color?: string; + /** The opacity of the line @default 1 */ + opacity?: number; +}; + +export type PatternCircleDef = { + /** The radius of the circle @default 1 */ + radius?: number; + /** Stagger the circle layout @default false */ + stagger?: boolean; + /** The color of the circle @default 'var(--color-surface-content)' */ + color?: string; + /** The opacity of the circle @default 1 */ + opacity?: number; +}; + +export type PatternPropsWithoutHTML = { + /** The id of the pattern */ + id?: string; + + /** The size of the pattern (sets `width` and `height` as same value). */ + size?: number; + + /** The width of the pattern for custom patterns (set by `lines`, etc) */ + width?: number; + + /** The height of the pattern for custom patterns (set by `lines`, etc) */ + height?: number; + + /** The number of lines to render */ + lines?: boolean | PatternLineDef | PatternLineDef[]; + + /** The number of circles to render */ + circles?: boolean | PatternCircleDef | PatternCircleDef[]; + + /** The background color of the pattern */ + background?: string; + + /** Render as a child of the pattern. Note: only supported on the `` layer. */ + patternContent?: Snippet; + + children?: Snippet<[{ id: string; pattern: string }]>; +}; + +export type PatternProps = PatternPropsWithoutHTML & + Without, PatternPropsWithoutHTML>; + +export type CircleShape = { + type: 'circle'; + cx: number; + cy: number; + r: number; + fill: string; + opacity: number; +}; +export type LineShape = { + type: 'line'; + path: string; + stroke: string; + strokeWidth: string | number; + opacity: number; +}; +export type PatternShape = CircleShape | LineShape; + +/** + * Build the SVG/canvas shape descriptors for a pattern's lines/circles. + * Pure function — no reactivity. + */ +export function buildPatternShapes( + linesProp: PatternPropsWithoutHTML['lines'], + circlesProp: PatternPropsWithoutHTML['circles'], + size: number, + width: number, + height: number +): PatternShape[] { + const shapes: PatternShape[] = []; + + if (linesProp) { + const lineDefs = Array.isArray(linesProp) ? linesProp : linesProp === true ? [{}] : [linesProp]; + for (const line of lineDefs) { + const stroke = line.color ?? 'var(--color-surface-content, currentColor)'; + const strokeWidth = line.width ?? 1; + const opacity = line.opacity ?? 1; + + let rotate = Math.round(line.rotate ?? 0) % 360; + if (rotate > 180) rotate = rotate - 360; + else if (rotate > 90) rotate = rotate - 180; + else if (rotate < -180) rotate = rotate + 360; + else if (rotate < -90) rotate = rotate + 180; + + let path = ''; + if (rotate === 0) { + path = ` + M 0 0 L ${width} 0 + M 0 ${height} L ${width} ${height} + `; + } else if (rotate === 90) { + path = ` + M 0 0 L 0 ${height} + M ${width} 0 L ${width} ${height} + `; + } else if (rotate > 0) { + path = ` + M 0 ${-height} L ${width * 2} ${height} + M ${-width} ${-height} L ${width} ${height} + M ${-width} 0 L ${width} ${height * 2} + `; + } else { + path = ` + M ${-width} ${height} L ${width} ${-height} + M ${-width} ${height * 2} L ${width * 2} ${-height} + M 0 ${height * 2} L ${width * 2} 0 + `; + } + + shapes.push({ type: 'line', path, stroke, strokeWidth, opacity }); + } + } + + if (circlesProp) { + const circleDefs = Array.isArray(circlesProp) + ? circlesProp + : circlesProp === true + ? [{}] + : [circlesProp]; + for (const circle of circleDefs) { + const fill = circle.color ?? 'var(--color-surface-content, currentColor)'; + const opacity = circle.opacity ?? 1; + const r = circle.radius ?? 1; + if (circle.stagger) { + shapes.push( + { type: 'circle', cx: size / 4, cy: size / 4, r, fill, opacity }, + { type: 'circle', cx: (size * 3) / 4, cy: (size * 3) / 4, r, fill, opacity } + ); + } else { + shapes.push({ type: 'circle', cx: size / 2, cy: size / 2, r, fill, opacity }); + } + } + } + + return shapes; +} diff --git a/packages/layerchart/src/lib/components/Pattern/Pattern.svelte b/packages/layerchart/src/lib/components/Pattern/Pattern.svelte new file mode 100644 index 000000000..ad79bd88d --- /dev/null +++ b/packages/layerchart/src/lib/components/Pattern/Pattern.svelte @@ -0,0 +1,29 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Pattern/Pattern.svg.svelte b/packages/layerchart/src/lib/components/Pattern/Pattern.svg.svelte new file mode 100644 index 000000000..4535091a1 --- /dev/null +++ b/packages/layerchart/src/lib/components/Pattern/Pattern.svg.svelte @@ -0,0 +1,69 @@ + + + + + + + {#if patternContent} + {@render patternContent?.()} + {:else} + {#if background} + + {/if} + + {#each shapes.filter((s) => s.type === 'line') as line} + + {/each} + + {#each shapes.filter((s) => s.type === 'circle') as circle} + + {/each} + {/if} + + + +{@render children?.({ id, pattern: `url(#${id})` })} diff --git a/packages/layerchart/src/lib/components/Polygon.svelte b/packages/layerchart/src/lib/components/Polygon.svelte deleted file mode 100644 index d25238195..000000000 --- a/packages/layerchart/src/lib/components/Polygon.svelte +++ /dev/null @@ -1,538 +0,0 @@ - - - - -{#if layerCtx === 'svg'} - {#if dataMode} - {#each resolvedData as d, i (keyFn(d, i))} - {@const pathData = resolvePolygon(d)} - {@const resolvedFill = resolveColorProp(fill, d, chartCtx.cScale)} - {@const resolvedStroke = resolveColorProp(stroke, d, chartCtx.cScale)} - {@const resolvedFillOpacity = resolveStyleProp(fillOpacity, d)} - {@const resolvedStrokeWidth = resolveStyleProp(strokeWidth, d)} - {@const resolvedOpacity = resolveStyleProp(opacity, d)} - {@const resolvedClass = resolveStyleProp(className, d)} - - {/each} - {:else} - - {/if} -{/if} - - diff --git a/packages/layerchart/src/lib/components/Polygon/Polygon.canvas.svelte b/packages/layerchart/src/lib/components/Polygon/Polygon.canvas.svelte new file mode 100644 index 000000000..172f7f25c --- /dev/null +++ b/packages/layerchart/src/lib/components/Polygon/Polygon.canvas.svelte @@ -0,0 +1,110 @@ + + + diff --git a/packages/layerchart/src/lib/components/Polygon/Polygon.shared.svelte.ts b/packages/layerchart/src/lib/components/Polygon/Polygon.shared.svelte.ts new file mode 100644 index 000000000..9833bb6ea --- /dev/null +++ b/packages/layerchart/src/lib/components/Polygon/Polygon.shared.svelte.ts @@ -0,0 +1,301 @@ +import { untrack } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import { interpolatePath } from 'd3-interpolate-path'; + +import type { Without } from '$lib/utils/types.js'; +import type { DataProp, DataDrivenStyleProps } from '$lib/utils/dataProp.js'; +import { + hasAnyDataProp, + resolveDataProp, + resolveGeoDataPair, +} from '$lib/utils/dataProp.js'; +import { chartDataArray } from '$lib/utils/common.js'; +import { polygon } from '$lib/utils/shape.js'; +import { roundedPolygonPath } from '$lib/utils/path.js'; +import { + createMotion, + createDataMotionMap, + extractTweenConfig, + type MotionProp, + type ResolvedMotion, +} from '$lib/utils/motion.svelte.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import { getGeoContext } from '$lib/contexts/geo.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import type { GeoState } from '$lib/states/geo.svelte.js'; + +export type PolygonPropsWithoutHTML = { + cx?: DataProp; + initialCx?: number; + cy?: DataProp; + initialCy?: number; + r?: DataProp; + initialR?: number; + data?: any[]; + key?: (d: any, index: number) => any; + /** The number of points or explicit points to create the polygon. @default 4 */ + points?: number | { x: number; y: number }[]; + /** The radius of the curve for rounded corners. @default 0 */ + cornerRadius?: number; + /** The rotation of the polygon. @default 0 */ + rotate?: number; + /** The percent to inset the odd points of the star (<1 inset, >1 outset). @default 0 */ + inset?: number; + /** The horizontal stretch factor of the polygon. @default 1 */ + scaleX?: number; + /** The vertical stretch factor of the polygon. @default 1 */ + scaleY?: number; + /** The skew angle in degrees along the X axis. @default 0 */ + skewX?: number; + /** The skew angle in degrees along the Y axis. @default 0 */ + skewY?: number; + /** The tilt factor for x-coordinates. @default 1 */ + tiltX?: number; + /** The tilt factor for y-coordinates. @default 1 */ + tiltY?: number; + /** A bindable reference to the `` element. @bindable */ + ref?: SVGPathElement; + motion?: MotionProp; +} & DataDrivenStyleProps; + +export type PolygonProps = PolygonPropsWithoutHTML & + Without, PolygonPropsWithoutHTML>; + +const defaultKey = (_: any, i: number) => i; + +export function polygonMarkInfo(props: PolygonProps, dataMode: boolean) { + if (!dataMode) return {}; + return { + data: props.data, + x: typeof props.cx === 'string' ? props.cx : undefined, + y: typeof props.cy === 'string' ? props.cy : undefined, + color: + typeof props.fill === 'string' + ? props.fill + : typeof props.stroke === 'string' + ? props.stroke + : undefined, + }; +} + +export class PolygonState { + #getProps: () => PolygonProps = () => ({}) as PolygonProps; + + chartCtx: ChartState = getChartContext(); + geo: GeoState = getGeoContext(); + + dataMode = $derived( + hasAnyDataProp(this.#getProps().cx, this.#getProps().cy, this.#getProps().r) + ); + + resolvedData: any[] = $derived( + this.dataMode ? (this.#getProps().data ?? chartDataArray(this.chartCtx.data)) : [] + ); + + resolvedItems = $derived.by(() => { + if (!this.dataMode) return []; + const props = this.#getProps(); + const keyFn = props.key ?? defaultKey; + return this.resolvedData.map((d, i) => { + const key = keyFn(d, i); + let resolvedCx: number, resolvedCy: number; + if (this.geo.projection) { + [resolvedCx, resolvedCy] = resolveGeoDataPair(props.cx, props.cy, d, this.geo.projection); + } else { + resolvedCx = resolveDataProp(props.cx, d, this.chartCtx.xScale, 0); + resolvedCy = resolveDataProp(props.cy, d, this.chartCtx.yScale, 0); + } + const resolvedR = resolveDataProp( + props.r, + d, + this.chartCtx.rScale, + typeof props.r === 'number' ? props.r : 1 + ); + const animated = this.#dataMotionMap?.get(key); + return { + d, + key, + cx: animated?.cx ?? resolvedCx, + cy: animated?.cy ?? resolvedCy, + r: animated?.r ?? resolvedR, + }; + }); + }); + + /** Resolve a single data item to a polygon path string. */ + resolvePolygonPath(d: any): string { + const props = this.#getProps(); + let resolvedCx: number, resolvedCy: number; + if (this.geo.projection) { + [resolvedCx, resolvedCy] = resolveGeoDataPair(props.cx, props.cy, d, this.geo.projection); + } else { + resolvedCx = resolveDataProp(props.cx, d, this.chartCtx.xScale, 0); + resolvedCy = resolveDataProp(props.cy, d, this.chartCtx.yScale, 0); + } + const resolvedR = resolveDataProp( + props.r, + d, + this.chartCtx.rScale, + typeof props.r === 'number' ? props.r : 1 + ); + + const pts = + typeof props.points === 'number' || props.points === undefined + ? polygon({ + cx: resolvedCx, + cy: resolvedCy, + count: typeof props.points === 'number' ? props.points : 4, + radius: resolvedR, + rotate: props.rotate ?? 0, + inset: props.inset ?? 0, + scaleX: props.scaleX ?? 1, + scaleY: props.scaleY ?? 1, + skewX: props.skewX ?? 0, + skewY: props.skewY ?? 0, + tiltX: props.tiltX ?? 0, + tiltY: props.tiltY ?? 0, + }) + : props.points; + + return roundedPolygonPath(pts, props.cornerRadius ?? 0); + } + + // Pixel-mode motion sources + #dataMotionMap: ReturnType = null; + #motionCx!: ReturnType>; + #motionCy!: ReturnType>; + #motionR!: ReturnType>; + #tweenedState!: ReturnType>; + + get motionCx() { + return this.#motionCx.current; + } + get motionCy() { + return this.#motionCy.current; + } + get motionR() { + return this.#motionR.current; + } + get tweenedPathData() { + return this.#tweenedState.current; + } + + // Pixel-mode polygon path string (depends on motion + transform props) + pixelPathData = $derived.by(() => { + const props = this.#getProps(); + const pts = + typeof props.points === 'number' || props.points === undefined + ? polygon({ + cx: this.motionCx, + cy: this.motionCy, + count: typeof props.points === 'number' ? props.points : 4, + radius: this.motionR, + rotate: props.rotate ?? 0, + inset: props.inset ?? 0, + scaleX: props.scaleX ?? 1, + scaleY: props.scaleY ?? 1, + skewX: props.skewX ?? 0, + skewY: props.skewY ?? 0, + tiltX: props.tiltX ?? 0, + tiltY: props.tiltY ?? 0, + }) + : props.points; + return roundedPolygonPath(pts, props.cornerRadius ?? 0); + }); + + staticFill = $derived( + typeof this.#getProps().fill === 'string' ? (this.#getProps().fill as string) : undefined + ); + staticFillOpacity = $derived( + typeof this.#getProps().fillOpacity === 'number' + ? (this.#getProps().fillOpacity as number) + : undefined + ); + staticStroke = $derived( + typeof this.#getProps().stroke === 'string' ? (this.#getProps().stroke as string) : undefined + ); + staticStrokeWidth = $derived( + typeof this.#getProps().strokeWidth === 'number' + ? (this.#getProps().strokeWidth as number) + : undefined + ); + staticOpacity = $derived( + typeof this.#getProps().opacity === 'number' + ? (this.#getProps().opacity as number) + : undefined + ); + staticClassName = $derived( + typeof this.#getProps().class === 'string' ? (this.#getProps().class as string) : undefined + ); + + constructor(getProps: () => PolygonProps) { + this.#getProps = getProps; + + const initial = getProps(); + const initialCx = initial.initialCx ?? (typeof initial.cx === 'number' ? initial.cx : 0); + const initialCy = initial.initialCy ?? (typeof initial.cy === 'number' ? initial.cy : 0); + const initialR = initial.initialR ?? (typeof initial.r === 'number' ? initial.r : 1); + + this.#motionCx = createMotion( + initialCx, + () => (typeof getProps().cx === 'number' ? (getProps().cx as number) : 0), + initial.motion + ); + this.#motionCy = createMotion( + initialCy, + () => (typeof getProps().cy === 'number' ? (getProps().cy as number) : 0), + initial.motion + ); + this.#motionR = createMotion( + initialR, + () => (typeof getProps().r === 'number' ? (getProps().r as number) : 1), + initial.motion + ); + + const extractedTween = extractTweenConfig(initial.motion); + const tweenedOptions: ResolvedMotion | undefined = extractedTween + ? { + type: extractedTween.type, + options: { interpolate: interpolatePath, ...extractedTween.options }, + } + : undefined; + this.#tweenedState = createMotion( + null, + () => this.pixelPathData, + tweenedOptions + ); + + this.#dataMotionMap = createDataMotionMap(initial.motion); + if (this.#dataMotionMap) { + const motionMap = this.#dataMotionMap; + $effect(() => { + if (!this.dataMode) return; + const props = getProps(); + const keyFn = props.key ?? defaultKey; + const activeKeys = new Set(); + for (let i = 0; i < this.resolvedData.length; i++) { + const d = this.resolvedData[i]; + const key = keyFn(d, i); + activeKeys.add(key); + let resolvedCx: number, resolvedCy: number; + if (this.geo.projection) { + [resolvedCx, resolvedCy] = resolveGeoDataPair(props.cx, props.cy, d, this.geo.projection); + } else { + resolvedCx = resolveDataProp(props.cx, d, this.chartCtx.xScale, 0); + resolvedCy = resolveDataProp(props.cy, d, this.chartCtx.yScale, 0); + } + const resolvedR = resolveDataProp( + props.r, + d, + this.chartCtx.rScale, + typeof props.r === 'number' ? props.r : 1 + ); + untrack(() => + motionMap.update(key, { cx: resolvedCx, cy: resolvedCy, r: resolvedR }) + ); + } + untrack(() => motionMap.cleanup(activeKeys)); + }); + } + } +} diff --git a/packages/layerchart/src/lib/components/Polygon/Polygon.svelte b/packages/layerchart/src/lib/components/Polygon/Polygon.svelte new file mode 100644 index 000000000..b3fa98b00 --- /dev/null +++ b/packages/layerchart/src/lib/components/Polygon/Polygon.svelte @@ -0,0 +1,23 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Polygon.svelte.test.ts b/packages/layerchart/src/lib/components/Polygon/Polygon.svelte.test.ts similarity index 97% rename from packages/layerchart/src/lib/components/Polygon.svelte.test.ts rename to packages/layerchart/src/lib/components/Polygon/Polygon.svelte.test.ts index 4d58e1cf6..8d4a1ffda 100644 --- a/packages/layerchart/src/lib/components/Polygon.svelte.test.ts +++ b/packages/layerchart/src/lib/components/Polygon/Polygon.svelte.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; -import TestHarness, { componentTestId } from './tests/TestHarness.svelte'; +import TestHarness, { componentTestId } from '../tests/TestHarness.svelte'; import Polygon from './Polygon.svelte'; describe('Polygon', () => { diff --git a/packages/layerchart/src/lib/components/Polygon/Polygon.svg.svelte b/packages/layerchart/src/lib/components/Polygon/Polygon.svg.svelte new file mode 100644 index 000000000..b57c2699a --- /dev/null +++ b/packages/layerchart/src/lib/components/Polygon/Polygon.svg.svelte @@ -0,0 +1,82 @@ + + + + +{#if c.dataMode} + {#each c.resolvedData as d, i (rest.key ? rest.key(d, i) : i)} + {@const pathData = c.resolvePolygonPath(d)} + {@const resolvedFill = resolveColorProp(rest.fill, d, c.chartCtx.cScale)} + {@const resolvedStroke = resolveColorProp(rest.stroke, d, c.chartCtx.cScale)} + {@const resolvedFillOpacity = resolveStyleProp(rest.fillOpacity, d)} + {@const resolvedStrokeWidth = resolveStyleProp(rest.strokeWidth, d)} + {@const resolvedOpacity = resolveStyleProp(rest.opacity, d)} + {@const resolvedClass = resolveStyleProp(rest.class, d)} + + {/each} +{:else} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/RadialGradient.svelte b/packages/layerchart/src/lib/components/RadialGradient.svelte deleted file mode 100644 index 741ee3615..000000000 --- a/packages/layerchart/src/lib/components/RadialGradient.svelte +++ /dev/null @@ -1,190 +0,0 @@ - - - - -{#if layerCtx === 'canvas'} - {@render children?.({ id, gradient: canvasGradient as any })} -{:else if layerCtx === 'svg'} - - - {#if stopsContent} - {@render stopsContent()} - {:else if stops} - {@const stopClass = cls('lc-radial-gradient-stop', className)} - {#each stops as stop, i} - {#if Array.isArray(stop)} - - {:else} - - {/if} - {/each} - {/if} - - - - {@render children?.({ id, gradient: `url(#${id})` })} -{/if} diff --git a/packages/layerchart/src/lib/components/RadialGradient/RadialGradient.canvas.svelte b/packages/layerchart/src/lib/components/RadialGradient/RadialGradient.canvas.svelte new file mode 100644 index 000000000..aa97ecfb1 --- /dev/null +++ b/packages/layerchart/src/lib/components/RadialGradient/RadialGradient.canvas.svelte @@ -0,0 +1,69 @@ + + + + +{@render children?.({ id, gradient: canvasGradient as any })} diff --git a/packages/layerchart/src/lib/components/RadialGradient/RadialGradient.shared.svelte.ts b/packages/layerchart/src/lib/components/RadialGradient/RadialGradient.shared.svelte.ts new file mode 100644 index 000000000..fd8502ab7 --- /dev/null +++ b/packages/layerchart/src/lib/components/RadialGradient/RadialGradient.shared.svelte.ts @@ -0,0 +1,57 @@ +import type { Snippet } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import type { Without } from '$lib/utils/types.js'; + +export type RadialGradientPropsWithoutHTML = { + /** Unique id for radialGradient */ + id?: string; + + /** + * Array of strings (colors), distributed equally from 0-100%. + * If array of tuples, will use first value as the offset, and second as color. + * + * @default ['var(--tw-gradient-from)', 'var(--tw-gradient-to)'] + */ + stops?: string[] | [string | number, string][]; + + /** The x coordinate of the center of the gradient @default '50%' */ + cx?: string; + + /** The y coordinate of the center of the gradient @default '50%' */ + cy?: string; + + /** The x coordinate of the focal point of the gradient @default cx */ + fx?: string; + + /** The y coordinate of the focal point of the gradient @default cy */ + fy?: string; + + /** The radius of the gradient */ + r?: string; + + /** + * Indicates how the gradient behaves if it starts or ends inside the bounds + * of the shape containing the gradient + * + * @default 'pad' + */ + spreadMethod?: 'pad' | 'reflect' | 'repeat'; + + /** Transform attribute for the gradient */ + transform?: string | null; + + /** + * Define the coordinate system for attributes (i.e. gradientUnits) + * + * @default 'objectBoundingBox' + */ + units?: 'objectBoundingBox' | 'userSpaceOnUse'; + + children?: Snippet<[{ id: string; gradient: string }]>; + + /** Render as a child of the gradient and will opt out of the default stops being rendered. */ + stopsContent?: Snippet; +}; + +export type RadialGradientProps = RadialGradientPropsWithoutHTML & + Without, RadialGradientPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/RadialGradient/RadialGradient.svelte b/packages/layerchart/src/lib/components/RadialGradient/RadialGradient.svelte new file mode 100644 index 000000000..4fbd017d5 --- /dev/null +++ b/packages/layerchart/src/lib/components/RadialGradient/RadialGradient.svelte @@ -0,0 +1,23 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/RadialGradient/RadialGradient.svg.svelte b/packages/layerchart/src/lib/components/RadialGradient/RadialGradient.svg.svelte new file mode 100644 index 000000000..638e38528 --- /dev/null +++ b/packages/layerchart/src/lib/components/RadialGradient/RadialGradient.svg.svelte @@ -0,0 +1,66 @@ + + + + + + + {#if stopsContent} + {@render stopsContent()} + {:else if stops} + {@const stopClass = cls('lc-radial-gradient-stop', className)} + {#each stops as stop, i} + {#if Array.isArray(stop)} + + {:else} + + {/if} + {/each} + {/if} + + + +{@render children?.({ id, gradient: `url(#${id})` })} diff --git a/packages/layerchart/src/lib/components/Raster.svelte b/packages/layerchart/src/lib/components/Raster.svelte index 576d6be67..f87fb05eb 100644 --- a/packages/layerchart/src/lib/components/Raster.svelte +++ b/packages/layerchart/src/lib/components/Raster.svelte @@ -67,7 +67,7 @@ import { getGeoContext } from '$lib/contexts/geo.js'; import { gridCellCenterToBounds, resolveRasterBounds } from '$lib/utils/index.js'; import { interpolateGrid } from '$lib/utils/rasterInterpolate.js'; - import Image from './Image.svelte'; + import Image from './Image/Image.svelte'; const ctx = getChartContext(); const geo = getGeoContext(); diff --git a/packages/layerchart/src/lib/components/RectClipPath.svelte b/packages/layerchart/src/lib/components/RectClipPath.svelte index 09d87add6..bb5cbb4b2 100644 --- a/packages/layerchart/src/lib/components/RectClipPath.svelte +++ b/packages/layerchart/src/lib/components/RectClipPath.svelte @@ -70,7 +70,7 @@
- {#key chartState.isMounted} - - {@const { - domainExtent: _de, - constrain: _uc, - apply: _apply, - scaleExtent: _se, - translateExtent: _te, - ...transformProps - } = transform ?? {}} - - {#if brush} - {#await import('./BrushContext.svelte')} - - - - - - {:then { default: BrushContext }} - - - - - - - - {/await} - {:else} - - - - - {/if} - - {/key} -
-{/if} - - diff --git a/packages/layerchart/src/lib/components/Chart/Chart.base.svelte b/packages/layerchart/src/lib/components/Chart/Chart.base.svelte new file mode 100644 index 000000000..88cf3143c --- /dev/null +++ b/packages/layerchart/src/lib/components/Chart/Chart.base.svelte @@ -0,0 +1,508 @@ + + + + +{#if ssr === true || typeof window !== 'undefined'} +
+ {#key chartState.isMounted} + + {@const { + domainExtent: _de, + constrain: _uc, + apply: _apply, + scaleExtent: _se, + translateExtent: _te, + ...transformProps + } = transform ?? {}} + + {#if brush} + {#await import('../BrushContext.svelte')} + + + + + {:then { default: BrushContext }} + + + + + + + + {/await} + {:else} + + + + + {/if} + + {/key} +
+{/if} + + diff --git a/packages/layerchart/src/lib/components/Chart/Chart.canvas.svelte b/packages/layerchart/src/lib/components/Chart/Chart.canvas.svelte new file mode 100644 index 000000000..2af82863c --- /dev/null +++ b/packages/layerchart/src/lib/components/Chart/Chart.canvas.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Chart/Chart.html.svelte b/packages/layerchart/src/lib/components/Chart/Chart.html.svelte new file mode 100644 index 000000000..2a7b01cc2 --- /dev/null +++ b/packages/layerchart/src/lib/components/Chart/Chart.html.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Chart/Chart.shared.svelte.ts b/packages/layerchart/src/lib/components/Chart/Chart.shared.svelte.ts new file mode 100644 index 000000000..b3934ca1a --- /dev/null +++ b/packages/layerchart/src/lib/components/Chart/Chart.shared.svelte.ts @@ -0,0 +1,261 @@ +import type { ComponentProps, Snippet } from 'svelte'; +import type { HTMLAttributes } from 'svelte/elements'; +import type { TimeInterval } from 'd3-time'; +import type { HierarchyNode } from 'd3-hierarchy'; +import type { SankeyGraph } from 'd3-sankey'; + +import type { Accessor } from '$lib/utils/common.js'; +import type { MotionProp } from '$lib/utils/motion.svelte.js'; +import type { AnyScale, DomainType } from '$lib/utils/scales.svelte.js'; +import type { + BaseRange, + Nice, + PaddingArray, + Without, + XRangeWithScale, + YRangeWithScale, +} from '$lib/utils/types.js'; +import type { GeoStateProps } from '$lib/states/geo.svelte.js'; +import type { StackLayout } from '$lib/states/series.svelte.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; + +import type TooltipContext from '../tooltip/TooltipContext.svelte'; +import type TransformContext from '../TransformContext.svelte'; +import type BrushContext from '../BrushContext.svelte'; +import type { ChartChildrenProps } from '../ChartChildren/ChartChildren.shared.svelte.js'; +import type { SeriesData } from '../charts/types.js'; + +export type ChartResizeDetail = { + width: number; + height: number; + containerWidth: number; + containerHeight: number; +}; + +export type PreservedChartConfig< + T, + XScale extends AnyScale = AnyScale, + YScale extends AnyScale = AnyScale, +> = Pick< + ChartPropsWithoutHTML, + | 'x' + | 'y' + | 'z' + | 'r' + | 'c' + | 'x1' + | 'y1' + | 'xRange' + | 'yRange' + | 'cDomain' + | 'zDomain' + | 'xDomain' + | 'yDomain' + | 'rDomain' + | 'x1Domain' + | 'y1Domain' + | 'zRange' + | 'rRange' + | 'cRange' + | 'x1Range' + | 'y1Range' +>; + +export type LayerChartInternalMeta = { + /** + * The current chart type. + * The default is `'default'` which is any chart being composed + * that isn't a "simplified chart". + */ + type: + | 'default' + | 'simplified-area' + | 'simplified-bar' + | 'simplified-line' + | 'simplified-pie' + | 'simplified-scatter'; +}; + +export type ChartPropsWithoutHTML< + T, + XScale extends AnyScale = AnyScale, + YScale extends AnyScale = AnyScale, +> = { + /** + * Whether this chart should be rendered server side + * + * @default false + */ + ssr?: boolean; + + /** + * Whether to allow pointer events via CSS. + * Set this to `false` to set `pointer-events: none;` on all components, disabling + * all mouse interactions. + * + * @default true + */ + pointerEvents?: boolean; + + /** + * Determine the positioning of the wrapper div. + * Set this to `'absolute'` when you want to stack layers. + * + * @default 'relative' + */ + position?: string; + + /** + * If `true`, set all scale ranges to `[0, 100]`. + * Ranges reversed via `xReverse`, `yReverse`, or `rReverse` props will + * continue to be reversed as usual. + * @default false + */ + percentRange?: boolean; + + /** + * A bindable reference to the root container element. + */ + ref?: HTMLElement; + + /** + * If `data` is not a flat array of objects and you want to use any of the scales, set a flat + * version of the data via the `flatData` prop. + */ + data?: T[] | readonly T[] | HierarchyNode | SankeyGraph; + + /** A flat version of data. */ + flatData?: T[] | readonly T[] | HierarchyNode | SankeyGraph; + + x?: Accessor; + y?: Accessor; + z?: Accessor; + r?: Accessor; + x1?: Accessor; + y1?: Accessor; + c?: Accessor; + + xDomain?: DomainType; + yDomain?: DomainType; + zDomain?: DomainType; + rDomain?: DomainType; + x1Domain?: DomainType; + y1Domain?: DomainType; + cDomain?: DomainType; + + xNice?: Nice; + yNice?: Nice; + zNice?: Nice; + rNice?: Nice; + + xPadding?: PaddingArray; + yPadding?: PaddingArray; + zPadding?: PaddingArray; + rPadding?: PaddingArray; + + xScale?: XScale; + yScale?: YScale; + zScale?: AnyScale; + rScale?: AnyScale; + x1Scale?: AnyScale; + y1Scale?: AnyScale; + cScale?: AnyScale; + + xRange?: BaseRange; + yRange?: BaseRange; + zRange?: BaseRange; + rRange?: BaseRange; + x1Range?: XRangeWithScale; + y1Range?: YRangeWithScale; + cRange?: string[] | readonly string[]; + + xReverse?: boolean; + yReverse?: boolean; + zReverse?: boolean; + rReverse?: boolean; + + xDomainSort?: boolean; + yDomainSort?: boolean; + zDomainSort?: boolean; + rDomainSort?: boolean; + + padding?: { top?: number; right?: number; bottom?: number; left?: number } | number; + + extents?: { + x?: [min: number, max: number]; + y?: [min: number, max: number]; + r?: [min: number, max: number]; + z?: [min: number, max: number]; + }; + + meta?: Record; + debug?: boolean; + verbose?: boolean; + xBaseline?: number | null; + yBaseline?: number | null; + xInterval?: TimeInterval | null; + yInterval?: TimeInterval | null; + valueAxis?: 'x' | 'y'; + radial?: boolean; + + children?: Snippet<[{ context: ChartState }]>; + + context?: ChartState; + + geo?: Partial; + + tooltipContext?: Partial> | boolean; + + transform?: Partial> & { + apply?: { + rotation?: boolean; + scale?: boolean; + translate?: boolean; + }; + domainExtent?: { + x?: { + min?: number | Date | 'data'; + max?: number | Date | 'data'; + minRange?: number; + }; + y?: { + min?: number | Date | 'data'; + max?: number | Date | 'data'; + minRange?: number; + }; + }; + }; + + brush?: + | (Partial> & { + zoomOnBrush?: boolean; + }) + | boolean; + + series?: SeriesData[]; + + seriesLayout?: StackLayout | 'group'; + + bandPadding?: number; + groupPadding?: number; + + onResize?: (e: ChartResizeDetail) => void; + + clip?: boolean; + + motion?: MotionProp; + + ondragstart?: ComponentProps['ondragstart']; + ondragend?: ComponentProps['ondragend']; + onTransform?: ComponentProps['onTransform']; + + width?: number; + height?: number; +} & ChartChildrenProps; + +export type ChartProps< + T, + XScale extends AnyScale = AnyScale, + YScale extends AnyScale = AnyScale, +> = ChartPropsWithoutHTML & + Without, ChartPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/Chart/Chart.svelte b/packages/layerchart/src/lib/components/Chart/Chart.svelte new file mode 100644 index 000000000..430965d44 --- /dev/null +++ b/packages/layerchart/src/lib/components/Chart/Chart.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Chart/Chart.svg.svelte b/packages/layerchart/src/lib/components/Chart/Chart.svg.svelte new file mode 100644 index 000000000..be3ce27f4 --- /dev/null +++ b/packages/layerchart/src/lib/components/Chart/Chart.svg.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/ChartChildren.svelte b/packages/layerchart/src/lib/components/ChartChildren.svelte deleted file mode 100644 index 4e4cb1526..000000000 --- a/packages/layerchart/src/lib/components/ChartChildren.svelte +++ /dev/null @@ -1,334 +0,0 @@ - - - - -{#if childrenProp} - {@render childrenProp(snippetProps)} -{:else} - {@render belowContext?.(snippetProps)} - - - {#if typeof grid === 'function'} - {@render grid(snippetProps)} - {:else if grid} - - {/if} - - - {#if annotations.length > 0} - {#await import('./charts/ChartAnnotations.svelte') then { default: ChartAnnotations }} - - {/await} - {/if} - - {@render belowMarks?.(snippetProps)} - {@render marks?.(snippetProps)} - {@render aboveMarks?.(snippetProps)} - - - {#if typeof axis === 'function'} - {@render axis(snippetProps)} - - {#if typeof rule === 'function'} - {@render rule(snippetProps)} - {:else if rule} - - {/if} - {:else if axis} - {#if axis !== 'x'} - - - {/if} - - {#if axis !== 'y'} - - - {/if} - - {#if typeof rule === 'function'} - {@render rule(snippetProps)} - {:else if rule} - - {/if} - {/if} - - - - {#if typeof points === 'function'} - {@render points(snippetProps)} - {:else if points} - {#await import('./Points.svelte') then { default: Points }} - {#each context.series.visibleSeries as s, i (s.key)} - - {/each} - {/await} - {/if} - - {#if typeof labels === 'function'} - {@render labels(snippetProps)} - {:else if labels} - {#await import('./Labels.svelte') then { default: Labels }} - {@const labelSeriesKey = typeof labels === 'object' ? labels.seriesKey : undefined} - {#each context.series.visibleSeries.filter((s) => !labelSeriesKey || s.key === labelSeriesKey) as s, i (s.key)} - - {/each} - {/await} - {/if} - - {#if typeof highlight === 'function'} - {@render highlight(snippetProps)} - {:else if highlight} - - {/if} - - {#if annotations.length > 0} - {#await import('./charts/ChartAnnotations.svelte') then { default: ChartAnnotations }} - - {/await} - {/if} - - - - {@render aboveContext?.(snippetProps)} - - {#if typeof legend === 'function'} - {@render legend(snippetProps)} - {:else if legend} - {#await import('./Legend.svelte') then { default: Legend }} - - {/await} - {/if} - - {#if typeof tooltip === 'function'} - {@render tooltip(snippetProps)} - {:else if tooltipContext} - {#await import('./charts/DefaultTooltip.svelte') then { default: DefaultTooltip }} - - {/await} - {/if} -{/if} diff --git a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.base.svelte b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.base.svelte new file mode 100644 index 000000000..2932edc98 --- /dev/null +++ b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.base.svelte @@ -0,0 +1,210 @@ + + + + +{#if childrenProp} + {@render childrenProp(snippetProps)} +{:else} + {@render belowContext?.(snippetProps)} + + + {#if typeof grid === 'function'} + {@render grid(snippetProps)} + {:else if grid} + + {/if} + + + {#if annotations.length > 0} + {#await import('../charts/ChartAnnotations.svelte') then { default: ChartAnnotations }} + + {/await} + {/if} + + {@render belowMarks?.(snippetProps)} + {@render marks?.(snippetProps)} + {@render aboveMarks?.(snippetProps)} + + + {#if typeof axis === 'function'} + {@render axis(snippetProps)} + + {#if typeof rule === 'function'} + {@render rule(snippetProps)} + {:else if rule} + + {/if} + {:else if axis} + {#if axis !== 'x'} + + + {/if} + + {#if axis !== 'y'} + + + {/if} + + {#if typeof rule === 'function'} + {@render rule(snippetProps)} + {:else if rule} + + {/if} + {/if} + + + + {#if typeof points === 'function'} + {@render points(snippetProps)} + {:else if points} + {#await import('../Points.svelte') then { default: Points }} + {#each context.series.visibleSeries as s, i (s.key)} + + {/each} + {/await} + {/if} + + {#if typeof labels === 'function'} + {@render labels(snippetProps)} + {:else if labels} + {#await import('../Labels.svelte') then { default: Labels }} + {@const labelSeriesKey = typeof labels === 'object' ? labels.seriesKey : undefined} + {#each context.series.visibleSeries.filter((s) => !labelSeriesKey || s.key === labelSeriesKey) as s, i (s.key)} + + {/each} + {/await} + {/if} + + {#if typeof highlight === 'function'} + {@render highlight(snippetProps)} + {:else if highlight} + + {/if} + + {#if annotations.length > 0} + {#await import('../charts/ChartAnnotations.svelte') then { default: ChartAnnotations }} + + {/await} + {/if} + + + + {@render aboveContext?.(snippetProps)} + + {#if typeof legend === 'function'} + {@render legend(snippetProps)} + {:else if legend} + {#await import('../Legend.svelte') then { default: Legend }} + + {/await} + {/if} + + {#if typeof tooltip === 'function'} + {@render tooltip(snippetProps)} + {:else if tooltipContext} + {#await import('../charts/DefaultTooltip.svelte') then { default: DefaultTooltip }} + + {/await} + {/if} +{/if} diff --git a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.canvas.svelte b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.canvas.svelte new file mode 100644 index 000000000..93ff4cd4a --- /dev/null +++ b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.canvas.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.html.svelte b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.html.svelte new file mode 100644 index 000000000..3a7c26ce9 --- /dev/null +++ b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.html.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts new file mode 100644 index 000000000..e15b8485f --- /dev/null +++ b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts @@ -0,0 +1,142 @@ +import type { ComponentProps, Snippet } from 'svelte'; + +import type { ChartState } from '$lib/contexts/chart.js'; +import type { AnyScale } from '$lib/utils/scales.svelte.js'; + +import type Axis from '../Axis/Axis.svelte'; +import type Area from '../Area.svelte'; +import type Arc from '../Arc.svelte'; +import type Bars from '../Bars.svelte'; +import type BrushContext from '../BrushContext.svelte'; +import type Canvas from '../layers/Canvas.svelte'; +import type Grid from '../Grid/Grid.svelte'; +import type Group from '../Group/Group.svelte'; +import type Highlight from '../Highlight.svelte'; +import type Labels from '../Labels.svelte'; +import type Legend from '../Legend.svelte'; +import type Line from '../Line/Line.svelte'; +import type Pie from '../Pie.svelte'; +import type Points from '../Points.svelte'; +import type Rule from '../Rule/Rule.svelte'; +import type Spline from '../Spline.svelte'; +import type Svg from '../layers/Svg.svelte'; +import type TooltipContext from '../tooltip/TooltipContext.svelte'; +import type Tooltip from '../tooltip/Tooltip.svelte'; +import type TooltipHeader from '../tooltip/TooltipHeader.svelte'; +import type TooltipList from '../tooltip/TooltipList.svelte'; +import type TooltipItem from '../tooltip/TooltipItem.svelte'; +import type TooltipSeparator from '../tooltip/TooltipSeparator.svelte'; +import type { ChartAnnotations as ChartAnnotationsType } from '../charts/types.js'; + +// BaseChartProps +export type ChartChildrenProps< + TData, + XScale extends AnyScale = AnyScale, + YScale extends AnyScale = AnyScale, + ChartSnippet = Snippet<[{ context: ChartState }]>, +> = { + /** + * The axis to be used for the chart. + * + * @default true + */ + axis?: ComponentProps | 'x' | 'y' | boolean | ChartSnippet; + + /** + * The grid to be used for the chart. + * + * @default true + */ + grid?: ComponentProps | boolean | ChartSnippet; + + /** + * The labels to be used for the chart. + * + * @default false + */ + labels?: ComponentProps> | boolean | ChartSnippet; + + /** + * The legend to be used for the chart. + * + * @default false + */ + legend?: ComponentProps | boolean | ChartSnippet; + + /** + * The points to be used for the chart. + * + * @default false + */ + points?: ComponentProps | boolean | ChartSnippet; + + /** + * The rule to be used for the chart. + * + * @default true + */ + rule?: ComponentProps | boolean | ChartSnippet; + + /** + * The tooltip snippet to be used for the chart. + */ + tooltip?: ChartSnippet; + + /** + * Tooltip context to be used for the chart. + */ + tooltipContext?: Partial> | boolean; + + highlight?: boolean | Partial> | ChartSnippet; + + /** Annotations to show on chart */ + annotations?: ChartAnnotationsType; + + /** + * Callback when tooltip data is clicked. + */ + onTooltipClick?: (e: MouseEvent, details: { data: any }) => void; + + /** + * Additional props to be passed to the components rendered internally by Chart components. + * This is useful for customizing the behavior of individual components, without having + * to fully override them via snippets. + */ + props?: Partial<{ + area: Partial>; + arc: Partial>; + bars: Partial>; + brush: Partial>; + canvas: Partial>; + grid: Partial>; + group: Partial>; + highlight: Partial>; + labels: Partial>>; + legend: Partial>; + line: Partial>; + pie: Partial>; + spline: Partial>; + points: Partial>; + rule: Partial>; + svg: Partial>; + tooltip: { + context?: Partial>; + root?: Omit>, 'context'>; + header?: Partial>; + list?: Partial>; + item?: Partial>; + separator?: Partial>; + hideTotal?: boolean; + }; + xAxis: Partial>; + yAxis: Partial>; + }>; + + // Snippets + children?: ChartSnippet; + belowContext?: ChartSnippet; + belowMarks?: ChartSnippet; + marks?: ChartSnippet; + aboveMarks?: ChartSnippet; + aboveContext?: ChartSnippet; +}; diff --git a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svelte b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svelte new file mode 100644 index 000000000..e8e0352a6 --- /dev/null +++ b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svg.svelte b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svg.svelte new file mode 100644 index 000000000..b27c750cb --- /dev/null +++ b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svg.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/ArcChart.svelte b/packages/layerchart/src/lib/components/charts/ArcChart.svelte index 9155c3364..de991b2d4 100644 --- a/packages/layerchart/src/lib/components/charts/ArcChart.svelte +++ b/packages/layerchart/src/lib/components/charts/ArcChart.svelte @@ -1,6 +1,6 @@ - + diff --git a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.html.svelte b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.html.svelte index 3a7c26ce9..b64d131cd 100644 --- a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.html.svelte +++ b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.html.svelte @@ -11,6 +11,8 @@ import Axis from '../Axis/Axis.html.svelte'; import Grid from '../Grid/Grid.html.svelte'; import Rule from '../Rule/Rule.html.svelte'; + import Highlight from '../Highlight/Highlight.html.svelte'; + import ChartClipPath from '../ChartClipPath/ChartClipPath.html.svelte'; import type { AnyScale } from '$lib/utils/scales.svelte.js'; import type { ChartChildrenProps } from './ChartChildren.shared.svelte.js'; @@ -18,4 +20,4 @@ let props: ChartChildrenProps = $props(); - + diff --git a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts index e15b8485f..4db63ab71 100644 --- a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts +++ b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts @@ -11,7 +11,7 @@ import type BrushContext from '../BrushContext.svelte'; import type Canvas from '../layers/Canvas.svelte'; import type Grid from '../Grid/Grid.svelte'; import type Group from '../Group/Group.svelte'; -import type Highlight from '../Highlight.svelte'; +import type Highlight from '../Highlight/Highlight.svelte'; import type Labels from '../Labels.svelte'; import type Legend from '../Legend.svelte'; import type Line from '../Line/Line.svelte'; diff --git a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svelte b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svelte index e8e0352a6..abba2751b 100644 --- a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svelte +++ b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svelte @@ -11,6 +11,8 @@ import Axis from '../Axis/Axis.svelte'; import Grid from '../Grid/Grid.svelte'; import Rule from '../Rule/Rule.svelte'; + import Highlight from '../Highlight/Highlight.svelte'; + import ChartClipPath from '../ChartClipPath/ChartClipPath.svelte'; import type { AnyScale } from '$lib/utils/scales.svelte.js'; import type { ChartChildrenProps } from './ChartChildren.shared.svelte.js'; @@ -18,4 +20,4 @@ let props: ChartChildrenProps = $props(); - + diff --git a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svg.svelte b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svg.svelte index b27c750cb..33ef67637 100644 --- a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svg.svelte +++ b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.svg.svelte @@ -11,6 +11,8 @@ import Axis from '../Axis/Axis.svg.svelte'; import Grid from '../Grid/Grid.svg.svelte'; import Rule from '../Rule/Rule.svg.svelte'; + import Highlight from '../Highlight/Highlight.svg.svelte'; + import ChartClipPath from '../ChartClipPath/ChartClipPath.svg.svelte'; import type { AnyScale } from '$lib/utils/scales.svelte.js'; import type { ChartChildrenProps } from './ChartChildren.shared.svelte.js'; @@ -18,4 +20,4 @@ let props: ChartChildrenProps = $props(); - + diff --git a/packages/layerchart/src/lib/components/ChartClipPath.svelte b/packages/layerchart/src/lib/components/ChartClipPath.svelte deleted file mode 100644 index 4a66ec580..000000000 --- a/packages/layerchart/src/lib/components/ChartClipPath.svelte +++ /dev/null @@ -1,45 +0,0 @@ - - - - - diff --git a/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.base.svelte b/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.base.svelte new file mode 100644 index 000000000..976c13dce --- /dev/null +++ b/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.base.svelte @@ -0,0 +1,33 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.canvas.svelte b/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.canvas.svelte new file mode 100644 index 000000000..da13cac4d --- /dev/null +++ b/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.canvas.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.html.svelte b/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.html.svelte new file mode 100644 index 000000000..2460492a4 --- /dev/null +++ b/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.html.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.shared.svelte.ts b/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.shared.svelte.ts new file mode 100644 index 000000000..fe240a763 --- /dev/null +++ b/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.shared.svelte.ts @@ -0,0 +1,14 @@ +import type { Snippet } from 'svelte'; +import type { Without } from '$lib/utils/types.js'; +import type { RectClipPathProps } from '../RectClipPath/RectClipPath.shared.svelte.js'; + +export type ChartClipPathPropsWithoutHTML = { + /** Include padding area (ex. axis) @default false */ + full?: boolean; + /** Disable clipping (show all) @default false */ + disabled?: boolean; + children?: Snippet; +}; + +export type ChartClipPathProps = ChartClipPathPropsWithoutHTML & + Without, ChartClipPathPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.svelte b/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.svelte new file mode 100644 index 000000000..02412916a --- /dev/null +++ b/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.svelte @@ -0,0 +1,23 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.svg.svelte b/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.svg.svelte new file mode 100644 index 000000000..e3a72dd29 --- /dev/null +++ b/packages/layerchart/src/lib/components/ChartClipPath/ChartClipPath.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Highlight.svelte b/packages/layerchart/src/lib/components/Highlight.svelte deleted file mode 100644 index c5978b394..000000000 --- a/packages/layerchart/src/lib/components/Highlight.svelte +++ /dev/null @@ -1,773 +0,0 @@ - - - - -{#if highlightData} - {#if area} - {#if typeof area === 'function'} - {@render area({ area: _area })} - {:else if ctx.radial} - - onAreaClick(e, { data: highlightData }))} - /> - {:else} - onAreaClick(e, { data: highlightData }))} - /> - {/if} - {/if} - - {#if bar} - {#if typeof bar === 'function'} - {@render bar()} - {:else} - {#await import('./Bar.svelte') then { default: Bar }} - onBarClick(e, { data: highlightData }))} - /> - {/await} - {/if} - {/if} - - {#if linesProp} - {#if typeof linesProp === 'function'} - {@render linesProp({ lines: _lines })} - {:else} - {#each _lines as line} - - {/each} - {/if} - {/if} - - {#if points} - {#if typeof points === 'function'} - {@render points({ points: _points })} - {:else} - {#each _points as point} - {@const pointOpacity = - opacity ?? - (point.seriesKey - ? ctx.series.isHighlighted(point.seriesKey, true) - ? 1 - : 0.1 - : undefined)} - { - // Do not propagate `pointerdown` event to `BrushContext` if `onclick` is provided - e.stopPropagation(); - })} - onclick={onPointClick && ((e) => onPointClick(e, { point, data: highlightData }))} - onpointerenter={(e) => { - if (onPointClick) { - asAny(e.target).style.cursor = 'pointer'; - } - if (point.seriesKey) { - ctx.series.highlightKey = point.seriesKey; - } - onPointEnter?.(e, { point, data: highlightData }); - }} - onpointerleave={(e) => { - if (onPointClick) { - asAny(e.target).style.cursor = 'default'; - } - if (point.seriesKey) { - ctx.series.highlightKey = null; - } - onPointLeave?.(e, { point, data: highlightData }); - }} - /> - {/each} - {/if} - {/if} -{/if} - - diff --git a/packages/layerchart/src/lib/components/Highlight/Highlight.base.svelte b/packages/layerchart/src/lib/components/Highlight/Highlight.base.svelte new file mode 100644 index 000000000..fa023dff0 --- /dev/null +++ b/packages/layerchart/src/lib/components/Highlight/Highlight.base.svelte @@ -0,0 +1,199 @@ + + + + +{#if c.highlightData} + {#if area} + {#if typeof area === 'function'} + {@render area({ area: c.area })} + {:else if c.ctx.radial} + onAreaClick(e, { data: c.highlightData }))} + /> + {:else} + onAreaClick(e, { data: c.highlightData }))} + /> + {/if} + {/if} + + {#if bar} + {#if typeof bar === 'function'} + {@render bar()} + {:else} + {#await import('../Bar.svelte') then { default: Bar }} + onBarClick(e, { data: c.highlightData }))} + /> + {/await} + {/if} + {/if} + + {#if linesProp} + {#if typeof linesProp === 'function'} + {@render linesProp({ lines: c.lines })} + {:else} + {#each c.lines as line} + + {/each} + {/if} + {/if} + + {#if points} + {#if typeof points === 'function'} + {@render points({ points: c.points })} + {:else} + {#each c.points as point} + {@const pointOpacity = + opacity ?? + (point.seriesKey + ? c.ctx.series.isHighlighted(point.seriesKey, true) + ? 1 + : 0.1 + : undefined)} + { + e.stopPropagation(); + })} + onclick={onPointClick && + ((e: MouseEvent) => onPointClick(e, { point, data: c.highlightData }))} + onpointerenter={(e: PointerEvent) => { + if (onPointClick) { + asAny(e.target).style.cursor = 'pointer'; + } + if (point.seriesKey) { + c.ctx.series.highlightKey = point.seriesKey; + } + onPointEnter?.(e, { point, data: c.highlightData }); + }} + onpointerleave={(e: PointerEvent) => { + if (onPointClick) { + asAny(e.target).style.cursor = 'default'; + } + if (point.seriesKey) { + c.ctx.series.highlightKey = null; + } + onPointLeave?.(e, { point, data: c.highlightData }); + }} + /> + {/each} + {/if} + {/if} +{/if} + + diff --git a/packages/layerchart/src/lib/components/Highlight/Highlight.canvas.svelte b/packages/layerchart/src/lib/components/Highlight/Highlight.canvas.svelte new file mode 100644 index 000000000..8b2910e5b --- /dev/null +++ b/packages/layerchart/src/lib/components/Highlight/Highlight.canvas.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Highlight/Highlight.html.svelte b/packages/layerchart/src/lib/components/Highlight/Highlight.html.svelte new file mode 100644 index 000000000..51f49ff0e --- /dev/null +++ b/packages/layerchart/src/lib/components/Highlight/Highlight.html.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Highlight/Highlight.shared.svelte.ts b/packages/layerchart/src/lib/components/Highlight/Highlight.shared.svelte.ts new file mode 100644 index 000000000..6c94e653f --- /dev/null +++ b/packages/layerchart/src/lib/components/Highlight/Highlight.shared.svelte.ts @@ -0,0 +1,520 @@ +import type { ComponentProps, Snippet } from 'svelte'; +import { max, min } from 'd3-array'; +import { pointRadial, type Series, type SeriesPoint } from 'd3-shape'; +import { notNull } from '@layerstack/utils'; + +import type Bar from '../Bar.svelte'; +import type Circle from '../Circle/Circle.svelte'; +import type Line from '../Line/Line.svelte'; +import type Rect from '../Rect/Rect.svelte'; +import { accessor, type Accessor } from '$lib/utils/common.js'; +import { isScaleBand, isScaleTime } from '$lib/utils/scales.svelte.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import type { MotionProp } from '$lib/utils/motion.svelte.js'; + +export type HighlightPointData = { x: any; y: any }; +export type HighlightPoint = { + x: number; + y: number; + r?: number; + fill: string; + data: HighlightPointData; + seriesKey?: string; +}; + +export type HighlightLineSegment = { x1: number; y1: number; x2: number; y2: number }; +export type HighlightArea = { x: number; y: number; width: number; height: number }; + +export type HighlightPropsWithoutHTML = { + /** Highlight specific data (annotate), espeically uses tooltip data */ + data?: any; + /** Override `x` from context */ + x?: Accessor; + /** Override `y` from context */ + y?: Accessor; + /** + * Use the chart's radius scale for highlight point size. + * When `true`, uses the `r` config from the chart context. + * When an accessor is provided, uses it to read the radius value from the data. + */ + r?: boolean | Accessor; + + axis?: 'x' | 'y' | 'both' | 'none'; + + /** + * Show points and pass props to Circles + * @default false + */ + points?: + | boolean + | Partial> + | Snippet< + [ + { + points: { + x: number; + y: number; + fill: string; + data: HighlightPointData; + }[]; + }, + ] + >; + + /** + * Show lines and pass props to Lines + * @default false + */ + lines?: + | boolean + | Partial> + | Snippet<[{ lines: HighlightLineSegment[] }]>; + + /** + * Show area and pass props to Rect + * @default false + */ + area?: + | boolean + | Partial> + | Snippet<[{ area: HighlightArea }]>; + + /** + * Show bar and pass props to Rect + * + * @default false + */ + bar?: boolean | Partial> | Snippet; + + /** + * Set to false to disable spring transitions + * + * @default true + */ + motion?: MotionProp; + + /** The opacity of the element. (0 to 1) */ + opacity?: number; + + onAreaClick?: (e: MouseEvent, detail: { data: any }) => void; + onBarClick?: (e: MouseEvent, detail: { data: any }) => void; + + onPointClick?: (e: MouseEvent, detail: { point: HighlightPoint; data: any }) => void; + onPointEnter?: (e: MouseEvent, detail: { point: HighlightPoint; data: any }) => void; + onPointLeave?: (e: MouseEvent, detail: { point: HighlightPoint; data: any }) => void; +}; + +export type HighlightProps = HighlightPropsWithoutHTML; + +/** + * Reactive state shared by every per-layer Highlight variant. Holds the + * derived `lines`, `area`, and `points` arrays the template renders, plus + * helpers like `axis` and `getPointRadius`. + */ +export class HighlightState { + #getProps: () => HighlightProps = () => ({}) as HighlightProps; + ctx: ChartState = getChartContext(); + + constructor(getProps: () => HighlightProps) { + this.#getProps = getProps; + } + + x = $derived(accessor(this.#getProps().x ?? this.ctx.x)); + y = $derived(accessor(this.#getProps().y ?? this.ctx.y)); + + highlightData = $derived(this.#getProps().data ?? this.ctx.tooltip.data); + + xValue = $derived(this.x(this.highlightData)); + xCoord = $derived( + Array.isArray(this.xValue) + ? this.xValue.map((v) => this.ctx.xScale(v)) + : this.ctx.xScale(this.xValue) + ); + xOffset = $derived( + isScaleBand(this.ctx.xScale) && !this.ctx.radial ? this.ctx.xScale.bandwidth() / 2 : 0 + ); + xCoordScalar = $derived( + Array.isArray(this.xCoord) + ? (this.xCoord[0] + this.xCoord[this.xCoord.length - 1]) / 2 + : this.xCoord + ); + + yValue = $derived(this.y(this.highlightData)); + yCoord = $derived( + Array.isArray(this.yValue) + ? this.yValue.map((v) => this.ctx.yScale(v)) + : this.ctx.yScale(this.yValue) + ); + yOffset = $derived( + isScaleBand(this.ctx.yScale) && !this.ctx.radial ? this.ctx.yScale.bandwidth() / 2 : 0 + ); + yCoordScalar = $derived( + Array.isArray(this.yCoord) + ? (this.yCoord[0] + this.yCoord[this.yCoord.length - 1]) / 2 + : this.yCoord + ); + + axis = $derived.by(() => { + const axisProp = this.#getProps().axis; + return axisProp == null + ? isScaleBand(this.ctx.yScale) || isScaleTime(this.ctx.yScale) || this.ctx.valueAxis === 'x' + ? 'y' + : 'x' + : axisProp; + }); + + /** Resolve radius for a data item using the chart's rScale */ + getPointRadius(d: any): number | undefined { + const rProp = this.#getProps().r; + if (!rProp || !d) return undefined; + if (rProp === true) { + return this.ctx.config.r ? this.ctx.rGet(d) : undefined; + } + const value = accessor(rProp)(d); + return value != null ? this.ctx.rScale(value) : undefined; + } + + lines = $derived.by(() => { + let tmpLines: HighlightLineSegment[] = []; + if (!this.highlightData) return tmpLines; + const axis = this.axis; + if (axis === 'x' || axis === 'both') { + if (Array.isArray(this.xCoord)) { + tmpLines = [ + ...tmpLines, + ...this.xCoord.filter(notNull).map((xItem) => ({ + x1: xItem + this.xOffset, + y1: min(this.ctx.yRange) as unknown as number, + x2: xItem + this.xOffset, + y2: max(this.ctx.yRange) as unknown as number, + })), + ]; + } else if (this.xCoord != null) { + tmpLines = [ + ...tmpLines, + { + x1: this.xCoord + this.xOffset, + y1: min(this.ctx.yRange) as unknown as number, + x2: this.xCoord + this.xOffset, + y2: max(this.ctx.yRange) as unknown as number, + }, + ]; + } + } + + if (axis === 'y' || axis === 'both') { + if (Array.isArray(this.yCoord)) { + tmpLines = [ + ...tmpLines, + ...this.yCoord.filter(notNull).map((yItem) => ({ + x1: min(this.ctx.xRange) as unknown as number, + y1: yItem + this.yOffset, + x2: max(this.ctx.xRange) as unknown as number, + y2: yItem + this.yOffset, + })), + ]; + } else if (this.yCoord != null) { + tmpLines = [ + ...tmpLines, + { + x1: min(this.ctx.xRange) as unknown as number, + y1: this.yCoord + this.yOffset, + x2: max(this.ctx.xRange) as unknown as number, + y2: this.yCoord + this.yOffset, + }, + ]; + } + } + + if (this.ctx.radial) { + tmpLines = tmpLines.map((l) => { + const [x1, y1] = pointRadial(l.x1, l.y1); + const [x2, y2] = pointRadial(l.x2, l.y2); + return { ...l, x1, y1, x2, y2 }; + }); + } + + return tmpLines; + }); + + area = $derived.by(() => { + const tmpArea: HighlightArea = { x: 0, y: 0, width: 0, height: 0 }; + if (!this.highlightData) return tmpArea; + const axis = this.axis; + + if (axis === 'x' || axis === 'both') { + if (Array.isArray(this.xCoord)) { + tmpArea.x = min(this.xCoord)!; + tmpArea.width = (max(this.xCoord)! - min(this.xCoord)!) as number; + } else if (isScaleBand(this.ctx.xScale)) { + tmpArea.x = + (this.xCoord as number) - (this.ctx.xScale.padding() * this.ctx.xScale.step()) / 2; + tmpArea.width = this.ctx.xScale.step(); + } else if (this.ctx.xInterval) { + const start = this.ctx.xInterval.floor(this.xValue); + const end = this.ctx.xInterval.offset(start); + const xStart = this.ctx.xScale(start); + const xEnd = this.ctx.xScale(end); + tmpArea.x = Math.min(xStart, xEnd); + tmpArea.width = Math.abs(xEnd - xStart); + } else { + const index = this.ctx.flatData.findIndex( + (d) => Number(this.x(d)) === Number(this.x(this.highlightData)) + ); + const isLastPoint = index + 1 === this.ctx.flatData.length; + const nextDataPoint = isLastPoint + ? max(this.ctx.xDomain) + : this.x(this.ctx.flatData[index + 1]); + tmpArea.x = this.xCoord as number; + tmpArea.width = (this.ctx.xScale(nextDataPoint) ?? 0) - ((this.xCoord as number) ?? 0); + } + + if (axis === 'x') { + tmpArea.y = min(this.ctx.yRange) as unknown as number; + tmpArea.height = (max(this.ctx.yRange) - min(this.ctx.yRange)) as unknown as number; + } + } + + if (axis === 'y' || axis === 'both') { + if (Array.isArray(this.yCoord)) { + tmpArea.y = min(this.yCoord)!; + tmpArea.height = (max(this.yCoord)! - min(this.yCoord)!) as number; + } else if (isScaleBand(this.ctx.yScale)) { + tmpArea.y = + (this.yCoord as number) - (this.ctx.yScale.padding() * this.ctx.yScale.step()) / 2; + tmpArea.height = this.ctx.yScale.step(); + } else if (this.ctx.yInterval) { + const start = this.ctx.yInterval.floor(this.yValue); + const end = this.ctx.yInterval.offset(start); + const yStart = this.ctx.yScale(start); + const yEnd = this.ctx.yScale(end); + tmpArea.y = Math.min(yStart, yEnd); + tmpArea.height = Math.abs(yEnd - yStart); + } else { + const index = this.ctx.flatData.findIndex( + (d) => Number(this.y(d)) === Number(this.y(this.highlightData)) + ); + const isLastPoint = index + 1 === this.ctx.flatData.length; + const nextDataPoint = isLastPoint + ? max(this.ctx.yDomain) + : this.y(this.ctx.flatData[index + 1]); + tmpArea.y = this.yCoord as number; + tmpArea.height = (this.ctx.yScale(nextDataPoint) ?? 0) - ((this.yCoord as number) ?? 0); + } + + if (axis === 'y') { + tmpArea.width = max(this.ctx.xRange) as unknown as number; + } + } + return tmpArea; + }); + + points = $derived.by(() => { + let tmpPoints: HighlightPoint[] = []; + if (!this.highlightData) return tmpPoints; + const props = this.#getProps(); + + if (props.data === undefined && this.ctx.tooltip.series.length > 0) { + tmpPoints = this.ctx.tooltip.series + .flatMap((seriesInfo) => { + if (!seriesInfo.visible) return []; + + let pointX: number; + let pointY: number; + let dataX: any; + let dataY: any; + + if (this.ctx.series.isStacked) { + const matchingData = this.ctx.flatData.find((d) => this.x(d) === this.xValue); + const stackValue = matchingData + ? this.ctx.series.getStackValue(seriesInfo.key, matchingData) + : null; + const stackedY1 = stackValue + ? this.ctx.series.stackLayout === 'stackDiverging' && stackValue[1] <= 0 + ? stackValue[0] + : stackValue[1] + : 0; + + if (this.ctx.valueAxis === 'x') { + pointX = this.ctx.xScale(stackedY1) + this.xOffset; + pointY = (this.yCoordScalar as number) + this.yOffset; + dataX = stackedY1; + dataY = this.yValue; + } else { + pointX = (this.xCoordScalar as number) + this.xOffset; + pointY = this.ctx.yScale(stackedY1) + this.yOffset; + dataX = this.xValue; + dataY = stackedY1; + } + } else { + const seriesValue = seriesInfo.value; + + if (seriesValue == null) { + return []; + } + + if (Array.isArray(seriesValue)) { + if (this.ctx.valueAxis === 'x') { + return seriesValue.map((sv) => ({ + x: this.ctx.xScale(sv) + this.xOffset, + y: (this.yCoordScalar as number) + this.yOffset, + fill: seriesInfo.color ?? '', + data: { x: sv, y: this.yValue }, + seriesKey: seriesInfo.key, + })); + } else { + return seriesValue.map((sv) => ({ + x: (this.xCoordScalar as number) + this.xOffset, + y: this.ctx.yScale(sv) + this.yOffset, + fill: seriesInfo.color ?? '', + data: { x: this.xValue, y: sv }, + seriesKey: seriesInfo.key, + })); + } + } + + if (this.ctx.valueAxis === 'x') { + pointX = this.ctx.xScale(seriesValue) + this.xOffset; + pointY = (this.yCoordScalar as number) + this.yOffset; + dataX = seriesValue; + dataY = this.yValue; + } else { + pointX = (this.xCoordScalar as number) + this.xOffset; + pointY = this.ctx.yScale(seriesValue) + this.yOffset; + dataX = this.xValue; + dataY = seriesValue; + } + } + + return { + x: pointX, + y: pointY, + fill: seriesInfo.color ?? '', + data: { x: dataX, y: dataY }, + seriesKey: seriesInfo.key, + }; + }) + .filter(notNull); + } else if (Array.isArray(this.xCoord)) { + if (Array.isArray(this.highlightData)) { + const highlightSeriesPoint = this.highlightData as SeriesPoint; + if (Array.isArray(this.ctx.data)) { + const seriesPointsData = (this.ctx.data as any[]) + .map((series: Series) => ({ + series, + point: series.find((d) => this.y(d) === this.y(highlightSeriesPoint))!, + })) + .filter((d) => d.point); + + tmpPoints = seriesPointsData + .map((seriesPoint) => { + const fill = this.ctx.config.c ? this.ctx.cGet(seriesPoint.series) : null; + return { + x: this.ctx.xScale(seriesPoint.point[1]) + this.xOffset, + y: (this.yCoordScalar as number) + this.yOffset, + fill, + data: { x: seriesPoint.point[1], y: this.yValue }, + seriesKey: undefined, + }; + }) + .filter(notNull) as HighlightPoint[]; + } + } else { + tmpPoints = this.xCoord + .map((xItem, i) => { + if (xItem == null) return null; + // @ts-expect-error - TODO: fix type + const _key = this.ctx.config.x?.[i]; + + const fill = this.ctx.config.c + ? this.ctx.cGet({ ...this.highlightData, $key: _key }) + : null; + + return { + x: xItem + this.xOffset, + y: (this.yCoordScalar as number) + this.yOffset, + fill, + data: { x: this.xValue, y: this.yValue }, + seriesKey: _key, + }; + }) + .filter(notNull) as HighlightPoint[]; + } + } else if (Array.isArray(this.yCoord)) { + if (Array.isArray(this.highlightData)) { + const highlightSeriesPoint = this.highlightData as SeriesPoint; + if (Array.isArray(this.ctx.data)) { + const seriesPointsData = (this.ctx.data as any[]) + .map((series: Series) => ({ + series, + point: series.find((d) => this.x(d) === this.x(highlightSeriesPoint))!, + })) + .filter((d) => d.point); + + tmpPoints = seriesPointsData + .map((seriesPoint) => { + const fill = this.ctx.config.c ? this.ctx.cGet(seriesPoint.series) : null; + return { + x: (this.xCoord as number) + this.xOffset, + y: this.ctx.yScale(seriesPoint.point[1]) + this.yOffset, + fill, + data: { x: this.xValue, y: seriesPoint.point[1] }, + seriesKey: undefined, + }; + }) + .filter(notNull) as HighlightPoint[]; + } + } else { + tmpPoints = this.yCoord + .map((yItem, i) => { + if (yItem == null) return null; + // @ts-expect-error - TODO: fix type + const _key = this.ctx.config.y[i]; + + const fill = this.ctx.config.c + ? this.ctx.cGet({ ...this.highlightData, $key: _key }) + : null; + + return { + x: (this.xCoord as number) + this.xOffset, + y: yItem + this.yOffset, + fill, + data: { x: this.xValue, y: this.yValue }, + seriesKey: _key, + }; + }) + .filter(notNull) as HighlightPoint[]; + } + } else if (this.xCoord != null && this.yCoord != null) { + const fill = this.ctx.config.c ? this.ctx.cGet(this.highlightData) : null; + tmpPoints = [ + { + x: (this.xCoord as number) + this.xOffset, + y: (this.yCoord as number) + this.yOffset, + fill: fill as string, + data: { x: this.xValue, y: this.yValue }, + seriesKey: undefined, + }, + ]; + } else { + tmpPoints = []; + } + + if (this.ctx.radial) { + tmpPoints = tmpPoints.map((p) => { + const [x, y] = pointRadial(p.x, p.y); + return { ...p, x, y }; + }); + } + + if (props.r) { + const pointR = this.getPointRadius(this.highlightData); + if (pointR != null) { + tmpPoints = tmpPoints.map((p) => ({ ...p, r: pointR })); + } + } + + return tmpPoints; + }); +} diff --git a/packages/layerchart/src/lib/components/Highlight/Highlight.svelte b/packages/layerchart/src/lib/components/Highlight/Highlight.svelte new file mode 100644 index 000000000..a0570413e --- /dev/null +++ b/packages/layerchart/src/lib/components/Highlight/Highlight.svelte @@ -0,0 +1,23 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Highlight/Highlight.svg.svelte b/packages/layerchart/src/lib/components/Highlight/Highlight.svg.svelte new file mode 100644 index 000000000..4f57150ef --- /dev/null +++ b/packages/layerchart/src/lib/components/Highlight/Highlight.svg.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/RectClipPath.svelte b/packages/layerchart/src/lib/components/RectClipPath.svelte deleted file mode 100644 index bb5cbb4b2..000000000 --- a/packages/layerchart/src/lib/components/RectClipPath.svelte +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - {#snippet children({ url })} - {@render childrenProp?.({ id, url })} - {/snippet} - diff --git a/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.base.svelte b/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.base.svelte new file mode 100644 index 000000000..91e6b39b2 --- /dev/null +++ b/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.base.svelte @@ -0,0 +1,36 @@ + + + + + + {#snippet children({ url }: { url: string })} + {@render childrenProp?.({ id, url })} + {/snippet} + diff --git a/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.canvas.svelte b/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.canvas.svelte new file mode 100644 index 000000000..678ba49a6 --- /dev/null +++ b/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.canvas.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.html.svelte b/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.html.svelte new file mode 100644 index 000000000..5448ba7bd --- /dev/null +++ b/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.html.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.shared.svelte.ts b/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.shared.svelte.ts new file mode 100644 index 000000000..9c95db231 --- /dev/null +++ b/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.shared.svelte.ts @@ -0,0 +1,32 @@ +import type { CommonEvents, Without } from '$lib/utils/types.js'; +import type { SVGAttributes } from 'svelte/elements'; +import type { Snippet } from 'svelte'; +import type { RectPropsWithoutHTML } from '../Rect/Rect.shared.svelte.js'; +import type { MotionProp } from '$lib/utils/motion.svelte.js'; + +export type BaseRectClipPathPropsWithoutHTML = { + /** A unique id for the clipPath. */ + id?: string; + /** The x position of the clipPath. @default 0 */ + x?: number; + /** The y position of the clipPath. @default 0 */ + y?: number; + /** The width of the clipPath. @required */ + width: number; + /** The height of the clipPath. @required */ + height: number; + /** Whether to disable clipping (show all). @default false */ + disabled?: boolean; + /** Invert the clip — content renders *outside* the rect. @default false */ + invert?: boolean; + /** The default children snippet which provides the id and url for the clipPath. */ + children?: Snippet<[{ id: string; url: string }]>; + motion?: MotionProp<'x' | 'y' | 'width' | 'height'>; +}; + +export type RectClipPathPropsWithoutHTML = BaseRectClipPathPropsWithoutHTML & + Without; + +export type RectClipPathProps = RectClipPathPropsWithoutHTML & + Without, RectClipPathPropsWithoutHTML> & + CommonEvents; diff --git a/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.svelte b/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.svelte new file mode 100644 index 000000000..88080fedf --- /dev/null +++ b/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.svelte @@ -0,0 +1,23 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.svg.svelte b/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.svg.svelte new file mode 100644 index 000000000..2625022e5 --- /dev/null +++ b/packages/layerchart/src/lib/components/RectClipPath/RectClipPath.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/AreaChart.svelte b/packages/layerchart/src/lib/components/charts/AreaChart.svelte index 07783edbf..53bc9ec21 100644 --- a/packages/layerchart/src/lib/components/charts/AreaChart.svelte +++ b/packages/layerchart/src/lib/components/charts/AreaChart.svelte @@ -1,6 +1,6 @@ - - - -{#if track} - -{/if} - - { - ontouchmove?.(e); - if (tooltip) { - // Prevent touch to not interfere with pointer when using tooltip - e.preventDefault(); - } - }} -/> - -{@render children?.({ - centroid: trackArcCentroid, - boundingBox, - value: motionEndAngle.current, - startAngle, - endAngle: arcEndAngle, - innerRadius, - outerRadius, - getTrackTextProps: getTrackTextProps, - getArcTextProps: getArcTextProps, -})} diff --git a/packages/layerchart/src/lib/components/Arc/Arc.base.svelte b/packages/layerchart/src/lib/components/Arc/Arc.base.svelte new file mode 100644 index 000000000..8fd7d1d0b --- /dev/null +++ b/packages/layerchart/src/lib/components/Arc/Arc.base.svelte @@ -0,0 +1,109 @@ + + + + +{#if track} + c.trackRef, (v: SVGPathElement | undefined) => (c.trackRef = v)} + {...extractLayerProps(track, 'lc-arc-track')} + /> +{/if} + + { + ontouchmove?.(e); + if (tooltip) { + e.preventDefault(); + } + }) satisfies TouchEventHandler} +/> + +{@render children?.({ + centroid: c.trackArcCentroid, + boundingBox: c.boundingBox, + value: c.motionEndAngleValue, + startAngle: c.startAngle, + endAngle: c.arcEndAngle, + innerRadius: c.innerRadius, + outerRadius: c.outerRadius, + getTrackTextProps: c.getTrackTextProps, + getArcTextProps: c.getArcTextProps, +})} diff --git a/packages/layerchart/src/lib/components/Arc/Arc.canvas.svelte b/packages/layerchart/src/lib/components/Arc/Arc.canvas.svelte new file mode 100644 index 000000000..4b9cc6273 --- /dev/null +++ b/packages/layerchart/src/lib/components/Arc/Arc.canvas.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Arc/Arc.shared.svelte.ts b/packages/layerchart/src/lib/components/Arc/Arc.shared.svelte.ts new file mode 100644 index 000000000..5d64fdeb6 --- /dev/null +++ b/packages/layerchart/src/lib/components/Arc/Arc.shared.svelte.ts @@ -0,0 +1,252 @@ +import type { ComponentProps, Snippet } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; + +import { arc as d3arc } from 'd3-shape'; +import { scaleLinear } from 'd3-scale'; +import { max } from 'd3-array'; + +import type Path from '../Path/Path.svelte'; +import type { PathPropsWithoutHTML } from '../Path/Path.shared.svelte.js'; +import { createMotion, type MotionProp } from '$lib/utils/motion.svelte.js'; +import type { CommonStyleProps, Without } from '$lib/utils/types.js'; +import { degreesToRadians } from '$lib/utils/math.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import { + createArcTextProps, + type ArcTextOptions, + type ArcTextPosition, + type GetArcTextProps, +} from '$lib/utils/arcText.svelte.js'; + +export type ArcPropsWithoutHTML = { + value?: number; + initialValue?: number; + /** Domain [min,max] in degrees @default [0, 100] */ + domain?: [number, number]; + /** Range [min,max] in degrees. See also startAngle/endAngle @default [0, 360] */ + range?: [number, number]; + /** Start angle in radians */ + startAngle?: number; + /** End angle in radians */ + endAngle?: number; + /** + * Define innerRadius. Defaults to yRange min + * • value >= 1: discrete value + * • value < 1: percent of `outerRadius` + * • value < 0: offset of `outerRadius` + */ + innerRadius?: number; + /** + * Define outerRadius. Defaults to smallest width (xRange) or height (yRange) dimension (/2) + */ + outerRadius?: number; + /** @default 0 */ + cornerRadius?: number; + /** @default 0 */ + padAngle?: number; + trackStartAngle?: number; + trackEndAngle?: number; + trackInnerRadius?: number; + trackOuterRadius?: number; + /** @default 0 */ + trackCornerRadius?: number; + /** @default 0 */ + trackPadAngle?: number; + /** @default 0 */ + offset?: number; + /** Setup pointer events to show tooltip for related data. **Must set `data` prop as well** */ + tooltip?: boolean; + /** Data to set when showing tooltip */ + data?: any; + /** + * Pass true to enable the track with default props, or pass an object + * of props to enable the track. + */ + track?: boolean | Partial>; + /** A reference to the track element @bindable */ + trackRef?: SVGPathElement; + /** A reference to the arc element @bindable */ + ref?: SVGPathElement; + children?: Snippet< + [ + { + centroid: [number, number]; + boundingBox: DOMRect; + value: number; + startAngle: number; + endAngle: number; + innerRadius: number; + outerRadius: number; + getTrackTextProps: GetArcTextProps; + getArcTextProps: GetArcTextProps; + }, + ] + >; + motion?: MotionProp; +} & CommonStyleProps; + +export type ArcProps = ArcPropsWithoutHTML & + Without, ArcPropsWithoutHTML & PathPropsWithoutHTML>; + +function getOuterRadius(outerRadius: number | undefined, chartRadius: number) { + if (!outerRadius) return chartRadius; + if (outerRadius > 1) return outerRadius; + if (outerRadius > 0) return chartRadius * outerRadius; + if (outerRadius < 0) return chartRadius + outerRadius; + return outerRadius; +} + +/** + * Reactive state shared by every per-layer Arc variant. Derives the d3-arc + * generators (`arc`, `trackArc`), resolved radii/angles, and the centroid + * used for snippet props. + */ +export class ArcState { + #getProps: () => ArcProps = () => ({}) as ArcProps; + ctx: ChartState = getChartContext(); + + trackRef = $state(); + + #motionEndAngle!: ReturnType>; + + constructor(getProps: () => ArcProps) { + this.#getProps = getProps; + const initial = getProps(); + this.#motionEndAngle = createMotion( + initial.initialValue ?? 0, + () => getProps().value ?? 0, + initial.motion + ); + } + + get motionEndAngleValue() { + return this.#motionEndAngle.current; + } + + range = $derived(this.#getProps().range ?? ([0, 360] as [number, number])); + domain = $derived(this.#getProps().domain ?? ([0, 100] as [number, number])); + + endAngle = $derived.by(() => { + const props = this.#getProps(); + return ( + props.endAngle ?? + degreesToRadians( + (this.ctx.config.xRange ? max(this.ctx.config.xRange as number[]) : max(this.range))! + ) + ); + }); + + scale = $derived(scaleLinear().domain(this.domain).range(this.range)); + + chartRadius = $derived((Math.min(this.ctx.width, this.ctx.height) ?? 0) / 2); + + outerRadius = $derived(getOuterRadius(this.#getProps().outerRadius, this.chartRadius)); + trackOuterRadius = $derived.by(() => { + const trackOuterRadiusProp = this.#getProps().trackOuterRadius; + return trackOuterRadiusProp + ? getOuterRadius(trackOuterRadiusProp, this.chartRadius) + : this.outerRadius; + }); + + #getInnerRadius(innerRadius: number | undefined, outerRadius: number) { + if (innerRadius == null) return Math.min(...this.ctx.yRange); + if (innerRadius > 1) return innerRadius; + if (innerRadius > 0) return outerRadius * innerRadius; + if (innerRadius < 0) return outerRadius + innerRadius; + return innerRadius; + } + + innerRadius = $derived( + this.#getInnerRadius(this.#getProps().innerRadius, this.outerRadius) + ); + trackInnerRadius = $derived.by(() => { + const trackInnerRadiusProp = this.#getProps().trackInnerRadius; + return trackInnerRadiusProp + ? this.#getInnerRadius(trackInnerRadiusProp, this.trackOuterRadius) + : this.innerRadius; + }); + + startAngle = $derived(this.#getProps().startAngle ?? degreesToRadians(this.range[0])); + trackStartAngle = $derived( + this.#getProps().trackStartAngle ?? + this.#getProps().startAngle ?? + degreesToRadians(this.range[0]) + ); + trackEndAngle = $derived( + this.#getProps().trackEndAngle ?? + this.#getProps().endAngle ?? + degreesToRadians(this.range[1]) + ); + trackCornerRadius = $derived( + this.#getProps().trackCornerRadius ?? this.#getProps().cornerRadius ?? 0 + ); + trackPadAngle = $derived(this.#getProps().trackPadAngle ?? this.#getProps().padAngle ?? 0); + + arcEndAngle = $derived( + this.#getProps().endAngle ?? degreesToRadians(this.scale(this.motionEndAngleValue)) + ); + + arc = $derived.by(() => { + const props = this.#getProps(); + return d3arc() + .innerRadius(this.innerRadius) + .outerRadius(this.outerRadius) + .startAngle(this.startAngle) + .endAngle(this.arcEndAngle) + .cornerRadius(props.cornerRadius ?? 0) + .padAngle(props.padAngle ?? 0); + }); + + trackArc = $derived( + d3arc() + .innerRadius(this.trackInnerRadius) + .outerRadius(this.trackOuterRadius) + .startAngle(this.trackStartAngle) + .endAngle(this.trackEndAngle) + .cornerRadius(this.trackCornerRadius) + .padAngle(this.trackPadAngle) + ); + + angle = $derived(((this.startAngle ?? 0) + (this.endAngle ?? 0)) / 2); + xOffset = $derived(Math.sin(this.angle) * (this.#getProps().offset ?? 0)); + yOffset = $derived(-Math.cos(this.angle) * (this.#getProps().offset ?? 0)); + + trackArcCentroid = $derived.by<[number, number]>(() => { + // @ts-expect-error - this is fine. + const centroid = this.trackArc.centroid() as [number, number]; + return [centroid[0] + this.xOffset, centroid[1] + this.yOffset]; + }); + + boundingBox = $derived(this.trackRef ? this.trackRef.getBBox() : ({} as DOMRect)); + + getTrackTextProps = (position: ArcTextPosition, opts: ArcTextOptions = {}) => { + return createArcTextProps( + { + startAngle: () => this.trackStartAngle, + endAngle: () => this.trackEndAngle, + outerRadius: () => this.trackOuterRadius + (opts.outerPadding ?? 0), + innerRadius: () => this.trackInnerRadius - (opts.innerPadding ?? 0), + cornerRadius: () => this.trackCornerRadius, + centroid: () => this.trackArcCentroid, + }, + opts, + position + ).current; + }; + + getArcTextProps = (position: ArcTextPosition, opts: ArcTextOptions = {}) => { + return createArcTextProps( + { + startAngle: () => this.startAngle, + endAngle: () => this.arcEndAngle, + outerRadius: () => this.outerRadius + (opts.outerPadding ?? 0), + innerRadius: () => this.innerRadius - (opts.innerPadding ?? 0), + cornerRadius: () => this.#getProps().cornerRadius ?? 0, + centroid: () => this.trackArcCentroid, + }, + opts, + position + ).current; + }; +} diff --git a/packages/layerchart/src/lib/components/Arc/Arc.svelte b/packages/layerchart/src/lib/components/Arc/Arc.svelte new file mode 100644 index 000000000..e05f9ac1c --- /dev/null +++ b/packages/layerchart/src/lib/components/Arc/Arc.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Arc.svelte.test.ts b/packages/layerchart/src/lib/components/Arc/Arc.svelte.test.ts similarity index 99% rename from packages/layerchart/src/lib/components/Arc.svelte.test.ts rename to packages/layerchart/src/lib/components/Arc/Arc.svelte.test.ts index 17060ebbe..5457b3fc5 100644 --- a/packages/layerchart/src/lib/components/Arc.svelte.test.ts +++ b/packages/layerchart/src/lib/components/Arc/Arc.svelte.test.ts @@ -3,9 +3,9 @@ import { render } from 'vitest-browser-svelte'; import { page, type Locator } from 'vitest/browser'; import type { ComponentProps } from 'svelte'; -import TestHarness, { componentTestId } from './tests/TestHarness.svelte'; +import TestHarness, { componentTestId } from '../tests/TestHarness.svelte'; import Arc from './Arc.svelte'; -import Text from './Text/Text.svelte'; +import Text from '../Text/Text.svelte'; import type { ChartState } from '$lib/states/chart.svelte.js'; const defaultProps: Partial> = { diff --git a/packages/layerchart/src/lib/components/Arc/Arc.svg.svelte b/packages/layerchart/src/lib/components/Arc/Arc.svg.svelte new file mode 100644 index 000000000..7b3e5e11a --- /dev/null +++ b/packages/layerchart/src/lib/components/Arc/Arc.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/ArcLabel.svelte.test.ts b/packages/layerchart/src/lib/components/ArcLabel.svelte.test.ts index 2b95e79b6..cef34afa7 100644 --- a/packages/layerchart/src/lib/components/ArcLabel.svelte.test.ts +++ b/packages/layerchart/src/lib/components/ArcLabel.svelte.test.ts @@ -4,7 +4,7 @@ import { page } from 'vitest/browser'; import type { ComponentProps } from 'svelte'; import TestHarness, { componentTestId } from './tests/TestHarness.svelte'; -import Arc from './Arc.svelte'; +import Arc from './Arc/Arc.svelte'; import ArcLabel from './ArcLabel.svelte'; const defaultArcProps: Partial> = { diff --git a/packages/layerchart/src/lib/components/Area.svelte b/packages/layerchart/src/lib/components/Area.svelte deleted file mode 100644 index f44883ad4..000000000 --- a/packages/layerchart/src/lib/components/Area.svelte +++ /dev/null @@ -1,251 +0,0 @@ - - - - -{#if line} - -{/if} - - diff --git a/packages/layerchart/src/lib/components/Area/Area.base.svelte b/packages/layerchart/src/lib/components/Area/Area.base.svelte new file mode 100644 index 000000000..a460a93fa --- /dev/null +++ b/packages/layerchart/src/lib/components/Area/Area.base.svelte @@ -0,0 +1,73 @@ + + + + +{#if line} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/Area/Area.canvas.svelte b/packages/layerchart/src/lib/components/Area/Area.canvas.svelte new file mode 100644 index 000000000..9f90e7522 --- /dev/null +++ b/packages/layerchart/src/lib/components/Area/Area.canvas.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Area/Area.shared.svelte.ts b/packages/layerchart/src/lib/components/Area/Area.shared.svelte.ts new file mode 100644 index 000000000..66d352069 --- /dev/null +++ b/packages/layerchart/src/lib/components/Area/Area.shared.svelte.ts @@ -0,0 +1,218 @@ +import type { ComponentProps } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import { + type Area as D3Area, + area as d3Area, + areaRadial, + type CurveFactory, +} from 'd3-shape'; +import { min } from 'd3-array'; +import { interpolatePath } from 'd3-interpolate-path'; + +import type { CommonStyleProps, Without } from '$lib/utils/types.js'; +import { accessor, type Accessor } from '$lib/utils/common.js'; +import { isScaleBand } from '$lib/utils/scales.svelte.js'; +import { flattenPathData } from '$lib/utils/path.js'; +import { + createMotion, + extractTweenConfig, + type MotionProp, + type ResolvedMotion, +} from '$lib/utils/motion.svelte.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import type Spline from '../Spline/Spline.svelte'; +import type { PathProps } from '../Path/Path.shared.svelte.js'; + +export type AreaPropsWithoutHTML = { + /** Override data instead of using context */ + data?: any; + /** Pass `` explicitly instead of calculating from data / context */ + pathData?: string | null; + /** Override x accessor */ + x?: Accessor; + /** Override y0 accessor. Defaults to max($yRange) */ + y0?: Accessor; + /** Override y1 accessor. Defaults to y accessor */ + y1?: Accessor; + /** Series key to use for accessor. */ + seriesKey?: string; + /** Whether to tween the interpolated path data using d3-interpolate-path */ + motion?: MotionProp; + curve?: CurveFactory; + defined?: Parameters['defined']>[0]; + /** Enable showing line @default false */ + line?: boolean | Partial>; +} & Omit; + +export type AreaProps = AreaPropsWithoutHTML & + Without, AreaPropsWithoutHTML>; + +/** + * Reactive state shared by every per-layer Area variant. Holds the + * computed `d` (path data), tween state, accessors, and `lineYAccessor`. + */ +export class AreaState { + #getProps: () => AreaProps = () => ({}) as AreaProps; + ctx: ChartState = getChartContext(); + + #tweenState!: ReturnType>; + + constructor(getProps: () => AreaProps) { + this.#getProps = getProps; + + const initial = getProps(); + this.ctx.registerComponent({ + name: 'Area', + kind: 'composite-mark', + markInfo: () => { + const p = getProps(); + return { + data: p.data, + x: p.x, + y: p.y1 ?? p.y0, + seriesKey: p.seriesKey, + color: p.fill as string | undefined, + }; + }, + }); + + const extractedTween = extractTweenConfig(initial.motion); + const tweenOptions: ResolvedMotion | undefined = extractedTween + ? { + type: extractedTween.type, + options: { interpolate: interpolatePath, ...extractedTween.options }, + } + : undefined; + + this.#tweenState = createMotion(this.#defaultPathData(tweenOptions), () => this.d, tweenOptions); + } + + series = $derived( + this.ctx.series.series.find((s) => s.key === this.#getProps().seriesKey) + ); + seriesData = $derived(this.series?.data); + seriesAccessor = $derived( + this.series?.value ?? (this.series?.data ? undefined : this.series?.key) + ); + + stackAccessors = $derived.by(() => { + const seriesKey = this.#getProps().seriesKey; + return seriesKey && this.ctx.series.isStacked + ? this.ctx.series.getStackAccessors(seriesKey) + : null; + }); + + xAccessor = $derived.by(() => { + const x = this.#getProps().x; + return x ? accessor(x) : this.ctx.x; + }); + + y0Accessor = $derived.by(() => { + const props = this.#getProps(); + if (props.y0) return accessor(props.y0); + if (this.stackAccessors) return this.stackAccessors.y0; + if (Array.isArray(this.seriesAccessor)) return accessor(this.seriesAccessor[0]); + if (Array.isArray(this.ctx.config.y) && this.ctx.config.y[0] === 0) { + return (d: any) => this.ctx.y(d)[0]; + } + if (this.ctx.props.yBaseline != null) return () => this.ctx.props.yBaseline as number; + return () => min(this.ctx.yScale.domain()) as number; + }); + + y1Accessor = $derived.by(() => { + const props = this.#getProps(); + if (props.y1) return accessor(props.y1); + if (this.stackAccessors) return this.stackAccessors.y1; + if (Array.isArray(this.seriesAccessor)) return accessor(this.seriesAccessor[1]); + if (this.seriesAccessor) return accessor(this.seriesAccessor as Accessor); + if (Array.isArray(this.ctx.config.y) && this.ctx.config.y[1] === 1) { + return (d: any) => this.ctx.y(d)[1]; + } + return this.ctx.y; + }); + + resolvedData = $derived(this.#getProps().data ?? this.seriesData ?? this.ctx.data); + + xOffset = $derived(isScaleBand(this.ctx.xScale) ? this.ctx.xScale.bandwidth() / 2 : 0); + yOffset = $derived(isScaleBand(this.ctx.yScale) ? this.ctx.yScale.bandwidth() / 2 : 0); + + #defaultPathData(tweenOptions: ResolvedMotion | undefined): string { + const props = this.#getProps(); + if (!tweenOptions) return ''; + if (props.pathData) { + return flattenPathData(props.pathData, Math.min(this.ctx.yScale(0), this.ctx.yRange[0])); + } + if (this.ctx.config.x) { + const path = this.ctx.radial + ? areaRadial() + .angle((d) => this.ctx.xScale(this.xAccessor(d))) + .innerRadius(() => Math.min(this.ctx.yScale(0), this.ctx.yRange[0])) + .outerRadius(() => Math.min(this.ctx.yScale(0), this.ctx.yRange[0])) + : d3Area() + .x((d) => this.ctx.xScale(this.xAccessor(d)) + this.xOffset) + .y0(() => Math.min(this.ctx.yScale(0), this.ctx.yRange[0])) + .y1(() => Math.min(this.ctx.yScale(0), this.ctx.yRange[0])); + + path.defined( + props.defined ?? ((d) => this.xAccessor(d) != null && this.y1Accessor(d) != null) + ); + if (props.curve) path.curve(props.curve); + + return path(this.resolvedData) ?? ''; + } + return ''; + } + + d = $derived.by(() => { + const props = this.#getProps(); + const _path = this.ctx.radial + ? areaRadial() + .angle((d) => this.ctx.xScale(this.xAccessor(d))) + .innerRadius((d) => this.ctx.yScale(this.y0Accessor(d))) + .outerRadius((d) => this.ctx.yScale(this.y1Accessor(d))) + : d3Area() + .x((d) => this.ctx.xScale(this.xAccessor(d)) + this.xOffset) + .y0((d) => this.ctx.yScale(this.y0Accessor(d)) + this.yOffset) + .y1((d) => this.ctx.yScale(this.y1Accessor(d)) + this.yOffset); + + _path.defined( + props.defined ?? ((d: any) => this.xAccessor(d) != null && this.y1Accessor(d) != null) + ); + if (props.curve) _path.curve(props.curve); + + return props.pathData ?? _path(this.resolvedData) ?? ''; + }); + + get tweenedPath() { + return this.#tweenState.current; + } + + lineYAccessor = $derived.by(() => { + const props = this.#getProps(); + if (this.stackAccessors && this.ctx.series.stackLayout === 'stackDiverging') { + const firstPoint = this.resolvedData?.[0]; + if (firstPoint) { + const val = this.stackAccessors.value(firstPoint); + if (val && val[1] <= 0) return this.y0Accessor; + } + } + return props.y1 || + this.stackAccessors || + Array.isArray(this.seriesAccessor) || + this.seriesAccessor + ? this.y1Accessor + : undefined; + }); + + pathOpacity = $derived.by(() => { + if ( + this.series?.key == null || + this.ctx.series.visibleSeries.length <= 1 || + this.ctx.series.isHighlighted(this.series.key, true) + ) { + return 1; + } + return 0.1; + }); +} diff --git a/packages/layerchart/src/lib/components/Area/Area.svelte b/packages/layerchart/src/lib/components/Area/Area.svelte new file mode 100644 index 000000000..c2c0b4e0b --- /dev/null +++ b/packages/layerchart/src/lib/components/Area/Area.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Area/Area.svg.svelte b/packages/layerchart/src/lib/components/Area/Area.svg.svelte new file mode 100644 index 000000000..3e846ba1a --- /dev/null +++ b/packages/layerchart/src/lib/components/Area/Area.svg.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Bar.svelte b/packages/layerchart/src/lib/components/Bar.svelte index d73b7d31e..552c6185f 100644 --- a/packages/layerchart/src/lib/components/Bar.svelte +++ b/packages/layerchart/src/lib/components/Bar.svelte @@ -112,7 +112,7 @@ import type { CommonEvents, CommonStyleProps, Without } from '$lib/utils/types.js'; import { extractLayerProps } from '$lib/utils/attributes.js'; import { type MotionProp } from '$lib/utils/motion.svelte.js'; - import Arc from './Arc.svelte'; + import Arc from './Arc/Arc.svelte'; const ctx = getChartContext(); diff --git a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts index 4db63ab71..1b5d3dd10 100644 --- a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts +++ b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts @@ -4,8 +4,8 @@ import type { ChartState } from '$lib/contexts/chart.js'; import type { AnyScale } from '$lib/utils/scales.svelte.js'; import type Axis from '../Axis/Axis.svelte'; -import type Area from '../Area.svelte'; -import type Arc from '../Arc.svelte'; +import type Area from '../Area/Area.svelte'; +import type Arc from '../Arc/Arc.svelte'; import type Bars from '../Bars.svelte'; import type BrushContext from '../BrushContext.svelte'; import type Canvas from '../layers/Canvas.svelte'; @@ -18,7 +18,7 @@ import type Line from '../Line/Line.svelte'; import type Pie from '../Pie.svelte'; import type Points from '../Points.svelte'; import type Rule from '../Rule/Rule.svelte'; -import type Spline from '../Spline.svelte'; +import type Spline from '../Spline/Spline.svelte'; import type Svg from '../layers/Svg.svelte'; import type TooltipContext from '../tooltip/TooltipContext.svelte'; import type Tooltip from '../tooltip/Tooltip.svelte'; diff --git a/packages/layerchart/src/lib/components/Grid/Grid.base.svelte b/packages/layerchart/src/lib/components/Grid/Grid.base.svelte index eff404e07..afc0103fa 100644 --- a/packages/layerchart/src/lib/components/Grid/Grid.base.svelte +++ b/packages/layerchart/src/lib/components/Grid/Grid.base.svelte @@ -134,7 +134,7 @@ class={cls('lc-grid-y-radial-circle', classes.line, splineProps?.class)} /> {:else} - {#await import('../Spline.svelte') then { default: Spline }} + {#await import('../Spline/Spline.svelte') then { default: Spline }} ({ x: tx, y: tick }))} x="x" diff --git a/packages/layerchart/src/lib/components/Highlight/Highlight.canvas.svelte b/packages/layerchart/src/lib/components/Highlight/Highlight.canvas.svelte index 8b2910e5b..683e7d57a 100644 --- a/packages/layerchart/src/lib/components/Highlight/Highlight.canvas.svelte +++ b/packages/layerchart/src/lib/components/Highlight/Highlight.canvas.svelte @@ -7,8 +7,7 @@ import Circle from '../Circle/Circle.canvas.svelte'; import Line from '../Line/Line.canvas.svelte'; import Rect from '../Rect/Rect.canvas.svelte'; - // Arc is not split yet — uses agnostic dispatcher. - import Arc from '../Arc.svelte'; + import Arc from '../Arc/Arc.canvas.svelte'; import type { HighlightProps } from './Highlight.shared.svelte.js'; diff --git a/packages/layerchart/src/lib/components/Highlight/Highlight.html.svelte b/packages/layerchart/src/lib/components/Highlight/Highlight.html.svelte index 51f49ff0e..329fa11f6 100644 --- a/packages/layerchart/src/lib/components/Highlight/Highlight.html.svelte +++ b/packages/layerchart/src/lib/components/Highlight/Highlight.html.svelte @@ -7,8 +7,10 @@ import Circle from '../Circle/Circle.html.svelte'; import Line from '../Line/Line.html.svelte'; import Rect from '../Rect/Rect.html.svelte'; - // Arc is not split yet — uses agnostic dispatcher. - import Arc from '../Arc.svelte'; + // Arc has no html variant — radial highlight area uses SVG path. Highlight.html + // never enters the radial branch (no html chart context is radial), but we + // import Arc.svg here so the per-layer wrapper avoids the agnostic dispatcher. + import Arc from '../Arc/Arc.svg.svelte'; import type { HighlightProps } from './Highlight.shared.svelte.js'; diff --git a/packages/layerchart/src/lib/components/Highlight/Highlight.svg.svelte b/packages/layerchart/src/lib/components/Highlight/Highlight.svg.svelte index 4f57150ef..1f65cbbaf 100644 --- a/packages/layerchart/src/lib/components/Highlight/Highlight.svg.svelte +++ b/packages/layerchart/src/lib/components/Highlight/Highlight.svg.svelte @@ -7,8 +7,7 @@ import Circle from '../Circle/Circle.svg.svelte'; import Line from '../Line/Line.svg.svelte'; import Rect from '../Rect/Rect.svg.svelte'; - // Arc is not split yet — uses agnostic dispatcher. - import Arc from '../Arc.svelte'; + import Arc from '../Arc/Arc.svg.svelte'; import type { HighlightProps } from './Highlight.shared.svelte.js'; diff --git a/packages/layerchart/src/lib/components/Hull.svelte b/packages/layerchart/src/lib/components/Hull.svelte index e2d66d5c7..e6ba413c2 100644 --- a/packages/layerchart/src/lib/components/Hull.svelte +++ b/packages/layerchart/src/lib/components/Hull.svelte @@ -62,7 +62,7 @@ import GeoPath from './geo/GeoPath.svelte'; import Group, { type GroupProps } from './Group/Group.svelte'; - import Spline from './Spline.svelte'; + import Spline from './Spline/Spline.svelte'; import { getChartContext } from '$lib/contexts/chart.js'; import { getGeoContext } from '$lib/contexts/geo.js'; diff --git a/packages/layerchart/src/lib/components/Pie.svelte b/packages/layerchart/src/lib/components/Pie.svelte index ba3641409..30ce25cac 100644 --- a/packages/layerchart/src/lib/components/Pie.svelte +++ b/packages/layerchart/src/lib/components/Pie.svelte @@ -75,7 +75,7 @@ - - - - - - -{#if segments} - {#each segments as seg, i (i)} - - {/each} -{:else} - -{/if} diff --git a/packages/layerchart/src/lib/components/Spline/Spline.base.svelte b/packages/layerchart/src/lib/components/Spline/Spline.base.svelte new file mode 100644 index 000000000..68a5a97f5 --- /dev/null +++ b/packages/layerchart/src/lib/components/Spline/Spline.base.svelte @@ -0,0 +1,56 @@ + + + + +{#if c.segments} + {#each c.segments as seg, i (i)} + + {/each} +{:else} + +{/if} diff --git a/packages/layerchart/src/lib/components/Spline/Spline.canvas.svelte b/packages/layerchart/src/lib/components/Spline/Spline.canvas.svelte new file mode 100644 index 000000000..a5e0195c7 --- /dev/null +++ b/packages/layerchart/src/lib/components/Spline/Spline.canvas.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Spline/Spline.shared.svelte.ts b/packages/layerchart/src/lib/components/Spline/Spline.shared.svelte.ts new file mode 100644 index 000000000..9ab2f8bc7 --- /dev/null +++ b/packages/layerchart/src/lib/components/Spline/Spline.shared.svelte.ts @@ -0,0 +1,272 @@ +import type { SVGAttributes } from 'svelte/elements'; +import type { CurveFactory, CurveFactoryLineOnly, Line } from 'd3-shape'; +import { line as d3Line, lineRadial } from 'd3-shape'; +import { geoPath as d3GeoPath } from 'd3-geo'; +import { max } from 'd3-array'; +import { interpolatePath } from 'd3-interpolate-path'; + +import { accessor, type Accessor } from '$lib/utils/common.js'; +import { isScaleBand } from '$lib/utils/scales.svelte.js'; +import { + createMotion, + extractTweenConfig, + type MotionProp, +} from '$lib/utils/motion.svelte.js'; +import { resolveColorProp, resolveStyleProp } from '$lib/utils/dataProp.js'; +import type { ColorProp, StyleProp } from '$lib/utils/dataProp.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import { getGeoContext } from '$lib/contexts/geo.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import type { GeoState } from '$lib/states/geo.svelte.js'; +import type { Without } from '$lib/utils/types.js'; +import type { PathProps } from '../Path/Path.shared.svelte.js'; + +export type SplinePropsWithoutHTML = { + /** Override data instead of using context */ + data?: any; + /** Override `x` accessor from Chart context */ + x?: Accessor; + /** Override `y` accessor from Chart context */ + y?: Accessor; + /** Series key to use for accessor. Only applicable if `` uses `series` and `x`/`y` are not set. */ + seriesKey?: string; + /** Function to determine if a point is defined */ + defined?: Parameters['defined']>[0]; + /** Curve of path drawn. Imported via d3-shape. */ + curve?: CurveFactory | CurveFactoryLineOnly; + /** Stroke color or function returning stroke per data point. */ + stroke?: ColorProp; + /** Fill color or function returning fill per data point. */ + fill?: ColorProp; + /** Opacity or function returning opacity per data point. */ + opacity?: StyleProp; + /** Whether to animate the path using tweened interpolation. */ + motion?: MotionProp; +} & Omit; + +export type SplineProps = SplinePropsWithoutHTML & + Without, SplinePropsWithoutHTML>; + +export type SplineSegment = { + stroke?: string; + fill?: string; + opacity?: number; + d: string; +}; + +/** + * Reactive state shared by every per-layer Spline variant. Holds the + * computed `d` (path data), `segments` (per-style-grouped paths when + * stroke/fill/opacity are functions), and tween state. + */ +export class SplineState { + #getProps: () => SplineProps = () => ({}) as SplineProps; + ctx: ChartState = getChartContext(); + geo: GeoState = getGeoContext(); + + #tweenState!: ReturnType>; + + constructor(getProps: () => SplineProps) { + this.#getProps = getProps; + + const initial = getProps(); + this.ctx.registerComponent({ + name: 'Spline', + kind: 'mark', + markInfo: () => { + const p = getProps(); + return { + data: p.data, + x: p.x, + y: p.y, + seriesKey: p.seriesKey, + color: typeof p.stroke === 'string' ? p.stroke : undefined, + }; + }, + }); + + this.#tweenState = createMotion(this.#defaultPathData(), () => this.d, { + type: 'tween', + interpolate: interpolatePath, + }); + } + + #getScaleValue( + data: any, + scale: typeof this.ctx.xScale | typeof this.ctx.yScale, + accessorFn: Function + ) { + let value = accessorFn(data); + if (Array.isArray(value)) value = max(value); + if (scale.domain().length) return scale(value); + return value; + } + + series = $derived( + this.ctx.series.series.find((s) => s.key === this.#getProps().seriesKey) + ); + seriesAccessor = $derived( + this.series?.value ?? (this.series?.data ? undefined : this.series?.key) + ); + + xAccessor = $derived( + accessor( + this.#getProps().x ?? + (this.ctx.valueAxis === 'x' ? this.seriesAccessor : undefined) ?? + this.ctx.x + ) + ); + yAccessor = $derived( + accessor( + this.#getProps().y ?? + (this.ctx.valueAxis === 'y' ? this.seriesAccessor : undefined) ?? + this.ctx.y + ) + ); + + xOffset = $derived(isScaleBand(this.ctx.xScale) ? this.ctx.xScale.bandwidth() / 2 : 0); + yOffset = $derived(isScaleBand(this.ctx.yScale) ? this.ctx.yScale.bandwidth() / 2 : 0); + + #buildPath(resolvedData: any[]): string { + const props = this.#getProps(); + const path = this.ctx.radial + ? lineRadial() + .angle((d) => this.#getScaleValue(d, this.ctx.xScale, this.xAccessor) + 0) + .radius((d) => this.#getScaleValue(d, this.ctx.yScale, this.yAccessor) + this.yOffset) + : d3Line() + .x((d) => this.#getScaleValue(d, this.ctx.xScale, this.xAccessor) + this.xOffset) + .y((d) => this.#getScaleValue(d, this.ctx.yScale, this.yAccessor) + this.yOffset); + + path.defined( + props.defined ?? ((d) => this.xAccessor(d) != null && this.yAccessor(d) != null) + ); + if (props.curve) path.curve(props.curve); + + return path(resolvedData) ?? ''; + } + + hasAnyStyleFn = $derived.by(() => { + const p = this.#getProps(); + return ( + typeof p.stroke === 'function' || + typeof p.fill === 'function' || + typeof p.opacity === 'function' + ); + }); + + d = $derived.by(() => { + const props = this.#getProps(); + if (this.hasAnyStyleFn && !this.geo.projection) return ''; + + const resolvedData = props.data ?? this.series?.data ?? this.ctx.data; + + if (this.geo.projection) { + const coordinates = resolvedData + .filter((d: any) => { + if (props.defined) return props.defined(d, 0, resolvedData); + return this.xAccessor(d) != null && this.yAccessor(d) != null; + }) + .map((d: any) => [this.xAccessor(d), this.yAccessor(d)]); + + const lineString = { type: 'LineString' as const, coordinates }; + return d3GeoPath(this.geo.projection)(lineString) ?? ''; + } + + return this.#buildPath(resolvedData); + }); + + segments = $derived.by(() => { + if (!this.hasAnyStyleFn) return null; + const props = this.#getProps(); + const resolvedData = props.data ?? this.series?.data ?? this.ctx.data; + if (this.geo.projection) return null; + + const groups = groupConsecutive(resolvedData, (d, i, arr) => { + const s = resolveColorProp(props.stroke, d, this.ctx.cScale, i, arr); + const f = resolveColorProp(props.fill, d, this.ctx.cScale, i, arr); + const o = resolveStyleProp(props.opacity, d, i, arr); + return { key: `${s}\0${f}\0${o}`, style: { stroke: s, fill: f, opacity: o } }; + }); + + return groups.map((group) => ({ + ...group.style, + d: this.#buildPath(group.data), + })); + }); + + #defaultPathData(): string { + const props = this.#getProps(); + if (!extractTweenConfig(props.motion)) return ''; + + if (this.ctx.config.x) { + const resolvedData = props.data ?? this.series?.data ?? this.ctx.data; + const baseline = Math.min(this.ctx.yScale(0) ?? this.ctx.yRange[0], this.ctx.yRange[0]); + + const path = this.ctx.radial + ? lineRadial() + .angle((d) => this.#getScaleValue(d, this.ctx.xScale, this.xAccessor) + 0) + .radius(() => baseline) + : d3Line() + .x((d) => this.#getScaleValue(d, this.ctx.xScale, this.xAccessor) + this.xOffset) + .y(() => baseline); + + path.defined( + props.defined ?? ((d) => this.xAccessor(d) != null && this.yAccessor(d) != null) + ); + if (props.curve) path.curve(props.curve); + + return path(resolvedData) ?? ''; + } + + return ''; + } + + isTweened = $derived(extractTweenConfig(this.#getProps().motion) != null); + + get tweenedPath() { + return this.#tweenState.current; + } + + seriesOpacity = $derived.by(() => { + if ( + this.series?.key == null || + this.ctx.series.visibleSeries.length <= 1 || + this.ctx.series.isHighlighted(this.series.key, true) + ) { + return 1; + } + return 0.1; + }); +} + +type SegmentStyle = { stroke?: string; fill?: string; opacity?: number }; + +/** + * Groups consecutive data points by a composite key derived from function-valued style props. + * The key at index `i` determines the style for the segment from point `i` to point `i+1`. + * Each group includes an overlap of 1 point at boundaries for curve continuity. + */ +function groupConsecutive( + data: any[], + keyFn: (d: any, index: number, data: any[]) => { key: string; style: SegmentStyle } +): Array<{ style: SegmentStyle; data: any[] }> { + if (data.length < 2) return []; + + const groups: Array<{ style: SegmentStyle; data: any[] }> = []; + let current = keyFn(data[0], 0, data); + let startIdx = 0; + + for (let i = 1; i < data.length; i++) { + const next = keyFn(data[i], i, data); + if (next.key !== current.key) { + groups.push({ style: current.style, data: data.slice(startIdx, i + 1) }); + startIdx = i; + current = next; + } + } + if (data.length - startIdx >= 2) { + groups.push({ style: current.style, data: data.slice(startIdx) }); + } + + return groups; +} diff --git a/packages/layerchart/src/lib/components/Spline/Spline.svelte b/packages/layerchart/src/lib/components/Spline/Spline.svelte new file mode 100644 index 000000000..3c140dc69 --- /dev/null +++ b/packages/layerchart/src/lib/components/Spline/Spline.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Spline/Spline.svg.svelte b/packages/layerchart/src/lib/components/Spline/Spline.svg.svelte new file mode 100644 index 000000000..0a6682713 --- /dev/null +++ b/packages/layerchart/src/lib/components/Spline/Spline.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Threshold.svelte b/packages/layerchart/src/lib/components/Threshold.svelte index ffdf0a3e0..37ab19e28 100644 --- a/packages/layerchart/src/lib/components/Threshold.svelte +++ b/packages/layerchart/src/lib/components/Threshold.svelte @@ -42,7 +42,7 @@ import { min, max } from 'd3-array'; - import Area from './Area.svelte'; + import Area from './Area/Area.svelte'; import ClipPath from './ClipPath/ClipPath.svelte'; import { getChartContext } from '$lib/contexts/chart.js'; diff --git a/packages/layerchart/src/lib/components/charts/ArcChart.svelte b/packages/layerchart/src/lib/components/charts/ArcChart.svelte index de991b2d4..615ddf51e 100644 --- a/packages/layerchart/src/lib/components/charts/ArcChart.svelte +++ b/packages/layerchart/src/lib/components/charts/ArcChart.svelte @@ -2,11 +2,11 @@ import type { ComponentProps, Snippet } from 'svelte'; import type { ChartProps } from "../Chart/Chart.svelte"; import type { ChartState } from '$lib/contexts/chart.js'; - import type { ArcPropsWithoutHTML } from '../Arc.svelte'; + import type { ArcPropsWithoutHTML } from '../Arc/Arc.svelte'; import type { Accessor } from '$lib/utils/common.js'; import type { SeriesData } from './types.js'; - import Arc from '../Arc.svelte'; + import Arc from '../Arc/Arc.svelte'; import ArcLabel, { type ArcLabelConfig } from '../ArcLabel.svelte'; import Group from '../Group/Group.svelte'; diff --git a/packages/layerchart/src/lib/components/charts/AreaChart.svelte b/packages/layerchart/src/lib/components/charts/AreaChart.svelte index 53bc9ec21..6f4ef1819 100644 --- a/packages/layerchart/src/lib/components/charts/AreaChart.svelte +++ b/packages/layerchart/src/lib/components/charts/AreaChart.svelte @@ -3,7 +3,7 @@ import type { HighlightPoint } from '../Highlight/Highlight.svelte'; import type { SeriesData } from './types.js'; - import Area from '../Area.svelte'; + import Area from '../Area/Area.svelte'; // Use explicit data prop for TData inference, with rest from ChartPropsWithoutHTML export type AreaChartProps = { diff --git a/packages/layerchart/src/lib/components/charts/LineChart.svelte b/packages/layerchart/src/lib/components/charts/LineChart.svelte index 0d8a5a7f8..e485baf1e 100644 --- a/packages/layerchart/src/lib/components/charts/LineChart.svelte +++ b/packages/layerchart/src/lib/components/charts/LineChart.svelte @@ -3,7 +3,7 @@ import type { HighlightPointData } from '../Highlight/Highlight.svelte'; import type { SeriesData } from './types.js'; - import Spline from '../Spline.svelte'; + import Spline from '../Spline/Spline.svelte'; // Use explicit data prop for TData inference, with rest from ChartPropsWithoutHTML export type LineChartProps = { diff --git a/packages/layerchart/src/lib/components/charts/PieChart.svelte b/packages/layerchart/src/lib/components/charts/PieChart.svelte index 5ec0cd0e8..61396dd89 100644 --- a/packages/layerchart/src/lib/components/charts/PieChart.svelte +++ b/packages/layerchart/src/lib/components/charts/PieChart.svelte @@ -5,7 +5,7 @@ import type { Accessor } from '$lib/utils/common.js'; import type { SeriesData } from './types.js'; - import Arc from '../Arc.svelte'; + import Arc from '../Arc/Arc.svelte'; import ArcLabel, { type ArcLabelConfig } from '../ArcLabel.svelte'; import Group from '../Group/Group.svelte'; import Pie from '../Pie.svelte'; diff --git a/packages/layerchart/src/lib/components/index.ts b/packages/layerchart/src/lib/components/index.ts index 3b0b7592c..64ca50fd4 100644 --- a/packages/layerchart/src/lib/components/index.ts +++ b/packages/layerchart/src/lib/components/index.ts @@ -7,12 +7,12 @@ export { default as AnnotationPoint } from './AnnotationPoint.svelte'; export * from './AnnotationPoint.svelte'; export { default as AnnotationRange } from './AnnotationRange.svelte'; export * from './AnnotationRange.svelte'; -export { default as Arc } from './Arc.svelte'; -export * from './Arc.svelte'; +export { default as Arc } from './Arc/Arc.svelte'; +export * from './Arc/Arc.svelte'; export { default as ArcLabel } from './ArcLabel.svelte'; export * from './ArcLabel.svelte'; -export { default as Area } from './Area.svelte'; -export * from './Area.svelte'; +export { default as Area } from './Area/Area.svelte'; +export * from './Area/Area.svelte'; export { default as Axis } from './Axis/Axis.svelte'; export * from './Axis/Axis.svelte'; export { default as Bar } from './Bar.svelte'; @@ -75,8 +75,8 @@ export { default as Legend } from './Legend.svelte'; export * from './Legend.svelte'; export { default as Line } from './Line/Line.svelte'; export * from './Line/Line.svelte'; -export { default as Spline } from './Spline.svelte'; -export * from './Spline.svelte'; +export { default as Spline } from './Spline/Spline.svelte'; +export * from './Spline/Spline.svelte'; export { default as LinearGradient } from './LinearGradient/LinearGradient.svelte'; export * from './LinearGradient/LinearGradient.svelte'; export { default as Link } from './Link.svelte'; diff --git a/packages/layerchart/src/lib/components/tooltip/TooltipContext.svelte b/packages/layerchart/src/lib/components/tooltip/TooltipContext.svelte index 652441aef..4271acf19 100644 --- a/packages/layerchart/src/lib/components/tooltip/TooltipContext.svelte +++ b/packages/layerchart/src/lib/components/tooltip/TooltipContext.svelte @@ -750,7 +750,7 @@ {#each rects as rect} {#if ctx.radial} - {#await import('../Arc.svelte') then { default: Arc }} + {#await import('../Arc/Arc.svelte') then { default: Arc }} import ServerChart from './ServerChart.svelte'; import type { CaptureTarget } from './captureStore.js'; - import Area from '$lib/components/Area.svelte'; - import Spline from '$lib/components/Spline.svelte'; + import Area from '$lib/components/Area/Area.svelte'; + import Spline from '$lib/components/Spline/Spline.svelte'; let { data, diff --git a/packages/layerchart/src/lib/svg.ts b/packages/layerchart/src/lib/svg.ts index 62c9d19c5..2909ed98e 100644 --- a/packages/layerchart/src/lib/svg.ts +++ b/packages/layerchart/src/lib/svg.ts @@ -111,6 +111,18 @@ export type { HighlightPoint, HighlightPointData, } from './components/Highlight/Highlight.shared.svelte.js'; +export { default as Arc } from './components/Arc/Arc.svg.svelte'; +export type { ArcProps, ArcPropsWithoutHTML } from './components/Arc/Arc.shared.svelte.js'; +export { default as Spline } from './components/Spline/Spline.svg.svelte'; +export type { + SplineProps, + SplinePropsWithoutHTML, +} from './components/Spline/Spline.shared.svelte.js'; +export { default as Area } from './components/Area/Area.svg.svelte'; +export type { + AreaProps, + AreaPropsWithoutHTML, +} from './components/Area/Area.shared.svelte.js'; export { default as RectClipPath } from './components/RectClipPath/RectClipPath.svg.svelte'; export type { RectClipPathProps, From 573025059593f4684fb7dce86bc983d5efc95a61 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 28 Apr 2026 12:49:16 -0400 Subject: [PATCH 17/36] split ArcLabel, Bar, Bars, Labels, Pie, and Points into 3 layer-specific components --- bundle-analyzer/bundle-reports/latest.json | 330 +++++++++++++---- bundle-analyzer/bundle-scenarios.ts | 23 ++ packages/layerchart/src/lib/canvas.ts | 26 ++ .../src/lib/components/ArcLabel.svelte | 259 -------------- .../lib/components/ArcLabel.svelte.test.ts | 2 +- .../components/ArcLabel/ArcLabel.base.svelte | 61 ++++ .../ArcLabel/ArcLabel.canvas.svelte | 19 + .../ArcLabel/ArcLabel.shared.svelte.ts | 205 +++++++++++ .../lib/components/ArcLabel/ArcLabel.svelte | 24 ++ .../components/ArcLabel/ArcLabel.svg.svelte | 19 + .../layerchart/src/lib/components/Bar.svelte | 333 ------------------ .../src/lib/components/Bar/Bar.base.svelte | 128 +++++++ .../src/lib/components/Bar/Bar.canvas.svelte | 15 + .../lib/components/Bar/Bar.shared.svelte.ts | 229 ++++++++++++ .../src/lib/components/Bar/Bar.svelte | 20 ++ .../src/lib/components/Bar/Bar.svg.svelte | 15 + .../layerchart/src/lib/components/Bars.svelte | 97 ----- .../src/lib/components/Bars/Bars.base.svelte | 67 ++++ .../lib/components/Bars/Bars.canvas.svelte | 15 + .../lib/components/Bars/Bars.shared.svelte.ts | 54 +++ .../src/lib/components/Bars/Bars.svelte | 20 ++ .../src/lib/components/Bars/Bars.svg.svelte | 15 + .../ChartChildren/ChartChildren.base.svelte | 4 +- .../ChartChildren.shared.svelte.ts | 8 +- .../Highlight/Highlight.base.svelte | 2 +- .../Highlight/Highlight.shared.svelte.ts | 2 +- .../src/lib/components/Labels.svelte | 313 ---------------- .../lib/components/Labels/Labels.base.svelte | 92 +++++ .../components/Labels/Labels.canvas.svelte | 16 + .../lib/components/Labels/Labels.html.svelte | 16 + .../components/Labels/Labels.shared.svelte.ts | 225 ++++++++++++ .../src/lib/components/Labels/Labels.svelte | 23 ++ .../lib/components/Labels/Labels.svg.svelte | 16 + .../layerchart/src/lib/components/Pie.svelte | 156 -------- .../src/lib/components/Pie/Pie.base.svelte | 69 ++++ .../src/lib/components/Pie/Pie.canvas.svelte | 14 + .../lib/components/Pie/Pie.shared.svelte.ts | 93 +++++ .../src/lib/components/Pie/Pie.svelte | 20 ++ .../src/lib/components/Pie/Pie.svg.svelte | 14 + .../src/lib/components/Points.svelte | 200 ----------- .../lib/components/Points/Points.base.svelte | 75 ++++ .../components/Points/Points.canvas.svelte | 14 + .../lib/components/Points/Points.html.svelte | 14 + .../components/Points/Points.shared.svelte.ts | 170 +++++++++ .../src/lib/components/Points/Points.svelte | 23 ++ .../lib/components/Points/Points.svg.svelte | 14 + .../src/lib/components/charts/ArcChart.svelte | 2 +- .../src/lib/components/charts/BarChart.svelte | 2 +- .../charts/BarChartFixedWidthTest.svelte | 2 +- .../src/lib/components/charts/PieChart.svelte | 4 +- .../lib/components/charts/ScatterChart.svelte | 2 +- .../layerchart/src/lib/components/index.ts | 24 +- packages/layerchart/src/lib/html.ts | 11 + .../src/lib/server/TestBarChart.svelte | 2 +- packages/layerchart/src/lib/svg.ts | 26 ++ 55 files changed, 2193 insertions(+), 1451 deletions(-) delete mode 100644 packages/layerchart/src/lib/components/ArcLabel.svelte create mode 100644 packages/layerchart/src/lib/components/ArcLabel/ArcLabel.base.svelte create mode 100644 packages/layerchart/src/lib/components/ArcLabel/ArcLabel.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/ArcLabel/ArcLabel.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/ArcLabel/ArcLabel.svelte create mode 100644 packages/layerchart/src/lib/components/ArcLabel/ArcLabel.svg.svelte delete mode 100644 packages/layerchart/src/lib/components/Bar.svelte create mode 100644 packages/layerchart/src/lib/components/Bar/Bar.base.svelte create mode 100644 packages/layerchart/src/lib/components/Bar/Bar.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Bar/Bar.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Bar/Bar.svelte create mode 100644 packages/layerchart/src/lib/components/Bar/Bar.svg.svelte delete mode 100644 packages/layerchart/src/lib/components/Bars.svelte create mode 100644 packages/layerchart/src/lib/components/Bars/Bars.base.svelte create mode 100644 packages/layerchart/src/lib/components/Bars/Bars.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Bars/Bars.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Bars/Bars.svelte create mode 100644 packages/layerchart/src/lib/components/Bars/Bars.svg.svelte delete mode 100644 packages/layerchart/src/lib/components/Labels.svelte create mode 100644 packages/layerchart/src/lib/components/Labels/Labels.base.svelte create mode 100644 packages/layerchart/src/lib/components/Labels/Labels.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Labels/Labels.html.svelte create mode 100644 packages/layerchart/src/lib/components/Labels/Labels.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Labels/Labels.svelte create mode 100644 packages/layerchart/src/lib/components/Labels/Labels.svg.svelte delete mode 100644 packages/layerchart/src/lib/components/Pie.svelte create mode 100644 packages/layerchart/src/lib/components/Pie/Pie.base.svelte create mode 100644 packages/layerchart/src/lib/components/Pie/Pie.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Pie/Pie.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Pie/Pie.svelte create mode 100644 packages/layerchart/src/lib/components/Pie/Pie.svg.svelte delete mode 100644 packages/layerchart/src/lib/components/Points.svelte create mode 100644 packages/layerchart/src/lib/components/Points/Points.base.svelte create mode 100644 packages/layerchart/src/lib/components/Points/Points.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Points/Points.html.svelte create mode 100644 packages/layerchart/src/lib/components/Points/Points.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Points/Points.svelte create mode 100644 packages/layerchart/src/lib/components/Points/Points.svg.svelte diff --git a/bundle-analyzer/bundle-reports/latest.json b/bundle-analyzer/bundle-reports/latest.json index 9a7ee1cd5..a713f38ea 100644 --- a/bundle-analyzer/bundle-reports/latest.json +++ b/bundle-analyzer/bundle-reports/latest.json @@ -1,12 +1,12 @@ { - "timestamp": "2026-04-28T16:29:46.448Z", + "timestamp": "2026-04-28T16:46:11.527Z", "results": [ { "scenario": "core", "description": "Core charting components without rendering layer", "group": "Foundation", - "size": 392647, - "gzipSize": 90432, + "size": 366902, + "gzipSize": 84800, "imports": [ "Chart", "Svg" @@ -16,8 +16,8 @@ "scenario": "core-svg", "description": "Svg-based rendering", "group": "Foundation", - "size": 360369, - "gzipSize": 83878, + "size": 350946, + "gzipSize": 82037, "imports": [ "Chart", "Svg" @@ -28,7 +28,7 @@ "description": "Canvas-based rendering", "group": "Foundation", "size": 357197, - "gzipSize": 83648, + "gzipSize": 83646, "imports": [ "Chart", "Canvas" @@ -38,8 +38,8 @@ "scenario": "core-html", "description": "HTML-based rendering", "group": "Foundation", - "size": 362300, - "gzipSize": 84307, + "size": 352837, + "gzipSize": 82478, "imports": [ "Chart", "Html" @@ -49,8 +49,8 @@ "scenario": "line-chart", "description": "Basic line chart with axes and grid", "group": "Cartesian charts", - "size": 407299, - "gzipSize": 93286, + "size": 384782, + "gzipSize": 88531, "imports": [ "Chart", "Svg", @@ -63,8 +63,8 @@ "scenario": "line-chart-svg", "description": "Line chart composed from `layerchart/svg`", "group": "Cartesian charts", - "size": 360393, - "gzipSize": 83890, + "size": 350970, + "gzipSize": 82048, "imports": [ "Chart", "Layer", @@ -78,7 +78,7 @@ "description": "Line chart composed from `layerchart/canvas`", "group": "Cartesian charts", "size": 357221, - "gzipSize": 83671, + "gzipSize": 83667, "imports": [ "Chart", "Layer", @@ -91,8 +91,8 @@ "scenario": "line-chart-html", "description": "Line chart composed from `layerchart/html`", "group": "Cartesian charts", - "size": 362324, - "gzipSize": 84333, + "size": 352861, + "gzipSize": 82498, "imports": [ "Chart", "Layer", @@ -105,8 +105,8 @@ "scenario": "line-chart-interactive", "description": "Line chart with tooltip and highlight", "group": "Cartesian charts", - "size": 422136, - "gzipSize": 96040, + "size": 399619, + "gzipSize": 92185, "imports": [ "Chart", "Svg", @@ -121,8 +121,8 @@ "scenario": "LineChart", "description": "High-level `LineChart` component", "group": "Cartesian charts", - "size": 416766, - "gzipSize": 96791, + "size": 391021, + "gzipSize": 91122, "imports": [ "LineChart" ] @@ -131,8 +131,8 @@ "scenario": "area-chart", "description": "Area chart with axes", "group": "Cartesian charts", - "size": 406658, - "gzipSize": 93425, + "size": 380913, + "gzipSize": 87773, "imports": [ "Chart", "Svg", @@ -145,8 +145,8 @@ "scenario": "AreaChart", "description": "High-level `AreaChart` component", "group": "Cartesian charts", - "size": 410248, - "gzipSize": 94298, + "size": 384503, + "gzipSize": 88649, "imports": [ "AreaChart" ] @@ -155,8 +155,8 @@ "scenario": "bar-chart", "description": "Bar chart with axes", "group": "Cartesian charts", - "size": 421930, - "gzipSize": 97772, + "size": 371241, + "gzipSize": 85586, "imports": [ "Chart", "Svg", @@ -169,8 +169,8 @@ "scenario": "BarChart", "description": "High-level `BarChart` component", "group": "Cartesian charts", - "size": 427066, - "gzipSize": 98959, + "size": 376376, + "gzipSize": 86640, "imports": [ "BarChart" ] @@ -179,8 +179,8 @@ "scenario": "scatter-chart", "description": "Scatter plot with points", "group": "Cartesian charts", - "size": 408256, - "gzipSize": 94106, + "size": 368179, + "gzipSize": 85289, "imports": [ "Chart", "Svg", @@ -194,8 +194,8 @@ "scenario": "ScatterChart", "description": "High-level `ScatterChart` component", "group": "Cartesian charts", - "size": 411004, - "gzipSize": 94761, + "size": 370927, + "gzipSize": 85936, "imports": [ "ScatterChart" ] @@ -204,8 +204,8 @@ "scenario": "pie-chart", "description": "Pie/donut chart with arcs", "group": "Cartesian charts", - "size": 445459, - "gzipSize": 103698, + "size": 379646, + "gzipSize": 87857, "imports": [ "Chart", "Svg", @@ -218,8 +218,8 @@ "scenario": "PieChart", "description": "High-level `PieChart` component", "group": "Cartesian charts", - "size": 470915, - "gzipSize": 108744, + "size": 405102, + "gzipSize": 93604, "imports": [ "PieChart" ] @@ -228,8 +228,8 @@ "scenario": "ArcChart", "description": "High-level `ArcChart` component", "group": "Cartesian charts", - "size": 466253, - "gzipSize": 108336, + "size": 398598, + "gzipSize": 92351, "imports": [ "ArcChart" ] @@ -238,8 +238,8 @@ "scenario": "geo", "description": "Geographic map with paths", "group": "Geo", - "size": 426007, - "gzipSize": 98152, + "size": 406939, + "gzipSize": 94072, "imports": [ "Chart", "Svg", @@ -252,8 +252,8 @@ "scenario": "geo-tiles", "description": "Geographic map with tile layer", "group": "Geo", - "size": 443781, - "gzipSize": 103230, + "size": 411384, + "gzipSize": 95625, "imports": [ "Chart", "Svg", @@ -267,8 +267,8 @@ "scenario": "geo-full", "description": "Full geo setup with all geo components", "group": "Geo", - "size": 502909, - "gzipSize": 118770, + "size": 459774, + "gzipSize": 109107, "imports": [ "Chart", "Svg", @@ -290,8 +290,8 @@ "scenario": "hierarchy-tree", "description": "Tree layout with links", "group": "Hierarchy", - "size": 469728, - "gzipSize": 108824, + "size": 412416, + "gzipSize": 95884, "imports": [ "Chart", "Svg", @@ -305,8 +305,8 @@ "scenario": "hierarchy-treemap", "description": "Treemap layout", "group": "Hierarchy", - "size": 423726, - "gzipSize": 98308, + "size": 391324, + "gzipSize": 90607, "imports": [ "Chart", "Svg", @@ -320,8 +320,8 @@ "scenario": "hierarchy-pack", "description": "Circle packing layout", "group": "Hierarchy", - "size": 434826, - "gzipSize": 100528, + "size": 391642, + "gzipSize": 90767, "imports": [ "Chart", "Svg", @@ -334,8 +334,8 @@ "scenario": "force", "description": "Force-directed graph layout", "group": "Graph / network", - "size": 472191, - "gzipSize": 109653, + "size": 414879, + "gzipSize": 96744, "imports": [ "Chart", "Svg", @@ -349,8 +349,8 @@ "scenario": "dagre", "description": "Dagre directed graph layout", "group": "Graph / network", - "size": 529617, - "gzipSize": 125128, + "size": 472305, + "gzipSize": 112286, "imports": [ "Chart", "Svg", @@ -364,8 +364,8 @@ "scenario": "sankey", "description": "Sankey flow diagram", "group": "Graph / network", - "size": 460570, - "gzipSize": 106871, + "size": 413997, + "gzipSize": 95876, "imports": [ "Chart", "Svg", @@ -379,8 +379,8 @@ "scenario": "chord", "description": "Chord diagram", "group": "Graph / network", - "size": 402398, - "gzipSize": 92818, + "size": 376653, + "gzipSize": 87063, "imports": [ "Chart", "Svg", @@ -883,7 +883,7 @@ "description": "Standalone Axis (agnostic) — baseline", "group": "Components", "size": 198533, - "gzipSize": 44724, + "gzipSize": 44626, "imports": [ "Axis" ] @@ -1002,8 +1002,8 @@ "scenario": "Highlight", "description": "Standalone Highlight (agnostic) — baseline", "group": "Components", - "size": 45824, - "gzipSize": 8589, + "size": 48347, + "gzipSize": 8827, "imports": [ "Highlight" ] @@ -1013,7 +1013,7 @@ "description": "Standalone Highlight from `layerchart/svg`", "group": "Components", "size": 35567, - "gzipSize": 6786, + "gzipSize": 6784, "imports": [ "Highlight" ] @@ -1032,8 +1032,8 @@ "scenario": "Highlight.html", "description": "Standalone Highlight from `layerchart/html`", "group": "Components", - "size": 34700, - "gzipSize": 6814, + "size": 37229, + "gzipSize": 7003, "imports": [ "Highlight" ] @@ -1208,12 +1208,212 @@ "Area" ] }, + { + "scenario": "Pie", + "description": "Standalone Pie (agnostic) — baseline", + "group": "Components", + "size": 140628, + "gzipSize": 37499, + "imports": [ + "Pie" + ] + }, + { + "scenario": "Pie.svg", + "description": "Standalone Pie from `layerchart/svg`", + "group": "Components", + "size": 129373, + "gzipSize": 34152, + "imports": [ + "Pie" + ] + }, + { + "scenario": "Pie.canvas", + "description": "Standalone Pie from `layerchart/canvas`", + "group": "Components", + "size": 119975, + "gzipSize": 32920, + "imports": [ + "Pie" + ] + }, + { + "scenario": "ArcLabel", + "description": "Standalone ArcLabel (agnostic) — baseline", + "group": "Components", + "size": 150476, + "gzipSize": 37198, + "imports": [ + "ArcLabel" + ] + }, + { + "scenario": "ArcLabel.svg", + "description": "Standalone ArcLabel from `layerchart/svg`", + "group": "Components", + "size": 139392, + "gzipSize": 34011, + "imports": [ + "ArcLabel" + ] + }, + { + "scenario": "ArcLabel.canvas", + "description": "Standalone ArcLabel from `layerchart/canvas`", + "group": "Components", + "size": 126432, + "gzipSize": 32060, + "imports": [ + "ArcLabel" + ] + }, + { + "scenario": "Bar", + "description": "Standalone Bar (agnostic) — baseline", + "group": "Components", + "size": 165007, + "gzipSize": 42342, + "imports": [ + "Bar" + ] + }, + { + "scenario": "Bar.svg", + "description": "Standalone Bar from `layerchart/svg`", + "group": "Components", + "size": 150439, + "gzipSize": 38431, + "imports": [ + "Bar" + ] + }, + { + "scenario": "Bar.canvas", + "description": "Standalone Bar from `layerchart/canvas`", + "group": "Components", + "size": 144062, + "gzipSize": 37901, + "imports": [ + "Bar" + ] + }, + { + "scenario": "Bars", + "description": "Standalone Bars (agnostic) — baseline", + "group": "Components", + "size": 168863, + "gzipSize": 42988, + "imports": [ + "Bars" + ] + }, + { + "scenario": "Bars.svg", + "description": "Standalone Bars from `layerchart/svg`", + "group": "Components", + "size": 154068, + "gzipSize": 39037, + "imports": [ + "Bars" + ] + }, + { + "scenario": "Bars.canvas", + "description": "Standalone Bars from `layerchart/canvas`", + "group": "Components", + "size": 151590, + "gzipSize": 39184, + "imports": [ + "Bars" + ] + }, + { + "scenario": "Points", + "description": "Standalone Points (agnostic) — baseline", + "group": "Components", + "size": 75396, + "gzipSize": 18769, + "imports": [ + "Points" + ] + }, + { + "scenario": "Points.svg", + "description": "Standalone Points from `layerchart/svg`", + "group": "Components", + "size": 61088, + "gzipSize": 14969, + "imports": [ + "Points" + ] + }, + { + "scenario": "Points.canvas", + "description": "Standalone Points from `layerchart/canvas`", + "group": "Components", + "size": 69880, + "gzipSize": 17752, + "imports": [ + "Points" + ] + }, + { + "scenario": "Points.html", + "description": "Standalone Points from `layerchart/html`", + "group": "Components", + "size": 61725, + "gzipSize": 15064, + "imports": [ + "Points" + ] + }, + { + "scenario": "Labels", + "description": "Standalone Labels (agnostic) — baseline", + "group": "Components", + "size": 156676, + "gzipSize": 36456, + "imports": [ + "Labels" + ] + }, + { + "scenario": "Labels.svg", + "description": "Standalone Labels from `layerchart/svg`", + "group": "Components", + "size": 134877, + "gzipSize": 31974, + "imports": [ + "Labels" + ] + }, + { + "scenario": "Labels.canvas", + "description": "Standalone Labels from `layerchart/canvas`", + "group": "Components", + "size": 136089, + "gzipSize": 32596, + "imports": [ + "Labels" + ] + }, + { + "scenario": "Labels.html", + "description": "Standalone Labels from `layerchart/html`", + "group": "Components", + "size": 133211, + "gzipSize": 31541, + "imports": [ + "Labels" + ] + }, { "scenario": "all", "description": "Everything from layerchart (worst case)", "group": "Worst case", - "size": 1033737, - "gzipSize": 247893, + "size": 968389, + "gzipSize": 230999, "imports": [ "*" ] diff --git a/bundle-analyzer/bundle-scenarios.ts b/bundle-analyzer/bundle-scenarios.ts index 6b431b33b..a2137bc87 100644 --- a/bundle-analyzer/bundle-scenarios.ts +++ b/bundle-analyzer/bundle-scenarios.ts @@ -846,6 +846,29 @@ export const scenarios: Scenario[] = [ layers: { Area: 'canvas' }, }, + // Pie / ArcLabel / Bar / Bars / Points / Labels — heavy compounds built on + // primitives. Per-layer variants use the corresponding per-layer primitives. + { name: 'Pie', group: 'Components', description: 'Standalone Pie (agnostic) — baseline', imports: ['Pie'] }, + { name: 'Pie.svg', group: 'Components', description: 'Standalone Pie from `layerchart/svg`', imports: ['Pie'], layers: { Pie: 'svg' } }, + { name: 'Pie.canvas', group: 'Components', description: 'Standalone Pie from `layerchart/canvas`', imports: ['Pie'], layers: { Pie: 'canvas' } }, + { name: 'ArcLabel', group: 'Components', description: 'Standalone ArcLabel (agnostic) — baseline', imports: ['ArcLabel'] }, + { name: 'ArcLabel.svg', group: 'Components', description: 'Standalone ArcLabel from `layerchart/svg`', imports: ['ArcLabel'], layers: { ArcLabel: 'svg' } }, + { name: 'ArcLabel.canvas', group: 'Components', description: 'Standalone ArcLabel from `layerchart/canvas`', imports: ['ArcLabel'], layers: { ArcLabel: 'canvas' } }, + { name: 'Bar', group: 'Components', description: 'Standalone Bar (agnostic) — baseline', imports: ['Bar'] }, + { name: 'Bar.svg', group: 'Components', description: 'Standalone Bar from `layerchart/svg`', imports: ['Bar'], layers: { Bar: 'svg' } }, + { name: 'Bar.canvas', group: 'Components', description: 'Standalone Bar from `layerchart/canvas`', imports: ['Bar'], layers: { Bar: 'canvas' } }, + { name: 'Bars', group: 'Components', description: 'Standalone Bars (agnostic) — baseline', imports: ['Bars'] }, + { name: 'Bars.svg', group: 'Components', description: 'Standalone Bars from `layerchart/svg`', imports: ['Bars'], layers: { Bars: 'svg' } }, + { name: 'Bars.canvas', group: 'Components', description: 'Standalone Bars from `layerchart/canvas`', imports: ['Bars'], layers: { Bars: 'canvas' } }, + { name: 'Points', group: 'Components', description: 'Standalone Points (agnostic) — baseline', imports: ['Points'] }, + { name: 'Points.svg', group: 'Components', description: 'Standalone Points from `layerchart/svg`', imports: ['Points'], layers: { Points: 'svg' } }, + { name: 'Points.canvas', group: 'Components', description: 'Standalone Points from `layerchart/canvas`', imports: ['Points'], layers: { Points: 'canvas' } }, + { name: 'Points.html', group: 'Components', description: 'Standalone Points from `layerchart/html`', imports: ['Points'], layers: { Points: 'html' } }, + { name: 'Labels', group: 'Components', description: 'Standalone Labels (agnostic) — baseline', imports: ['Labels'] }, + { name: 'Labels.svg', group: 'Components', description: 'Standalone Labels from `layerchart/svg`', imports: ['Labels'], layers: { Labels: 'svg' } }, + { name: 'Labels.canvas', group: 'Components', description: 'Standalone Labels from `layerchart/canvas`', imports: ['Labels'], layers: { Labels: 'canvas' } }, + { name: 'Labels.html', group: 'Components', description: 'Standalone Labels from `layerchart/html`', imports: ['Labels'], layers: { Labels: 'html' } }, + // --- Worst case --- { name: 'all', diff --git a/packages/layerchart/src/lib/canvas.ts b/packages/layerchart/src/lib/canvas.ts index 7f1fa81ff..bf9ac0e0f 100644 --- a/packages/layerchart/src/lib/canvas.ts +++ b/packages/layerchart/src/lib/canvas.ts @@ -118,6 +118,32 @@ export type { AreaProps, AreaPropsWithoutHTML, } from './components/Area/Area.shared.svelte.js'; +export { default as Pie } from './components/Pie/Pie.canvas.svelte'; +export type { PieProps, PiePropsWithoutHTML } from './components/Pie/Pie.shared.svelte.js'; +export { default as ArcLabel } from './components/ArcLabel/ArcLabel.canvas.svelte'; +export type { + ArcLabelProps, + ArcLabelConfig, + ArcLabelPlacement, +} from './components/ArcLabel/ArcLabel.shared.svelte.js'; +export { default as Bar } from './components/Bar/Bar.canvas.svelte'; +export type { BarProps, BarPropsWithoutHTML } from './components/Bar/Bar.shared.svelte.js'; +export { default as Bars } from './components/Bars/Bars.canvas.svelte'; +export type { + BarsProps, + BarsPropsWithoutHTML, +} from './components/Bars/Bars.shared.svelte.js'; +export { default as Points } from './components/Points/Points.canvas.svelte'; +export type { + PointsProps, + PointsPropsWithoutHTML, + Point, +} from './components/Points/Points.shared.svelte.js'; +export { default as Labels } from './components/Labels/Labels.canvas.svelte'; +export type { + LabelsProps, + LabelsPropsWithoutHTML, +} from './components/Labels/Labels.shared.svelte.js'; export { default as RectClipPath } from './components/RectClipPath/RectClipPath.canvas.svelte'; export type { RectClipPathProps, diff --git a/packages/layerchart/src/lib/components/ArcLabel.svelte b/packages/layerchart/src/lib/components/ArcLabel.svelte deleted file mode 100644 index df78438d6..000000000 --- a/packages/layerchart/src/lib/components/ArcLabel.svelte +++ /dev/null @@ -1,259 +0,0 @@ - - - - -{#if placement === 'callout' && calloutGeometry} - -{/if} - - diff --git a/packages/layerchart/src/lib/components/ArcLabel.svelte.test.ts b/packages/layerchart/src/lib/components/ArcLabel.svelte.test.ts index cef34afa7..682966eb3 100644 --- a/packages/layerchart/src/lib/components/ArcLabel.svelte.test.ts +++ b/packages/layerchart/src/lib/components/ArcLabel.svelte.test.ts @@ -5,7 +5,7 @@ import type { ComponentProps } from 'svelte'; import TestHarness, { componentTestId } from './tests/TestHarness.svelte'; import Arc from './Arc/Arc.svelte'; -import ArcLabel from './ArcLabel.svelte'; +import ArcLabel from './ArcLabel/ArcLabel.svelte'; const defaultArcProps: Partial> = { fill: 'currentColor', diff --git a/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.base.svelte b/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.base.svelte new file mode 100644 index 000000000..c77d34e11 --- /dev/null +++ b/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.base.svelte @@ -0,0 +1,61 @@ + + + + +{#if placement === 'callout' && c.calloutGeometry} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.canvas.svelte b/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.canvas.svelte new file mode 100644 index 000000000..13ebecf6a --- /dev/null +++ b/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.canvas.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.shared.svelte.ts b/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.shared.svelte.ts new file mode 100644 index 000000000..84508ea1e --- /dev/null +++ b/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.shared.svelte.ts @@ -0,0 +1,205 @@ +import { radiansToDegrees } from '$lib/utils/math.js'; +import type { PathProps } from '../Path/Path.shared.svelte.js'; +import type { TextProps } from '../Text/Text.shared.svelte.js'; +import type { GetArcTextProps, ArcTextOptions } from '$lib/utils/arcText.svelte.js'; + +/** + * Placement options for `ArcLabel`. + * - `centroid`: at the arc centroid (horizontal text) + * - `centroid-rotated`: at the arc centroid, rotated to follow the arc tangent + * - `centroid-radial`: at the arc centroid, rotated to read radially (center → outside) + * - `inner` / `middle` / `outer`: along the inner / middle / outer arc path + * - `callout`: outside the arc connected by a polyline with a bend + */ +export type ArcLabelPlacement = + | 'centroid' + | 'centroid-rotated' + | 'centroid-radial' + | 'inner' + | 'middle' + | 'outer' + | 'callout'; + +export type ArcLabelConfig = { + /** @default 'centroid' */ + placement?: ArcLabelPlacement; + /** @default 16 */ + calloutLineLength?: number; + /** @default 12 */ + calloutLabelOffset?: number; + /** @default 4 */ + calloutPadding?: number; + /** Props applied to the leader line `` when using `callout` placement. */ + line?: Omit; + /** Radial offset for the label, interpreted per-placement. @default 0 */ + offset?: number; +} & ArcTextOptions & + Omit; + +export type ArcLabelProps = { + /** Function from `Arc` children snippet for `inner`/`middle`/`outer` placements. */ + getArcTextProps?: GetArcTextProps; + /** Centroid `[x, y]` of the arc (from `Arc` children snippet) */ + centroid?: [number, number]; + /** Arc start angle in radians (from `Arc` children snippet) */ + startAngle?: number; + /** Arc end angle in radians (from `Arc` children snippet) */ + endAngle?: number; + /** Arc inner radius (from `Arc` children snippet) */ + innerRadius?: number; + /** Arc outer radius (from `Arc` children snippet) */ + outerRadius?: number; +} & ArcLabelConfig; + +export type ArcLabelCalloutGeometry = { + pathData: string; + labelX: number; + labelY: number; + textAnchor: 'start' | 'end'; +}; + +/** + * Reactive state shared by every per-layer ArcLabel variant. Holds derived + * geometry (callout path, offset centroid, rotation) and resolved text props. + */ +export class ArcLabelState { + #getProps: () => ArcLabelProps = () => ({}) as ArcLabelProps; + + constructor(getProps: () => ArcLabelProps) { + this.#getProps = getProps; + } + + midAngle = $derived.by(() => { + const { startAngle, endAngle } = this.#getProps(); + return startAngle != null && endAngle != null ? (startAngle + endAngle) / 2 : 0; + }); + + offsetCentroid = $derived.by<[number, number] | undefined>(() => { + const { centroid, offset = 0, startAngle, endAngle } = this.#getProps(); + if (!centroid) return centroid; + if (!offset || startAngle == null || endAngle == null) return centroid; + const angle = this.midAngle - Math.PI / 2; + return [centroid[0] + Math.cos(angle) * offset, centroid[1] + Math.sin(angle) * offset]; + }); + + effectiveOuterPadding = $derived.by(() => { + const props = this.#getProps(); + const base = props.outerPadding ?? 0; + if (props.placement === 'outer' || props.placement === 'middle') { + return base + (props.offset ?? 0); + } + return base; + }); + + effectiveInnerPadding = $derived.by(() => { + const props = this.#getProps(); + if (props.placement === 'inner' || props.placement === 'middle') return props.offset ?? 0; + return 0; + }); + + effectiveCalloutLineLength = $derived( + (this.#getProps().calloutLineLength ?? 16) + (this.#getProps().offset ?? 0) + ); + + centroidRotation = $derived.by(() => { + const { startAngle, endAngle, placement } = this.#getProps(); + if (startAngle == null || endAngle == null) return 0; + let deg = radiansToDegrees(this.midAngle); + if (placement === 'centroid-radial') { + deg = deg - 90; + } else if (placement !== 'centroid-rotated') { + return 0; + } + deg = ((deg + 180) % 360) - 180; + if (deg > 90) deg -= 180; + else if (deg < -90) deg += 180; + return deg; + }); + + calloutGeometry = $derived.by(() => { + const { + placement, + startAngle, + endAngle, + outerRadius, + calloutLabelOffset = 12, + calloutPadding = 4, + } = this.#getProps(); + if (placement !== 'callout' || startAngle == null || endAngle == null || outerRadius == null) { + return null; + } + + const angle = this.midAngle - Math.PI / 2; + const cos = Math.cos(angle); + const sin = Math.sin(angle); + + const x0 = cos * outerRadius; + const y0 = sin * outerRadius; + + const bendRadius = outerRadius + this.effectiveCalloutLineLength; + const x1 = cos * bendRadius; + const y1 = sin * bendRadius; + + const onRightSide = cos >= 0; + const x2 = x1 + (onRightSide ? calloutLabelOffset : -calloutLabelOffset); + const y2 = y1; + + return { + pathData: `M${x0},${y0}L${x1},${y1}L${x2},${y2}`, + labelX: x2 + (onRightSide ? calloutPadding : -calloutPadding), + labelY: y2, + textAnchor: onRightSide ? 'start' : 'end', + }; + }); + + arcTextProps = $derived.by(() => { + const props = this.#getProps(); + const { placement = 'centroid', startOffset, outerPadding, getArcTextProps } = props; + + if (placement === 'centroid') { + if (this.offsetCentroid) { + return { + x: this.offsetCentroid[0], + y: this.offsetCentroid[1], + textAnchor: 'middle' as const, + verticalAnchor: 'middle' as const, + }; + } + return getArcTextProps?.('centroid', { startOffset, outerPadding }) ?? {}; + } + + if (placement === 'centroid-rotated' || placement === 'centroid-radial') { + if (this.offsetCentroid) { + return { + x: this.offsetCentroid[0], + y: this.offsetCentroid[1], + textAnchor: 'middle' as const, + verticalAnchor: 'middle' as const, + rotate: this.centroidRotation, + }; + } + return getArcTextProps?.('centroid', { startOffset, outerPadding }) ?? {}; + } + + if (placement === 'callout') { + const g = this.calloutGeometry; + if (g) { + return { + x: g.labelX, + y: g.labelY, + textAnchor: g.textAnchor, + verticalAnchor: 'middle' as const, + }; + } + return {}; + } + + return ( + getArcTextProps?.(placement, { + startOffset, + outerPadding: this.effectiveOuterPadding, + innerPadding: this.effectiveInnerPadding, + }) ?? {} + ); + }); +} diff --git a/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.svelte b/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.svelte new file mode 100644 index 000000000..c3d5a1bb4 --- /dev/null +++ b/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.svelte @@ -0,0 +1,24 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.svg.svelte b/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.svg.svelte new file mode 100644 index 000000000..cc65aae5f --- /dev/null +++ b/packages/layerchart/src/lib/components/ArcLabel/ArcLabel.svg.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Bar.svelte b/packages/layerchart/src/lib/components/Bar.svelte deleted file mode 100644 index 552c6185f..000000000 --- a/packages/layerchart/src/lib/components/Bar.svelte +++ /dev/null @@ -1,333 +0,0 @@ - - - - -{#if ctx.radial} - -{:else} - -{/if} diff --git a/packages/layerchart/src/lib/components/Bar/Bar.base.svelte b/packages/layerchart/src/lib/components/Bar/Bar.base.svelte new file mode 100644 index 000000000..6a0da54e7 --- /dev/null +++ b/packages/layerchart/src/lib/components/Bar/Bar.base.svelte @@ -0,0 +1,128 @@ + + + + +{#if c.ctx.radial} + +{:else} + +{/if} diff --git a/packages/layerchart/src/lib/components/Bar/Bar.canvas.svelte b/packages/layerchart/src/lib/components/Bar/Bar.canvas.svelte new file mode 100644 index 000000000..f7a0bf9f8 --- /dev/null +++ b/packages/layerchart/src/lib/components/Bar/Bar.canvas.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Bar/Bar.shared.svelte.ts b/packages/layerchart/src/lib/components/Bar/Bar.shared.svelte.ts new file mode 100644 index 000000000..4de4d3aca --- /dev/null +++ b/packages/layerchart/src/lib/components/Bar/Bar.shared.svelte.ts @@ -0,0 +1,229 @@ +import type { SVGAttributes } from 'svelte/elements'; +import { greatestAbs } from '@layerstack/utils'; + +import { createDimensionGetter, type Insets } from '$lib/utils/rect.svelte.js'; +import { accessor, type Accessor } from '$lib/utils/common.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import { type MotionProp } from '$lib/utils/motion.svelte.js'; +import type { CommonEvents, CommonStyleProps, Without } from '$lib/utils/types.js'; + +export type BarPropsWithoutHTML = { + /** Data to render the bar from */ + data: Object; + /** Override `x` from context. @default ctx.x */ + x?: Accessor; + /** Override `y` from context. @default ctx.y */ + y?: Accessor; + /** Override `x1` from context. @default ctx.x1 */ + x1?: Accessor; + /** Override `y1` from context. @default ctx.y1 */ + y1?: Accessor; + /** Series key to use for accessor. */ + seriesKey?: string; + /** Padding between stacked bars. */ + stackPadding?: number; + radius?: number; + insets?: Insets; + initialX?: number; + initialY?: number; + initialHeight?: number; + initialWidth?: number; + /** Fixed width in pixels. */ + width?: number; + /** Fixed height in pixels. */ + height?: number; + rounded?: + | 'all' + | 'none' + | 'edge' + | 'top' + | 'bottom' + | 'left' + | 'right' + | 'top-left' + | 'top-right' + | 'bottom-left' + | 'bottom-right'; + motion?: MotionProp<'x' | 'y' | 'width' | 'height'>; + /** Setup pointer events to show tooltip for related data. */ + tooltip?: boolean; +} & CommonStyleProps; + +export type BarProps = BarPropsWithoutHTML & + Without< + Omit, 'width' | 'height' | 'x' | 'y' | 'offset'>, + BarPropsWithoutHTML + > & + CommonEvents; + +/** + * Reactive state shared by every per-layer Bar variant. Holds the resolved + * accessors, dimensions, corner-rounding flags, and motion-aware initial values. + */ +export class BarState { + #getProps: () => BarProps = () => ({}) as BarProps; + ctx: ChartState = getChartContext(); + + constructor(getProps: () => BarProps) { + this.#getProps = getProps; + } + + series = $derived.by(() => { + const seriesKey = this.#getProps().seriesKey; + return seriesKey ? this.ctx.series.series.find((s) => s.key === seriesKey) : undefined; + }); + + seriesAccessor = $derived( + this.series ? this.series.value ?? (this.series.data ? undefined : this.series.key) : undefined + ); + + stackAccessors = $derived.by(() => { + const seriesKey = this.#getProps().seriesKey; + return seriesKey && this.ctx.series.isStacked + ? this.ctx.series.getStackAccessors(seriesKey) + : null; + }); + + x = $derived.by(() => { + const xProp = this.#getProps().x; + return ( + xProp ?? + (this.ctx.valueAxis === 'x' ? this.stackAccessors?.value ?? this.seriesAccessor : undefined) ?? + this.ctx.x + ); + }); + y = $derived.by(() => { + const yProp = this.#getProps().y; + return ( + yProp ?? + (this.ctx.valueAxis === 'y' ? this.stackAccessors?.value ?? this.seriesAccessor : undefined) ?? + this.ctx.y + ); + }); + x1 = $derived(this.#getProps().x1); + y1 = $derived(this.#getProps().y1); + + seriesIndex = $derived.by(() => { + const seriesKey = this.#getProps().seriesKey; + return seriesKey + ? this.ctx.series.visibleSeries.findIndex((s) => s.key === seriesKey) + : undefined; + }); + seriesCount = $derived(this.ctx.series.visibleSeries.length); + + stackInsets = $derived.by(() => { + const stackPadding = this.#getProps().stackPadding ?? 0; + if (!this.ctx.series.isStacked || stackPadding === 0 || this.seriesIndex === undefined) { + return undefined; + } + + const isFirst = this.seriesIndex === 0; + const isLast = this.seriesIndex === this.seriesCount - 1; + const stackInset = stackPadding / 2; + + if (this.ctx.valueAxis === 'y') { + return { + bottom: isFirst ? undefined : stackInset, + top: isLast ? undefined : stackInset, + }; + } + return { + left: isFirst ? undefined : stackInset, + right: isLast ? undefined : stackInset, + }; + }); + + insets = $derived(this.#getProps().insets ?? this.stackInsets); + + getDimensions = $derived( + createDimensionGetter(this.ctx, () => ({ + x: this.x, + y: this.y, + x1: this.x1, + y1: this.y1, + insets: this.insets, + })) + ); + + scaleDimensions = $derived( + this.getDimensions(this.#getProps().data) ?? { x: 0, y: 0, width: 0, height: 0 } + ); + + dimensions = $derived.by(() => { + let { x, y, width, height } = this.scaleDimensions; + const props = this.#getProps(); + + if (props.width != null) { + x = x + (width - props.width) / 2; + width = props.width; + } + + if (props.height != null) { + y = y + (height - props.height) / 2; + height = props.height; + } + + return { x, y, width, height }; + }); + + valueAccessor = $derived(accessor(this.ctx.valueAxis === 'y' ? this.y : this.x)); + resolvedValue = $derived.by(() => { + const value = this.valueAccessor(this.#getProps().data); + return Array.isArray(value) ? greatestAbs(value) : value; + }); + + /** Resolved `rounded="edge"` based on orientation and value */ + rounded = $derived.by(() => { + const roundedProp = this.#getProps().rounded ?? 'all'; + if (roundedProp !== 'edge') return roundedProp; + if (this.ctx.valueAxis === 'y') { + return this.resolvedValue >= 0 && this.ctx.yRange[0] > this.ctx.yRange[1] + ? 'top' + : 'bottom'; + } + return this.resolvedValue >= 0 && this.ctx.xRange[0] < this.ctx.xRange[1] ? 'right' : 'left'; + }); + + corners = $derived.by<[number, number, number, number]>(() => { + const radius = this.#getProps().radius ?? 0; + const rounded = this.rounded; + const topLeft = ['all', 'top', 'left', 'top-left'].includes(rounded); + const topRight = ['all', 'top', 'right', 'top-right'].includes(rounded); + const bottomLeft = ['all', 'bottom', 'left', 'bottom-left'].includes(rounded); + const bottomRight = ['all', 'bottom', 'right', 'bottom-right'].includes(rounded); + return [ + topLeft ? radius : 0, + topRight ? radius : 0, + bottomRight ? radius : 0, + bottomLeft ? radius : 0, + ]; + }); + + resolvedInitialY = $derived.by(() => { + const props = this.#getProps(); + return ( + props.initialY ?? + (props.motion && this.ctx.valueAxis === 'y' + ? Math.max(this.ctx.yRange[0], this.ctx.yRange[1]) + : undefined) + ); + }); + resolvedInitialHeight = $derived.by(() => { + const props = this.#getProps(); + return props.initialHeight ?? (props.motion && this.ctx.valueAxis === 'y' ? 0 : undefined); + }); + resolvedInitialX = $derived.by(() => { + const props = this.#getProps(); + return ( + props.initialX ?? + (props.motion && this.ctx.valueAxis === 'x' + ? Math.min(this.ctx.xRange[0], this.ctx.xRange[1]) + : undefined) + ); + }); + resolvedInitialWidth = $derived.by(() => { + const props = this.#getProps(); + return props.initialWidth ?? (props.motion && this.ctx.valueAxis === 'x' ? 0 : undefined); + }); +} diff --git a/packages/layerchart/src/lib/components/Bar/Bar.svelte b/packages/layerchart/src/lib/components/Bar/Bar.svelte new file mode 100644 index 000000000..375a903e7 --- /dev/null +++ b/packages/layerchart/src/lib/components/Bar/Bar.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Bar/Bar.svg.svelte b/packages/layerchart/src/lib/components/Bar/Bar.svg.svelte new file mode 100644 index 000000000..d4ca0705e --- /dev/null +++ b/packages/layerchart/src/lib/components/Bar/Bar.svg.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Bars.svelte b/packages/layerchart/src/lib/components/Bars.svelte deleted file mode 100644 index d6cefaa17..000000000 --- a/packages/layerchart/src/lib/components/Bars.svelte +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - {#if children} - {@render children()} - {:else} - {#each data as d, i (key(d, i))} - onBarClick(e, { data: d })} - {...extractLayerProps(restProps, 'lc-bars-bar')} - /> - {/each} - {/if} - diff --git a/packages/layerchart/src/lib/components/Bars/Bars.base.svelte b/packages/layerchart/src/lib/components/Bars/Bars.base.svelte new file mode 100644 index 000000000..4775b1cd3 --- /dev/null +++ b/packages/layerchart/src/lib/components/Bars/Bars.base.svelte @@ -0,0 +1,67 @@ + + + + + + {#if children} + {@render children()} + {:else} + {#each c.data as d, i (key(d, i))} + onBarClick(e, { data: d })} + {...extractLayerProps(restProps, 'lc-bars-bar')} + /> + {/each} + {/if} + diff --git a/packages/layerchart/src/lib/components/Bars/Bars.canvas.svelte b/packages/layerchart/src/lib/components/Bars/Bars.canvas.svelte new file mode 100644 index 000000000..8467d2f29 --- /dev/null +++ b/packages/layerchart/src/lib/components/Bars/Bars.canvas.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Bars/Bars.shared.svelte.ts b/packages/layerchart/src/lib/components/Bars/Bars.shared.svelte.ts new file mode 100644 index 000000000..5ad7ba837 --- /dev/null +++ b/packages/layerchart/src/lib/components/Bars/Bars.shared.svelte.ts @@ -0,0 +1,54 @@ +import type { Snippet } from 'svelte'; +import type { BarProps, BarPropsWithoutHTML } from '../Bar/Bar.shared.svelte.js'; +import { chartDataArray } from '$lib/utils/common.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; + +export type BarsPropsWithoutHTML = { + /** Override the data from the context. */ + data?: any; + /** @default (d, index) => index */ + key?: (d: any, index: number) => any; + /** Event dispatched when an individual bar is clicked. */ + onBarClick?: (e: MouseEvent, detail: { data: any }) => void; + /** Series key to use for accessor. */ + seriesKey?: string; + /** Padding between stacked bars. */ + stackPadding?: number; + children?: Snippet; + // TODO: investigate + [key: string]: any; +} & Omit; + +export type BarsProps = BarsPropsWithoutHTML & Omit; + +/** + * Reactive state shared by every per-layer Bars variant. + */ +export class BarsState { + #getProps: () => BarsProps = () => ({}) as BarsProps; + ctx: ChartState = getChartContext(); + + constructor(getProps: () => BarsProps) { + this.#getProps = getProps; + this.ctx.registerComponent({ + name: 'Bars', + kind: 'mark', + markInfo: () => { + const p = getProps(); + return { + data: p.data, + seriesKey: p.seriesKey, + color: p.fill as string | undefined, + }; + }, + }); + } + + series = $derived.by(() => { + const seriesKey = this.#getProps().seriesKey; + return seriesKey ? this.ctx.series.series.find((s) => s.key === seriesKey) : undefined; + }); + seriesData = $derived(this.series?.data); + data = $derived(chartDataArray(this.#getProps().data ?? this.seriesData ?? this.ctx.data)); +} diff --git a/packages/layerchart/src/lib/components/Bars/Bars.svelte b/packages/layerchart/src/lib/components/Bars/Bars.svelte new file mode 100644 index 000000000..f81ada97c --- /dev/null +++ b/packages/layerchart/src/lib/components/Bars/Bars.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Bars/Bars.svg.svelte b/packages/layerchart/src/lib/components/Bars/Bars.svg.svelte new file mode 100644 index 000000000..654530c2a --- /dev/null +++ b/packages/layerchart/src/lib/components/Bars/Bars.svg.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.base.svelte b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.base.svelte index 7e8d192bb..74e372510 100644 --- a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.base.svelte +++ b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.base.svelte @@ -155,7 +155,7 @@ {#if typeof points === 'function'} {@render points(snippetProps)} {:else if points} - {#await import('../Points.svelte') then { default: Points }} + {#await import('../Points/Points.svelte') then { default: Points }} {#each context.series.visibleSeries as s, i (s.key)} !labelSeriesKey || s.key === labelSeriesKey) as s, i (s.key)} diff --git a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts index 1b5d3dd10..bf750fcc7 100644 --- a/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts +++ b/packages/layerchart/src/lib/components/ChartChildren/ChartChildren.shared.svelte.ts @@ -6,17 +6,17 @@ import type { AnyScale } from '$lib/utils/scales.svelte.js'; import type Axis from '../Axis/Axis.svelte'; import type Area from '../Area/Area.svelte'; import type Arc from '../Arc/Arc.svelte'; -import type Bars from '../Bars.svelte'; +import type Bars from '../Bars/Bars.svelte'; import type BrushContext from '../BrushContext.svelte'; import type Canvas from '../layers/Canvas.svelte'; import type Grid from '../Grid/Grid.svelte'; import type Group from '../Group/Group.svelte'; import type Highlight from '../Highlight/Highlight.svelte'; -import type Labels from '../Labels.svelte'; +import type Labels from '../Labels/Labels.svelte'; import type Legend from '../Legend.svelte'; import type Line from '../Line/Line.svelte'; -import type Pie from '../Pie.svelte'; -import type Points from '../Points.svelte'; +import type Pie from '../Pie/Pie.svelte'; +import type Points from '../Points/Points.svelte'; import type Rule from '../Rule/Rule.svelte'; import type Spline from '../Spline/Spline.svelte'; import type Svg from '../layers/Svg.svelte'; diff --git a/packages/layerchart/src/lib/components/Highlight/Highlight.base.svelte b/packages/layerchart/src/lib/components/Highlight/Highlight.base.svelte index fa023dff0..d43e8fc4d 100644 --- a/packages/layerchart/src/lib/components/Highlight/Highlight.base.svelte +++ b/packages/layerchart/src/lib/components/Highlight/Highlight.base.svelte @@ -86,7 +86,7 @@ {#if typeof bar === 'function'} {@render bar()} {:else} - {#await import('../Bar.svelte') then { default: Bar }} + {#await import('../Bar/Bar.svelte') then { default: Bar }} - import Text, { type TextProps } from './Text/Text.svelte'; - import { type ComponentProps, type Snippet } from 'svelte'; - import { format as formatValue, type FormatType, type FormatConfig } from '@layerstack/utils'; - import type { Without } from '$lib/utils/types.js'; - import Points, { type Point } from './Points.svelte'; - import { accessor, type Accessor } from '../utils/common.js'; - - export type LabelsPropsWithoutHTML = { - /** - * Override data instead of using context - */ - data?: T; - - /** - * Override display value accessor. By default, uses `y` unless yScale is band scale - */ - value?: Accessor; - - /** - * The fill color of the label, which can either be a string or an accessor function - * that returns a valid `fill` color value. - * - * The accessor is useful for dynamic fill colors based on the data the label represents. - */ - fill?: string | Accessor; - - /** - * Override `x` accessor from Chart context - */ - x?: Accessor; - - /** - * Override `y` accessor from Chart context - */ - y?: Accessor; - - /** - * Series key to use for accessor. Only applicable if `` uses `series` and `x`/`y` are not set. - */ - seriesKey?: string; - - /** - * The placement of the label relative to the point. - * - `outside`: outside the bar/point. - * - `inside`: inside the bar/point near the value edge. - * - `middle`: aligned to the value edge with a middle anchor. - * - `center`: centered within the bar body (between the value edge and baseline). - * - `smart`: dynamically positions labels based on neighboring point values (peak, trough, rising, falling). - * @default 'outside' - */ - placement?: 'inside' | 'outside' | 'middle' | 'center' | 'smart'; - - /** - * The offset of the label from the point - * - * @default placement === 'center' || placement === 'middle' ? 0 : 4 - */ - offset?: number; - - /** - * The format of the label - */ - format?: FormatType | FormatConfig; - - /** - * Define unique value for {#each} `(key)` expressions to improve transitions. - * `index` position used by default - * - * @default (d, index) => index - */ - key?: (d: T, index: number) => any; - - children?: Snippet<[{ data: Point; textProps: ComponentProps }]>; - }; - - export type LabelsProps = LabelsPropsWithoutHTML & - Without>; - - - - - - - {#snippet children({ points })} - {#each points as point, i (key(point.data, i))} - {@const baseProps = getTextProps(point, points, i)} - {@const textProps = extractLayerProps(baseProps, 'lc-labels-text')} - {#if childrenProp} - {@render childrenProp({ data: point, textProps })} - {:else} - - {/if} - {/each} - {/snippet} - - - - diff --git a/packages/layerchart/src/lib/components/Labels/Labels.base.svelte b/packages/layerchart/src/lib/components/Labels/Labels.base.svelte new file mode 100644 index 000000000..4301a897c --- /dev/null +++ b/packages/layerchart/src/lib/components/Labels/Labels.base.svelte @@ -0,0 +1,92 @@ + + + + + + + {#snippet children({ points }: { points: Point[] })} + {#each points as point, i (key(point.data, i))} + {@const baseProps = c.getTextProps(point, points, i)} + {@const textProps = extractLayerProps(baseProps, 'lc-labels-text')} + {#if childrenProp} + {@render childrenProp({ data: point, textProps })} + {:else} + + {/if} + {/each} + {/snippet} + + + + diff --git a/packages/layerchart/src/lib/components/Labels/Labels.canvas.svelte b/packages/layerchart/src/lib/components/Labels/Labels.canvas.svelte new file mode 100644 index 000000000..12755e7f9 --- /dev/null +++ b/packages/layerchart/src/lib/components/Labels/Labels.canvas.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Labels/Labels.html.svelte b/packages/layerchart/src/lib/components/Labels/Labels.html.svelte new file mode 100644 index 000000000..2cc55dd51 --- /dev/null +++ b/packages/layerchart/src/lib/components/Labels/Labels.html.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Labels/Labels.shared.svelte.ts b/packages/layerchart/src/lib/components/Labels/Labels.shared.svelte.ts new file mode 100644 index 000000000..9aa81434e --- /dev/null +++ b/packages/layerchart/src/lib/components/Labels/Labels.shared.svelte.ts @@ -0,0 +1,225 @@ +import type { ComponentProps, Snippet } from 'svelte'; +import { format as formatValue, type FormatType, type FormatConfig } from '@layerstack/utils'; + +import type { Without } from '$lib/utils/types.js'; +import { accessor, type Accessor } from '$lib/utils/common.js'; +import { isScaleBand } from '$lib/utils/scales.svelte.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import { createDimensionGetter } from '$lib/utils/rect.svelte.js'; +import type { TextProps } from '../Text/Text.shared.svelte.js'; +import type { Point } from '../Points/Points.shared.svelte.js'; + +export type LabelsPropsWithoutHTML = { + /** Override data instead of using context */ + data?: T; + /** Override display value accessor. By default, uses `y` unless yScale is band scale */ + value?: Accessor; + /** The fill color of the label, string or accessor */ + fill?: string | Accessor; + /** Override `x` accessor from Chart context */ + x?: Accessor; + /** Override `y` accessor from Chart context */ + y?: Accessor; + /** Series key to use for accessor. */ + seriesKey?: string; + /** @default 'outside' */ + placement?: 'inside' | 'outside' | 'middle' | 'center' | 'smart'; + /** @default placement === 'center' || placement === 'middle' ? 0 : 4 */ + offset?: number; + /** The format of the label */ + format?: FormatType | FormatConfig; + /** @default (d, index) => index */ + key?: (d: T, index: number) => any; + children?: Snippet<[{ data: Point; textProps: TextProps }]>; +}; + +export type LabelsProps = LabelsPropsWithoutHTML & + Without>; + +/** + * Reactive state shared by every per-layer Labels variant. Holds the + * `getTextProps(point, points, i)` helper that computes per-point label + * positioning + opacity, plus the resolved opacity. + */ +export class LabelsState { + #getProps: () => LabelsProps = () => ({}) as LabelsProps; + ctx: ChartState = getChartContext(); + + constructor(getProps: () => LabelsProps) { + this.#getProps = getProps; + this.ctx.registerComponent({ name: 'Labels', kind: 'composite-mark' }); + } + + getDimensions = $derived( + createDimensionGetter(this.ctx, () => ({ + x: this.#getProps().x, + y: this.#getProps().y, + })) + ); + + series = $derived.by(() => { + const seriesKey = this.#getProps().seriesKey; + return seriesKey ? this.ctx.series.series.find((s) => s.key === seriesKey) : undefined; + }); + + derivedOpacity = $derived.by(() => { + const opacity = (this.#getProps() as any).opacity as number | undefined; + return ( + opacity ?? + (this.series?.key == null || + this.ctx.series.visibleSeries.length <= 1 || + this.ctx.series.isHighlighted(this.series.key, true) + ? 1 + : 0.1) + ); + }); + + getTextProps(point: Point, points?: Point[], i?: number): TextProps { + const props = this.#getProps(); + const placement = props.placement ?? 'outside'; + const offset = props.offset ?? (placement === 'center' || placement === 'middle' ? 0 : 4); + + const pointValue = isScaleBand(this.ctx.yScale) ? point.xValue : point.yValue; + const isLowEdge = point.edgeIndex != null ? point.edgeIndex === 0 : pointValue < 0; + + const fillValue = + typeof props.fill === 'function' ? accessor(props.fill)(point.data) : props.fill; + + const displayValue = props.value + ? accessor(props.value)(point.data) + : isScaleBand(this.ctx.yScale) + ? point.xValue + : point.yValue; + + const formattedValue = formatValue( + displayValue, + // @ts-expect-error - improve types + props.format ?? + (props.value + ? undefined + : isScaleBand(this.ctx.yScale) + ? this.ctx.xScale.tickFormat?.() + : this.ctx.yScale.tickFormat?.()) + ); + + let result: TextProps; + + if (isScaleBand(this.ctx.yScale)) { + if (placement === 'center') { + const dims = + this.getDimensions(point.data) ?? { x: point.x, y: point.y, width: 0, height: 0 }; + result = { + value: formattedValue, + fill: fillValue, + x: dims.x + dims.width / 2, + y: dims.y + dims.height / 2, + textAnchor: 'middle', + verticalAnchor: 'middle', + capHeight: '.6rem', + } as TextProps; + } else if (isLowEdge) { + result = { + value: formattedValue, + fill: fillValue, + x: point.x + (placement === 'outside' ? -offset : offset), + y: point.y, + textAnchor: + placement === 'middle' ? 'middle' : placement === 'outside' ? 'end' : 'start', + verticalAnchor: 'middle', + capHeight: '.6rem', + } as TextProps; + } else { + result = { + value: formattedValue, + fill: fillValue, + x: point.x + (placement === 'outside' ? offset : -offset), + y: point.y, + textAnchor: + placement === 'middle' ? 'middle' : placement === 'outside' ? 'start' : 'end', + verticalAnchor: 'middle', + capHeight: '.6rem', + } as TextProps; + } + } else { + if (placement === 'center') { + const dims = + this.getDimensions(point.data) ?? { x: point.x, y: point.y, width: 0, height: 0 }; + result = { + value: formattedValue, + fill: fillValue, + x: dims.x + dims.width / 2, + y: dims.y + dims.height / 2, + capHeight: '.6rem', + textAnchor: 'middle', + verticalAnchor: 'middle', + } as TextProps; + } else if (isLowEdge) { + result = { + value: formattedValue, + fill: fillValue, + x: point.x, + y: point.y + (placement === 'outside' ? offset : -offset), + capHeight: '.6rem', + textAnchor: 'middle', + verticalAnchor: + placement === 'middle' ? 'middle' : placement === 'outside' ? 'start' : 'end', + } as TextProps; + } else { + result = { + value: formattedValue, + fill: fillValue, + x: point.x, + y: point.y + (placement === 'outside' ? -offset : offset), + capHeight: '.6rem', + textAnchor: 'middle', + verticalAnchor: + placement === 'middle' ? 'middle' : placement === 'outside' ? 'end' : 'start', + } as TextProps; + } + } + + if (placement === 'smart' && points != null && i != null) { + const getValue = (p: Point): number => + isScaleBand(this.ctx.yScale) ? p.xValue : p.yValue; + const curr = getValue(point); + const prev = i > 0 ? getValue(points[i - 1]) : curr; + const next = i < points.length - 1 ? getValue(points[i + 1]) : curr; + + const xPrevTight = Math.abs(prev - curr) < offset; + const xNextTight = Math.abs(curr - next) < offset; + const isPeak = (prev <= curr && curr >= next) || (xPrevTight && xNextTight); + const isTrough = (prev >= curr && curr <= next) || (xPrevTight && xNextTight); + const isRising = !isPeak && !isTrough && prev < curr; + const isFalling = !isPeak && !isTrough && prev >= curr; + + return { + ...result, + x: point.x, + y: point.y, + dx: isRising + ? xPrevTight + ? offset + : -offset + : isFalling + ? xNextTight + ? -offset + : offset + : 0, + dy: isPeak ? -offset : isTrough ? offset : 0, + textAnchor: isRising + ? xPrevTight + ? 'start' + : 'end' + : isFalling + ? xNextTight + ? 'end' + : 'start' + : 'middle', + verticalAnchor: isPeak ? 'end' : isTrough ? 'start' : 'middle', + } as TextProps; + } + + return result; + } +} diff --git a/packages/layerchart/src/lib/components/Labels/Labels.svelte b/packages/layerchart/src/lib/components/Labels/Labels.svelte new file mode 100644 index 000000000..464af18da --- /dev/null +++ b/packages/layerchart/src/lib/components/Labels/Labels.svelte @@ -0,0 +1,23 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Labels/Labels.svg.svelte b/packages/layerchart/src/lib/components/Labels/Labels.svg.svelte new file mode 100644 index 000000000..854d4a545 --- /dev/null +++ b/packages/layerchart/src/lib/components/Labels/Labels.svg.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Pie.svelte b/packages/layerchart/src/lib/components/Pie.svelte deleted file mode 100644 index 30ce25cac..000000000 --- a/packages/layerchart/src/lib/components/Pie.svelte +++ /dev/null @@ -1,156 +0,0 @@ - - - - -{#if children} - {@render children({ arcs })} -{:else} - {#each arcs as arc} - - {/each} -{/if} diff --git a/packages/layerchart/src/lib/components/Pie/Pie.base.svelte b/packages/layerchart/src/lib/components/Pie/Pie.base.svelte new file mode 100644 index 000000000..db8bac235 --- /dev/null +++ b/packages/layerchart/src/lib/components/Pie/Pie.base.svelte @@ -0,0 +1,69 @@ + + + + +{#if children} + {@render children({ arcs: c.arcs })} +{:else} + {#each c.arcs as arc} + + {/each} +{/if} diff --git a/packages/layerchart/src/lib/components/Pie/Pie.canvas.svelte b/packages/layerchart/src/lib/components/Pie/Pie.canvas.svelte new file mode 100644 index 000000000..6dc9145a3 --- /dev/null +++ b/packages/layerchart/src/lib/components/Pie/Pie.canvas.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Pie/Pie.shared.svelte.ts b/packages/layerchart/src/lib/components/Pie/Pie.shared.svelte.ts new file mode 100644 index 000000000..0283de0be --- /dev/null +++ b/packages/layerchart/src/lib/components/Pie/Pie.shared.svelte.ts @@ -0,0 +1,93 @@ +import type { Snippet } from 'svelte'; +import { pie as d3pie, type PieArcDatum } from 'd3-shape'; +import { min, max } from 'd3-array'; + +import { degreesToRadians } from '$lib/utils/math.js'; +import { createMotion, type MotionProp } from '$lib/utils/motion.svelte.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; + +export type PiePropsWithoutHTML = { + data?: any[]; + /** Range [min,max] in degrees. See also startAngle/endAngle @default [0, 360] */ + range?: [number, number] | number[]; + /** Start angle in radians */ + startAngle?: number; + /** End angle in radians */ + endAngle?: number; + /** + * Define innerRadius. + * value >= 1: discrete value + * value > 0: percent of `outerRadius` + * value < 0: offset of `outerRadius` + * default: yRange min + */ + innerRadius?: number; + /** Define outerRadius. Defaults to yRange max/2 (ie. chart height / 2) */ + outerRadius?: number; + /** @default 0 */ + cornerRadius?: number; + /** @default 0 */ + padAngle?: number; + /** @default 0 */ + offset?: number; + /** Setup pointer events to show tooltip for related data */ + tooltip?: boolean; + /** Sort function to sort the arcs */ + sort?: ((a: any, b: any) => number) | null; + children?: Snippet<[{ arcs: PieArcDatum[] }]>; + motion?: MotionProp; +}; + +export type PieProps = PiePropsWithoutHTML; + +/** + * Reactive state shared by every per-layer Pie variant. Builds the + * d3-pie generator and resolved `arcs` array. + */ +export class PieState { + #getProps: () => PieProps = () => ({}) as PieProps; + ctx: ChartState = getChartContext(); + + #motionEndAngle!: ReturnType>; + + constructor(getProps: () => PieProps) { + this.#getProps = getProps; + this.#motionEndAngle = createMotion(0, () => this.endAngle, getProps().motion); + } + + range = $derived(this.#getProps().range ?? ([0, 360] as [number, number])); + + endAngle = $derived.by(() => { + const props = this.#getProps(); + return ( + props.endAngle ?? + degreesToRadians( + (this.ctx.config.xRange ? max(this.ctx.config.xRange as number[]) : max(this.range))! + ) + ); + }); + + pie = $derived.by(() => { + const props = this.#getProps(); + let _pie = d3pie() + .startAngle( + props.startAngle ?? + degreesToRadians( + (this.ctx.config.xRange ? min(this.ctx.config.xRange as number[]) : min(this.range))! + ) + ) + .endAngle(this.#motionEndAngle.current) + .padAngle(props.padAngle ?? 0) + .value(this.ctx.x); + + if (props.sort === null) { + _pie = _pie.sort(null); + } else if (props.sort) { + _pie = _pie.sort(props.sort); + } + return _pie; + }); + + arcs = $derived(this.pie(this.#getProps().data ?? (Array.isArray(this.ctx.data) ? this.ctx.data : []))); +} diff --git a/packages/layerchart/src/lib/components/Pie/Pie.svelte b/packages/layerchart/src/lib/components/Pie/Pie.svelte new file mode 100644 index 000000000..ea5e3c103 --- /dev/null +++ b/packages/layerchart/src/lib/components/Pie/Pie.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Pie/Pie.svg.svelte b/packages/layerchart/src/lib/components/Pie/Pie.svg.svelte new file mode 100644 index 000000000..a3c0cca65 --- /dev/null +++ b/packages/layerchart/src/lib/components/Pie/Pie.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Points.svelte b/packages/layerchart/src/lib/components/Points.svelte deleted file mode 100644 index 4a07f62f2..000000000 --- a/packages/layerchart/src/lib/components/Points.svelte +++ /dev/null @@ -1,200 +0,0 @@ - - - - -{#if children} - {@render children({ points })} -{:else} - {#each points as point} - - {/each} -{/if} diff --git a/packages/layerchart/src/lib/components/Points/Points.base.svelte b/packages/layerchart/src/lib/components/Points/Points.base.svelte new file mode 100644 index 000000000..71fe38549 --- /dev/null +++ b/packages/layerchart/src/lib/components/Points/Points.base.svelte @@ -0,0 +1,75 @@ + + + + +{#if children} + {@render children({ points: c.points })} +{:else} + {#each c.points as point} + + {/each} +{/if} diff --git a/packages/layerchart/src/lib/components/Points/Points.canvas.svelte b/packages/layerchart/src/lib/components/Points/Points.canvas.svelte new file mode 100644 index 000000000..d54e705f3 --- /dev/null +++ b/packages/layerchart/src/lib/components/Points/Points.canvas.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Points/Points.html.svelte b/packages/layerchart/src/lib/components/Points/Points.html.svelte new file mode 100644 index 000000000..a40c67226 --- /dev/null +++ b/packages/layerchart/src/lib/components/Points/Points.html.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Points/Points.shared.svelte.ts b/packages/layerchart/src/lib/components/Points/Points.shared.svelte.ts new file mode 100644 index 000000000..4c9ef5b10 --- /dev/null +++ b/packages/layerchart/src/lib/components/Points/Points.shared.svelte.ts @@ -0,0 +1,170 @@ +import type { Snippet } from 'svelte'; +import { pointRadial } from 'd3-shape'; + +import type { CommonStyleProps, Without } from '$lib/utils/types.js'; +import { isScaleBand, type AnyScale } from '$lib/utils/scales.svelte.js'; +import { accessor, type Accessor } from '$lib/utils/common.js'; +import { getChartContext } from '$lib/contexts/chart.js'; +import type { ChartState } from '$lib/states/chart.svelte.js'; +import type { CircleProps } from '../Circle/Circle.shared.svelte.js'; + +export type Point = { + x: number; + y: number; + r: number; + xValue: any; + yValue: any; + data: any; + /** Index within array accessor (0 = start/low edge, 1 = end/high edge). Undefined for single-value points. */ + edgeIndex?: number; +}; + +type Offset = number | ((value: number, context: any) => number) | undefined; + +export type PointsPropsWithoutHTML = { + /** Override data instead of using context */ + data?: any; + /** Override `x` accessor from Chart context */ + x?: Accessor; + /** Override `y` accessor from Chart context */ + y?: Accessor; + /** Series key to use for accessor. */ + seriesKey?: string; + /** Override `r` accessor from Chart context @default 5 */ + r?: number; + /** The offset of the point in the x direction */ + offsetX?: Offset; + /** The offset of the point in the y direction */ + offsetY?: Offset; + children?: Snippet<[{ points: Point[] }]>; +} & CommonStyleProps; + +export type PointsProps = PointsPropsWithoutHTML & + Omit, 'ref'>; + +/** + * Reactive state shared by every per-layer Points variant. Holds the + * computed `points` array from the chart context. + */ +export class PointsState { + #getProps: () => PointsProps = () => ({}) as PointsProps; + ctx: ChartState = getChartContext(); + + constructor(getProps: () => PointsProps) { + this.#getProps = getProps; + this.ctx.registerComponent({ + name: 'Points', + kind: 'mark', + markInfo: () => { + const p = getProps(); + return { + data: p.data, + x: p.x, + y: p.y, + seriesKey: p.seriesKey, + color: (p.fill ?? p.stroke) as string | undefined, + }; + }, + }); + } + + series = $derived( + this.ctx.series.series.find((s) => s.key === this.#getProps().seriesKey) + ); + seriesAccessor = $derived( + this.series?.value ?? (this.series?.data ? undefined : this.series?.key) + ); + + stackAccessors = $derived.by(() => { + const seriesKey = this.#getProps().seriesKey; + return seriesKey && this.ctx.series.isStacked + ? this.ctx.series.getStackAccessors(seriesKey) + : null; + }); + + xAccessor = $derived( + accessor( + this.#getProps().x ?? + (this.ctx.valueAxis === 'x' ? this.seriesAccessor : undefined) ?? + this.ctx.x + ) + ); + + yAccessor = $derived.by(() => { + const props = this.#getProps(); + if (props.y) return accessor(props.y); + if (this.stackAccessors) return this.stackAccessors.y1; + if (Array.isArray(this.seriesAccessor) && this.ctx.valueAxis === 'y') { + return accessor(this.seriesAccessor[1]); + } + return accessor((this.ctx.valueAxis === 'y' ? this.seriesAccessor : undefined) ?? this.ctx.y); + }); + + pointsData = $derived(this.#getProps().data ?? this.series?.data ?? this.ctx.data); + + #getOffset( + value: any, + offset: Offset, + scale: AnyScale, + subScale?: AnyScale + ): number { + const seriesKey = this.#getProps().seriesKey; + if (typeof offset === 'function') { + return offset(value, this.ctx); + } else if (offset != null) { + return offset; + } else if (subScale && seriesKey) { + return subScale(seriesKey) + (subScale.bandwidth?.() ?? 0) / 2; + } else if (isScaleBand(scale) && !this.ctx.radial) { + return scale.bandwidth() / 2; + } + return 0; + } + + #getPointObject(xVal: number, yVal: number, d: any, edgeIndex?: number): Point { + const props = this.#getProps(); + const scaledX: number = this.ctx.xScale(xVal); + const scaledY: number = this.ctx.yScale(yVal); + + const x = + scaledX + + this.#getOffset(scaledX, props.offsetX, this.ctx.xScale, this.ctx.x1Scale ?? undefined); + const y = + scaledY + + this.#getOffset(scaledY, props.offsetY, this.ctx.yScale, this.ctx.y1Scale ?? undefined); + + const radialPoint = pointRadial(x, y); + + return { + x: this.ctx.radial ? radialPoint[0] : x, + y: this.ctx.radial ? radialPoint[1] : y, + r: this.ctx.config.r ? this.ctx.rGet(d) : props.r ?? 5, + xValue: xVal, + yValue: yVal, + data: d, + edgeIndex, + }; + } + + points = $derived.by(() => { + return this.pointsData.flatMap((d: any) => { + const xValue: number | number[] = this.xAccessor(d); + const yValue: number | number[] = this.yAccessor(d); + + if (Array.isArray(xValue)) { + return xValue + .filter(Boolean) + .map((xVal: number, i: number) => + this.#getPointObject(xVal, yValue as number, d, i) + ); + } else if (Array.isArray(yValue)) { + return yValue + .filter(Boolean) + .map((yVal: number, i: number) => this.#getPointObject(xValue as number, yVal, d, i)); + } else if (xValue != null && yValue != null) { + return this.#getPointObject(xValue as number, yValue as number, d); + } + return []; + }); + }); +} diff --git a/packages/layerchart/src/lib/components/Points/Points.svelte b/packages/layerchart/src/lib/components/Points/Points.svelte new file mode 100644 index 000000000..e42f5b5a8 --- /dev/null +++ b/packages/layerchart/src/lib/components/Points/Points.svelte @@ -0,0 +1,23 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Points/Points.svg.svelte b/packages/layerchart/src/lib/components/Points/Points.svg.svelte new file mode 100644 index 000000000..8c4599f86 --- /dev/null +++ b/packages/layerchart/src/lib/components/Points/Points.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/ArcChart.svelte b/packages/layerchart/src/lib/components/charts/ArcChart.svelte index 615ddf51e..317749919 100644 --- a/packages/layerchart/src/lib/components/charts/ArcChart.svelte +++ b/packages/layerchart/src/lib/components/charts/ArcChart.svelte @@ -7,7 +7,7 @@ import type { SeriesData } from './types.js'; import Arc from '../Arc/Arc.svelte'; - import ArcLabel, { type ArcLabelConfig } from '../ArcLabel.svelte'; + import ArcLabel, { type ArcLabelConfig } from '../ArcLabel/ArcLabel.svelte'; import Group from '../Group/Group.svelte'; export type ArcChartExtraSnippetProps = { diff --git a/packages/layerchart/src/lib/components/charts/BarChart.svelte b/packages/layerchart/src/lib/components/charts/BarChart.svelte index cf38e8381..a49ce4fc8 100644 --- a/packages/layerchart/src/lib/components/charts/BarChart.svelte +++ b/packages/layerchart/src/lib/components/charts/BarChart.svelte @@ -2,7 +2,7 @@ import type { ChartProps } from "../Chart/Chart.svelte"; import type { SeriesData } from './types.js'; - import Bars from '../Bars.svelte'; + import Bars from '../Bars/Bars.svelte'; // Use explicit data prop for TData inference, with rest from ChartPropsWithoutHTML export type BarChartProps = { diff --git a/packages/layerchart/src/lib/components/charts/BarChartFixedWidthTest.svelte b/packages/layerchart/src/lib/components/charts/BarChartFixedWidthTest.svelte index 5ac38e2bc..4bacd82c0 100644 --- a/packages/layerchart/src/lib/components/charts/BarChartFixedWidthTest.svelte +++ b/packages/layerchart/src/lib/components/charts/BarChartFixedWidthTest.svelte @@ -2,7 +2,7 @@ import { scaleBand } from 'd3-scale'; import Chart from "../Chart/Chart.svelte"; import Layer from '../layers/Layer.svelte'; - import Bars from '../Bars.svelte'; + import Bars from '../Bars/Bars.svelte'; let { data, diff --git a/packages/layerchart/src/lib/components/charts/PieChart.svelte b/packages/layerchart/src/lib/components/charts/PieChart.svelte index 61396dd89..a83491e74 100644 --- a/packages/layerchart/src/lib/components/charts/PieChart.svelte +++ b/packages/layerchart/src/lib/components/charts/PieChart.svelte @@ -6,9 +6,9 @@ import type { SeriesData } from './types.js'; import Arc from '../Arc/Arc.svelte'; - import ArcLabel, { type ArcLabelConfig } from '../ArcLabel.svelte'; + import ArcLabel, { type ArcLabelConfig } from '../ArcLabel/ArcLabel.svelte'; import Group from '../Group/Group.svelte'; - import Pie from '../Pie.svelte'; + import Pie from '../Pie/Pie.svelte'; export type PieChartExtraSnippetProps = { key: Accessor; diff --git a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte b/packages/layerchart/src/lib/components/charts/ScatterChart.svelte index 47faee31d..d3562b0f1 100644 --- a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte +++ b/packages/layerchart/src/lib/components/charts/ScatterChart.svelte @@ -2,7 +2,7 @@ import type { ChartProps } from "../Chart/Chart.svelte"; import type { SeriesData } from './types.js'; - import Points from '../Points.svelte'; + import Points from '../Points/Points.svelte'; // Use explicit data prop for TData inference, with rest from ChartPropsWithoutHTML export type ScatterChartProps = { diff --git a/packages/layerchart/src/lib/components/index.ts b/packages/layerchart/src/lib/components/index.ts index 64ca50fd4..48a5eb97f 100644 --- a/packages/layerchart/src/lib/components/index.ts +++ b/packages/layerchart/src/lib/components/index.ts @@ -9,16 +9,16 @@ export { default as AnnotationRange } from './AnnotationRange.svelte'; export * from './AnnotationRange.svelte'; export { default as Arc } from './Arc/Arc.svelte'; export * from './Arc/Arc.svelte'; -export { default as ArcLabel } from './ArcLabel.svelte'; -export * from './ArcLabel.svelte'; +export { default as ArcLabel } from './ArcLabel/ArcLabel.svelte'; +export * from './ArcLabel/ArcLabel.svelte'; export { default as Area } from './Area/Area.svelte'; export * from './Area/Area.svelte'; export { default as Axis } from './Axis/Axis.svelte'; export * from './Axis/Axis.svelte'; -export { default as Bar } from './Bar.svelte'; -export * from './Bar.svelte'; -export { default as Bars } from './Bars.svelte'; -export * from './Bars.svelte'; +export { default as Bar } from './Bar/Bar.svelte'; +export * from './Bar/Bar.svelte'; +export { default as Bars } from './Bars/Bars.svelte'; +export * from './Bars/Bars.svelte'; export { default as Blur } from './Blur.svelte'; export * from './Blur.svelte'; export { default as BoxPlot } from './BoxPlot.svelte'; @@ -65,8 +65,8 @@ export { default as Hull } from './Hull.svelte'; export * from './Hull.svelte'; export { default as Image } from './Image/Image.svelte'; export * from './Image/Image.svelte'; -export { default as Labels } from './Labels.svelte'; -export * from './Labels.svelte'; +export { default as Labels } from './Labels/Labels.svelte'; +export * from './Labels/Labels.svelte'; export { default as Layer } from './layers/Layer.svelte'; export * from './layers/Layer.svelte'; export { default as CircleLegend } from './CircleLegend.svelte'; @@ -89,12 +89,12 @@ export { default as Path } from './Path/Path.svelte'; export * from './Path/Path.svelte'; export { default as Pattern } from './Pattern/Pattern.svelte'; export * from './Pattern/Pattern.svelte'; -export { default as Pie } from './Pie.svelte'; -export * from './Pie.svelte'; +export { default as Pie } from './Pie/Pie.svelte'; +export * from './Pie/Pie.svelte'; export { default as Point } from './Point.svelte'; export * from './Point.svelte'; -export { default as Points } from './Points.svelte'; -export * from './Points.svelte'; +export { default as Points } from './Points/Points.svelte'; +export * from './Points/Points.svelte'; export { default as Polygon } from './Polygon/Polygon.svelte'; export * from './Polygon/Polygon.svelte'; export { default as RadialGradient } from './RadialGradient/RadialGradient.svelte'; diff --git a/packages/layerchart/src/lib/html.ts b/packages/layerchart/src/lib/html.ts index 929268186..b72641691 100644 --- a/packages/layerchart/src/lib/html.ts +++ b/packages/layerchart/src/lib/html.ts @@ -101,3 +101,14 @@ export type { ChartClipPathProps, ChartClipPathPropsWithoutHTML, } from './components/ChartClipPath/ChartClipPath.shared.svelte.js'; +export { default as Points } from './components/Points/Points.html.svelte'; +export type { + PointsProps, + PointsPropsWithoutHTML, + Point, +} from './components/Points/Points.shared.svelte.js'; +export { default as Labels } from './components/Labels/Labels.html.svelte'; +export type { + LabelsProps, + LabelsPropsWithoutHTML, +} from './components/Labels/Labels.shared.svelte.js'; diff --git a/packages/layerchart/src/lib/server/TestBarChart.svelte b/packages/layerchart/src/lib/server/TestBarChart.svelte index 5654e1a54..d4b2c539a 100644 --- a/packages/layerchart/src/lib/server/TestBarChart.svelte +++ b/packages/layerchart/src/lib/server/TestBarChart.svelte @@ -2,7 +2,7 @@ import { scaleBand } from 'd3-scale'; import ServerChart from './ServerChart.svelte'; import type { CaptureTarget } from './captureStore.js'; - import Bars from '$lib/components/Bars.svelte'; + import Bars from '$lib/components/Bars/Bars.svelte'; let { data, diff --git a/packages/layerchart/src/lib/svg.ts b/packages/layerchart/src/lib/svg.ts index 2909ed98e..e4a7a33ee 100644 --- a/packages/layerchart/src/lib/svg.ts +++ b/packages/layerchart/src/lib/svg.ts @@ -123,6 +123,32 @@ export type { AreaProps, AreaPropsWithoutHTML, } from './components/Area/Area.shared.svelte.js'; +export { default as Pie } from './components/Pie/Pie.svg.svelte'; +export type { PieProps, PiePropsWithoutHTML } from './components/Pie/Pie.shared.svelte.js'; +export { default as ArcLabel } from './components/ArcLabel/ArcLabel.svg.svelte'; +export type { + ArcLabelProps, + ArcLabelConfig, + ArcLabelPlacement, +} from './components/ArcLabel/ArcLabel.shared.svelte.js'; +export { default as Bar } from './components/Bar/Bar.svg.svelte'; +export type { BarProps, BarPropsWithoutHTML } from './components/Bar/Bar.shared.svelte.js'; +export { default as Bars } from './components/Bars/Bars.svg.svelte'; +export type { + BarsProps, + BarsPropsWithoutHTML, +} from './components/Bars/Bars.shared.svelte.js'; +export { default as Points } from './components/Points/Points.svg.svelte'; +export type { + PointsProps, + PointsPropsWithoutHTML, + Point, +} from './components/Points/Points.shared.svelte.js'; +export { default as Labels } from './components/Labels/Labels.svg.svelte'; +export type { + LabelsProps, + LabelsPropsWithoutHTML, +} from './components/Labels/Labels.shared.svelte.js'; export { default as RectClipPath } from './components/RectClipPath/RectClipPath.svg.svelte'; export type { RectClipPathProps, From a970273b752494b0d4d5d2e16d2ec58e9c543ad0 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 28 Apr 2026 13:29:59 -0400 Subject: [PATCH 18/36] split Frame, Cell, Threshold, AnnotationLine, AnnotationPoint, and Trail into 3 layer-specific components --- bundle-analyzer/bundle-reports/latest.json | 322 ++++++++++++++---- bundle-analyzer/bundle-scenarios.ts | 22 ++ packages/layerchart/src/lib/canvas.ts | 27 ++ .../AnnotationLine.base.svelte} | 91 ++--- .../AnnotationLine.canvas.svelte | 17 + .../AnnotationLine.shared.svelte.ts | 27 ++ .../AnnotationLine/AnnotationLine.svelte | 23 ++ .../AnnotationLine/AnnotationLine.svg.svelte | 17 + .../AnnotationPoint.base.svelte | 164 +++++++++ .../AnnotationPoint.canvas.svelte | 19 ++ .../AnnotationPoint.shared.svelte.ts | 27 ++ .../AnnotationPoint.svelte | 8 +- .../AnnotationPoint.svg.svelte | 19 ++ .../AnnotationRange.svelte | 10 +- .../layerchart/src/lib/components/Cell.svelte | 64 ---- .../src/lib/components/Cell/Cell.base.svelte | 41 +++ .../lib/components/Cell/Cell.canvas.svelte | 15 + .../src/lib/components/Cell/Cell.html.svelte | 15 + .../lib/components/Cell/Cell.shared.svelte.ts | 25 ++ .../src/lib/components/Cell/Cell.svelte | 23 ++ .../src/lib/components/Cell/Cell.svg.svelte | 15 + .../{Frame.svelte => Frame/Frame.base.svelte} | 25 +- .../lib/components/Frame/Frame.canvas.svelte | 13 + .../lib/components/Frame/Frame.html.svelte | 13 + .../components/Frame/Frame.shared.svelte.ts | 12 + .../src/lib/components/Frame/Frame.svelte | 23 ++ .../src/lib/components/Frame/Frame.svg.svelte | 13 + .../src/lib/components/{ => Link}/Link.svelte | 6 +- .../src/lib/components/Threshold.svelte | 75 ---- .../Threshold/Threshold.base.svelte | 42 +++ .../Threshold/Threshold.canvas.svelte | 14 + .../Threshold/Threshold.shared.svelte.ts | 20 ++ .../lib/components/Threshold/Threshold.svelte | 20 ++ .../components/Threshold/Threshold.svg.svelte | 14 + .../{Trail.svelte => Trail/Trail.base.svelte} | 139 +------- .../lib/components/Trail/Trail.canvas.svelte | 13 + .../components/Trail/Trail.shared.svelte.ts | 30 ++ .../src/lib/components/Trail/Trail.svelte | 20 ++ .../src/lib/components/Trail/Trail.svg.svelte | 13 + .../lib/components/{ => Vector}/Vector.svelte | 2 +- .../components/{ => Voronoi}/Voronoi.svelte | 8 +- .../components/charts/ChartAnnotations.svelte | 6 +- .../layerchart/src/lib/components/index.ts | 40 +-- .../src/lib/components/layers/Layer.svelte | 2 +- .../components/tooltip/TooltipContext.svelte | 2 +- packages/layerchart/src/lib/html.ts | 7 + packages/layerchart/src/lib/svg.ts | 27 ++ 47 files changed, 1144 insertions(+), 446 deletions(-) rename packages/layerchart/src/lib/components/{AnnotationLine.svelte => AnnotationLine/AnnotationLine.base.svelte} (57%) create mode 100644 packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.svelte create mode 100644 packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.svg.svelte create mode 100644 packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.base.svelte create mode 100644 packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.shared.svelte.ts rename packages/layerchart/src/lib/components/{ => AnnotationPoint}/AnnotationPoint.svelte (97%) create mode 100644 packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.svg.svelte rename packages/layerchart/src/lib/components/{ => AnnotationRange}/AnnotationRange.svelte (94%) delete mode 100644 packages/layerchart/src/lib/components/Cell.svelte create mode 100644 packages/layerchart/src/lib/components/Cell/Cell.base.svelte create mode 100644 packages/layerchart/src/lib/components/Cell/Cell.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Cell/Cell.html.svelte create mode 100644 packages/layerchart/src/lib/components/Cell/Cell.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Cell/Cell.svelte create mode 100644 packages/layerchart/src/lib/components/Cell/Cell.svg.svelte rename packages/layerchart/src/lib/components/{Frame.svelte => Frame/Frame.base.svelte} (56%) create mode 100644 packages/layerchart/src/lib/components/Frame/Frame.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Frame/Frame.html.svelte create mode 100644 packages/layerchart/src/lib/components/Frame/Frame.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Frame/Frame.svelte create mode 100644 packages/layerchart/src/lib/components/Frame/Frame.svg.svelte rename packages/layerchart/src/lib/components/{ => Link}/Link.svelte (98%) delete mode 100644 packages/layerchart/src/lib/components/Threshold.svelte create mode 100644 packages/layerchart/src/lib/components/Threshold/Threshold.base.svelte create mode 100644 packages/layerchart/src/lib/components/Threshold/Threshold.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Threshold/Threshold.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Threshold/Threshold.svelte create mode 100644 packages/layerchart/src/lib/components/Threshold/Threshold.svg.svelte rename packages/layerchart/src/lib/components/{Trail.svelte => Trail/Trail.base.svelte} (53%) create mode 100644 packages/layerchart/src/lib/components/Trail/Trail.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Trail/Trail.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Trail/Trail.svelte create mode 100644 packages/layerchart/src/lib/components/Trail/Trail.svg.svelte rename packages/layerchart/src/lib/components/{ => Vector}/Vector.svelte (99%) rename packages/layerchart/src/lib/components/{ => Voronoi}/Voronoi.svelte (96%) diff --git a/bundle-analyzer/bundle-reports/latest.json b/bundle-analyzer/bundle-reports/latest.json index a713f38ea..179c2676b 100644 --- a/bundle-analyzer/bundle-reports/latest.json +++ b/bundle-analyzer/bundle-reports/latest.json @@ -1,12 +1,12 @@ { - "timestamp": "2026-04-28T16:46:11.527Z", + "timestamp": "2026-04-28T17:07:04.927Z", "results": [ { "scenario": "core", "description": "Core charting components without rendering layer", "group": "Foundation", - "size": 366902, - "gzipSize": 84800, + "size": 370937, + "gzipSize": 85725, "imports": [ "Chart", "Svg" @@ -16,8 +16,8 @@ "scenario": "core-svg", "description": "Svg-based rendering", "group": "Foundation", - "size": 350946, - "gzipSize": 82037, + "size": 350474, + "gzipSize": 81862, "imports": [ "Chart", "Svg" @@ -27,8 +27,8 @@ "scenario": "core-canvas", "description": "Canvas-based rendering", "group": "Foundation", - "size": 357197, - "gzipSize": 83646, + "size": 357008, + "gzipSize": 83588, "imports": [ "Chart", "Canvas" @@ -38,8 +38,8 @@ "scenario": "core-html", "description": "HTML-based rendering", "group": "Foundation", - "size": 352837, - "gzipSize": 82478, + "size": 355729, + "gzipSize": 83221, "imports": [ "Chart", "Html" @@ -49,8 +49,8 @@ "scenario": "line-chart", "description": "Basic line chart with axes and grid", "group": "Cartesian charts", - "size": 384782, - "gzipSize": 88531, + "size": 371432, + "gzipSize": 85760, "imports": [ "Chart", "Svg", @@ -63,8 +63,8 @@ "scenario": "line-chart-svg", "description": "Line chart composed from `layerchart/svg`", "group": "Cartesian charts", - "size": 350970, - "gzipSize": 82048, + "size": 350498, + "gzipSize": 81877, "imports": [ "Chart", "Layer", @@ -77,8 +77,8 @@ "scenario": "line-chart-canvas", "description": "Line chart composed from `layerchart/canvas`", "group": "Cartesian charts", - "size": 357221, - "gzipSize": 83667, + "size": 357032, + "gzipSize": 83610, "imports": [ "Chart", "Layer", @@ -91,8 +91,8 @@ "scenario": "line-chart-html", "description": "Line chart composed from `layerchart/html`", "group": "Cartesian charts", - "size": 352861, - "gzipSize": 82498, + "size": 355753, + "gzipSize": 83234, "imports": [ "Chart", "Layer", @@ -105,8 +105,8 @@ "scenario": "line-chart-interactive", "description": "Line chart with tooltip and highlight", "group": "Cartesian charts", - "size": 399619, - "gzipSize": 92185, + "size": 386273, + "gzipSize": 89048, "imports": [ "Chart", "Svg", @@ -121,8 +121,8 @@ "scenario": "LineChart", "description": "High-level `LineChart` component", "group": "Cartesian charts", - "size": 391021, - "gzipSize": 91122, + "size": 395056, + "gzipSize": 92080, "imports": [ "LineChart" ] @@ -131,8 +131,8 @@ "scenario": "area-chart", "description": "Area chart with axes", "group": "Cartesian charts", - "size": 380913, - "gzipSize": 87773, + "size": 384948, + "gzipSize": 88748, "imports": [ "Chart", "Svg", @@ -145,8 +145,8 @@ "scenario": "AreaChart", "description": "High-level `AreaChart` component", "group": "Cartesian charts", - "size": 384503, - "gzipSize": 88649, + "size": 388538, + "gzipSize": 89631, "imports": [ "AreaChart" ] @@ -155,8 +155,8 @@ "scenario": "bar-chart", "description": "Bar chart with axes", "group": "Cartesian charts", - "size": 371241, - "gzipSize": 85586, + "size": 375274, + "gzipSize": 86461, "imports": [ "Chart", "Svg", @@ -169,8 +169,8 @@ "scenario": "BarChart", "description": "High-level `BarChart` component", "group": "Cartesian charts", - "size": 376376, - "gzipSize": 86640, + "size": 380407, + "gzipSize": 87602, "imports": [ "BarChart" ] @@ -179,8 +179,8 @@ "scenario": "scatter-chart", "description": "Scatter plot with points", "group": "Cartesian charts", - "size": 368179, - "gzipSize": 85289, + "size": 372214, + "gzipSize": 86228, "imports": [ "Chart", "Svg", @@ -194,8 +194,8 @@ "scenario": "ScatterChart", "description": "High-level `ScatterChart` component", "group": "Cartesian charts", - "size": 370927, - "gzipSize": 85936, + "size": 374962, + "gzipSize": 86842, "imports": [ "ScatterChart" ] @@ -204,8 +204,8 @@ "scenario": "pie-chart", "description": "Pie/donut chart with arcs", "group": "Cartesian charts", - "size": 379646, - "gzipSize": 87857, + "size": 383681, + "gzipSize": 88840, "imports": [ "Chart", "Svg", @@ -218,8 +218,8 @@ "scenario": "PieChart", "description": "High-level `PieChart` component", "group": "Cartesian charts", - "size": 405102, - "gzipSize": 93604, + "size": 409137, + "gzipSize": 94567, "imports": [ "PieChart" ] @@ -228,8 +228,8 @@ "scenario": "ArcChart", "description": "High-level `ArcChart` component", "group": "Cartesian charts", - "size": 398598, - "gzipSize": 92351, + "size": 402633, + "gzipSize": 93329, "imports": [ "ArcChart" ] @@ -238,8 +238,8 @@ "scenario": "geo", "description": "Geographic map with paths", "group": "Geo", - "size": 406939, - "gzipSize": 94072, + "size": 407463, + "gzipSize": 94301, "imports": [ "Chart", "Svg", @@ -252,8 +252,8 @@ "scenario": "geo-tiles", "description": "Geographic map with tile layer", "group": "Geo", - "size": 411384, - "gzipSize": 95625, + "size": 411908, + "gzipSize": 95835, "imports": [ "Chart", "Svg", @@ -267,8 +267,8 @@ "scenario": "geo-full", "description": "Full geo setup with all geo components", "group": "Geo", - "size": 459774, - "gzipSize": 109107, + "size": 460296, + "gzipSize": 109369, "imports": [ "Chart", "Svg", @@ -290,8 +290,8 @@ "scenario": "hierarchy-tree", "description": "Tree layout with links", "group": "Hierarchy", - "size": 412416, - "gzipSize": 95884, + "size": 412941, + "gzipSize": 95929, "imports": [ "Chart", "Svg", @@ -305,8 +305,8 @@ "scenario": "hierarchy-treemap", "description": "Treemap layout", "group": "Hierarchy", - "size": 391324, - "gzipSize": 90607, + "size": 392490, + "gzipSize": 90932, "imports": [ "Chart", "Svg", @@ -320,8 +320,8 @@ "scenario": "hierarchy-pack", "description": "Circle packing layout", "group": "Hierarchy", - "size": 391642, - "gzipSize": 90767, + "size": 392167, + "gzipSize": 91014, "imports": [ "Chart", "Svg", @@ -334,8 +334,8 @@ "scenario": "force", "description": "Force-directed graph layout", "group": "Graph / network", - "size": 414879, - "gzipSize": 96744, + "size": 415400, + "gzipSize": 96817, "imports": [ "Chart", "Svg", @@ -349,8 +349,8 @@ "scenario": "dagre", "description": "Dagre directed graph layout", "group": "Graph / network", - "size": 472305, - "gzipSize": 112286, + "size": 472826, + "gzipSize": 112310, "imports": [ "Chart", "Svg", @@ -364,8 +364,8 @@ "scenario": "sankey", "description": "Sankey flow diagram", "group": "Graph / network", - "size": 413997, - "gzipSize": 95876, + "size": 415168, + "gzipSize": 96169, "imports": [ "Chart", "Svg", @@ -379,8 +379,8 @@ "scenario": "chord", "description": "Chord diagram", "group": "Graph / network", - "size": 376653, - "gzipSize": 87063, + "size": 380685, + "gzipSize": 87998, "imports": [ "Chart", "Svg", @@ -1408,12 +1408,212 @@ "Labels" ] }, + { + "scenario": "Frame", + "description": "Standalone Frame (agnostic) — baseline", + "group": "Components", + "size": 77940, + "gzipSize": 19344, + "imports": [ + "Frame" + ] + }, + { + "scenario": "Frame.svg", + "description": "Standalone Frame from `layerchart/svg`", + "group": "Components", + "size": 62299, + "gzipSize": 15130, + "imports": [ + "Frame" + ] + }, + { + "scenario": "Frame.canvas", + "description": "Standalone Frame from `layerchart/canvas`", + "group": "Components", + "size": 71449, + "gzipSize": 18128, + "imports": [ + "Frame" + ] + }, + { + "scenario": "Frame.html", + "description": "Standalone Frame from `layerchart/html`", + "group": "Components", + "size": 61735, + "gzipSize": 15114, + "imports": [ + "Frame" + ] + }, + { + "scenario": "Cell", + "description": "Standalone Cell (agnostic) — baseline", + "group": "Components", + "size": 97795, + "gzipSize": 22000, + "imports": [ + "Cell" + ] + }, + { + "scenario": "Cell.svg", + "description": "Standalone Cell from `layerchart/svg`", + "group": "Components", + "size": 74339, + "gzipSize": 17169, + "imports": [ + "Cell" + ] + }, + { + "scenario": "Cell.canvas", + "description": "Standalone Cell from `layerchart/canvas`", + "group": "Components", + "size": 82010, + "gzipSize": 19591, + "imports": [ + "Cell" + ] + }, + { + "scenario": "Cell.html", + "description": "Standalone Cell from `layerchart/html`", + "group": "Components", + "size": 74549, + "gzipSize": 17048, + "imports": [ + "Cell" + ] + }, + { + "scenario": "Threshold", + "description": "Standalone Threshold (agnostic) — baseline", + "group": "Components", + "size": 126571, + "gzipSize": 30882, + "imports": [ + "Threshold" + ] + }, + { + "scenario": "Threshold.svg", + "description": "Standalone Threshold from `layerchart/svg`", + "group": "Components", + "size": 113853, + "gzipSize": 27475, + "imports": [ + "Threshold" + ] + }, + { + "scenario": "Threshold.canvas", + "description": "Standalone Threshold from `layerchart/canvas`", + "group": "Components", + "size": 104686, + "gzipSize": 26129, + "imports": [ + "Threshold" + ] + }, + { + "scenario": "AnnotationLine", + "description": "Standalone AnnotationLine (agnostic) — baseline", + "group": "Components", + "size": 134549, + "gzipSize": 32610, + "imports": [ + "AnnotationLine" + ] + }, + { + "scenario": "AnnotationLine.svg", + "description": "Standalone AnnotationLine from `layerchart/svg`", + "group": "Components", + "size": 123116, + "gzipSize": 29710, + "imports": [ + "AnnotationLine" + ] + }, + { + "scenario": "AnnotationLine.canvas", + "description": "Standalone AnnotationLine from `layerchart/canvas`", + "group": "Components", + "size": 120192, + "gzipSize": 29522, + "imports": [ + "AnnotationLine" + ] + }, + { + "scenario": "AnnotationPoint", + "description": "Standalone AnnotationPoint (agnostic) — baseline", + "group": "Components", + "size": 186689, + "gzipSize": 44958, + "imports": [ + "AnnotationPoint" + ] + }, + { + "scenario": "AnnotationPoint.svg", + "description": "Standalone AnnotationPoint from `layerchart/svg`", + "group": "Components", + "size": 174698, + "gzipSize": 42425, + "imports": [ + "AnnotationPoint" + ] + }, + { + "scenario": "AnnotationPoint.canvas", + "description": "Standalone AnnotationPoint from `layerchart/canvas`", + "group": "Components", + "size": 171814, + "gzipSize": 41656, + "imports": [ + "AnnotationPoint" + ] + }, + { + "scenario": "Trail", + "description": "Standalone Trail (agnostic) — baseline", + "group": "Components", + "size": 95748, + "gzipSize": 24551, + "imports": [ + "Trail" + ] + }, + { + "scenario": "Trail.svg", + "description": "Standalone Trail from `layerchart/svg`", + "group": "Components", + "size": 84669, + "gzipSize": 21482, + "imports": [ + "Trail" + ] + }, + { + "scenario": "Trail.canvas", + "description": "Standalone Trail from `layerchart/canvas`", + "group": "Components", + "size": 76717, + "gzipSize": 20430, + "imports": [ + "Trail" + ] + }, { "scenario": "all", "description": "Everything from layerchart (worst case)", "group": "Worst case", - "size": 968389, - "gzipSize": 230999, + "size": 974742, + "gzipSize": 232053, "imports": [ "*" ] diff --git a/bundle-analyzer/bundle-scenarios.ts b/bundle-analyzer/bundle-scenarios.ts index a2137bc87..62ac235af 100644 --- a/bundle-analyzer/bundle-scenarios.ts +++ b/bundle-analyzer/bundle-scenarios.ts @@ -869,6 +869,28 @@ export const scenarios: Scenario[] = [ { name: 'Labels.canvas', group: 'Components', description: 'Standalone Labels from `layerchart/canvas`', imports: ['Labels'], layers: { Labels: 'canvas' } }, { name: 'Labels.html', group: 'Components', description: 'Standalone Labels from `layerchart/html`', imports: ['Labels'], layers: { Labels: 'html' } }, + // Niche compounds. + { name: 'Frame', group: 'Components', description: 'Standalone Frame (agnostic) — baseline', imports: ['Frame'] }, + { name: 'Frame.svg', group: 'Components', description: 'Standalone Frame from `layerchart/svg`', imports: ['Frame'], layers: { Frame: 'svg' } }, + { name: 'Frame.canvas', group: 'Components', description: 'Standalone Frame from `layerchart/canvas`', imports: ['Frame'], layers: { Frame: 'canvas' } }, + { name: 'Frame.html', group: 'Components', description: 'Standalone Frame from `layerchart/html`', imports: ['Frame'], layers: { Frame: 'html' } }, + { name: 'Cell', group: 'Components', description: 'Standalone Cell (agnostic) — baseline', imports: ['Cell'] }, + { name: 'Cell.svg', group: 'Components', description: 'Standalone Cell from `layerchart/svg`', imports: ['Cell'], layers: { Cell: 'svg' } }, + { name: 'Cell.canvas', group: 'Components', description: 'Standalone Cell from `layerchart/canvas`', imports: ['Cell'], layers: { Cell: 'canvas' } }, + { name: 'Cell.html', group: 'Components', description: 'Standalone Cell from `layerchart/html`', imports: ['Cell'], layers: { Cell: 'html' } }, + { name: 'Threshold', group: 'Components', description: 'Standalone Threshold (agnostic) — baseline', imports: ['Threshold'] }, + { name: 'Threshold.svg', group: 'Components', description: 'Standalone Threshold from `layerchart/svg`', imports: ['Threshold'], layers: { Threshold: 'svg' } }, + { name: 'Threshold.canvas', group: 'Components', description: 'Standalone Threshold from `layerchart/canvas`', imports: ['Threshold'], layers: { Threshold: 'canvas' } }, + { name: 'AnnotationLine', group: 'Components', description: 'Standalone AnnotationLine (agnostic) — baseline', imports: ['AnnotationLine'] }, + { name: 'AnnotationLine.svg', group: 'Components', description: 'Standalone AnnotationLine from `layerchart/svg`', imports: ['AnnotationLine'], layers: { AnnotationLine: 'svg' } }, + { name: 'AnnotationLine.canvas', group: 'Components', description: 'Standalone AnnotationLine from `layerchart/canvas`', imports: ['AnnotationLine'], layers: { AnnotationLine: 'canvas' } }, + { name: 'AnnotationPoint', group: 'Components', description: 'Standalone AnnotationPoint (agnostic) — baseline', imports: ['AnnotationPoint'] }, + { name: 'AnnotationPoint.svg', group: 'Components', description: 'Standalone AnnotationPoint from `layerchart/svg`', imports: ['AnnotationPoint'], layers: { AnnotationPoint: 'svg' } }, + { name: 'AnnotationPoint.canvas', group: 'Components', description: 'Standalone AnnotationPoint from `layerchart/canvas`', imports: ['AnnotationPoint'], layers: { AnnotationPoint: 'canvas' } }, + { name: 'Trail', group: 'Components', description: 'Standalone Trail (agnostic) — baseline', imports: ['Trail'] }, + { name: 'Trail.svg', group: 'Components', description: 'Standalone Trail from `layerchart/svg`', imports: ['Trail'], layers: { Trail: 'svg' } }, + { name: 'Trail.canvas', group: 'Components', description: 'Standalone Trail from `layerchart/canvas`', imports: ['Trail'], layers: { Trail: 'canvas' } }, + // --- Worst case --- { name: 'all', diff --git a/packages/layerchart/src/lib/canvas.ts b/packages/layerchart/src/lib/canvas.ts index bf9ac0e0f..13ba2c425 100644 --- a/packages/layerchart/src/lib/canvas.ts +++ b/packages/layerchart/src/lib/canvas.ts @@ -144,6 +144,33 @@ export type { LabelsProps, LabelsPropsWithoutHTML, } from './components/Labels/Labels.shared.svelte.js'; +export { default as Frame } from './components/Frame/Frame.canvas.svelte'; +export type { + FrameProps, + FramePropsWithoutHTML, +} from './components/Frame/Frame.shared.svelte.js'; +export { default as Cell } from './components/Cell/Cell.canvas.svelte'; +export type { CellProps } from './components/Cell/Cell.shared.svelte.js'; +export { default as Threshold } from './components/Threshold/Threshold.canvas.svelte'; +export type { + ThresholdProps, + ThresholdSnippetProps, +} from './components/Threshold/Threshold.shared.svelte.js'; +export { default as AnnotationLine } from './components/AnnotationLine/AnnotationLine.canvas.svelte'; +export type { + AnnotationLineProps, + AnnotationLinePropsWithoutHTML, +} from './components/AnnotationLine/AnnotationLine.shared.svelte.js'; +export { default as AnnotationPoint } from './components/AnnotationPoint/AnnotationPoint.canvas.svelte'; +export type { + AnnotationPointProps, + AnnotationPointPropsWithoutHTML, +} from './components/AnnotationPoint/AnnotationPoint.shared.svelte.js'; +export { default as Trail } from './components/Trail/Trail.canvas.svelte'; +export type { + TrailProps, + TrailPropsWithoutHTML, +} from './components/Trail/Trail.shared.svelte.js'; export { default as RectClipPath } from './components/RectClipPath/RectClipPath.canvas.svelte'; export type { RectClipPathProps, diff --git a/packages/layerchart/src/lib/components/AnnotationLine.svelte b/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.base.svelte similarity index 57% rename from packages/layerchart/src/lib/components/AnnotationLine.svelte rename to packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.base.svelte index deaf7043d..cb5a29087 100644 --- a/packages/layerchart/src/lib/components/AnnotationLine.svelte +++ b/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.base.svelte @@ -1,60 +1,22 @@ + + + + diff --git a/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.shared.svelte.ts b/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.shared.svelte.ts new file mode 100644 index 000000000..c5d26dd76 --- /dev/null +++ b/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.shared.svelte.ts @@ -0,0 +1,27 @@ +import type { ComponentProps } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import type { CommonStyleProps, Without } from '$lib/utils/types.js'; +import type { SingleDomainType } from '$lib/utils/scales.svelte.js'; +import type Line from '../Line/Line.svelte'; +import type Text from '../Text/Text.svelte'; +import type { Placement } from '../types.js'; + +export type AnnotationLinePropsWithoutHTML = { + x?: SingleDomainType; + y?: SingleDomainType; + x1?: SingleDomainType; + y1?: SingleDomainType; + x2?: SingleDomainType; + y2?: SingleDomainType; + label?: string; + labelPlacement?: Placement; + labelXOffset?: number; + labelYOffset?: number; + props?: { + label?: Partial>; + line?: Partial>; + }; +} & CommonStyleProps; + +export type AnnotationLineProps = AnnotationLinePropsWithoutHTML & + Without, AnnotationLinePropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.svelte b/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.svelte new file mode 100644 index 000000000..8b44a10ce --- /dev/null +++ b/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.svelte @@ -0,0 +1,23 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.svg.svelte b/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.svg.svelte new file mode 100644 index 000000000..6615ad6df --- /dev/null +++ b/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.svg.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.base.svelte b/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.base.svelte new file mode 100644 index 000000000..53b32cf4a --- /dev/null +++ b/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.base.svelte @@ -0,0 +1,164 @@ + + + + + + +{#if linkEndpoints} + +{/if} + +{#if label} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.canvas.svelte b/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.canvas.svelte new file mode 100644 index 000000000..079b3d73c --- /dev/null +++ b/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.canvas.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.shared.svelte.ts b/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.shared.svelte.ts new file mode 100644 index 000000000..4b3775768 --- /dev/null +++ b/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.shared.svelte.ts @@ -0,0 +1,27 @@ +import type { ComponentProps } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import type { CommonStyleProps, Without } from '$lib/utils/types.js'; +import type { SingleDomainType } from '$lib/utils/scales.svelte.js'; +import type Circle from '../Circle/Circle.svelte'; +import type Link from '../Link/Link.svelte'; +import type Text from '../Text/Text.svelte'; +import type { Placement } from '../types.js'; + +export type AnnotationPointPropsWithoutHTML = { + x?: SingleDomainType; + y?: SingleDomainType; + r?: number; + label?: string; + labelPlacement?: Placement; + labelXOffset?: number; + labelYOffset?: number; + link?: boolean | Partial>; + details?: any; + props?: { + label?: Partial>; + circle?: Partial>; + }; +} & CommonStyleProps; + +export type AnnotationPointProps = AnnotationPointPropsWithoutHTML & + Without, AnnotationPointPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/AnnotationPoint.svelte b/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.svelte similarity index 97% rename from packages/layerchart/src/lib/components/AnnotationPoint.svelte rename to packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.svelte index fe2012122..dd5c9cdb6 100644 --- a/packages/layerchart/src/lib/components/AnnotationPoint.svelte +++ b/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.svelte @@ -50,10 +50,10 @@ + + + + diff --git a/packages/layerchart/src/lib/components/AnnotationRange.svelte b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svelte similarity index 94% rename from packages/layerchart/src/lib/components/AnnotationRange.svelte rename to packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svelte index c04208492..e82c7a4d6 100644 --- a/packages/layerchart/src/lib/components/AnnotationRange.svelte +++ b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svelte @@ -48,11 +48,11 @@ - - - -{#if shape === 'circle'} - - - -{:else} - -{/if} diff --git a/packages/layerchart/src/lib/components/Cell/Cell.base.svelte b/packages/layerchart/src/lib/components/Cell/Cell.base.svelte new file mode 100644 index 000000000..1766597e6 --- /dev/null +++ b/packages/layerchart/src/lib/components/Cell/Cell.base.svelte @@ -0,0 +1,41 @@ + + + + +{#if shape === 'circle'} + + + +{:else} + +{/if} diff --git a/packages/layerchart/src/lib/components/Cell/Cell.canvas.svelte b/packages/layerchart/src/lib/components/Cell/Cell.canvas.svelte new file mode 100644 index 000000000..f89fa9183 --- /dev/null +++ b/packages/layerchart/src/lib/components/Cell/Cell.canvas.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Cell/Cell.html.svelte b/packages/layerchart/src/lib/components/Cell/Cell.html.svelte new file mode 100644 index 000000000..447834bd3 --- /dev/null +++ b/packages/layerchart/src/lib/components/Cell/Cell.html.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Cell/Cell.shared.svelte.ts b/packages/layerchart/src/lib/components/Cell/Cell.shared.svelte.ts new file mode 100644 index 000000000..77fa1dd69 --- /dev/null +++ b/packages/layerchart/src/lib/components/Cell/Cell.shared.svelte.ts @@ -0,0 +1,25 @@ +import type { RectProps } from '../Rect/Rect.shared.svelte.js'; +import type { DataProp } from '$lib/utils/dataProp.js'; + +type BaseRectCellProps = Omit< + RectProps, + | 'width' + | 'height' + | 'x0' + | 'x1' + | 'y0' + | 'y1' + | 'initialX' + | 'initialY' + | 'initialWidth' + | 'initialHeight' + | 'motion' + | 'ref' +>; + +export type CellProps = BaseRectCellProps & { + /** @default 'rect' */ + shape?: 'rect' | 'circle'; + /** Radius for circle shape. */ + r?: DataProp; +}; diff --git a/packages/layerchart/src/lib/components/Cell/Cell.svelte b/packages/layerchart/src/lib/components/Cell/Cell.svelte new file mode 100644 index 000000000..fd174afed --- /dev/null +++ b/packages/layerchart/src/lib/components/Cell/Cell.svelte @@ -0,0 +1,23 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Cell/Cell.svg.svelte b/packages/layerchart/src/lib/components/Cell/Cell.svg.svelte new file mode 100644 index 000000000..531420e96 --- /dev/null +++ b/packages/layerchart/src/lib/components/Cell/Cell.svg.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Frame.svelte b/packages/layerchart/src/lib/components/Frame/Frame.base.svelte similarity index 56% rename from packages/layerchart/src/lib/components/Frame.svelte rename to packages/layerchart/src/lib/components/Frame/Frame.base.svelte index ae2449ca6..c99c31c9a 100644 --- a/packages/layerchart/src/lib/components/Frame.svelte +++ b/packages/layerchart/src/lib/components/Frame/Frame.base.svelte @@ -1,27 +1,24 @@ + + + + diff --git a/packages/layerchart/src/lib/components/Frame/Frame.html.svelte b/packages/layerchart/src/lib/components/Frame/Frame.html.svelte new file mode 100644 index 000000000..493bc44b6 --- /dev/null +++ b/packages/layerchart/src/lib/components/Frame/Frame.html.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Frame/Frame.shared.svelte.ts b/packages/layerchart/src/lib/components/Frame/Frame.shared.svelte.ts new file mode 100644 index 000000000..f718789e1 --- /dev/null +++ b/packages/layerchart/src/lib/components/Frame/Frame.shared.svelte.ts @@ -0,0 +1,12 @@ +import type { Without } from '$lib/utils/types.js'; +import type { RectProps, RectPropsWithoutHTML } from '../Rect/Rect.shared.svelte.js'; + +export type FramePropsWithoutHTML = RectPropsWithoutHTML & { + /** Whether to include padding area @default false */ + full?: boolean; +}; + +export type FrameProps = Omit< + FramePropsWithoutHTML & Without, + 'width' | 'height' +>; diff --git a/packages/layerchart/src/lib/components/Frame/Frame.svelte b/packages/layerchart/src/lib/components/Frame/Frame.svelte new file mode 100644 index 000000000..e128ecd96 --- /dev/null +++ b/packages/layerchart/src/lib/components/Frame/Frame.svelte @@ -0,0 +1,23 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Frame/Frame.svg.svelte b/packages/layerchart/src/lib/components/Frame/Frame.svg.svelte new file mode 100644 index 000000000..a68e3b16d --- /dev/null +++ b/packages/layerchart/src/lib/components/Frame/Frame.svg.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Link.svelte b/packages/layerchart/src/lib/components/Link/Link.svelte similarity index 98% rename from packages/layerchart/src/lib/components/Link.svelte rename to packages/layerchart/src/lib/components/Link/Link.svelte index 8713f0080..b9d4a8862 100644 --- a/packages/layerchart/src/lib/components/Link.svelte +++ b/packages/layerchart/src/lib/components/Link/Link.svelte @@ -1,10 +1,10 @@ - - - - -{#key curve} - - {#snippet clip()} - ctx.y(d)[1]} y1={(d) => max(ctx.yDomain)} {curve} {defined} /> - {/snippet} - {@render above?.({ curve, defined })} - - - - {#snippet clip()} - min(ctx.yDomain)} y1={(d) => ctx.y(d)[1]} {curve} {defined} /> - {/snippet} - - {@render below?.({ curve, defined })} - - - {@render children?.({ curve, defined })} -{/key} diff --git a/packages/layerchart/src/lib/components/Threshold/Threshold.base.svelte b/packages/layerchart/src/lib/components/Threshold/Threshold.base.svelte new file mode 100644 index 000000000..79beafcf2 --- /dev/null +++ b/packages/layerchart/src/lib/components/Threshold/Threshold.base.svelte @@ -0,0 +1,42 @@ + + + + +{#key curve} + + {#snippet clip()} + ctx.y(d)[1]} y1={(d: any) => max(ctx.yDomain)} {curve} {defined} /> + {/snippet} + {@render above?.({ curve, defined })} + + + + {#snippet clip()} + min(ctx.yDomain)} y1={(d: any) => ctx.y(d)[1]} {curve} {defined} /> + {/snippet} + + {@render below?.({ curve, defined })} + + + {@render children?.({ curve, defined })} +{/key} diff --git a/packages/layerchart/src/lib/components/Threshold/Threshold.canvas.svelte b/packages/layerchart/src/lib/components/Threshold/Threshold.canvas.svelte new file mode 100644 index 000000000..2a723d7cf --- /dev/null +++ b/packages/layerchart/src/lib/components/Threshold/Threshold.canvas.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Threshold/Threshold.shared.svelte.ts b/packages/layerchart/src/lib/components/Threshold/Threshold.shared.svelte.ts new file mode 100644 index 000000000..0f24b8f60 --- /dev/null +++ b/packages/layerchart/src/lib/components/Threshold/Threshold.shared.svelte.ts @@ -0,0 +1,20 @@ +import type { ComponentProps, Snippet } from 'svelte'; +import type { CurveFactory } from 'd3-shape'; +import type Area from '../Area/Area.svelte'; + +export type ThresholdSnippetProps = { + curve?: CurveFactory; + defined?: ComponentProps['defined']; +}; + +export type ThresholdProps = { + /** The curve factory to use for the area. */ + curve?: CurveFactory; + /** Function to determine if a point is defined. */ + defined?: ComponentProps['defined']; + /** Content to render above the threshold area. */ + above?: Snippet<[ThresholdSnippetProps]>; + /** Content to render below the threshold area. */ + below?: Snippet<[ThresholdSnippetProps]>; + children?: Snippet<[ThresholdSnippetProps]>; +}; diff --git a/packages/layerchart/src/lib/components/Threshold/Threshold.svelte b/packages/layerchart/src/lib/components/Threshold/Threshold.svelte new file mode 100644 index 000000000..46fb550c2 --- /dev/null +++ b/packages/layerchart/src/lib/components/Threshold/Threshold.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Threshold/Threshold.svg.svelte b/packages/layerchart/src/lib/components/Threshold/Threshold.svg.svelte new file mode 100644 index 000000000..8d1e0d9b1 --- /dev/null +++ b/packages/layerchart/src/lib/components/Threshold/Threshold.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Trail.svelte b/packages/layerchart/src/lib/components/Trail/Trail.base.svelte similarity index 53% rename from packages/layerchart/src/lib/components/Trail.svelte rename to packages/layerchart/src/lib/components/Trail/Trail.base.svelte index 55bf7cf59..db58b6b35 100644 --- a/packages/layerchart/src/lib/components/Trail.svelte +++ b/packages/layerchart/src/lib/components/Trail/Trail.base.svelte @@ -1,107 +1,12 @@ + + + + diff --git a/packages/layerchart/src/lib/components/Trail/Trail.shared.svelte.ts b/packages/layerchart/src/lib/components/Trail/Trail.shared.svelte.ts new file mode 100644 index 000000000..045f2f931 --- /dev/null +++ b/packages/layerchart/src/lib/components/Trail/Trail.shared.svelte.ts @@ -0,0 +1,30 @@ +import type { CurveFactory, CurveFactoryLineOnly, Line } from 'd3-shape'; + +import type { Accessor } from '$lib/utils/common.js'; +import type { MotionProp } from '$lib/utils/motion.svelte.js'; +import type { DataProp } from '$lib/utils/dataProp.js'; +import type { TrailCap } from '$lib/utils/trail.js'; +import type { PathProps } from '../Path/Path.shared.svelte.js'; + +export type TrailPropsWithoutHTML = { + data?: any; + x?: Accessor; + y?: Accessor; + seriesKey?: string; + defined?: Parameters['defined']>[0]; + /** Width at each point. @default 4 */ + r?: DataProp; + curve?: CurveFactory | CurveFactoryLineOnly; + /** @default 'round' */ + cap?: TrailCap; + tension?: number; + resolution?: number; + fill?: string; + fillOpacity?: number; + opacity?: number; + class?: string; + motion?: MotionProp; +}; + +export type TrailProps = TrailPropsWithoutHTML & + Omit; diff --git a/packages/layerchart/src/lib/components/Trail/Trail.svelte b/packages/layerchart/src/lib/components/Trail/Trail.svelte new file mode 100644 index 000000000..036e4e24d --- /dev/null +++ b/packages/layerchart/src/lib/components/Trail/Trail.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Trail/Trail.svg.svelte b/packages/layerchart/src/lib/components/Trail/Trail.svg.svelte new file mode 100644 index 000000000..14eef00ac --- /dev/null +++ b/packages/layerchart/src/lib/components/Trail/Trail.svg.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Vector.svelte b/packages/layerchart/src/lib/components/Vector/Vector.svelte similarity index 99% rename from packages/layerchart/src/lib/components/Vector.svelte rename to packages/layerchart/src/lib/components/Vector/Vector.svelte index 05cbf815c..8a7ac3d9c 100644 --- a/packages/layerchart/src/lib/components/Vector.svelte +++ b/packages/layerchart/src/lib/components/Vector/Vector.svelte @@ -143,7 +143,7 @@ vectorSpikePath, transformVectorPath, } from '$lib/utils/path.js'; - import Path from './Path/Path.svelte'; + import Path from '../Path/Path.svelte'; let { x = 0, diff --git a/packages/layerchart/src/lib/components/Voronoi.svelte b/packages/layerchart/src/lib/components/Voronoi/Voronoi.svelte similarity index 96% rename from packages/layerchart/src/lib/components/Voronoi.svelte rename to packages/layerchart/src/lib/components/Voronoi/Voronoi.svelte index 2866d83c0..67be75811 100644 --- a/packages/layerchart/src/lib/components/Voronoi.svelte +++ b/packages/layerchart/src/lib/components/Voronoi/Voronoi.svelte @@ -75,13 +75,13 @@ import { pointRadial } from 'd3-shape'; import { cls } from '@layerstack/tailwind'; - import GeoPath from './geo/GeoPath.svelte'; - import Group, { type GroupProps } from './Group/Group.svelte'; - import Path from './Path/Path.svelte'; + import GeoPath from '../geo/GeoPath.svelte'; + import Group, { type GroupProps } from '../Group/Group.svelte'; + import Path from '../Path/Path.svelte'; import { getChartContext } from '$lib/contexts/chart.js'; import { getGeoContext } from '$lib/contexts/geo.js'; import { accessor } from '$lib/utils/common.js'; - import CircleClipPath from './CircleClipPath.svelte'; + import CircleClipPath from '../CircleClipPath.svelte'; let { data, diff --git a/packages/layerchart/src/lib/components/charts/ChartAnnotations.svelte b/packages/layerchart/src/lib/components/charts/ChartAnnotations.svelte index 118136d21..550dd1041 100644 --- a/packages/layerchart/src/lib/components/charts/ChartAnnotations.svelte +++ b/packages/layerchart/src/lib/components/charts/ChartAnnotations.svelte @@ -1,9 +1,9 @@ + + + +{#if fill || className} + +{/if} + +{#if gradient} + + {#snippet children({ gradient }: { gradient: string })} + + {/snippet} + +{/if} + +{#if pattern} + + {#snippet children({ pattern }: { pattern: string })} + + {/snippet} + +{/if} + +{#if label} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.canvas.svelte b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.canvas.svelte new file mode 100644 index 000000000..e386f4b1a --- /dev/null +++ b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.canvas.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.shared.svelte.ts b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.shared.svelte.ts new file mode 100644 index 000000000..9ae9f555a --- /dev/null +++ b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.shared.svelte.ts @@ -0,0 +1,29 @@ +import type { ComponentProps } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import type { CommonStyleProps, Without } from '$lib/utils/types.js'; +import type { SingleDomainType } from '$lib/utils/scales.svelte.js'; +import type LinearGradient from '../LinearGradient/LinearGradient.svelte'; +import type Pattern from '../Pattern/Pattern.svelte'; +import type Rect from '../Rect/Rect.svelte'; +import type Text from '../Text/Text.svelte'; +import type { Placement } from '../types.js'; + +export type AnnotationRangePropsWithoutHTML = { + x?: [SingleDomainType, SingleDomainType] | SingleDomainType[]; + y?: [SingleDomainType, SingleDomainType] | SingleDomainType[]; + label?: string; + labelPlacement?: Placement; + labelXOffset?: number; + labelYOffset?: number; + fill?: string; + class?: string; + gradient?: ComponentProps; + pattern?: ComponentProps; + props?: { + label?: Partial>; + rect?: Partial>; + }; +} & CommonStyleProps; + +export type AnnotationRangeProps = AnnotationRangePropsWithoutHTML & + Without, AnnotationRangePropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svelte b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svelte index e82c7a4d6..d1282d4ce 100644 --- a/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svelte +++ b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svelte @@ -1,175 +1,23 @@ -{#if fill || className} - +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + {/if} - -{#if gradient} - - {#snippet children({ gradient })} - - {/snippet} - -{/if} - -{#if pattern} - - {#snippet children({ pattern })} - - {/snippet} - -{/if} - -{#if label} - -{/if} - - diff --git a/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svg.svelte b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svg.svelte new file mode 100644 index 000000000..8b6a859d0 --- /dev/null +++ b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svg.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/BoxPlot.svelte b/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.svelte similarity index 98% rename from packages/layerchart/src/lib/components/BoxPlot.svelte rename to packages/layerchart/src/lib/components/BoxPlot/BoxPlot.svelte index 640f03bad..0b8a73810 100644 --- a/packages/layerchart/src/lib/components/BoxPlot.svelte +++ b/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.svelte @@ -55,10 +55,10 @@ import { cls } from '@layerstack/tailwind'; import { quantile } from 'd3-array'; - import Group from './Group/Group.svelte'; - import Rect from './Rect/Rect.svelte'; - import Line from './Line/Line.svelte'; - import Circle from './Circle/Circle.svelte'; + import Group from '../Group/Group.svelte'; + import Rect from '../Rect/Rect.svelte'; + import Line from '../Line/Line.svelte'; + import Circle from '../Circle/Circle.svelte'; import { accessor } from '$lib/utils/common.js'; import { getChartContext } from '$lib/contexts/chart.js'; import { isScaleBand } from '$lib/utils/scales.svelte.js'; diff --git a/packages/layerchart/src/lib/components/Calendar.svelte b/packages/layerchart/src/lib/components/Calendar/Calendar.base.svelte similarity index 57% rename from packages/layerchart/src/lib/components/Calendar.svelte rename to packages/layerchart/src/lib/components/Calendar/Calendar.base.svelte index 03e6a250b..43ae264dd 100644 --- a/packages/layerchart/src/lib/components/Calendar.svelte +++ b/packages/layerchart/src/lib/components/Calendar/Calendar.base.svelte @@ -1,74 +1,29 @@ + + + + diff --git a/packages/layerchart/src/lib/components/Calendar/Calendar.shared.svelte.ts b/packages/layerchart/src/lib/components/Calendar/Calendar.shared.svelte.ts new file mode 100644 index 000000000..e5d35abde --- /dev/null +++ b/packages/layerchart/src/lib/components/Calendar/Calendar.shared.svelte.ts @@ -0,0 +1,30 @@ +import type { ComponentProps, Snippet } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import type { Without } from '$lib/utils/types.js'; +import type MonthPath from '../MonthPath.svelte'; +import type Text from '../Text/Text.svelte'; +import type { RectPropsWithoutHTML } from '../Rect/Rect.shared.svelte.js'; + +export type CalendarCell = { + x: number; + y: number; + color: any; + data: any; +}; + +export type CalendarPropsWithoutHTML = { + start: Date; + end: Date; + cellSize?: number | [number, number]; + /** @default false */ + monthPath?: boolean | Partial>; + monthLabel?: boolean | Partial>; + tooltip?: boolean; + children?: Snippet<[{ cells: CalendarCell[]; cellSize: [number, number] }]>; +} & Omit< + RectPropsWithoutHTML, + 'children' | 'x' | 'y' | 'width' | 'height' | 'fill' | 'onpointermove' | 'onpointerleave' +>; + +export type CalendarProps = CalendarPropsWithoutHTML & + Without, CalendarPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/Calendar/Calendar.svelte b/packages/layerchart/src/lib/components/Calendar/Calendar.svelte new file mode 100644 index 000000000..95a5752a2 --- /dev/null +++ b/packages/layerchart/src/lib/components/Calendar/Calendar.svelte @@ -0,0 +1,24 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Calendar/Calendar.svg.svelte b/packages/layerchart/src/lib/components/Calendar/Calendar.svg.svelte new file mode 100644 index 000000000..dc1d81a90 --- /dev/null +++ b/packages/layerchart/src/lib/components/Calendar/Calendar.svg.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/CircleClipPath.svelte b/packages/layerchart/src/lib/components/CircleClipPath.svelte deleted file mode 100644 index 7caac3fff..000000000 --- a/packages/layerchart/src/lib/components/CircleClipPath.svelte +++ /dev/null @@ -1,83 +0,0 @@ - - - - - diff --git a/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.base.svelte b/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.base.svelte new file mode 100644 index 000000000..d800c683b --- /dev/null +++ b/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.base.svelte @@ -0,0 +1,34 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.canvas.svelte b/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.canvas.svelte new file mode 100644 index 000000000..7a0221088 --- /dev/null +++ b/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.canvas.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.html.svelte b/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.html.svelte new file mode 100644 index 000000000..b3d62a77b --- /dev/null +++ b/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.html.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.shared.svelte.ts b/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.shared.svelte.ts new file mode 100644 index 000000000..deab06f61 --- /dev/null +++ b/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.shared.svelte.ts @@ -0,0 +1,21 @@ +import type { MotionProp } from '$lib/utils/motion.svelte.js'; +import type { ClipPathPropsWithoutHTML } from '../ClipPath/ClipPath.shared.svelte.js'; + +export type CircleClipPathPropsWithoutHTML = { + /** A unique id for the clipPath. */ + id?: string; + /** The center x position of the circle. @default 0 */ + cx?: number; + /** The center y position of the circle. @default 0 */ + cy?: number; + /** The radius of the circle. @required */ + r: number; + /** Whether to disable clipping (show all). @default false */ + disabled?: boolean; + /** Invert the clip — content renders *outside* the circle. @default false */ + invert?: boolean; + /** A bindable reference to the underlying `` element @bindable */ + ref?: SVGCircleElement; + children?: ClipPathPropsWithoutHTML['children']; + motion?: MotionProp; +}; diff --git a/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.svelte b/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.svelte new file mode 100644 index 000000000..607fb87f9 --- /dev/null +++ b/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.svelte @@ -0,0 +1,23 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + +{/if} diff --git a/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.svg.svelte b/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.svg.svelte new file mode 100644 index 000000000..a776d58f5 --- /dev/null +++ b/packages/layerchart/src/lib/components/CircleClipPath/CircleClipPath.svg.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Contour.svelte b/packages/layerchart/src/lib/components/Contour/Contour.svelte similarity index 99% rename from packages/layerchart/src/lib/components/Contour.svelte rename to packages/layerchart/src/lib/components/Contour/Contour.svelte index f343e5b65..92561e76d 100644 --- a/packages/layerchart/src/lib/components/Contour.svelte +++ b/packages/layerchart/src/lib/components/Contour/Contour.svelte @@ -62,8 +62,8 @@ import { interpolateYlGnBu } from 'd3-scale-chromatic'; import { max, min } from 'd3-array'; - import Group from './Group/Group.svelte'; - import Path from './Path/Path.svelte'; + import Group from '../Group/Group.svelte'; + import Path from '../Path/Path.svelte'; import { accessor as resolveAccessor, chartDataArray } from '$lib/utils/common.js'; import { getChartContext } from '$lib/contexts/chart.js'; import { getGeoContext } from '$lib/contexts/geo.js'; diff --git a/packages/layerchart/src/lib/components/Density.svelte b/packages/layerchart/src/lib/components/Density/Density.base.svelte similarity index 69% rename from packages/layerchart/src/lib/components/Density.svelte rename to packages/layerchart/src/lib/components/Density/Density.base.svelte index 51c639b6e..cf3a90c22 100644 --- a/packages/layerchart/src/lib/components/Density.svelte +++ b/packages/layerchart/src/lib/components/Density/Density.base.svelte @@ -1,25 +1,13 @@ + + + + diff --git a/packages/layerchart/src/lib/components/Density/Density.shared.svelte.ts b/packages/layerchart/src/lib/components/Density/Density.shared.svelte.ts new file mode 100644 index 000000000..ca54048c3 --- /dev/null +++ b/packages/layerchart/src/lib/components/Density/Density.shared.svelte.ts @@ -0,0 +1,17 @@ +import type { SVGAttributes } from 'svelte/elements'; +import type { CommonStyleProps, Without } from '$lib/utils/types.js'; +import type { Accessor } from '$lib/utils/common.js'; + +export type DensityPropsWithoutHTML = { + data?: any[]; + x?: Accessor; + y?: Accessor; + weight?: Accessor; + /** @default 20 */ + bandwidth?: number; + /** @default 20 */ + thresholds?: number; +} & CommonStyleProps; + +export type DensityProps = DensityPropsWithoutHTML & + Without, DensityPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/Density/Density.svelte b/packages/layerchart/src/lib/components/Density/Density.svelte new file mode 100644 index 000000000..67c474f53 --- /dev/null +++ b/packages/layerchart/src/lib/components/Density/Density.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Density/Density.svg.svelte b/packages/layerchart/src/lib/components/Density/Density.svg.svelte new file mode 100644 index 000000000..5d360efd1 --- /dev/null +++ b/packages/layerchart/src/lib/components/Density/Density.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Hull.svelte b/packages/layerchart/src/lib/components/Hull/Hull.base.svelte similarity index 56% rename from packages/layerchart/src/lib/components/Hull.svelte rename to packages/layerchart/src/lib/components/Hull/Hull.base.svelte index e6ba413c2..acf47d389 100644 --- a/packages/layerchart/src/lib/components/Hull.svelte +++ b/packages/layerchart/src/lib/components/Hull/Hull.base.svelte @@ -1,55 +1,13 @@ + + + + diff --git a/packages/layerchart/src/lib/components/Hull/Hull.shared.svelte.ts b/packages/layerchart/src/lib/components/Hull/Hull.shared.svelte.ts new file mode 100644 index 000000000..a178943d8 --- /dev/null +++ b/packages/layerchart/src/lib/components/Hull/Hull.shared.svelte.ts @@ -0,0 +1,29 @@ +import type { Without, CommonStyleProps } from '$lib/utils/types.js'; +import type { SVGAttributes } from 'svelte/elements'; +import type { ComponentProps } from 'svelte'; +import type { Delaunay } from 'd3-delaunay'; +import type Spline from '../Spline/Spline.svelte'; +import type { GroupProps } from '../Group/Group.shared.svelte.js'; + +export type HullPropsWithoutHTML = { + data?: any; + /** @default curveLinearClosed */ + curve?: ComponentProps['curve']; + classes?: { + root?: string; + path?: string; + }; + onpointermove?: ( + e: PointerEvent, + details: { points: [number, number][]; polygon: Delaunay.Polygon } + ) => void; + onclick?: ( + e: MouseEvent, + details: { points: [number, number][]; polygon: Delaunay.Polygon } + ) => void; + onpointerleave?: (e: PointerEvent) => void; + /** @bindable */ + ref?: SVGGElement; +} & CommonStyleProps; + +export type HullProps = HullPropsWithoutHTML & Without; diff --git a/packages/layerchart/src/lib/components/Hull/Hull.svelte b/packages/layerchart/src/lib/components/Hull/Hull.svelte new file mode 100644 index 000000000..3317db8ca --- /dev/null +++ b/packages/layerchart/src/lib/components/Hull/Hull.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Hull/Hull.svg.svelte b/packages/layerchart/src/lib/components/Hull/Hull.svg.svelte new file mode 100644 index 000000000..fca61f8ea --- /dev/null +++ b/packages/layerchart/src/lib/components/Hull/Hull.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Link/Link.base.svelte b/packages/layerchart/src/lib/components/Link/Link.base.svelte new file mode 100644 index 000000000..ff21c4012 --- /dev/null +++ b/packages/layerchart/src/lib/components/Link/Link.base.svelte @@ -0,0 +1,260 @@ + + + + +{#if isArrayMode} + {#each arrayRows as d, i (i)} + {@const { source, target } = resolveArrayCoords(d)} + {@const resolvedStroke = + resolvePerDatum(strokeProp, d) ?? (ctx.config.c ? ctx.cGet(d) : undefined)} + + {/each} +{:else} + +{/if} diff --git a/packages/layerchart/src/lib/components/Link/Link.canvas.svelte b/packages/layerchart/src/lib/components/Link/Link.canvas.svelte new file mode 100644 index 000000000..5c368d75b --- /dev/null +++ b/packages/layerchart/src/lib/components/Link/Link.canvas.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Link/Link.shared.svelte.ts b/packages/layerchart/src/lib/components/Link/Link.shared.svelte.ts new file mode 100644 index 000000000..3bd1736b8 --- /dev/null +++ b/packages/layerchart/src/lib/components/Link/Link.shared.svelte.ts @@ -0,0 +1,47 @@ +import { curveBumpX, curveBumpY, type CurveFactory } from 'd3-shape'; +import type { MarkerOptions } from '../MarkerWrapper.svelte'; +import type { Without } from '$lib/utils/types.js'; +import type { MotionNoneOption, MotionTweenOption } from '$lib/utils/motion.svelte.js'; +import type { LinkSweep, LinkType } from '$lib/utils/linkUtils.js'; +import type { PathProps, PathPropsWithoutHTML } from '../Path/Path.shared.svelte.js'; +import type { Accessor } from '$lib/utils/common.js'; + +export type LinkPropsWithoutHTML = { + x1?: Accessor; + y1?: Accessor; + x2?: Accessor; + y2?: Accessor; + data?: any; + /** @default false */ + sankey?: boolean; + source?: (d: any) => any; + target?: (d: any) => any; + x?: (d: any) => any; + y?: (d: any) => any; + /** @default 'd3' */ + type?: LinkType; + /** @default 20 */ + radius?: number; + /** @default 22.5 */ + bend?: number; + curve?: CurveFactory; + sweep?: LinkSweep; + orientation?: 'vertical' | 'horizontal'; + radial?: boolean; + marker?: MarkerOptions; + markerMid?: MarkerOptions; + markerStart?: MarkerOptions; + markerEnd?: MarkerOptions; + motion?: MotionTweenOption | MotionNoneOption; + class?: string | ((d: any) => string); +} & Omit; + +export type LinkProps = LinkPropsWithoutHTML & Without; + +export const LINK_FALLBACK_COORDS = { x: 0, y: 0 }; + +export function isAccessorAccessor(value: Accessor | undefined): boolean { + return typeof value === 'string' || typeof value === 'function'; +} + +export { curveBumpX, curveBumpY }; diff --git a/packages/layerchart/src/lib/components/Link/Link.svelte b/packages/layerchart/src/lib/components/Link/Link.svelte index b9d4a8862..ffc6df0db 100644 --- a/packages/layerchart/src/lib/components/Link/Link.svelte +++ b/packages/layerchart/src/lib/components/Link/Link.svelte @@ -1,376 +1,20 @@ -{#if isArrayMode} - {#each arrayRows as d, i (i)} - {@const { source, target } = resolveArrayCoords(d)} - {@const resolvedStroke = - resolvePerDatum(strokeProp, d) ?? (ctx.config.c ? ctx.cGet(d) : undefined)} - - {/each} -{:else} - +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + {/if} diff --git a/packages/layerchart/src/lib/components/Link/Link.svg.svelte b/packages/layerchart/src/lib/components/Link/Link.svg.svelte new file mode 100644 index 000000000..85d074aee --- /dev/null +++ b/packages/layerchart/src/lib/components/Link/Link.svg.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Month.svelte b/packages/layerchart/src/lib/components/Month/Month.svelte similarity index 97% rename from packages/layerchart/src/lib/components/Month.svelte rename to packages/layerchart/src/lib/components/Month/Month.svelte index a0a4a15f9..6bbc1b710 100644 --- a/packages/layerchart/src/lib/components/Month.svelte +++ b/packages/layerchart/src/lib/components/Month/Month.svelte @@ -82,10 +82,10 @@ import { index } from 'd3-array'; import { format } from '@layerstack/utils'; - import Rect, { type RectPropsWithoutHTML } from './Rect/Rect.svelte'; - import Group from './Group/Group.svelte'; - import Text from './Text/Text.svelte'; - import { chartDataArray } from '../utils/common.js'; + import Rect, { type RectPropsWithoutHTML } from '../Rect/Rect.svelte'; + import Group from '../Group/Group.svelte'; + import Text from '../Text/Text.svelte'; + import { chartDataArray } from '../../utils/common.js'; import { getChartContext } from '$lib/contexts/chart.js'; import type { SVGAttributes } from 'svelte/elements'; import type { Without } from '$lib/utils/types.js'; diff --git a/packages/layerchart/src/lib/components/Raster.svelte b/packages/layerchart/src/lib/components/Raster/Raster.svelte similarity index 99% rename from packages/layerchart/src/lib/components/Raster.svelte rename to packages/layerchart/src/lib/components/Raster/Raster.svelte index f87fb05eb..d6f297b06 100644 --- a/packages/layerchart/src/lib/components/Raster.svelte +++ b/packages/layerchart/src/lib/components/Raster/Raster.svelte @@ -67,7 +67,7 @@ import { getGeoContext } from '$lib/contexts/geo.js'; import { gridCellCenterToBounds, resolveRasterBounds } from '$lib/utils/index.js'; import { interpolateGrid } from '$lib/utils/rasterInterpolate.js'; - import Image from './Image/Image.svelte'; + import Image from '../Image/Image.svelte'; const ctx = getChartContext(); const geo = getGeoContext(); diff --git a/packages/layerchart/src/lib/components/Vector/Vector.base.svelte b/packages/layerchart/src/lib/components/Vector/Vector.base.svelte new file mode 100644 index 000000000..3a38d2296 --- /dev/null +++ b/packages/layerchart/src/lib/components/Vector/Vector.base.svelte @@ -0,0 +1,264 @@ + + + + +{#if children} + {#if dataMode} + {#each resolvedItems as item (item.key)} + {@const offset = getAnchorOffset(item.length)} + {@const resolvedClass = resolveStyleProp(className, item.d)} + + + {@render children({ length: item.length, d: item.d })} + + + {/each} + {:else} + {@const offset = getAnchorOffset(motionLength.current)} + + + {@render children({ length: motionLength.current })} + + + {/if} +{:else if dataMode && hasPerItemStyles} + {#each resolvedItems as item (item.key)} + {@const resolvedFill = resolveColorProp(fill, item.d, chartCtx.cScale)} + {@const resolvedStroke = resolveColorProp(stroke, item.d, chartCtx.cScale)} + {@const resolvedFillOpacity = resolveStyleProp(fillOpacity, item.d)} + {@const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d)} + {@const resolvedOpacity = resolveStyleProp(opacity, item.d)} + {@const resolvedClass = resolveStyleProp(className, item.d)} + + {/each} +{:else} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/Vector/Vector.canvas.svelte b/packages/layerchart/src/lib/components/Vector/Vector.canvas.svelte new file mode 100644 index 000000000..b5e012d17 --- /dev/null +++ b/packages/layerchart/src/lib/components/Vector/Vector.canvas.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Vector/Vector.shared.svelte.ts b/packages/layerchart/src/lib/components/Vector/Vector.shared.svelte.ts new file mode 100644 index 000000000..0b5e62a6b --- /dev/null +++ b/packages/layerchart/src/lib/components/Vector/Vector.shared.svelte.ts @@ -0,0 +1,38 @@ +import type { Snippet } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import type { Without } from '$lib/utils/types.js'; +import type { DataProp, DataDrivenStyleProps } from '$lib/utils/dataProp.js'; +import type { VectorAnchor } from '$lib/utils/path.js'; +import type { MotionProp } from '$lib/utils/motion.svelte.js'; + +export type VectorShape = 'arrow' | 'arrow-filled' | 'spike'; + +export type VectorPropsWithoutHTML = { + /** @default 0 */ + x?: DataProp; + /** @default x */ + initialX?: number; + /** @default 0 */ + y?: DataProp; + /** @default y */ + initialY?: number; + /** @default 12 */ + length?: DataProp; + /** @default length */ + initialLength?: number; + /** @default 0 */ + rotate?: DataProp; + /** @default 'arrow' */ + shape?: VectorShape; + anchor?: VectorAnchor; + /** Width of the vector shape in pixels. */ + width?: number; + data?: any[]; + /** @default (d, i) => i */ + key?: (d: any, index: number) => any; + motion?: MotionProp; + children?: Snippet<[{ length: number; d?: any }]>; +} & DataDrivenStyleProps; + +export type VectorProps = VectorPropsWithoutHTML & + Without, VectorPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/Vector/Vector.svelte b/packages/layerchart/src/lib/components/Vector/Vector.svelte index 8a7ac3d9c..955c33dd1 100644 --- a/packages/layerchart/src/lib/components/Vector/Vector.svelte +++ b/packages/layerchart/src/lib/components/Vector/Vector.svelte @@ -1,399 +1,20 @@ -{#if children} - - {#if dataMode} - {#each resolvedItems as item (item.key)} - {@const offset = getAnchorOffset(item.length)} - {@const resolvedClass = resolveStyleProp(className, item.d)} - - - {@render children({ length: item.length, d: item.d })} - - - {/each} - {:else} - {@const offset = getAnchorOffset(motionLength.current)} - - - {@render children({ length: motionLength.current })} - - - {/if} -{:else if dataMode && hasPerItemStyles} - - {#each resolvedItems as item (item.key)} - {@const resolvedFill = resolveColorProp(fill, item.d, chartCtx.cScale)} - {@const resolvedStroke = resolveColorProp(stroke, item.d, chartCtx.cScale)} - {@const resolvedFillOpacity = resolveStyleProp(fillOpacity, item.d)} - {@const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d)} - {@const resolvedOpacity = resolveStyleProp(opacity, item.d)} - {@const resolvedClass = resolveStyleProp(className, item.d)} - - {/each} -{:else} - - +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + {/if} - - diff --git a/packages/layerchart/src/lib/components/Vector/Vector.svg.svelte b/packages/layerchart/src/lib/components/Vector/Vector.svg.svelte new file mode 100644 index 000000000..6ca68bbfc --- /dev/null +++ b/packages/layerchart/src/lib/components/Vector/Vector.svg.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Violin.svelte b/packages/layerchart/src/lib/components/Violin/Violin.svelte similarity index 98% rename from packages/layerchart/src/lib/components/Violin.svelte rename to packages/layerchart/src/lib/components/Violin/Violin.svelte index 9b917a645..2633df19f 100644 --- a/packages/layerchart/src/lib/components/Violin.svelte +++ b/packages/layerchart/src/lib/components/Violin/Violin.svelte @@ -64,10 +64,10 @@ import { cls } from '@layerstack/tailwind'; - import Group from './Group/Group.svelte'; - import Path from './Path/Path.svelte'; - import Rect from './Rect/Rect.svelte'; - import Line from './Line/Line.svelte'; + import Group from '../Group/Group.svelte'; + import Path from '../Path/Path.svelte'; + import Rect from '../Rect/Rect.svelte'; + import Line from '../Line/Line.svelte'; import { accessor } from '$lib/utils/common.js'; import { getChartContext } from '$lib/contexts/chart.js'; import { isScaleBand } from '$lib/utils/scales.svelte.js'; diff --git a/packages/layerchart/src/lib/components/Voronoi/Voronoi.svelte b/packages/layerchart/src/lib/components/Voronoi/Voronoi.svelte index 67be75811..c811e99f6 100644 --- a/packages/layerchart/src/lib/components/Voronoi/Voronoi.svelte +++ b/packages/layerchart/src/lib/components/Voronoi/Voronoi.svelte @@ -81,7 +81,7 @@ import { getChartContext } from '$lib/contexts/chart.js'; import { getGeoContext } from '$lib/contexts/geo.js'; import { accessor } from '$lib/utils/common.js'; - import CircleClipPath from '../CircleClipPath.svelte'; + import CircleClipPath from '../CircleClipPath/CircleClipPath.svelte'; let { data, diff --git a/packages/layerchart/src/lib/components/index.ts b/packages/layerchart/src/lib/components/index.ts index 9b1b62396..57b363124 100644 --- a/packages/layerchart/src/lib/components/index.ts +++ b/packages/layerchart/src/lib/components/index.ts @@ -21,14 +21,14 @@ export { default as Bars } from './Bars/Bars.svelte'; export * from './Bars/Bars.svelte'; export { default as Blur } from './Blur.svelte'; export * from './Blur.svelte'; -export { default as BoxPlot } from './BoxPlot.svelte'; -export * from './BoxPlot.svelte'; +export { default as BoxPlot } from './BoxPlot/BoxPlot.svelte'; +export * from './BoxPlot/BoxPlot.svelte'; export { default as Bounds } from './Bounds.svelte'; export * from './Bounds.svelte'; export { default as BrushContext } from './BrushContext.svelte'; export * from './BrushContext.svelte'; -export { default as Calendar } from './Calendar.svelte'; -export * from './Calendar.svelte'; +export { default as Calendar } from './Calendar/Calendar.svelte'; +export * from './Calendar/Calendar.svelte'; export { default as Cell } from './Cell/Cell.svelte'; export * from './Cell/Cell.svelte'; export { default as Canvas } from './layers/Canvas.svelte'; @@ -39,16 +39,16 @@ export { default as ChartClipPath } from './ChartClipPath/ChartClipPath.svelte'; export * from './ChartClipPath/ChartClipPath.svelte'; export { default as Circle } from './Circle/Circle.svelte'; export * from './Circle/Circle.svelte'; -export { default as CircleClipPath } from './CircleClipPath.svelte'; -export * from './CircleClipPath.svelte'; +export { default as CircleClipPath } from './CircleClipPath/CircleClipPath.svelte'; +export * from './CircleClipPath/CircleClipPath.svelte'; export { default as ClipPath } from './ClipPath/ClipPath.svelte'; export * from './ClipPath/ClipPath.svelte'; export { default as ColorRamp } from './ColorRamp.svelte'; export * from './ColorRamp.svelte'; -export { default as Contour } from './Contour.svelte'; -export * from './Contour.svelte'; -export { default as Density } from './Density.svelte'; -export * from './Density.svelte'; +export { default as Contour } from './Contour/Contour.svelte'; +export * from './Contour/Contour.svelte'; +export { default as Density } from './Density/Density.svelte'; +export * from './Density/Density.svelte'; export { default as Ellipse } from './Ellipse/Ellipse.svelte'; export * from './Ellipse/Ellipse.svelte'; export { default as Frame } from './Frame/Frame.svelte'; @@ -61,8 +61,8 @@ export { default as Highlight } from './Highlight/Highlight.svelte'; export * from './Highlight/Highlight.svelte'; export { default as Html } from './layers/Html.svelte'; export * from './layers/Html.svelte'; -export { default as Hull } from './Hull.svelte'; -export * from './Hull.svelte'; +export { default as Hull } from './Hull/Hull.svelte'; +export * from './Hull/Hull.svelte'; export { default as Image } from './Image/Image.svelte'; export * from './Image/Image.svelte'; export { default as Labels } from './Labels/Labels.svelte'; @@ -83,8 +83,8 @@ export { default as Link } from './Link/Link.svelte'; export * from './Link/Link.svelte'; export { default as MotionPath } from './MotionPath.svelte'; export * from './MotionPath.svelte'; -export { default as Month } from './Month.svelte'; -export * from './Month.svelte'; +export { default as Month } from './Month/Month.svelte'; +export * from './Month/Month.svelte'; export { default as Path } from './Path/Path.svelte'; export * from './Path/Path.svelte'; export { default as Pattern } from './Pattern/Pattern.svelte'; @@ -99,8 +99,8 @@ export { default as Polygon } from './Polygon/Polygon.svelte'; export * from './Polygon/Polygon.svelte'; export { default as RadialGradient } from './RadialGradient/RadialGradient.svelte'; export * from './RadialGradient/RadialGradient.svelte'; -export { default as Raster } from './Raster.svelte'; -export * from './Raster.svelte'; +export { default as Raster } from './Raster/Raster.svelte'; +export * from './Raster/Raster.svelte'; export { default as Rect } from './Rect/Rect.svelte'; export * from './Rect/Rect.svelte'; export { default as RectClipPath } from './RectClipPath/RectClipPath.svelte'; @@ -123,8 +123,8 @@ export * from './TransformContext.svelte'; export { default as Vector } from './Vector/Vector.svelte'; export * from './Vector/Vector.svelte'; -export { default as Violin } from './Violin.svelte'; -export * from './Violin.svelte'; +export { default as Violin } from './Violin/Violin.svelte'; +export * from './Violin/Violin.svelte'; export { default as Voronoi } from './Voronoi/Voronoi.svelte'; export * from './Voronoi/Voronoi.svelte'; export { default as WebGL } from './layers/WebGL.svelte'; diff --git a/packages/layerchart/src/lib/html.ts b/packages/layerchart/src/lib/html.ts index e10f72a05..6398392f4 100644 --- a/packages/layerchart/src/lib/html.ts +++ b/packages/layerchart/src/lib/html.ts @@ -119,3 +119,5 @@ export type { } from './components/Frame/Frame.shared.svelte.js'; export { default as Cell } from './components/Cell/Cell.html.svelte'; export type { CellProps } from './components/Cell/Cell.shared.svelte.js'; +export { default as CircleClipPath } from './components/CircleClipPath/CircleClipPath.html.svelte'; +export type { CircleClipPathPropsWithoutHTML } from './components/CircleClipPath/CircleClipPath.shared.svelte.js'; diff --git a/packages/layerchart/src/lib/svg.ts b/packages/layerchart/src/lib/svg.ts index 9965c62b2..818bc8c3f 100644 --- a/packages/layerchart/src/lib/svg.ts +++ b/packages/layerchart/src/lib/svg.ts @@ -176,6 +176,34 @@ export type { TrailProps, TrailPropsWithoutHTML, } from './components/Trail/Trail.shared.svelte.js'; +export { default as Vector } from './components/Vector/Vector.svg.svelte'; +export type { + VectorProps, + VectorPropsWithoutHTML, + VectorShape, +} from './components/Vector/Vector.shared.svelte.js'; +export { default as Link } from './components/Link/Link.svg.svelte'; +export type { LinkProps, LinkPropsWithoutHTML } from './components/Link/Link.shared.svelte.js'; +export { default as AnnotationRange } from './components/AnnotationRange/AnnotationRange.svg.svelte'; +export type { + AnnotationRangeProps, + AnnotationRangePropsWithoutHTML, +} from './components/AnnotationRange/AnnotationRange.shared.svelte.js'; +export { default as Hull } from './components/Hull/Hull.svg.svelte'; +export type { HullProps, HullPropsWithoutHTML } from './components/Hull/Hull.shared.svelte.js'; +export { default as Density } from './components/Density/Density.svg.svelte'; +export type { + DensityProps, + DensityPropsWithoutHTML, +} from './components/Density/Density.shared.svelte.js'; +export { default as Calendar } from './components/Calendar/Calendar.svg.svelte'; +export type { + CalendarProps, + CalendarPropsWithoutHTML, + CalendarCell, +} from './components/Calendar/Calendar.shared.svelte.js'; +export { default as CircleClipPath } from './components/CircleClipPath/CircleClipPath.svg.svelte'; +export type { CircleClipPathPropsWithoutHTML } from './components/CircleClipPath/CircleClipPath.shared.svelte.js'; export { default as RectClipPath } from './components/RectClipPath/RectClipPath.svg.svelte'; export type { RectClipPathProps, From 5cfa97812d6c3c0b2b22c11a9f7e5f5d3f5dc82a Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 28 Apr 2026 15:25:08 -0400 Subject: [PATCH 20/36] split BoxPlot, Violin, Raster, Month, Contour, and Voronoi into 3 layer-specific components --- bundle-analyzer/bundle-reports/latest.json | 310 +++++++++++--- bundle-analyzer/bundle-scenarios.ts | 20 + packages/layerchart/src/lib/canvas.ts | 31 ++ .../components/BoxPlot/BoxPlot.base.svelte | 195 +++++++++ .../components/BoxPlot/BoxPlot.canvas.svelte | 16 + .../BoxPlot/BoxPlot.shared.svelte.ts | 28 ++ .../src/lib/components/BoxPlot/BoxPlot.svelte | 391 +----------------- .../lib/components/BoxPlot/BoxPlot.svg.svelte | 16 + .../components/Contour/Contour.base.svelte | 246 +++++++++++ .../components/Contour/Contour.canvas.svelte | 14 + .../Contour/Contour.shared.svelte.ts | 29 ++ .../src/lib/components/Contour/Contour.svelte | 303 +------------- .../lib/components/Contour/Contour.svg.svelte | 14 + .../lib/components/Month/Month.base.svelte | 189 +++++++++ .../lib/components/Month/Month.canvas.svelte | 19 + .../components/Month/Month.shared.svelte.ts | 37 ++ .../src/lib/components/Month/Month.svelte | 281 +------------ .../src/lib/components/Month/Month.svg.svelte | 19 + .../lib/components/Raster/Raster.base.svelte | 281 +++++++++++++ .../components/Raster/Raster.canvas.svelte | 13 + .../lib/components/Raster/Raster.html.svelte | 13 + .../components/Raster/Raster.shared.svelte.ts | 29 ++ .../src/lib/components/Raster/Raster.svelte | 340 +-------------- .../lib/components/Raster/Raster.svg.svelte | 13 + .../lib/components/Violin/Violin.base.svelte | 252 +++++++++++ .../components/Violin/Violin.canvas.svelte | 16 + .../components/Violin/Violin.shared.svelte.ts | 25 ++ .../src/lib/components/Violin/Violin.svelte | 320 +------------- .../lib/components/Violin/Violin.svg.svelte | 16 + .../components/Voronoi/Voronoi.base.svelte | 151 +++++++ .../components/Voronoi/Voronoi.canvas.svelte | 15 + .../Voronoi/Voronoi.shared.svelte.ts | 35 ++ .../src/lib/components/Voronoi/Voronoi.svelte | 212 +--------- .../lib/components/Voronoi/Voronoi.svg.svelte | 15 + packages/layerchart/src/lib/html.ts | 5 + packages/layerchart/src/lib/svg.ts | 31 ++ 36 files changed, 2108 insertions(+), 1832 deletions(-) create mode 100644 packages/layerchart/src/lib/components/BoxPlot/BoxPlot.base.svelte create mode 100644 packages/layerchart/src/lib/components/BoxPlot/BoxPlot.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/BoxPlot/BoxPlot.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/BoxPlot/BoxPlot.svg.svelte create mode 100644 packages/layerchart/src/lib/components/Contour/Contour.base.svelte create mode 100644 packages/layerchart/src/lib/components/Contour/Contour.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Contour/Contour.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Contour/Contour.svg.svelte create mode 100644 packages/layerchart/src/lib/components/Month/Month.base.svelte create mode 100644 packages/layerchart/src/lib/components/Month/Month.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Month/Month.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Month/Month.svg.svelte create mode 100644 packages/layerchart/src/lib/components/Raster/Raster.base.svelte create mode 100644 packages/layerchart/src/lib/components/Raster/Raster.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Raster/Raster.html.svelte create mode 100644 packages/layerchart/src/lib/components/Raster/Raster.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Raster/Raster.svg.svelte create mode 100644 packages/layerchart/src/lib/components/Violin/Violin.base.svelte create mode 100644 packages/layerchart/src/lib/components/Violin/Violin.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Violin/Violin.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Violin/Violin.svg.svelte create mode 100644 packages/layerchart/src/lib/components/Voronoi/Voronoi.base.svelte create mode 100644 packages/layerchart/src/lib/components/Voronoi/Voronoi.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/Voronoi/Voronoi.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/Voronoi/Voronoi.svg.svelte diff --git a/bundle-analyzer/bundle-reports/latest.json b/bundle-analyzer/bundle-reports/latest.json index 55a81f5df..f04582dd6 100644 --- a/bundle-analyzer/bundle-reports/latest.json +++ b/bundle-analyzer/bundle-reports/latest.json @@ -1,12 +1,12 @@ { - "timestamp": "2026-04-28T17:43:07.104Z", + "timestamp": "2026-04-28T19:19:39.925Z", "results": [ { "scenario": "core", "description": "Core charting components without rendering layer", "group": "Foundation", - "size": 373472, - "gzipSize": 86295, + "size": 374970, + "gzipSize": 86618, "imports": [ "Chart", "Svg" @@ -16,8 +16,8 @@ "scenario": "core-svg", "description": "Svg-based rendering", "group": "Foundation", - "size": 350474, - "gzipSize": 81861, + "size": 351979, + "gzipSize": 82187, "imports": [ "Chart", "Svg" @@ -27,8 +27,8 @@ "scenario": "core-canvas", "description": "Canvas-based rendering", "group": "Foundation", - "size": 357008, - "gzipSize": 83586, + "size": 358506, + "gzipSize": 83936, "imports": [ "Chart", "Canvas" @@ -38,8 +38,8 @@ "scenario": "core-html", "description": "HTML-based rendering", "group": "Foundation", - "size": 358328, - "gzipSize": 83882, + "size": 359830, + "gzipSize": 84204, "imports": [ "Chart", "Html" @@ -49,8 +49,8 @@ "scenario": "line-chart", "description": "Basic line chart with axes and grid", "group": "Cartesian charts", - "size": 373967, - "gzipSize": 86324, + "size": 375465, + "gzipSize": 86650, "imports": [ "Chart", "Svg", @@ -63,8 +63,8 @@ "scenario": "line-chart-svg", "description": "Line chart composed from `layerchart/svg`", "group": "Cartesian charts", - "size": 350498, - "gzipSize": 81877, + "size": 352003, + "gzipSize": 82209, "imports": [ "Chart", "Layer", @@ -77,8 +77,8 @@ "scenario": "line-chart-canvas", "description": "Line chart composed from `layerchart/canvas`", "group": "Cartesian charts", - "size": 357032, - "gzipSize": 83608, + "size": 358530, + "gzipSize": 83963, "imports": [ "Chart", "Layer", @@ -91,8 +91,8 @@ "scenario": "line-chart-html", "description": "Line chart composed from `layerchart/html`", "group": "Cartesian charts", - "size": 358352, - "gzipSize": 83896, + "size": 359854, + "gzipSize": 84218, "imports": [ "Chart", "Layer", @@ -105,8 +105,8 @@ "scenario": "line-chart-interactive", "description": "Line chart with tooltip and highlight", "group": "Cartesian charts", - "size": 388808, - "gzipSize": 89665, + "size": 390306, + "gzipSize": 90006, "imports": [ "Chart", "Svg", @@ -121,8 +121,8 @@ "scenario": "LineChart", "description": "High-level `LineChart` component", "group": "Cartesian charts", - "size": 397591, - "gzipSize": 92683, + "size": 399089, + "gzipSize": 93026, "imports": [ "LineChart" ] @@ -131,8 +131,8 @@ "scenario": "area-chart", "description": "Area chart with axes", "group": "Cartesian charts", - "size": 387483, - "gzipSize": 89388, + "size": 388981, + "gzipSize": 89728, "imports": [ "Chart", "Svg", @@ -145,8 +145,8 @@ "scenario": "AreaChart", "description": "High-level `AreaChart` component", "group": "Cartesian charts", - "size": 391071, - "gzipSize": 90231, + "size": 392569, + "gzipSize": 90588, "imports": [ "AreaChart" ] @@ -155,8 +155,8 @@ "scenario": "bar-chart", "description": "Bar chart with axes", "group": "Cartesian charts", - "size": 377807, - "gzipSize": 87054, + "size": 379305, + "gzipSize": 87396, "imports": [ "Chart", "Svg", @@ -169,8 +169,8 @@ "scenario": "BarChart", "description": "High-level `BarChart` component", "group": "Cartesian charts", - "size": 382940, - "gzipSize": 88188, + "size": 384438, + "gzipSize": 88534, "imports": [ "BarChart" ] @@ -179,8 +179,8 @@ "scenario": "scatter-chart", "description": "Scatter plot with points", "group": "Cartesian charts", - "size": 374749, - "gzipSize": 86781, + "size": 376247, + "gzipSize": 87110, "imports": [ "Chart", "Svg", @@ -194,8 +194,8 @@ "scenario": "ScatterChart", "description": "High-level `ScatterChart` component", "group": "Cartesian charts", - "size": 377496, - "gzipSize": 87450, + "size": 378994, + "gzipSize": 87787, "imports": [ "ScatterChart" ] @@ -204,8 +204,8 @@ "scenario": "pie-chart", "description": "Pie/donut chart with arcs", "group": "Cartesian charts", - "size": 386214, - "gzipSize": 89437, + "size": 387712, + "gzipSize": 89808, "imports": [ "Chart", "Svg", @@ -218,8 +218,8 @@ "scenario": "PieChart", "description": "High-level `PieChart` component", "group": "Cartesian charts", - "size": 411672, - "gzipSize": 95159, + "size": 413165, + "gzipSize": 95513, "imports": [ "PieChart" ] @@ -228,8 +228,8 @@ "scenario": "ArcChart", "description": "High-level `ArcChart` component", "group": "Cartesian charts", - "size": 405168, - "gzipSize": 93924, + "size": 406661, + "gzipSize": 94270, "imports": [ "ArcChart" ] @@ -238,8 +238,8 @@ "scenario": "geo", "description": "Geographic map with paths", "group": "Geo", - "size": 407218, - "gzipSize": 94283, + "size": 407211, + "gzipSize": 94415, "imports": [ "Chart", "Svg", @@ -252,8 +252,8 @@ "scenario": "geo-tiles", "description": "Geographic map with tile layer", "group": "Geo", - "size": 411663, - "gzipSize": 95795, + "size": 411656, + "gzipSize": 95919, "imports": [ "Chart", "Svg", @@ -268,7 +268,7 @@ "description": "Full geo setup with all geo components", "group": "Geo", "size": 460021, - "gzipSize": 109396, + "gzipSize": 109495, "imports": [ "Chart", "Svg", @@ -290,8 +290,8 @@ "scenario": "hierarchy-tree", "description": "Tree layout with links", "group": "Hierarchy", - "size": 413578, - "gzipSize": 96112, + "size": 415076, + "gzipSize": 96457, "imports": [ "Chart", "Svg", @@ -305,8 +305,8 @@ "scenario": "hierarchy-treemap", "description": "Treemap layout", "group": "Hierarchy", - "size": 392396, - "gzipSize": 90916, + "size": 393889, + "gzipSize": 91267, "imports": [ "Chart", "Svg", @@ -320,8 +320,8 @@ "scenario": "hierarchy-pack", "description": "Circle packing layout", "group": "Hierarchy", - "size": 392110, - "gzipSize": 91006, + "size": 393608, + "gzipSize": 91347, "imports": [ "Chart", "Svg", @@ -334,8 +334,8 @@ "scenario": "force", "description": "Force-directed graph layout", "group": "Graph / network", - "size": 416035, - "gzipSize": 97005, + "size": 417533, + "gzipSize": 97346, "imports": [ "Chart", "Svg", @@ -349,8 +349,8 @@ "scenario": "dagre", "description": "Dagre directed graph layout", "group": "Graph / network", - "size": 473464, - "gzipSize": 112404, + "size": 474962, + "gzipSize": 112737, "imports": [ "Chart", "Svg", @@ -364,8 +364,8 @@ "scenario": "sankey", "description": "Sankey flow diagram", "group": "Graph / network", - "size": 415770, - "gzipSize": 96364, + "size": 417268, + "gzipSize": 96693, "imports": [ "Chart", "Svg", @@ -379,8 +379,8 @@ "scenario": "chord", "description": "Chord diagram", "group": "Graph / network", - "size": 389419, - "gzipSize": 90285, + "size": 389429, + "gzipSize": 90418, "imports": [ "Chart", "Svg", @@ -1828,12 +1828,202 @@ "CircleClipPath" ] }, + { + "scenario": "Voronoi", + "description": "Standalone Voronoi (agnostic) — baseline", + "group": "Components", + "size": 177185, + "gzipSize": 46704, + "imports": [ + "Voronoi" + ] + }, + { + "scenario": "Voronoi.svg", + "description": "Standalone Voronoi from `layerchart/svg`", + "group": "Components", + "size": 175162, + "gzipSize": 46464, + "imports": [ + "Voronoi" + ] + }, + { + "scenario": "Voronoi.canvas", + "description": "Standalone Voronoi from `layerchart/canvas`", + "group": "Components", + "size": 174164, + "gzipSize": 46000, + "imports": [ + "Voronoi" + ] + }, + { + "scenario": "Contour", + "description": "Standalone Contour (agnostic) — baseline", + "group": "Components", + "size": 165261, + "gzipSize": 45091, + "imports": [ + "Contour" + ] + }, + { + "scenario": "Contour.svg", + "description": "Standalone Contour from `layerchart/svg`", + "group": "Components", + "size": 154130, + "gzipSize": 41991, + "imports": [ + "Contour" + ] + }, + { + "scenario": "Contour.canvas", + "description": "Standalone Contour from `layerchart/canvas`", + "group": "Components", + "size": 151468, + "gzipSize": 42247, + "imports": [ + "Contour" + ] + }, + { + "scenario": "Month", + "description": "Standalone Month (agnostic) — baseline", + "group": "Components", + "size": 145395, + "gzipSize": 34818, + "imports": [ + "Month" + ] + }, + { + "scenario": "Month.svg", + "description": "Standalone Month from `layerchart/svg`", + "group": "Components", + "size": 131590, + "gzipSize": 31322, + "imports": [ + "Month" + ] + }, + { + "scenario": "Month.canvas", + "description": "Standalone Month from `layerchart/canvas`", + "group": "Components", + "size": 133179, + "gzipSize": 32172, + "imports": [ + "Month" + ] + }, + { + "scenario": "Raster", + "description": "Standalone Raster (agnostic) — baseline", + "group": "Components", + "size": 124893, + "gzipSize": 33929, + "imports": [ + "Raster" + ] + }, + { + "scenario": "Raster.svg", + "description": "Standalone Raster from `layerchart/svg`", + "group": "Components", + "size": 118793, + "gzipSize": 32840, + "imports": [ + "Raster" + ] + }, + { + "scenario": "Raster.canvas", + "description": "Standalone Raster from `layerchart/canvas`", + "group": "Components", + "size": 116920, + "gzipSize": 32309, + "imports": [ + "Raster" + ] + }, + { + "scenario": "Raster.html", + "description": "Standalone Raster from `layerchart/html`", + "group": "Components", + "size": 117693, + "gzipSize": 32508, + "imports": [ + "Raster" + ] + }, + { + "scenario": "Violin", + "description": "Standalone Violin (agnostic) — baseline", + "group": "Components", + "size": 134196, + "gzipSize": 31522, + "imports": [ + "Violin" + ] + }, + { + "scenario": "Violin.svg", + "description": "Standalone Violin from `layerchart/svg`", + "group": "Components", + "size": 117906, + "gzipSize": 27216, + "imports": [ + "Violin" + ] + }, + { + "scenario": "Violin.canvas", + "description": "Standalone Violin from `layerchart/canvas`", + "group": "Components", + "size": 112909, + "gzipSize": 27177, + "imports": [ + "Violin" + ] + }, + { + "scenario": "BoxPlot", + "description": "Standalone BoxPlot (agnostic) — baseline", + "group": "Components", + "size": 122645, + "gzipSize": 25706, + "imports": [ + "BoxPlot" + ] + }, + { + "scenario": "BoxPlot.svg", + "description": "Standalone BoxPlot from `layerchart/svg`", + "group": "Components", + "size": 105136, + "gzipSize": 21519, + "imports": [ + "BoxPlot" + ] + }, + { + "scenario": "BoxPlot.canvas", + "description": "Standalone BoxPlot from `layerchart/canvas`", + "group": "Components", + "size": 107149, + "gzipSize": 22436, + "imports": [ + "BoxPlot" + ] + }, { "scenario": "all", "description": "Everything from layerchart (worst case)", "group": "Worst case", - "size": 983871, - "gzipSize": 233508, + "size": 996557, + "gzipSize": 234983, "imports": [ "*" ] diff --git a/bundle-analyzer/bundle-scenarios.ts b/bundle-analyzer/bundle-scenarios.ts index 048a355af..2018cbe29 100644 --- a/bundle-analyzer/bundle-scenarios.ts +++ b/bundle-analyzer/bundle-scenarios.ts @@ -914,6 +914,26 @@ export const scenarios: Scenario[] = [ { name: 'CircleClipPath.canvas', group: 'Components', description: 'Standalone CircleClipPath from `layerchart/canvas`', imports: ['CircleClipPath'], layers: { CircleClipPath: 'canvas' } }, { name: 'CircleClipPath.html', group: 'Components', description: 'Standalone CircleClipPath from `layerchart/html`', imports: ['CircleClipPath'], layers: { CircleClipPath: 'html' } }, + { name: 'Voronoi', group: 'Components', description: 'Standalone Voronoi (agnostic) — baseline', imports: ['Voronoi'] }, + { name: 'Voronoi.svg', group: 'Components', description: 'Standalone Voronoi from `layerchart/svg`', imports: ['Voronoi'], layers: { Voronoi: 'svg' } }, + { name: 'Voronoi.canvas', group: 'Components', description: 'Standalone Voronoi from `layerchart/canvas`', imports: ['Voronoi'], layers: { Voronoi: 'canvas' } }, + { name: 'Contour', group: 'Components', description: 'Standalone Contour (agnostic) — baseline', imports: ['Contour'] }, + { name: 'Contour.svg', group: 'Components', description: 'Standalone Contour from `layerchart/svg`', imports: ['Contour'], layers: { Contour: 'svg' } }, + { name: 'Contour.canvas', group: 'Components', description: 'Standalone Contour from `layerchart/canvas`', imports: ['Contour'], layers: { Contour: 'canvas' } }, + { name: 'Month', group: 'Components', description: 'Standalone Month (agnostic) — baseline', imports: ['Month'] }, + { name: 'Month.svg', group: 'Components', description: 'Standalone Month from `layerchart/svg`', imports: ['Month'], layers: { Month: 'svg' } }, + { name: 'Month.canvas', group: 'Components', description: 'Standalone Month from `layerchart/canvas`', imports: ['Month'], layers: { Month: 'canvas' } }, + { name: 'Raster', group: 'Components', description: 'Standalone Raster (agnostic) — baseline', imports: ['Raster'] }, + { name: 'Raster.svg', group: 'Components', description: 'Standalone Raster from `layerchart/svg`', imports: ['Raster'], layers: { Raster: 'svg' } }, + { name: 'Raster.canvas', group: 'Components', description: 'Standalone Raster from `layerchart/canvas`', imports: ['Raster'], layers: { Raster: 'canvas' } }, + { name: 'Raster.html', group: 'Components', description: 'Standalone Raster from `layerchart/html`', imports: ['Raster'], layers: { Raster: 'html' } }, + { name: 'Violin', group: 'Components', description: 'Standalone Violin (agnostic) — baseline', imports: ['Violin'] }, + { name: 'Violin.svg', group: 'Components', description: 'Standalone Violin from `layerchart/svg`', imports: ['Violin'], layers: { Violin: 'svg' } }, + { name: 'Violin.canvas', group: 'Components', description: 'Standalone Violin from `layerchart/canvas`', imports: ['Violin'], layers: { Violin: 'canvas' } }, + { name: 'BoxPlot', group: 'Components', description: 'Standalone BoxPlot (agnostic) — baseline', imports: ['BoxPlot'] }, + { name: 'BoxPlot.svg', group: 'Components', description: 'Standalone BoxPlot from `layerchart/svg`', imports: ['BoxPlot'], layers: { BoxPlot: 'svg' } }, + { name: 'BoxPlot.canvas', group: 'Components', description: 'Standalone BoxPlot from `layerchart/canvas`', imports: ['BoxPlot'], layers: { BoxPlot: 'canvas' } }, + // --- Worst case --- { name: 'all', diff --git a/packages/layerchart/src/lib/canvas.ts b/packages/layerchart/src/lib/canvas.ts index c360a18f5..b7fdb336e 100644 --- a/packages/layerchart/src/lib/canvas.ts +++ b/packages/layerchart/src/lib/canvas.ts @@ -199,6 +199,37 @@ export type { } from './components/Calendar/Calendar.shared.svelte.js'; export { default as CircleClipPath } from './components/CircleClipPath/CircleClipPath.canvas.svelte'; export type { CircleClipPathPropsWithoutHTML } from './components/CircleClipPath/CircleClipPath.shared.svelte.js'; +export { default as Voronoi } from './components/Voronoi/Voronoi.canvas.svelte'; +export type { + VoronoiProps, + VoronoiPropsWithoutHTML, +} from './components/Voronoi/Voronoi.shared.svelte.js'; +export { default as Contour } from './components/Contour/Contour.canvas.svelte'; +export type { + ContourProps, + ContourPropsWithoutHTML, +} from './components/Contour/Contour.shared.svelte.js'; +export { default as Month } from './components/Month/Month.canvas.svelte'; +export type { + MonthProps, + MonthPropsWithoutHTML, + MonthCell, +} from './components/Month/Month.shared.svelte.js'; +export { default as Raster } from './components/Raster/Raster.canvas.svelte'; +export type { + RasterProps, + RasterPropsWithoutHTML, +} from './components/Raster/Raster.shared.svelte.js'; +export { default as Violin } from './components/Violin/Violin.canvas.svelte'; +export type { + ViolinProps, + ViolinPropsWithoutHTML, +} from './components/Violin/Violin.shared.svelte.js'; +export { default as BoxPlot } from './components/BoxPlot/BoxPlot.canvas.svelte'; +export type { + BoxPlotProps, + BoxPlotPropsWithoutHTML, +} from './components/BoxPlot/BoxPlot.shared.svelte.js'; export { default as RectClipPath } from './components/RectClipPath/RectClipPath.canvas.svelte'; export type { RectClipPathProps, diff --git a/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.base.svelte b/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.base.svelte new file mode 100644 index 000000000..12207ffdf --- /dev/null +++ b/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.base.svelte @@ -0,0 +1,195 @@ + + + + +{#if minVal != null && q1Val != null && medianVal != null && q3Val != null && maxVal != null} + + {#if isVertical} + + + + + + + {#each outliersVal as outlier, i (i)} + + {/each} + {:else} + + + + + + + {#each outliersVal as outlier, i (i)} + + {/each} + {/if} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.canvas.svelte b/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.canvas.svelte new file mode 100644 index 000000000..f4af99f21 --- /dev/null +++ b/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.canvas.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.shared.svelte.ts b/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.shared.svelte.ts new file mode 100644 index 000000000..0d6efb13a --- /dev/null +++ b/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.shared.svelte.ts @@ -0,0 +1,28 @@ +import type { SVGAttributes } from 'svelte/elements'; +import type { CommonStyleProps, CommonEvents, Without } from '$lib/utils/types.js'; +import type { Accessor } from '$lib/utils/common.js'; + +export type BoxPlotPropsWithoutHTML = { + data: Object; + min?: Accessor; + q1?: Accessor; + median?: Accessor; + q3?: Accessor; + max?: Accessor; + outliers?: Accessor; + values?: Accessor; + /** @default 1.5 */ + iqrMultiplier?: number; + width?: number; + /** @default 0.5 */ + capWidth?: number; + /** @default 0 */ + radius?: number; + /** @default 3 */ + outlierRadius?: number; + tooltip?: boolean; +} & CommonStyleProps; + +export type BoxPlotProps = BoxPlotPropsWithoutHTML & + Without, BoxPlotPropsWithoutHTML> & + CommonEvents; diff --git a/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.svelte b/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.svelte index 0b8a73810..d838fc706 100644 --- a/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.svelte +++ b/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.svelte @@ -1,389 +1,20 @@ -{#if minVal != null && q1Val != null && medianVal != null && q3Val != null && maxVal != null} - - {#if isVertical} - - - - - - - - - - - - - - - - - - - - {#each outliersVal as outlier, i (i)} - - {/each} - {:else} - - - - - - - - - - - - - - - - - - - - - - {#each outliersVal as outlier, i (i)} - - {/each} - {/if} - +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + {/if} - - diff --git a/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.svg.svelte b/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.svg.svelte new file mode 100644 index 000000000..73160f2e5 --- /dev/null +++ b/packages/layerchart/src/lib/components/BoxPlot/BoxPlot.svg.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Contour/Contour.base.svelte b/packages/layerchart/src/lib/components/Contour/Contour.base.svelte new file mode 100644 index 000000000..1ec2adbfd --- /dev/null +++ b/packages/layerchart/src/lib/components/Contour/Contour.base.svelte @@ -0,0 +1,246 @@ + + + + +{#if contourData.length > 0} + + {#each contourData as contour, i (i)} + + {/each} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/Contour/Contour.canvas.svelte b/packages/layerchart/src/lib/components/Contour/Contour.canvas.svelte new file mode 100644 index 000000000..c44b22e31 --- /dev/null +++ b/packages/layerchart/src/lib/components/Contour/Contour.canvas.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Contour/Contour.shared.svelte.ts b/packages/layerchart/src/lib/components/Contour/Contour.shared.svelte.ts new file mode 100644 index 000000000..e7ea61397 --- /dev/null +++ b/packages/layerchart/src/lib/components/Contour/Contour.shared.svelte.ts @@ -0,0 +1,29 @@ +import type { SVGAttributes } from 'svelte/elements'; +import type { CommonStyleProps, Without } from '$lib/utils/types.js'; +import type { Accessor } from '$lib/utils/common.js'; +import type { InterpolateMethod } from '$lib/utils/rasterInterpolate.js'; + +export type ContourPropsWithoutHTML = { + data?: number[] | Float64Array | any[]; + width?: number; + height?: number; + x1?: number; + y1?: number; + x2?: number; + y2?: number; + /** @default 'value' */ + value?: Accessor | ((x: number, y: number) => number); + x?: Accessor; + y?: Accessor; + /** @default 'barycentric' */ + interpolate?: InterpolateMethod; + /** @default 10 */ + thresholds?: number | number[]; + /** @default 0 */ + blur?: number; + /** @default true */ + smooth?: boolean; +} & CommonStyleProps; + +export type ContourProps = ContourPropsWithoutHTML & + Without, ContourPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/Contour/Contour.svelte b/packages/layerchart/src/lib/components/Contour/Contour.svelte index 92561e76d..a2217f189 100644 --- a/packages/layerchart/src/lib/components/Contour/Contour.svelte +++ b/packages/layerchart/src/lib/components/Contour/Contour.svelte @@ -1,301 +1,20 @@ -{#if contourData.length > 0} - - {#each contourData as contour, i (i)} - - {/each} - +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + {/if} - - diff --git a/packages/layerchart/src/lib/components/Contour/Contour.svg.svelte b/packages/layerchart/src/lib/components/Contour/Contour.svg.svelte new file mode 100644 index 000000000..f8f35305c --- /dev/null +++ b/packages/layerchart/src/lib/components/Contour/Contour.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Month/Month.base.svelte b/packages/layerchart/src/lib/components/Month/Month.base.svelte new file mode 100644 index 000000000..de2e64947 --- /dev/null +++ b/packages/layerchart/src/lib/components/Month/Month.base.svelte @@ -0,0 +1,189 @@ + + + + + + {#if children} + {@render children({ cells: allCells.cells, cellSize })} + {:else} + {#each allCells.cells as cell} + tooltip && ctx.tooltip?.show(e, cell.data)} + onpointerleave={() => tooltip && ctx.tooltip?.hide()} + {...extractLayerProps(restProps, 'lc-month-cell')} + /> + + {#if showDayNumber} + + {/if} + {/each} + {/if} + + {#if monthLabel} + {#each monthLabels as label} + + {/each} + {/if} + + + diff --git a/packages/layerchart/src/lib/components/Month/Month.canvas.svelte b/packages/layerchart/src/lib/components/Month/Month.canvas.svelte new file mode 100644 index 000000000..aed160ed2 --- /dev/null +++ b/packages/layerchart/src/lib/components/Month/Month.canvas.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Month/Month.shared.svelte.ts b/packages/layerchart/src/lib/components/Month/Month.shared.svelte.ts new file mode 100644 index 000000000..dcf7cadd6 --- /dev/null +++ b/packages/layerchart/src/lib/components/Month/Month.shared.svelte.ts @@ -0,0 +1,37 @@ +import type { ComponentProps, Snippet } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import type { Without } from '$lib/utils/types.js'; +import type Text from '../Text/Text.svelte'; +import type { RectPropsWithoutHTML } from '../Rect/Rect.shared.svelte.js'; + +export type MonthCell = { + x: number; + y: number; + color: any; + data: any; + date: Date; +}; + +export type MonthPropsWithoutHTML = { + start: Date; + end: Date; + /** @default 25 */ + cellSize?: number; + monthsPerRow?: number; + /** @default 1.2 */ + monthPadding?: number; + /** @default 8 */ + rowSpacing?: number; + /** @default true */ + showDayNumber?: boolean; + monthLabel?: boolean | Partial>; + dayNumberProps?: Partial>; + tooltip?: boolean; + children?: Snippet<[{ cells: MonthCell[]; cellSize: number }]>; +} & Omit< + RectPropsWithoutHTML, + 'children' | 'x' | 'y' | 'width' | 'height' | 'fill' | 'onpointermove' | 'onpointerleave' +>; + +export type MonthProps = MonthPropsWithoutHTML & + Without, MonthPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/Month/Month.svelte b/packages/layerchart/src/lib/components/Month/Month.svelte index 6bbc1b710..8d46bd578 100644 --- a/packages/layerchart/src/lib/components/Month/Month.svelte +++ b/packages/layerchart/src/lib/components/Month/Month.svelte @@ -1,273 +1,24 @@ - - - {#if children} - {@render children({ cells: allCells.cells, cellSize })} - {:else} - {#each allCells.cells as cell} - tooltip && ctx.tooltip?.show(e, cell.data)} - onpointerleave={(e) => tooltip && ctx.tooltip?.hide()} - {...extractLayerProps(restProps, 'lc-month-cell')} - /> - - {#if showDayNumber} - - {/if} - {/each} - {/if} - - - {#if monthLabel} - {#each monthLabels as label} - - {/each} - {/if} - - - +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Month/Month.svg.svelte b/packages/layerchart/src/lib/components/Month/Month.svg.svelte new file mode 100644 index 000000000..ec5235d56 --- /dev/null +++ b/packages/layerchart/src/lib/components/Month/Month.svg.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Raster/Raster.base.svelte b/packages/layerchart/src/lib/components/Raster/Raster.base.svelte new file mode 100644 index 000000000..80cddf15b --- /dev/null +++ b/packages/layerchart/src/lib/components/Raster/Raster.base.svelte @@ -0,0 +1,281 @@ + + + + +{#if imageDataUrl} + +{/if} diff --git a/packages/layerchart/src/lib/components/Raster/Raster.canvas.svelte b/packages/layerchart/src/lib/components/Raster/Raster.canvas.svelte new file mode 100644 index 000000000..22cf333e2 --- /dev/null +++ b/packages/layerchart/src/lib/components/Raster/Raster.canvas.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Raster/Raster.html.svelte b/packages/layerchart/src/lib/components/Raster/Raster.html.svelte new file mode 100644 index 000000000..2b2a322ef --- /dev/null +++ b/packages/layerchart/src/lib/components/Raster/Raster.html.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Raster/Raster.shared.svelte.ts b/packages/layerchart/src/lib/components/Raster/Raster.shared.svelte.ts new file mode 100644 index 000000000..f4e325ade --- /dev/null +++ b/packages/layerchart/src/lib/components/Raster/Raster.shared.svelte.ts @@ -0,0 +1,29 @@ +import type { SVGAttributes } from 'svelte/elements'; +import type { CommonStyleProps, Without } from '$lib/utils/types.js'; +import type { Accessor } from '$lib/utils/common.js'; +import type { InterpolateMethod } from '$lib/utils/rasterInterpolate.js'; + +export type RasterPropsWithoutHTML = { + data?: number[] | Float64Array | any[]; + width?: number; + height?: number; + x1?: number; + y1?: number; + x2?: number; + y2?: number; + /** @default 'value' */ + value?: Accessor | ((x: number, y: number) => number); + x?: Accessor; + y?: Accessor; + /** @default 'barycentric' */ + interpolate?: InterpolateMethod; + /** @default 1 */ + pixelSize?: number; + /** @default 0 */ + blur?: number; + /** @default 'auto' */ + imageRendering?: 'auto' | 'pixelated' | 'crisp-edges'; +} & Pick; + +export type RasterProps = RasterPropsWithoutHTML & + Without, RasterPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/Raster/Raster.svelte b/packages/layerchart/src/lib/components/Raster/Raster.svelte index d6f297b06..3ea3fb235 100644 --- a/packages/layerchart/src/lib/components/Raster/Raster.svelte +++ b/packages/layerchart/src/lib/components/Raster/Raster.svelte @@ -1,335 +1,23 @@ -{#if imageDataUrl} - +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + {/if} diff --git a/packages/layerchart/src/lib/components/Raster/Raster.svg.svelte b/packages/layerchart/src/lib/components/Raster/Raster.svg.svelte new file mode 100644 index 000000000..5cbc0b6f5 --- /dev/null +++ b/packages/layerchart/src/lib/components/Raster/Raster.svg.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Violin/Violin.base.svelte b/packages/layerchart/src/lib/components/Violin/Violin.base.svelte new file mode 100644 index 000000000..fd8ee0912 --- /dev/null +++ b/packages/layerchart/src/lib/components/Violin/Violin.base.svelte @@ -0,0 +1,252 @@ + + + + +{#if pathData} + + + + {#if showBox && stats} + {#if isVertical} + + {:else} + + {/if} + {/if} + + {#if showMedian && stats} + {#if isVertical} + + {:else} + + {/if} + {/if} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/Violin/Violin.canvas.svelte b/packages/layerchart/src/lib/components/Violin/Violin.canvas.svelte new file mode 100644 index 000000000..f55d4ab9f --- /dev/null +++ b/packages/layerchart/src/lib/components/Violin/Violin.canvas.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Violin/Violin.shared.svelte.ts b/packages/layerchart/src/lib/components/Violin/Violin.shared.svelte.ts new file mode 100644 index 000000000..1aafb0fea --- /dev/null +++ b/packages/layerchart/src/lib/components/Violin/Violin.shared.svelte.ts @@ -0,0 +1,25 @@ +import type { SVGAttributes } from 'svelte/elements'; +import type { CurveFactory } from 'd3-shape'; +import type { CommonStyleProps, CommonEvents, Without } from '$lib/utils/types.js'; +import type { Accessor } from '$lib/utils/common.js'; + +export type ViolinPropsWithoutHTML = { + data: Object; + density?: Accessor; + values?: Accessor; + bandwidth?: number; + /** @default 50 */ + thresholds?: number; + width?: number; + /** @default curveCardinal */ + curve?: CurveFactory; + /** @default false */ + median?: boolean; + /** @default false */ + box?: boolean | { width?: number }; + tooltip?: boolean; +} & CommonStyleProps; + +export type ViolinProps = ViolinPropsWithoutHTML & + Without, ViolinPropsWithoutHTML> & + CommonEvents; diff --git a/packages/layerchart/src/lib/components/Violin/Violin.svelte b/packages/layerchart/src/lib/components/Violin/Violin.svelte index 2633df19f..263158a8e 100644 --- a/packages/layerchart/src/lib/components/Violin/Violin.svelte +++ b/packages/layerchart/src/lib/components/Violin/Violin.svelte @@ -1,318 +1,20 @@ -{#if pathData} - - - - - {#if showBox && stats} - - {#if isVertical} - - {:else} - - {/if} - {/if} - - {#if showMedian && stats} - - {#if isVertical} - - {:else} - - {/if} - {/if} - +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + {/if} - - diff --git a/packages/layerchart/src/lib/components/Violin/Violin.svg.svelte b/packages/layerchart/src/lib/components/Violin/Violin.svg.svelte new file mode 100644 index 000000000..dbfee2473 --- /dev/null +++ b/packages/layerchart/src/lib/components/Violin/Violin.svg.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Voronoi/Voronoi.base.svelte b/packages/layerchart/src/lib/components/Voronoi/Voronoi.base.svelte new file mode 100644 index 000000000..ad03b675a --- /dev/null +++ b/packages/layerchart/src/lib/components/Voronoi/Voronoi.base.svelte @@ -0,0 +1,151 @@ + + + + + + {#if geo.projection} + {@const polygons = geoVoronoi().polygons(points)} + {#each polygons.features as feature} + {@const point = r ? geo.projection?.(feature.properties.sitecoordinates) : null} + + + onclick?.(e, { data: feature.properties.site.data, feature })} + onpointerenter={(e: PointerEvent) => + onpointerenter?.(e, { data: feature.properties.site.data, feature })} + onpointermove={(e: PointerEvent) => + onpointermove?.(e, { data: feature.properties.site.data, feature })} + onpointerdown={(e: PointerEvent) => + onpointerdown?.(e, { data: feature.properties.site.data, feature })} + ontouchmove={(e: TouchEvent) => { + e.preventDefault(); + }} + /> + + {/each} + {:else} + {@const voronoi = Delaunay.from(points).voronoi([0, 0, boundWidth, boundHeight])} + {#each points as point, i} + {@const pathData = voronoi.renderCell(i)} + {#if pathData} + + onclick?.(e, { data: (point as any).data, point })} + onpointerenter={(e: PointerEvent) => + onpointerenter?.(e, { data: (point as any).data, point })} + onpointermove={(e: PointerEvent) => + onpointermove?.(e, { data: (point as any).data, point })} + onpointerdown={(e: PointerEvent) => + onpointerdown?.(e, { data: (point as any).data, point })} + ontouchmove={(e: TouchEvent) => { + e.preventDefault(); + }} + /> + + {/if} + {/each} + {/if} + + + diff --git a/packages/layerchart/src/lib/components/Voronoi/Voronoi.canvas.svelte b/packages/layerchart/src/lib/components/Voronoi/Voronoi.canvas.svelte new file mode 100644 index 000000000..5566896c0 --- /dev/null +++ b/packages/layerchart/src/lib/components/Voronoi/Voronoi.canvas.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Voronoi/Voronoi.shared.svelte.ts b/packages/layerchart/src/lib/components/Voronoi/Voronoi.shared.svelte.ts new file mode 100644 index 000000000..02527fd4b --- /dev/null +++ b/packages/layerchart/src/lib/components/Voronoi/Voronoi.shared.svelte.ts @@ -0,0 +1,35 @@ +import type { Without } from '$lib/utils/types.js'; +import type { Accessor } from '$lib/utils/common.js'; +import type { GeoPermissibleObjects } from 'd3-geo'; +import type { GroupProps } from '../Group/Group.shared.svelte.js'; + +export type VoronoiPropsWithoutHTML = { + data?: any; + x?: Accessor; + y?: Accessor; + /** Radius to clip voronoi cells. `0` or `undefined` to disables clipping */ + r?: number; + classes?: { + root?: string; + path?: string; + }; + onclick?: ( + e: MouseEvent, + details: { data: any; point?: [number, number]; feature?: GeoPermissibleObjects } + ) => void; + onpointerenter?: ( + e: PointerEvent, + details: { data: any; point?: [number, number]; feature?: GeoPermissibleObjects } + ) => void; + onpointermove?: ( + e: PointerEvent, + details: { data: any; point?: [number, number]; feature?: GeoPermissibleObjects } + ) => void; + onpointerdown?: ( + e: PointerEvent, + details: { data: any; point?: [number, number]; feature?: GeoPermissibleObjects } + ) => void; +}; + +export type VoronoiProps = VoronoiPropsWithoutHTML & + Without, VoronoiPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/Voronoi/Voronoi.svelte b/packages/layerchart/src/lib/components/Voronoi/Voronoi.svelte index c811e99f6..749e49ebe 100644 --- a/packages/layerchart/src/lib/components/Voronoi/Voronoi.svelte +++ b/packages/layerchart/src/lib/components/Voronoi/Voronoi.svelte @@ -1,208 +1,20 @@ - - {#if geo.projection} - {@const polygons = geoVoronoi().polygons(points)} - {#each polygons.features as feature} - {@const point = r ? geo.projection?.(feature.properties.sitecoordinates) : null} - - onclick?.(e, { data: feature.properties.site.data, feature })} - onpointerenter={(e) => - onpointerenter?.(e, { data: feature.properties.site.data, feature })} - onpointermove={(e) => onpointermove?.(e, { data: feature.properties.site.data, feature })} - onpointerdown={(e) => onpointerdown?.(e, { data: feature.properties.site.data, feature })} - {onpointerleave} - ontouchmove={(e) => { - // Prevent touch to not interfere with pointer - e.preventDefault(); - }} - /> - - {/each} - {:else} - {@const voronoi = Delaunay.from(points).voronoi([0, 0, boundWidth, boundHeight])} - {#each points as point, i} - {@const pathData = voronoi.renderCell(i)} - - {#if pathData} - - onclick?.(e, { data: point.data, point })} - onpointerenter={(e) => onpointerenter?.(e, { data: point.data, point })} - onpointermove={(e) => onpointermove?.(e, { data: point.data, point })} - {onpointerleave} - onpointerdown={(e) => onpointerdown?.(e, { data: point.data, point })} - ontouchmove={(e) => { - // Prevent touch to not interfere with pointer - e.preventDefault(); - }} - /> - - {/if} - {/each} - {/if} - - - +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/Voronoi/Voronoi.svg.svelte b/packages/layerchart/src/lib/components/Voronoi/Voronoi.svg.svelte new file mode 100644 index 000000000..d145ff56b --- /dev/null +++ b/packages/layerchart/src/lib/components/Voronoi/Voronoi.svg.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/layerchart/src/lib/html.ts b/packages/layerchart/src/lib/html.ts index 6398392f4..69908422d 100644 --- a/packages/layerchart/src/lib/html.ts +++ b/packages/layerchart/src/lib/html.ts @@ -121,3 +121,8 @@ export { default as Cell } from './components/Cell/Cell.html.svelte'; export type { CellProps } from './components/Cell/Cell.shared.svelte.js'; export { default as CircleClipPath } from './components/CircleClipPath/CircleClipPath.html.svelte'; export type { CircleClipPathPropsWithoutHTML } from './components/CircleClipPath/CircleClipPath.shared.svelte.js'; +export { default as Raster } from './components/Raster/Raster.html.svelte'; +export type { + RasterProps, + RasterPropsWithoutHTML, +} from './components/Raster/Raster.shared.svelte.js'; diff --git a/packages/layerchart/src/lib/svg.ts b/packages/layerchart/src/lib/svg.ts index 818bc8c3f..a830e5665 100644 --- a/packages/layerchart/src/lib/svg.ts +++ b/packages/layerchart/src/lib/svg.ts @@ -204,6 +204,37 @@ export type { } from './components/Calendar/Calendar.shared.svelte.js'; export { default as CircleClipPath } from './components/CircleClipPath/CircleClipPath.svg.svelte'; export type { CircleClipPathPropsWithoutHTML } from './components/CircleClipPath/CircleClipPath.shared.svelte.js'; +export { default as Voronoi } from './components/Voronoi/Voronoi.svg.svelte'; +export type { + VoronoiProps, + VoronoiPropsWithoutHTML, +} from './components/Voronoi/Voronoi.shared.svelte.js'; +export { default as Contour } from './components/Contour/Contour.svg.svelte'; +export type { + ContourProps, + ContourPropsWithoutHTML, +} from './components/Contour/Contour.shared.svelte.js'; +export { default as Month } from './components/Month/Month.svg.svelte'; +export type { + MonthProps, + MonthPropsWithoutHTML, + MonthCell, +} from './components/Month/Month.shared.svelte.js'; +export { default as Raster } from './components/Raster/Raster.svg.svelte'; +export type { + RasterProps, + RasterPropsWithoutHTML, +} from './components/Raster/Raster.shared.svelte.js'; +export { default as Violin } from './components/Violin/Violin.svg.svelte'; +export type { + ViolinProps, + ViolinPropsWithoutHTML, +} from './components/Violin/Violin.shared.svelte.js'; +export { default as BoxPlot } from './components/BoxPlot/BoxPlot.svg.svelte'; +export type { + BoxPlotProps, + BoxPlotPropsWithoutHTML, +} from './components/BoxPlot/BoxPlot.shared.svelte.js'; export { default as RectClipPath } from './components/RectClipPath/RectClipPath.svg.svelte'; export type { RectClipPathProps, From 6ffc9acba2fa02264e6097ff90b4485cc2aa81ab Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 28 Apr 2026 15:45:08 -0400 Subject: [PATCH 21/36] split geo components (GeoPath, GeoSpline, etc) into 3 layer-specific components --- bundle-analyzer/bundle-reports/latest.json | 352 ++++++++++++++++-- bundle-analyzer/bundle-scenarios.ts | 29 ++ .../layerchart/src/lib/bench/GeoBench.svelte | 2 +- packages/layerchart/src/lib/canvas.ts | 44 +++ .../src/lib/components/Hull/Hull.base.svelte | 5 +- .../components/Voronoi/Voronoi.base.svelte | 2 +- .../src/lib/components/geo/GeoCircle.svelte | 37 -- .../geo/GeoCircle/GeoCircle.base.svelte | 27 ++ .../geo/GeoCircle/GeoCircle.canvas.svelte | 13 + .../geo/GeoCircle/GeoCircle.shared.svelte.ts | 14 + .../components/geo/GeoCircle/GeoCircle.svelte | 20 + .../geo/GeoCircle/GeoCircle.svg.svelte | 13 + .../src/lib/components/geo/GeoClipPath.svelte | 72 ---- .../geo/GeoClipPath/GeoClipPath.base.svelte | 36 ++ .../geo/GeoClipPath/GeoClipPath.canvas.svelte | 13 + .../GeoClipPath/GeoClipPath.shared.svelte.ts | 19 + .../geo/GeoClipPath/GeoClipPath.svelte | 20 + .../geo/GeoClipPath/GeoClipPath.svg.svelte | 13 + .../GeoEdgeFade.base.svelte} | 24 +- .../geo/GeoEdgeFade/GeoEdgeFade.canvas.svelte | 13 + .../GeoEdgeFade/GeoEdgeFade.shared.svelte.ts | 13 + .../geo/GeoEdgeFade/GeoEdgeFade.svelte | 20 + .../geo/GeoEdgeFade/GeoEdgeFade.svg.svelte | 13 + .../geo/{ => GeoLegend}/GeoLegend.svelte | 2 +- .../src/lib/components/geo/GeoPath.svelte | 161 -------- .../geo/GeoPath/GeoPath.base.svelte | 94 +++++ .../geo/GeoPath/GeoPath.canvas.svelte | 13 + .../geo/GeoPath/GeoPath.shared.svelte.ts | 40 ++ .../lib/components/geo/GeoPath/GeoPath.svelte | 20 + .../components/geo/GeoPath/GeoPath.svg.svelte | 13 + .../GeoPoint.base.svelte} | 39 +- .../geo/GeoPoint/GeoPoint.canvas.svelte | 14 + .../geo/GeoPoint/GeoPoint.shared.svelte.ts | 15 + .../components/geo/GeoPoint/GeoPoint.svelte | 20 + .../geo/GeoPoint/GeoPoint.svg.svelte | 14 + .../{ => GeoProjection}/GeoProjection.svelte | 0 .../geo/{ => GeoRaster}/GeoRaster.svelte | 0 .../src/lib/components/geo/GeoSpline.svelte | 78 ---- .../geo/GeoSpline/GeoSpline.base.svelte | 52 +++ .../geo/GeoSpline/GeoSpline.canvas.svelte | 13 + .../geo/GeoSpline/GeoSpline.shared.svelte.ts | 14 + .../components/geo/GeoSpline/GeoSpline.svelte | 20 + .../geo/GeoSpline/GeoSpline.svg.svelte | 13 + .../GeoTile.base.svelte} | 51 +-- .../geo/GeoTile/GeoTile.canvas.svelte | 14 + .../geo/GeoTile/GeoTile.shared.svelte.ts | 16 + .../lib/components/geo/GeoTile/GeoTile.svelte | 20 + .../components/geo/GeoTile/GeoTile.svg.svelte | 14 + .../geo/{ => GeoVisible}/GeoVisible.svelte | 0 .../src/lib/components/geo/Graticule.svelte | 45 --- .../geo/Graticule/Graticule.base.svelte | 47 +++ .../geo/Graticule/Graticule.canvas.svelte | 14 + .../geo/Graticule/Graticule.shared.svelte.ts | 14 + .../components/geo/Graticule/Graticule.svelte | 20 + .../geo/Graticule/Graticule.svg.svelte | 14 + .../TileImage.base.svelte} | 72 +--- .../geo/TileImage/TileImage.canvas.svelte | 13 + .../geo/TileImage/TileImage.shared.svelte.ts | 21 ++ .../components/geo/TileImage/TileImage.svelte | 20 + .../geo/TileImage/TileImage.svg.svelte | 13 + .../src/lib/components/geo/index.ts | 52 +-- packages/layerchart/src/lib/svg.ts | 44 +++ 62 files changed, 1336 insertions(+), 617 deletions(-) delete mode 100644 packages/layerchart/src/lib/components/geo/GeoCircle.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.base.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.svg.svelte delete mode 100644 packages/layerchart/src/lib/components/geo/GeoClipPath.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.base.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.svg.svelte rename packages/layerchart/src/lib/components/geo/{GeoEdgeFade.svelte => GeoEdgeFade/GeoEdgeFade.base.svelte} (68%) create mode 100644 packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.svg.svelte rename packages/layerchart/src/lib/components/geo/{ => GeoLegend}/GeoLegend.svelte (99%) delete mode 100644 packages/layerchart/src/lib/components/geo/GeoPath.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.base.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.svg.svelte rename packages/layerchart/src/lib/components/geo/{GeoPoint.svelte => GeoPoint/GeoPoint.base.svelte} (61%) create mode 100644 packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.svg.svelte rename packages/layerchart/src/lib/components/geo/{ => GeoProjection}/GeoProjection.svelte (100%) rename packages/layerchart/src/lib/components/geo/{ => GeoRaster}/GeoRaster.svelte (100%) delete mode 100644 packages/layerchart/src/lib/components/geo/GeoSpline.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.base.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.svg.svelte rename packages/layerchart/src/lib/components/geo/{GeoTile.svelte => GeoTile/GeoTile.base.svelte} (68%) create mode 100644 packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.svelte create mode 100644 packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.svg.svelte rename packages/layerchart/src/lib/components/geo/{ => GeoVisible}/GeoVisible.svelte (100%) delete mode 100644 packages/layerchart/src/lib/components/geo/Graticule.svelte create mode 100644 packages/layerchart/src/lib/components/geo/Graticule/Graticule.base.svelte create mode 100644 packages/layerchart/src/lib/components/geo/Graticule/Graticule.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/geo/Graticule/Graticule.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/geo/Graticule/Graticule.svelte create mode 100644 packages/layerchart/src/lib/components/geo/Graticule/Graticule.svg.svelte rename packages/layerchart/src/lib/components/geo/{TileImage.svelte => TileImage/TileImage.base.svelte} (61%) create mode 100644 packages/layerchart/src/lib/components/geo/TileImage/TileImage.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/geo/TileImage/TileImage.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/geo/TileImage/TileImage.svelte create mode 100644 packages/layerchart/src/lib/components/geo/TileImage/TileImage.svg.svelte diff --git a/bundle-analyzer/bundle-reports/latest.json b/bundle-analyzer/bundle-reports/latest.json index f04582dd6..40054c399 100644 --- a/bundle-analyzer/bundle-reports/latest.json +++ b/bundle-analyzer/bundle-reports/latest.json @@ -1,12 +1,12 @@ { - "timestamp": "2026-04-28T19:19:39.925Z", + "timestamp": "2026-04-28T19:38:55.794Z", "results": [ { "scenario": "core", "description": "Core charting components without rendering layer", "group": "Foundation", "size": 374970, - "gzipSize": 86618, + "gzipSize": 86619, "imports": [ "Chart", "Svg" @@ -17,7 +17,7 @@ "description": "Svg-based rendering", "group": "Foundation", "size": 351979, - "gzipSize": 82187, + "gzipSize": 82186, "imports": [ "Chart", "Svg" @@ -39,7 +39,7 @@ "description": "HTML-based rendering", "group": "Foundation", "size": 359830, - "gzipSize": 84204, + "gzipSize": 84205, "imports": [ "Chart", "Html" @@ -50,7 +50,7 @@ "description": "Basic line chart with axes and grid", "group": "Cartesian charts", "size": 375465, - "gzipSize": 86650, + "gzipSize": 86651, "imports": [ "Chart", "Svg", @@ -64,7 +64,7 @@ "description": "Line chart composed from `layerchart/svg`", "group": "Cartesian charts", "size": 352003, - "gzipSize": 82209, + "gzipSize": 82210, "imports": [ "Chart", "Layer", @@ -78,7 +78,7 @@ "description": "Line chart composed from `layerchart/canvas`", "group": "Cartesian charts", "size": 358530, - "gzipSize": 83963, + "gzipSize": 83962, "imports": [ "Chart", "Layer", @@ -92,7 +92,7 @@ "description": "Line chart composed from `layerchart/html`", "group": "Cartesian charts", "size": 359854, - "gzipSize": 84218, + "gzipSize": 84220, "imports": [ "Chart", "Layer", @@ -106,7 +106,7 @@ "description": "Line chart with tooltip and highlight", "group": "Cartesian charts", "size": 390306, - "gzipSize": 90006, + "gzipSize": 90005, "imports": [ "Chart", "Svg", @@ -122,7 +122,7 @@ "description": "High-level `LineChart` component", "group": "Cartesian charts", "size": 399089, - "gzipSize": 93026, + "gzipSize": 93025, "imports": [ "LineChart" ] @@ -132,7 +132,7 @@ "description": "Area chart with axes", "group": "Cartesian charts", "size": 388981, - "gzipSize": 89728, + "gzipSize": 89730, "imports": [ "Chart", "Svg", @@ -146,7 +146,7 @@ "description": "High-level `AreaChart` component", "group": "Cartesian charts", "size": 392569, - "gzipSize": 90588, + "gzipSize": 90589, "imports": [ "AreaChart" ] @@ -156,7 +156,7 @@ "description": "Bar chart with axes", "group": "Cartesian charts", "size": 379305, - "gzipSize": 87396, + "gzipSize": 87393, "imports": [ "Chart", "Svg", @@ -170,7 +170,7 @@ "description": "High-level `BarChart` component", "group": "Cartesian charts", "size": 384438, - "gzipSize": 88534, + "gzipSize": 88531, "imports": [ "BarChart" ] @@ -195,7 +195,7 @@ "description": "High-level `ScatterChart` component", "group": "Cartesian charts", "size": 378994, - "gzipSize": 87787, + "gzipSize": 87788, "imports": [ "ScatterChart" ] @@ -205,7 +205,7 @@ "description": "Pie/donut chart with arcs", "group": "Cartesian charts", "size": 387712, - "gzipSize": 89808, + "gzipSize": 89809, "imports": [ "Chart", "Svg", @@ -238,8 +238,8 @@ "scenario": "geo", "description": "Geographic map with paths", "group": "Geo", - "size": 407211, - "gzipSize": 94415, + "size": 397074, + "gzipSize": 92030, "imports": [ "Chart", "Svg", @@ -252,8 +252,8 @@ "scenario": "geo-tiles", "description": "Geographic map with tile layer", "group": "Geo", - "size": 411656, - "gzipSize": 95919, + "size": 402266, + "gzipSize": 93681, "imports": [ "Chart", "Svg", @@ -267,8 +267,8 @@ "scenario": "geo-full", "description": "Full geo setup with all geo components", "group": "Geo", - "size": 460021, - "gzipSize": 109495, + "size": 455732, + "gzipSize": 107596, "imports": [ "Chart", "Svg", @@ -291,7 +291,7 @@ "description": "Tree layout with links", "group": "Hierarchy", "size": 415076, - "gzipSize": 96457, + "gzipSize": 96456, "imports": [ "Chart", "Svg", @@ -306,7 +306,7 @@ "description": "Treemap layout", "group": "Hierarchy", "size": 393889, - "gzipSize": 91267, + "gzipSize": 91266, "imports": [ "Chart", "Svg", @@ -350,7 +350,7 @@ "description": "Dagre directed graph layout", "group": "Graph / network", "size": 474962, - "gzipSize": 112737, + "gzipSize": 112736, "imports": [ "Chart", "Svg", @@ -379,8 +379,8 @@ "scenario": "chord", "description": "Chord diagram", "group": "Graph / network", - "size": 389429, - "gzipSize": 90418, + "size": 384515, + "gzipSize": 88846, "imports": [ "Chart", "Svg", @@ -1702,8 +1702,8 @@ "scenario": "Hull", "description": "Standalone Hull (agnostic) — baseline", "group": "Components", - "size": 180829, - "gzipSize": 47999, + "size": 181180, + "gzipSize": 47930, "imports": [ "Hull" ] @@ -1712,8 +1712,8 @@ "scenario": "Hull.svg", "description": "Standalone Hull from `layerchart/svg`", "group": "Components", - "size": 180033, - "gzipSize": 48048, + "size": 180384, + "gzipSize": 48106, "imports": [ "Hull" ] @@ -1722,8 +1722,8 @@ "scenario": "Hull.canvas", "description": "Standalone Hull from `layerchart/canvas`", "group": "Components", - "size": 180037, - "gzipSize": 47785, + "size": 180388, + "gzipSize": 47864, "imports": [ "Hull" ] @@ -1832,8 +1832,8 @@ "scenario": "Voronoi", "description": "Standalone Voronoi (agnostic) — baseline", "group": "Components", - "size": 177185, - "gzipSize": 46704, + "size": 177536, + "gzipSize": 46642, "imports": [ "Voronoi" ] @@ -1842,8 +1842,8 @@ "scenario": "Voronoi.svg", "description": "Standalone Voronoi from `layerchart/svg`", "group": "Components", - "size": 175162, - "gzipSize": 46464, + "size": 175513, + "gzipSize": 46519, "imports": [ "Voronoi" ] @@ -1852,8 +1852,8 @@ "scenario": "Voronoi.canvas", "description": "Standalone Voronoi from `layerchart/canvas`", "group": "Components", - "size": 174164, - "gzipSize": 46000, + "size": 174515, + "gzipSize": 46072, "imports": [ "Voronoi" ] @@ -2018,12 +2018,282 @@ "BoxPlot" ] }, + { + "scenario": "GeoPath", + "description": "Standalone GeoPath (agnostic) — baseline", + "group": "Components", + "size": 100716, + "gzipSize": 25972, + "imports": [ + "GeoPath" + ] + }, + { + "scenario": "GeoPath.svg", + "description": "Standalone GeoPath from `layerchart/svg`", + "group": "Components", + "size": 89637, + "gzipSize": 22852, + "imports": [ + "GeoPath" + ] + }, + { + "scenario": "GeoPath.canvas", + "description": "Standalone GeoPath from `layerchart/canvas`", + "group": "Components", + "size": 80311, + "gzipSize": 21335, + "imports": [ + "GeoPath" + ] + }, + { + "scenario": "GeoSpline", + "description": "Standalone GeoSpline (agnostic) — baseline", + "group": "Components", + "size": 118553, + "gzipSize": 31572, + "imports": [ + "GeoSpline" + ] + }, + { + "scenario": "GeoSpline.svg", + "description": "Standalone GeoSpline from `layerchart/svg`", + "group": "Components", + "size": 107470, + "gzipSize": 28482, + "imports": [ + "GeoSpline" + ] + }, + { + "scenario": "GeoSpline.canvas", + "description": "Standalone GeoSpline from `layerchart/canvas`", + "group": "Components", + "size": 98160, + "gzipSize": 27065, + "imports": [ + "GeoSpline" + ] + }, + { + "scenario": "GeoPoint", + "description": "Standalone GeoPoint (agnostic) — baseline", + "group": "Components", + "size": 76843, + "gzipSize": 18884, + "imports": [ + "GeoPoint" + ] + }, + { + "scenario": "GeoPoint.svg", + "description": "Standalone GeoPoint from `layerchart/svg`", + "group": "Components", + "size": 64553, + "gzipSize": 15396, + "imports": [ + "GeoPoint" + ] + }, + { + "scenario": "GeoPoint.canvas", + "description": "Standalone GeoPoint from `layerchart/canvas`", + "group": "Components", + "size": 72046, + "gzipSize": 17822, + "imports": [ + "GeoPoint" + ] + }, + { + "scenario": "GeoCircle", + "description": "Standalone GeoCircle (agnostic) — baseline", + "group": "Components", + "size": 104782, + "gzipSize": 27135, + "imports": [ + "GeoCircle" + ] + }, + { + "scenario": "GeoCircle.svg", + "description": "Standalone GeoCircle from `layerchart/svg`", + "group": "Components", + "size": 93523, + "gzipSize": 23959, + "imports": [ + "GeoCircle" + ] + }, + { + "scenario": "GeoCircle.canvas", + "description": "Standalone GeoCircle from `layerchart/canvas`", + "group": "Components", + "size": 84197, + "gzipSize": 22436, + "imports": [ + "GeoCircle" + ] + }, + { + "scenario": "GeoTile", + "description": "Standalone GeoTile (agnostic) — baseline", + "group": "Components", + "size": 131416, + "gzipSize": 32447, + "imports": [ + "GeoTile" + ] + }, + { + "scenario": "GeoTile.svg", + "description": "Standalone GeoTile from `layerchart/svg`", + "group": "Components", + "size": 120805, + "gzipSize": 29581, + "imports": [ + "GeoTile" + ] + }, + { + "scenario": "GeoTile.canvas", + "description": "Standalone GeoTile from `layerchart/canvas`", + "group": "Components", + "size": 122195, + "gzipSize": 30421, + "imports": [ + "GeoTile" + ] + }, + { + "scenario": "TileImage", + "description": "Standalone TileImage (agnostic) — baseline", + "group": "Components", + "size": 119479, + "gzipSize": 29955, + "imports": [ + "TileImage" + ] + }, + { + "scenario": "TileImage.svg", + "description": "Standalone TileImage from `layerchart/svg`", + "group": "Components", + "size": 109907, + "gzipSize": 27283, + "imports": [ + "TileImage" + ] + }, + { + "scenario": "TileImage.canvas", + "description": "Standalone TileImage from `layerchart/canvas`", + "group": "Components", + "size": 112589, + "gzipSize": 28436, + "imports": [ + "TileImage" + ] + }, + { + "scenario": "Graticule", + "description": "Standalone Graticule (agnostic) — baseline", + "group": "Components", + "size": 105996, + "gzipSize": 27158, + "imports": [ + "Graticule" + ] + }, + { + "scenario": "Graticule.svg", + "description": "Standalone Graticule from `layerchart/svg`", + "group": "Components", + "size": 94685, + "gzipSize": 23998, + "imports": [ + "Graticule" + ] + }, + { + "scenario": "Graticule.canvas", + "description": "Standalone Graticule from `layerchart/canvas`", + "group": "Components", + "size": 92012, + "gzipSize": 24168, + "imports": [ + "Graticule" + ] + }, + { + "scenario": "GeoClipPath", + "description": "Standalone GeoClipPath (agnostic) — baseline", + "group": "Components", + "size": 15028, + "gzipSize": 4397, + "imports": [ + "GeoClipPath" + ] + }, + { + "scenario": "GeoClipPath.svg", + "description": "Standalone GeoClipPath from `layerchart/svg`", + "group": "Components", + "size": 13207, + "gzipSize": 4095, + "imports": [ + "GeoClipPath" + ] + }, + { + "scenario": "GeoClipPath.canvas", + "description": "Standalone GeoClipPath from `layerchart/canvas`", + "group": "Components", + "size": 12175, + "gzipSize": 3811, + "imports": [ + "GeoClipPath" + ] + }, + { + "scenario": "GeoEdgeFade", + "description": "Standalone GeoEdgeFade (agnostic) — baseline", + "group": "Components", + "size": 86116, + "gzipSize": 23790, + "imports": [ + "GeoEdgeFade" + ] + }, + { + "scenario": "GeoEdgeFade.svg", + "description": "Standalone GeoEdgeFade from `layerchart/svg`", + "group": "Components", + "size": 84596, + "gzipSize": 23322, + "imports": [ + "GeoEdgeFade" + ] + }, + { + "scenario": "GeoEdgeFade.canvas", + "description": "Standalone GeoEdgeFade from `layerchart/canvas`", + "group": "Components", + "size": 83293, + "gzipSize": 23085, + "imports": [ + "GeoEdgeFade" + ] + }, { "scenario": "all", "description": "Everything from layerchart (worst case)", "group": "Worst case", - "size": 996557, - "gzipSize": 234983, + "size": 1006072, + "gzipSize": 235747, "imports": [ "*" ] diff --git a/bundle-analyzer/bundle-scenarios.ts b/bundle-analyzer/bundle-scenarios.ts index 2018cbe29..42a60f359 100644 --- a/bundle-analyzer/bundle-scenarios.ts +++ b/bundle-analyzer/bundle-scenarios.ts @@ -934,6 +934,35 @@ export const scenarios: Scenario[] = [ { name: 'BoxPlot.svg', group: 'Components', description: 'Standalone BoxPlot from `layerchart/svg`', imports: ['BoxPlot'], layers: { BoxPlot: 'svg' } }, { name: 'BoxPlot.canvas', group: 'Components', description: 'Standalone BoxPlot from `layerchart/canvas`', imports: ['BoxPlot'], layers: { BoxPlot: 'canvas' } }, + // Geo components + { name: 'GeoPath', group: 'Components', description: 'Standalone GeoPath (agnostic) — baseline', imports: ['GeoPath'] }, + { name: 'GeoPath.svg', group: 'Components', description: 'Standalone GeoPath from `layerchart/svg`', imports: ['GeoPath'], layers: { GeoPath: 'svg' } }, + { name: 'GeoPath.canvas', group: 'Components', description: 'Standalone GeoPath from `layerchart/canvas`', imports: ['GeoPath'], layers: { GeoPath: 'canvas' } }, + { name: 'GeoSpline', group: 'Components', description: 'Standalone GeoSpline (agnostic) — baseline', imports: ['GeoSpline'] }, + { name: 'GeoSpline.svg', group: 'Components', description: 'Standalone GeoSpline from `layerchart/svg`', imports: ['GeoSpline'], layers: { GeoSpline: 'svg' } }, + { name: 'GeoSpline.canvas', group: 'Components', description: 'Standalone GeoSpline from `layerchart/canvas`', imports: ['GeoSpline'], layers: { GeoSpline: 'canvas' } }, + { name: 'GeoPoint', group: 'Components', description: 'Standalone GeoPoint (agnostic) — baseline', imports: ['GeoPoint'] }, + { name: 'GeoPoint.svg', group: 'Components', description: 'Standalone GeoPoint from `layerchart/svg`', imports: ['GeoPoint'], layers: { GeoPoint: 'svg' } }, + { name: 'GeoPoint.canvas', group: 'Components', description: 'Standalone GeoPoint from `layerchart/canvas`', imports: ['GeoPoint'], layers: { GeoPoint: 'canvas' } }, + { name: 'GeoCircle', group: 'Components', description: 'Standalone GeoCircle (agnostic) — baseline', imports: ['GeoCircle'] }, + { name: 'GeoCircle.svg', group: 'Components', description: 'Standalone GeoCircle from `layerchart/svg`', imports: ['GeoCircle'], layers: { GeoCircle: 'svg' } }, + { name: 'GeoCircle.canvas', group: 'Components', description: 'Standalone GeoCircle from `layerchart/canvas`', imports: ['GeoCircle'], layers: { GeoCircle: 'canvas' } }, + { name: 'GeoTile', group: 'Components', description: 'Standalone GeoTile (agnostic) — baseline', imports: ['GeoTile'] }, + { name: 'GeoTile.svg', group: 'Components', description: 'Standalone GeoTile from `layerchart/svg`', imports: ['GeoTile'], layers: { GeoTile: 'svg' } }, + { name: 'GeoTile.canvas', group: 'Components', description: 'Standalone GeoTile from `layerchart/canvas`', imports: ['GeoTile'], layers: { GeoTile: 'canvas' } }, + { name: 'TileImage', group: 'Components', description: 'Standalone TileImage (agnostic) — baseline', imports: ['TileImage'] }, + { name: 'TileImage.svg', group: 'Components', description: 'Standalone TileImage from `layerchart/svg`', imports: ['TileImage'], layers: { TileImage: 'svg' } }, + { name: 'TileImage.canvas', group: 'Components', description: 'Standalone TileImage from `layerchart/canvas`', imports: ['TileImage'], layers: { TileImage: 'canvas' } }, + { name: 'Graticule', group: 'Components', description: 'Standalone Graticule (agnostic) — baseline', imports: ['Graticule'] }, + { name: 'Graticule.svg', group: 'Components', description: 'Standalone Graticule from `layerchart/svg`', imports: ['Graticule'], layers: { Graticule: 'svg' } }, + { name: 'Graticule.canvas', group: 'Components', description: 'Standalone Graticule from `layerchart/canvas`', imports: ['Graticule'], layers: { Graticule: 'canvas' } }, + { name: 'GeoClipPath', group: 'Components', description: 'Standalone GeoClipPath (agnostic) — baseline', imports: ['GeoClipPath'] }, + { name: 'GeoClipPath.svg', group: 'Components', description: 'Standalone GeoClipPath from `layerchart/svg`', imports: ['GeoClipPath'], layers: { GeoClipPath: 'svg' } }, + { name: 'GeoClipPath.canvas', group: 'Components', description: 'Standalone GeoClipPath from `layerchart/canvas`', imports: ['GeoClipPath'], layers: { GeoClipPath: 'canvas' } }, + { name: 'GeoEdgeFade', group: 'Components', description: 'Standalone GeoEdgeFade (agnostic) — baseline', imports: ['GeoEdgeFade'] }, + { name: 'GeoEdgeFade.svg', group: 'Components', description: 'Standalone GeoEdgeFade from `layerchart/svg`', imports: ['GeoEdgeFade'], layers: { GeoEdgeFade: 'svg' } }, + { name: 'GeoEdgeFade.canvas', group: 'Components', description: 'Standalone GeoEdgeFade from `layerchart/canvas`', imports: ['GeoEdgeFade'], layers: { GeoEdgeFade: 'canvas' } }, + // --- Worst case --- { name: 'all', diff --git a/packages/layerchart/src/lib/bench/GeoBench.svelte b/packages/layerchart/src/lib/bench/GeoBench.svelte index 954d2f11b..1bf16638e 100644 --- a/packages/layerchart/src/lib/bench/GeoBench.svelte +++ b/packages/layerchart/src/lib/bench/GeoBench.svelte @@ -1,7 +1,7 @@ - - - - diff --git a/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.base.svelte b/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.base.svelte new file mode 100644 index 000000000..b42634be6 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.base.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.canvas.svelte b/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.canvas.svelte new file mode 100644 index 000000000..5eeb8189c --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.canvas.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.shared.svelte.ts b/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.shared.svelte.ts new file mode 100644 index 000000000..77ccfcdaa --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.shared.svelte.ts @@ -0,0 +1,14 @@ +import type { Without } from '$lib/utils/types.js'; +import type { GeoPathProps } from '../GeoPath/GeoPath.shared.svelte.js'; + +export type GeoCirclePropsWithoutHTML = { + /** @default 90 */ + radius?: number; + /** @default [0, 0] */ + center?: [number, number]; + /** @default 6 */ + precision?: number; +}; + +export type GeoCircleProps = GeoCirclePropsWithoutHTML & + Without; diff --git a/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.svelte b/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.svelte new file mode 100644 index 000000000..2c4ed705c --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.svg.svelte b/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.svg.svelte new file mode 100644 index 000000000..50186dacf --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoCircle/GeoCircle.svg.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoClipPath.svelte b/packages/layerchart/src/lib/components/geo/GeoClipPath.svelte deleted file mode 100644 index 446277b2c..000000000 --- a/packages/layerchart/src/lib/components/geo/GeoClipPath.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - - - - diff --git a/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.base.svelte b/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.base.svelte new file mode 100644 index 000000000..2b339250b --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.base.svelte @@ -0,0 +1,36 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.canvas.svelte b/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.canvas.svelte new file mode 100644 index 000000000..c85cbf5c8 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.canvas.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.shared.svelte.ts b/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.shared.svelte.ts new file mode 100644 index 000000000..aa165b6bd --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.shared.svelte.ts @@ -0,0 +1,19 @@ +import type { GeoPermissibleObjects } from 'd3-geo'; +import type { ClipPathPropsWithoutHTML } from '../../ClipPath/ClipPath.shared.svelte.js'; +import type { GeoPathPropsWithoutHTML } from '../GeoPath/GeoPath.shared.svelte.js'; +import type { Without } from '$lib/utils/types.js'; + +export type BaseGeoClipPathPropsWithoutHTML = { + id?: string; + geojson: GeoPermissibleObjects; + /** @default false */ + disabled?: boolean; + /** @default false */ + invert?: boolean; + children?: ClipPathPropsWithoutHTML['children']; +}; + +export type GeoClipPathPropsWithoutHTML = BaseGeoClipPathPropsWithoutHTML & + Without; + +export type GeoClipPathProps = GeoClipPathPropsWithoutHTML; diff --git a/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.svelte b/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.svelte new file mode 100644 index 000000000..eca2a6f48 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.svg.svelte b/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.svg.svelte new file mode 100644 index 000000000..a611ca423 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoClipPath/GeoClipPath.svg.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoEdgeFade.svelte b/packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.base.svelte similarity index 68% rename from packages/layerchart/src/lib/components/geo/GeoEdgeFade.svelte rename to packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.base.svelte index 756a23b3a..b155d3c0a 100644 --- a/packages/layerchart/src/lib/components/geo/GeoEdgeFade.svelte +++ b/packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.base.svelte @@ -1,20 +1,12 @@ + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.shared.svelte.ts b/packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.shared.svelte.ts new file mode 100644 index 000000000..48af84f07 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.shared.svelte.ts @@ -0,0 +1,13 @@ +import type { Snippet } from 'svelte'; +import type { Without } from '$lib/utils/types.js'; +import type { GroupProps } from '../../Group/Group.shared.svelte.js'; + +export type GeoEdgeFadePropsWithoutHTML = { + link: { source: [number, number]; target: [number, number] }; + /** @bindable */ + ref?: SVGGElement; + children?: Snippet; +}; + +export type GeoEdgeFadeProps = GeoEdgeFadePropsWithoutHTML & + Without; diff --git a/packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.svelte b/packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.svelte new file mode 100644 index 000000000..7e580b362 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.svg.svelte b/packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.svg.svelte new file mode 100644 index 000000000..3fdf7b4fd --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoEdgeFade/GeoEdgeFade.svg.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoLegend.svelte b/packages/layerchart/src/lib/components/geo/GeoLegend/GeoLegend.svelte similarity index 99% rename from packages/layerchart/src/lib/components/geo/GeoLegend.svelte rename to packages/layerchart/src/lib/components/geo/GeoLegend/GeoLegend.svelte index 3b09f8a43..6775ea72e 100644 --- a/packages/layerchart/src/lib/components/geo/GeoLegend.svelte +++ b/packages/layerchart/src/lib/components/geo/GeoLegend/GeoLegend.svelte @@ -1,5 +1,5 @@ - - - -{#if children} - {@render children({ geoPath })} -{:else} - -{/if} diff --git a/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.base.svelte b/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.base.svelte new file mode 100644 index 000000000..d35c6cd7d --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.base.svelte @@ -0,0 +1,94 @@ + + + + +{#if children} + {@render children({ geoPath })} +{:else} + +{/if} diff --git a/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.canvas.svelte b/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.canvas.svelte new file mode 100644 index 000000000..71cd9b736 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.canvas.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.shared.svelte.ts b/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.shared.svelte.ts new file mode 100644 index 000000000..06a661760 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.shared.svelte.ts @@ -0,0 +1,40 @@ +import type { Snippet } from 'svelte'; +import type { SVGAttributes } from 'svelte/elements'; +import type { + GeoIdentityTransform, + GeoPermissibleObjects, + GeoProjection, + GeoTransformPrototype, +} from 'd3-geo'; +import type { CurveFactory, CurveFactoryLineOnly } from 'd3-shape'; +import type { CommonStyleProps, Without } from '$lib/utils/types.js'; +import type { PathProps } from '../../Path/Path.shared.svelte.js'; +import type { geoCurvePath } from '$lib/utils/geo.js'; + +export type GeoPathPropsWithoutHTML = { + geojson?: GeoPermissibleObjects | null; + tooltip?: boolean; + onclick?: + | ((e: MouseEvent, geoPath: ReturnType | undefined) => void) + | undefined; + /** @default curveLinearClosed */ + curve?: CurveFactory | CurveFactoryLineOnly; + geoTransform?: (projection: GeoProjection | GeoIdentityTransform) => GeoTransformPrototype; + /** @bindable */ + ref?: SVGPathElement; + children?: Snippet<[{ geoPath: ReturnType | undefined }]>; +} & CommonStyleProps & + Pick< + PathProps, + | 'motion' + | 'draw' + | 'marker' + | 'markerStart' + | 'markerMid' + | 'markerEnd' + | 'startContent' + | 'endContent' + >; + +export type GeoPathProps = GeoPathPropsWithoutHTML & + Without, GeoPathPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.svelte b/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.svelte new file mode 100644 index 000000000..bffe74429 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.svg.svelte b/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.svg.svelte new file mode 100644 index 000000000..c763af138 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoPath/GeoPath.svg.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoPoint.svelte b/packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.base.svelte similarity index 61% rename from packages/layerchart/src/lib/components/geo/GeoPoint.svelte rename to packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.base.svelte index 6fee14d75..a88447a32 100644 --- a/packages/layerchart/src/lib/components/geo/GeoPoint.svelte +++ b/packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.base.svelte @@ -1,41 +1,23 @@ + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.shared.svelte.ts b/packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.shared.svelte.ts new file mode 100644 index 000000000..f03912322 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.shared.svelte.ts @@ -0,0 +1,15 @@ +import type { Snippet } from 'svelte'; +import type { CircleProps } from '../../Circle/Circle.shared.svelte.js'; +import type { Without } from '$lib/utils/types.js'; + +export type GeoPointPropsWithoutHTML = { + lat: number; + long: number; + ref?: Element; + children?: Snippet<[{ x: number; y: number }]>; +}; + +export type GeoPointProps = Omit< + GeoPointPropsWithoutHTML & Without, + 'x' | 'y' +>; diff --git a/packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.svelte b/packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.svelte new file mode 100644 index 000000000..dccca3d4a --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.svg.svelte b/packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.svg.svelte new file mode 100644 index 000000000..74e7ede82 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoPoint/GeoPoint.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoProjection.svelte b/packages/layerchart/src/lib/components/geo/GeoProjection/GeoProjection.svelte similarity index 100% rename from packages/layerchart/src/lib/components/geo/GeoProjection.svelte rename to packages/layerchart/src/lib/components/geo/GeoProjection/GeoProjection.svelte diff --git a/packages/layerchart/src/lib/components/geo/GeoRaster.svelte b/packages/layerchart/src/lib/components/geo/GeoRaster/GeoRaster.svelte similarity index 100% rename from packages/layerchart/src/lib/components/geo/GeoRaster.svelte rename to packages/layerchart/src/lib/components/geo/GeoRaster/GeoRaster.svelte diff --git a/packages/layerchart/src/lib/components/geo/GeoSpline.svelte b/packages/layerchart/src/lib/components/geo/GeoSpline.svelte deleted file mode 100644 index 912e893f5..000000000 --- a/packages/layerchart/src/lib/components/geo/GeoSpline.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - - - - diff --git a/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.base.svelte b/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.base.svelte new file mode 100644 index 000000000..b4f84de23 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.base.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.canvas.svelte b/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.canvas.svelte new file mode 100644 index 000000000..631005c22 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.canvas.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.shared.svelte.ts b/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.shared.svelte.ts new file mode 100644 index 000000000..5cd2d45fb --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.shared.svelte.ts @@ -0,0 +1,14 @@ +import type { CurveFactory, CurveFactoryLineOnly } from 'd3-shape'; +import type { Without } from '$lib/utils/types.js'; +import type { PathProps } from '../../Path/Path.shared.svelte.js'; + +export type GeoSplinePropsWithoutHTML = { + link: { source: [number, number]; target: [number, number] }; + /** @default 1.0 */ + loft?: number; + /** @default curveNatural */ + curve?: CurveFactory | CurveFactoryLineOnly; +}; + +export type GeoSplineProps = GeoSplinePropsWithoutHTML & + Without; diff --git a/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.svelte b/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.svelte new file mode 100644 index 000000000..5ac7fe877 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.svg.svelte b/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.svg.svelte new file mode 100644 index 000000000..91bf379eb --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoSpline/GeoSpline.svg.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoTile.svelte b/packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.base.svelte similarity index 68% rename from packages/layerchart/src/lib/components/geo/GeoTile.svelte rename to packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.base.svelte index 11c8772e8..4da12efc1 100644 --- a/packages/layerchart/src/lib/components/geo/GeoTile.svelte +++ b/packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.base.svelte @@ -1,43 +1,13 @@ + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.shared.svelte.ts b/packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.shared.svelte.ts new file mode 100644 index 000000000..1e26e7e7e --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.shared.svelte.ts @@ -0,0 +1,16 @@ +import type { ComponentProps, Snippet } from 'svelte'; +import type Group from '../../Group/Group.svelte'; + +export type GeoTilePropsWithoutHTML = { + url: (x: number, y: number, z: number) => string; + /** @default 0 */ + zoomDelta?: number; + /** @default 256 */ + tileSize?: number; + /** @default false */ + disableCache?: boolean; + group?: Partial>; + /** @default false */ + debug?: boolean; + children?: Snippet<[{ tiles: any[] }]>; +}; diff --git a/packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.svelte b/packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.svelte new file mode 100644 index 000000000..2e0dcf800 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.svg.svelte b/packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.svg.svelte new file mode 100644 index 000000000..0b7519902 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/GeoTile/GeoTile.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/GeoVisible.svelte b/packages/layerchart/src/lib/components/geo/GeoVisible/GeoVisible.svelte similarity index 100% rename from packages/layerchart/src/lib/components/geo/GeoVisible.svelte rename to packages/layerchart/src/lib/components/geo/GeoVisible/GeoVisible.svelte diff --git a/packages/layerchart/src/lib/components/geo/Graticule.svelte b/packages/layerchart/src/lib/components/geo/Graticule.svelte deleted file mode 100644 index a87a681cd..000000000 --- a/packages/layerchart/src/lib/components/geo/Graticule.svelte +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - {#if !lines && !outline} - - {/if} - - {#if lines} - {#each graticule.lines() as line} - - {/each} - {/if} - - {#if outline} - - {/if} - diff --git a/packages/layerchart/src/lib/components/geo/Graticule/Graticule.base.svelte b/packages/layerchart/src/lib/components/geo/Graticule/Graticule.base.svelte new file mode 100644 index 000000000..d7ebecf0d --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/Graticule/Graticule.base.svelte @@ -0,0 +1,47 @@ + + + + + + {#if !lines && !outline} + + {/if} + + {#if lines} + {#each graticule.lines() as line} + + {/each} + {/if} + + {#if outline} + + {/if} + diff --git a/packages/layerchart/src/lib/components/geo/Graticule/Graticule.canvas.svelte b/packages/layerchart/src/lib/components/geo/Graticule/Graticule.canvas.svelte new file mode 100644 index 000000000..4633ae089 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/Graticule/Graticule.canvas.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/Graticule/Graticule.shared.svelte.ts b/packages/layerchart/src/lib/components/geo/Graticule/Graticule.shared.svelte.ts new file mode 100644 index 000000000..b9a5bbf6f --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/Graticule/Graticule.shared.svelte.ts @@ -0,0 +1,14 @@ +import type { Without } from '$lib/utils/types.js'; +import type { ComponentProps } from 'svelte'; +import type GeoPath from '../GeoPath/GeoPath.svelte'; +import type { GeoPathProps } from '../GeoPath/GeoPath.shared.svelte.js'; + +export type GraticulePropsWithoutHTML = { + lines?: Partial> | boolean | undefined; + outline?: Partial> | boolean | undefined; + stepX?: number; + stepY?: number; +}; + +export type GraticuleProps = GraticulePropsWithoutHTML & + Without>; diff --git a/packages/layerchart/src/lib/components/geo/Graticule/Graticule.svelte b/packages/layerchart/src/lib/components/geo/Graticule/Graticule.svelte new file mode 100644 index 000000000..4b1c98dbd --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/Graticule/Graticule.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/geo/Graticule/Graticule.svg.svelte b/packages/layerchart/src/lib/components/geo/Graticule/Graticule.svg.svelte new file mode 100644 index 000000000..6957e493a --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/Graticule/Graticule.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/TileImage.svelte b/packages/layerchart/src/lib/components/geo/TileImage/TileImage.base.svelte similarity index 61% rename from packages/layerchart/src/lib/components/geo/TileImage.svelte rename to packages/layerchart/src/lib/components/geo/TileImage/TileImage.base.svelte index 37da0d6f6..6363818ed 100644 --- a/packages/layerchart/src/lib/components/geo/TileImage.svelte +++ b/packages/layerchart/src/lib/components/geo/TileImage/TileImage.base.svelte @@ -1,71 +1,20 @@ - {#key href} + export type { TileImageProps, TileImagePropsWithoutHTML } from './TileImage.shared.svelte.js'; + + + + + diff --git a/packages/layerchart/src/lib/components/geo/TileImage/TileImage.shared.svelte.ts b/packages/layerchart/src/lib/components/geo/TileImage/TileImage.shared.svelte.ts new file mode 100644 index 000000000..146b2f852 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/TileImage/TileImage.shared.svelte.ts @@ -0,0 +1,21 @@ +import type { SVGAttributes } from 'svelte/elements'; +import type { Without } from '$lib/utils/types.js'; + +export const tileCache = new Map>(); + +export type TileImagePropsWithoutHTML = { + x: number; + y: number; + z: number; + tx: number; + ty: number; + scale: number; + /** @default false */ + disableCache?: boolean; + /** @default false */ + debug?: boolean; + url: (x: number, y: number, z: number) => string; +}; + +export type TileImageProps = TileImagePropsWithoutHTML & + Omit, TileImagePropsWithoutHTML>, 'href'>; diff --git a/packages/layerchart/src/lib/components/geo/TileImage/TileImage.svelte b/packages/layerchart/src/lib/components/geo/TileImage/TileImage.svelte new file mode 100644 index 000000000..6c4f26124 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/TileImage/TileImage.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/geo/TileImage/TileImage.svg.svelte b/packages/layerchart/src/lib/components/geo/TileImage/TileImage.svg.svelte new file mode 100644 index 000000000..f50da2795 --- /dev/null +++ b/packages/layerchart/src/lib/components/geo/TileImage/TileImage.svg.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/geo/index.ts b/packages/layerchart/src/lib/components/geo/index.ts index ff15727bb..8e5f45355 100644 --- a/packages/layerchart/src/lib/components/geo/index.ts +++ b/packages/layerchart/src/lib/components/geo/index.ts @@ -1,26 +1,26 @@ -export { default as GeoCircle } from './GeoCircle.svelte'; -export * from './GeoCircle.svelte'; -export { default as GeoClipPath } from './GeoClipPath.svelte'; -export * from './GeoClipPath.svelte'; -export { default as GeoEdgeFade } from './GeoEdgeFade.svelte'; -export * from './GeoEdgeFade.svelte'; -export { default as GeoLegend } from './GeoLegend.svelte'; -export * from './GeoLegend.svelte'; -export { default as GeoPath } from './GeoPath.svelte'; -export * from './GeoPath.svelte'; -export { default as GeoPoint } from './GeoPoint.svelte'; -export * from './GeoPoint.svelte'; -export { default as GeoProjection } from './GeoProjection.svelte'; -export * from './GeoProjection.svelte'; -export { default as GeoRaster } from './GeoRaster.svelte'; -export * from './GeoRaster.svelte'; -export { default as GeoSpline } from './GeoSpline.svelte'; -export * from './GeoSpline.svelte'; -export { default as GeoTile } from './GeoTile.svelte'; -export * from './GeoTile.svelte'; -export { default as GeoVisible } from './GeoVisible.svelte'; -export * from './GeoVisible.svelte'; -export { default as Graticule } from './Graticule.svelte'; -export * from './Graticule.svelte'; -export { default as TileImage } from './TileImage.svelte'; -export * from './TileImage.svelte'; +export { default as GeoCircle } from './GeoCircle/GeoCircle.svelte'; +export * from './GeoCircle/GeoCircle.svelte'; +export { default as GeoClipPath } from './GeoClipPath/GeoClipPath.svelte'; +export * from './GeoClipPath/GeoClipPath.svelte'; +export { default as GeoEdgeFade } from './GeoEdgeFade/GeoEdgeFade.svelte'; +export * from './GeoEdgeFade/GeoEdgeFade.svelte'; +export { default as GeoLegend } from './GeoLegend/GeoLegend.svelte'; +export * from './GeoLegend/GeoLegend.svelte'; +export { default as GeoPath } from './GeoPath/GeoPath.svelte'; +export * from './GeoPath/GeoPath.svelte'; +export { default as GeoPoint } from './GeoPoint/GeoPoint.svelte'; +export * from './GeoPoint/GeoPoint.svelte'; +export { default as GeoProjection } from './GeoProjection/GeoProjection.svelte'; +export * from './GeoProjection/GeoProjection.svelte'; +export { default as GeoRaster } from './GeoRaster/GeoRaster.svelte'; +export * from './GeoRaster/GeoRaster.svelte'; +export { default as GeoSpline } from './GeoSpline/GeoSpline.svelte'; +export * from './GeoSpline/GeoSpline.svelte'; +export { default as GeoTile } from './GeoTile/GeoTile.svelte'; +export * from './GeoTile/GeoTile.svelte'; +export { default as GeoVisible } from './GeoVisible/GeoVisible.svelte'; +export * from './GeoVisible/GeoVisible.svelte'; +export { default as Graticule } from './Graticule/Graticule.svelte'; +export * from './Graticule/Graticule.svelte'; +export { default as TileImage } from './TileImage/TileImage.svelte'; +export * from './TileImage/TileImage.svelte'; diff --git a/packages/layerchart/src/lib/svg.ts b/packages/layerchart/src/lib/svg.ts index a830e5665..66255054c 100644 --- a/packages/layerchart/src/lib/svg.ts +++ b/packages/layerchart/src/lib/svg.ts @@ -235,6 +235,50 @@ export type { BoxPlotProps, BoxPlotPropsWithoutHTML, } from './components/BoxPlot/BoxPlot.shared.svelte.js'; + +// Geo components +export { default as GeoPath } from './components/geo/GeoPath/GeoPath.svg.svelte'; +export type { + GeoPathProps, + GeoPathPropsWithoutHTML, +} from './components/geo/GeoPath/GeoPath.shared.svelte.js'; +export { default as GeoSpline } from './components/geo/GeoSpline/GeoSpline.svg.svelte'; +export type { + GeoSplineProps, + GeoSplinePropsWithoutHTML, +} from './components/geo/GeoSpline/GeoSpline.shared.svelte.js'; +export { default as GeoPoint } from './components/geo/GeoPoint/GeoPoint.svg.svelte'; +export type { + GeoPointProps, + GeoPointPropsWithoutHTML, +} from './components/geo/GeoPoint/GeoPoint.shared.svelte.js'; +export { default as GeoCircle } from './components/geo/GeoCircle/GeoCircle.svg.svelte'; +export type { + GeoCircleProps, + GeoCirclePropsWithoutHTML, +} from './components/geo/GeoCircle/GeoCircle.shared.svelte.js'; +export { default as GeoClipPath } from './components/geo/GeoClipPath/GeoClipPath.svg.svelte'; +export type { + GeoClipPathProps, + GeoClipPathPropsWithoutHTML, +} from './components/geo/GeoClipPath/GeoClipPath.shared.svelte.js'; +export { default as GeoEdgeFade } from './components/geo/GeoEdgeFade/GeoEdgeFade.svg.svelte'; +export type { + GeoEdgeFadeProps, + GeoEdgeFadePropsWithoutHTML, +} from './components/geo/GeoEdgeFade/GeoEdgeFade.shared.svelte.js'; +export { default as GeoTile } from './components/geo/GeoTile/GeoTile.svg.svelte'; +export type { GeoTilePropsWithoutHTML } from './components/geo/GeoTile/GeoTile.shared.svelte.js'; +export { default as TileImage } from './components/geo/TileImage/TileImage.svg.svelte'; +export type { + TileImageProps, + TileImagePropsWithoutHTML, +} from './components/geo/TileImage/TileImage.shared.svelte.js'; +export { default as Graticule } from './components/geo/Graticule/Graticule.svg.svelte'; +export type { + GraticuleProps, + GraticulePropsWithoutHTML, +} from './components/geo/Graticule/Graticule.shared.svelte.js'; export { default as RectClipPath } from './components/RectClipPath/RectClipPath.svg.svelte'; export type { RectClipPathProps, From c9a43b2325e8eb89f023a3dd8fdb01d6c730f625 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 28 Apr 2026 16:00:31 -0400 Subject: [PATCH 22/36] split Ribbon into 3 layer-specific components. Re-export all layout/helper components --- bundle-analyzer/bundle-reports/latest.json | 20 +++--- packages/layerchart/src/lib/canvas.ts | 68 ++++++++++++++++++- .../Ribbon.base.svelte} | 67 +++--------------- .../graph/Ribbon/Ribbon.canvas.svelte | 13 ++++ .../graph/Ribbon/Ribbon.shared.svelte.ts | 23 +++++++ .../lib/components/graph/Ribbon/Ribbon.svelte | 20 ++++++ .../components/graph/Ribbon/Ribbon.svg.svelte | 13 ++++ .../src/lib/components/graph/index.ts | 4 +- packages/layerchart/src/lib/html.ts | 55 ++++++++++++++- packages/layerchart/src/lib/svg.ts | 68 ++++++++++++++++++- 10 files changed, 279 insertions(+), 72 deletions(-) rename packages/layerchart/src/lib/components/graph/{Ribbon.svelte => Ribbon/Ribbon.base.svelte} (51%) create mode 100644 packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.svelte create mode 100644 packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.svg.svelte diff --git a/bundle-analyzer/bundle-reports/latest.json b/bundle-analyzer/bundle-reports/latest.json index 40054c399..76f06d9a1 100644 --- a/bundle-analyzer/bundle-reports/latest.json +++ b/bundle-analyzer/bundle-reports/latest.json @@ -1,5 +1,5 @@ { - "timestamp": "2026-04-28T19:38:55.794Z", + "timestamp": "2026-04-28T19:56:41.701Z", "results": [ { "scenario": "core", @@ -28,7 +28,7 @@ "description": "Canvas-based rendering", "group": "Foundation", "size": 358506, - "gzipSize": 83936, + "gzipSize": 83935, "imports": [ "Chart", "Canvas" @@ -39,7 +39,7 @@ "description": "HTML-based rendering", "group": "Foundation", "size": 359830, - "gzipSize": 84205, + "gzipSize": 84202, "imports": [ "Chart", "Html" @@ -64,7 +64,7 @@ "description": "Line chart composed from `layerchart/svg`", "group": "Cartesian charts", "size": 352003, - "gzipSize": 82210, + "gzipSize": 82207, "imports": [ "Chart", "Layer", @@ -379,8 +379,8 @@ "scenario": "chord", "description": "Chord diagram", "group": "Graph / network", - "size": 384515, - "gzipSize": 88846, + "size": 384902, + "gzipSize": 88881, "imports": [ "Chart", "Svg", @@ -1013,7 +1013,7 @@ "description": "Standalone Highlight from `layerchart/svg`", "group": "Components", "size": 35567, - "gzipSize": 6784, + "gzipSize": 6785, "imports": [ "Highlight" ] @@ -1023,7 +1023,7 @@ "description": "Standalone Highlight from `layerchart/canvas`", "group": "Components", "size": 32884, - "gzipSize": 6215, + "gzipSize": 6217, "imports": [ "Highlight" ] @@ -2292,8 +2292,8 @@ "scenario": "all", "description": "Everything from layerchart (worst case)", "group": "Worst case", - "size": 1006072, - "gzipSize": 235747, + "size": 1007001, + "gzipSize": 235893, "imports": [ "*" ] diff --git a/packages/layerchart/src/lib/canvas.ts b/packages/layerchart/src/lib/canvas.ts index 7677007f7..c59da8d82 100644 --- a/packages/layerchart/src/lib/canvas.ts +++ b/packages/layerchart/src/lib/canvas.ts @@ -137,7 +137,7 @@ export { default as Points } from './components/Points/Points.canvas.svelte'; export type { PointsProps, PointsPropsWithoutHTML, - Point, + Point as PointDatum, } from './components/Points/Points.shared.svelte.js'; export { default as Labels } from './components/Labels/Labels.canvas.svelte'; export type { @@ -274,6 +274,72 @@ export type { GraticuleProps, GraticulePropsWithoutHTML, } from './components/geo/Graticule/Graticule.shared.svelte.js'; + +// Ribbon (graph) — uses Path +export { default as Ribbon } from './components/graph/Ribbon/Ribbon.canvas.svelte'; +export type { + RibbonProps, + RibbonPropsWithoutHTML, +} from './components/graph/Ribbon/Ribbon.shared.svelte.js'; + +// --- Layer-agnostic re-exports --- +// These components don't render layer-specific elements (pure logic, layout +// helpers, context providers, or composite chart wrappers). Re-exported here +// so the per-layer sub-path has a complete API. + +// Helpers / context providers +export { default as Blur } from './components/Blur.svelte'; +export * from './components/Blur.svelte'; +export { default as Bounds } from './components/Bounds.svelte'; +export * from './components/Bounds.svelte'; +export { default as BrushContext } from './components/BrushContext.svelte'; +export * from './components/BrushContext.svelte'; +export { default as CircleLegend } from './components/CircleLegend.svelte'; +export * from './components/CircleLegend.svelte'; +export { default as ColorRamp } from './components/ColorRamp.svelte'; +export * from './components/ColorRamp.svelte'; +export { default as Legend } from './components/Legend.svelte'; +export * from './components/Legend.svelte'; +export { default as MotionPath } from './components/MotionPath.svelte'; +export * from './components/MotionPath.svelte'; +export { default as Point } from './components/Point.svelte'; +export * from './components/Point.svelte'; +export { default as TransformContext } from './components/TransformContext.svelte'; +export * from './components/TransformContext.svelte'; +export * as Tooltip from './components/tooltip/index.js'; +export * from './components/tooltip/TooltipContext.svelte'; + +// High-level chart wrappers +export { default as LineChart } from './components/charts/LineChart.svelte'; +export { default as AreaChart } from './components/charts/AreaChart.svelte'; +export { default as BarChart } from './components/charts/BarChart.svelte'; +export { default as PieChart } from './components/charts/PieChart.svelte'; +export { default as ScatterChart } from './components/charts/ScatterChart.svelte'; +export { default as ArcChart } from './components/charts/ArcChart.svelte'; + +// Layout components +export { default as Tree } from './components/hierarchy/Tree.svelte'; +export * from './components/hierarchy/Tree.svelte'; +export { default as Treemap } from './components/hierarchy/Treemap.svelte'; +export * from './components/hierarchy/Treemap.svelte'; +export { default as Pack } from './components/hierarchy/Pack.svelte'; +export * from './components/hierarchy/Pack.svelte'; +export { default as Partition } from './components/hierarchy/Partition.svelte'; +export * from './components/hierarchy/Partition.svelte'; +export { default as Chord } from './components/graph/Chord.svelte'; +export * from './components/graph/Chord.svelte'; +export { default as Dagre } from './components/graph/Dagre.svelte'; +export * from './components/graph/Dagre.svelte'; +export { default as Sankey } from './components/graph/Sankey.svelte'; +export * from './components/graph/Sankey.svelte'; +export { default as ForceSimulation } from './components/force/ForceSimulation.svelte'; +export * from './components/force/ForceSimulation.svelte'; + +// Geo helpers (no per-layer rendering) +export { default as GeoLegend } from './components/geo/GeoLegend/GeoLegend.svelte'; +export { default as GeoProjection } from './components/geo/GeoProjection/GeoProjection.svelte'; +export { default as GeoRaster } from './components/geo/GeoRaster/GeoRaster.svelte'; +export { default as GeoVisible } from './components/geo/GeoVisible/GeoVisible.svelte'; export { default as RectClipPath } from './components/RectClipPath/RectClipPath.canvas.svelte'; export type { RectClipPathProps, diff --git a/packages/layerchart/src/lib/components/graph/Ribbon.svelte b/packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.base.svelte similarity index 51% rename from packages/layerchart/src/lib/components/graph/Ribbon.svelte rename to packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.base.svelte index 5d8bbfe80..3944874cc 100644 --- a/packages/layerchart/src/lib/components/graph/Ribbon.svelte +++ b/packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.base.svelte @@ -1,70 +1,23 @@ + + + + diff --git a/packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.shared.svelte.ts b/packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.shared.svelte.ts new file mode 100644 index 000000000..1e523a646 --- /dev/null +++ b/packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.shared.svelte.ts @@ -0,0 +1,23 @@ +import type { PointerEventHandler, SVGAttributes } from 'svelte/elements'; + +import type { PathPropsWithoutHTML } from '../../Path/Path.shared.svelte.js'; +import type { MotionProp } from '$lib/utils/motion.svelte.js'; +import type { CommonStyleProps, Without } from '$lib/utils/types.js'; + +export type RibbonPropsWithoutHTML = { + chord: import('d3-chord').Chord; + radius?: number; + /** @default false */ + directed?: boolean; + headRadius?: number; + tooltip?: boolean; + data?: any; + motion?: MotionProp; + onpointerenter?: PointerEventHandler; + onpointermove?: PointerEventHandler; + onpointerleave?: PointerEventHandler; + ontouchmove?: (e: TouchEvent & { currentTarget: SVGPathElement }) => void; +} & CommonStyleProps; + +export type RibbonProps = RibbonPropsWithoutHTML & + Without, RibbonPropsWithoutHTML & PathPropsWithoutHTML>; diff --git a/packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.svelte b/packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.svelte new file mode 100644 index 000000000..f6800da45 --- /dev/null +++ b/packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.svelte @@ -0,0 +1,20 @@ + + + + +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{/if} diff --git a/packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.svg.svelte b/packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.svg.svelte new file mode 100644 index 000000000..8fafeaf01 --- /dev/null +++ b/packages/layerchart/src/lib/components/graph/Ribbon/Ribbon.svg.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/graph/index.ts b/packages/layerchart/src/lib/components/graph/index.ts index 54b05e1dc..fc97fbd22 100644 --- a/packages/layerchart/src/lib/components/graph/index.ts +++ b/packages/layerchart/src/lib/components/graph/index.ts @@ -2,7 +2,7 @@ export { default as Chord } from './Chord.svelte'; export * from './Chord.svelte'; export { default as Dagre } from './Dagre.svelte'; export * from './Dagre.svelte'; -export { default as Ribbon } from './Ribbon.svelte'; -export * from './Ribbon.svelte'; +export { default as Ribbon } from './Ribbon/Ribbon.svelte'; +export * from './Ribbon/Ribbon.svelte'; export { default as Sankey } from './Sankey.svelte'; export * from './Sankey.svelte'; diff --git a/packages/layerchart/src/lib/html.ts b/packages/layerchart/src/lib/html.ts index 69908422d..e9f7d6932 100644 --- a/packages/layerchart/src/lib/html.ts +++ b/packages/layerchart/src/lib/html.ts @@ -105,7 +105,7 @@ export { default as Points } from './components/Points/Points.html.svelte'; export type { PointsProps, PointsPropsWithoutHTML, - Point, + Point as PointDatum, } from './components/Points/Points.shared.svelte.js'; export { default as Labels } from './components/Labels/Labels.html.svelte'; export type { @@ -126,3 +126,56 @@ export type { RasterProps, RasterPropsWithoutHTML, } from './components/Raster/Raster.shared.svelte.js'; + +// --- Layer-agnostic re-exports --- +// These components don't render layer-specific elements (pure logic, layout +// helpers, context providers, or composite chart wrappers). Re-exported here +// so the per-layer sub-path has a complete API. + +// Helpers / context providers +export { default as Blur } from './components/Blur.svelte'; +export * from './components/Blur.svelte'; +export { default as Bounds } from './components/Bounds.svelte'; +export * from './components/Bounds.svelte'; +export { default as BrushContext } from './components/BrushContext.svelte'; +export * from './components/BrushContext.svelte'; +export { default as CircleLegend } from './components/CircleLegend.svelte'; +export * from './components/CircleLegend.svelte'; +export { default as ColorRamp } from './components/ColorRamp.svelte'; +export * from './components/ColorRamp.svelte'; +export { default as Legend } from './components/Legend.svelte'; +export * from './components/Legend.svelte'; +export { default as MotionPath } from './components/MotionPath.svelte'; +export * from './components/MotionPath.svelte'; +export { default as Point } from './components/Point.svelte'; +export * from './components/Point.svelte'; +export { default as TransformContext } from './components/TransformContext.svelte'; +export * from './components/TransformContext.svelte'; +export * as Tooltip from './components/tooltip/index.js'; +export * from './components/tooltip/TooltipContext.svelte'; + +// High-level chart wrappers +export { default as LineChart } from './components/charts/LineChart.svelte'; +export { default as AreaChart } from './components/charts/AreaChart.svelte'; +export { default as BarChart } from './components/charts/BarChart.svelte'; +export { default as PieChart } from './components/charts/PieChart.svelte'; +export { default as ScatterChart } from './components/charts/ScatterChart.svelte'; +export { default as ArcChart } from './components/charts/ArcChart.svelte'; + +// Layout components +export { default as Tree } from './components/hierarchy/Tree.svelte'; +export * from './components/hierarchy/Tree.svelte'; +export { default as Treemap } from './components/hierarchy/Treemap.svelte'; +export * from './components/hierarchy/Treemap.svelte'; +export { default as Pack } from './components/hierarchy/Pack.svelte'; +export * from './components/hierarchy/Pack.svelte'; +export { default as Partition } from './components/hierarchy/Partition.svelte'; +export * from './components/hierarchy/Partition.svelte'; +export { default as Chord } from './components/graph/Chord.svelte'; +export * from './components/graph/Chord.svelte'; +export { default as Dagre } from './components/graph/Dagre.svelte'; +export * from './components/graph/Dagre.svelte'; +export { default as Sankey } from './components/graph/Sankey.svelte'; +export * from './components/graph/Sankey.svelte'; +export { default as ForceSimulation } from './components/force/ForceSimulation.svelte'; +export * from './components/force/ForceSimulation.svelte'; diff --git a/packages/layerchart/src/lib/svg.ts b/packages/layerchart/src/lib/svg.ts index 66255054c..fb2473d3c 100644 --- a/packages/layerchart/src/lib/svg.ts +++ b/packages/layerchart/src/lib/svg.ts @@ -142,7 +142,7 @@ export { default as Points } from './components/Points/Points.svg.svelte'; export type { PointsProps, PointsPropsWithoutHTML, - Point, + Point as PointDatum, } from './components/Points/Points.shared.svelte.js'; export { default as Labels } from './components/Labels/Labels.svg.svelte'; export type { @@ -279,6 +279,72 @@ export type { GraticuleProps, GraticulePropsWithoutHTML, } from './components/geo/Graticule/Graticule.shared.svelte.js'; + +// Ribbon (graph) — uses Path +export { default as Ribbon } from './components/graph/Ribbon/Ribbon.svg.svelte'; +export type { + RibbonProps, + RibbonPropsWithoutHTML, +} from './components/graph/Ribbon/Ribbon.shared.svelte.js'; + +// --- Layer-agnostic re-exports --- +// These components don't render layer-specific elements (pure logic, layout +// helpers, context providers, or composite chart wrappers). Re-exported here +// so the per-layer sub-path has a complete API. + +// Helpers / context providers +export { default as Blur } from './components/Blur.svelte'; +export * from './components/Blur.svelte'; +export { default as Bounds } from './components/Bounds.svelte'; +export * from './components/Bounds.svelte'; +export { default as BrushContext } from './components/BrushContext.svelte'; +export * from './components/BrushContext.svelte'; +export { default as CircleLegend } from './components/CircleLegend.svelte'; +export * from './components/CircleLegend.svelte'; +export { default as ColorRamp } from './components/ColorRamp.svelte'; +export * from './components/ColorRamp.svelte'; +export { default as Legend } from './components/Legend.svelte'; +export * from './components/Legend.svelte'; +export { default as MotionPath } from './components/MotionPath.svelte'; +export * from './components/MotionPath.svelte'; +export { default as Point } from './components/Point.svelte'; +export * from './components/Point.svelte'; +export { default as TransformContext } from './components/TransformContext.svelte'; +export * from './components/TransformContext.svelte'; +export * as Tooltip from './components/tooltip/index.js'; +export * from './components/tooltip/TooltipContext.svelte'; + +// High-level chart wrappers +export { default as LineChart } from './components/charts/LineChart.svelte'; +export { default as AreaChart } from './components/charts/AreaChart.svelte'; +export { default as BarChart } from './components/charts/BarChart.svelte'; +export { default as PieChart } from './components/charts/PieChart.svelte'; +export { default as ScatterChart } from './components/charts/ScatterChart.svelte'; +export { default as ArcChart } from './components/charts/ArcChart.svelte'; + +// Layout components +export { default as Tree } from './components/hierarchy/Tree.svelte'; +export * from './components/hierarchy/Tree.svelte'; +export { default as Treemap } from './components/hierarchy/Treemap.svelte'; +export * from './components/hierarchy/Treemap.svelte'; +export { default as Pack } from './components/hierarchy/Pack.svelte'; +export * from './components/hierarchy/Pack.svelte'; +export { default as Partition } from './components/hierarchy/Partition.svelte'; +export * from './components/hierarchy/Partition.svelte'; +export { default as Chord } from './components/graph/Chord.svelte'; +export * from './components/graph/Chord.svelte'; +export { default as Dagre } from './components/graph/Dagre.svelte'; +export * from './components/graph/Dagre.svelte'; +export { default as Sankey } from './components/graph/Sankey.svelte'; +export * from './components/graph/Sankey.svelte'; +export { default as ForceSimulation } from './components/force/ForceSimulation.svelte'; +export * from './components/force/ForceSimulation.svelte'; + +// Geo helpers (no per-layer rendering) +export { default as GeoLegend } from './components/geo/GeoLegend/GeoLegend.svelte'; +export { default as GeoProjection } from './components/geo/GeoProjection/GeoProjection.svelte'; +export { default as GeoRaster } from './components/geo/GeoRaster/GeoRaster.svelte'; +export { default as GeoVisible } from './components/geo/GeoVisible/GeoVisible.svelte'; export { default as RectClipPath } from './components/RectClipPath/RectClipPath.svg.svelte'; export type { RectClipPathProps, From 2b8ab5501ed83dc2182addd3cee1580377c3ce1d Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 28 Apr 2026 17:21:41 -0400 Subject: [PATCH 23/36] split high-level charts (BarChart, LineChart, etc) into 3 layer-specific components. Re-export all layout/helper components --- bundle-analyzer/bundle-reports/latest.json | 412 +++++++++++------- bundle-analyzer/bundle-scenarios.ts | 84 ++++ .../composable-vs-linechart.svelte.bench.ts | 2 +- packages/layerchart/src/lib/canvas.ts | 12 +- .../ArcChart.base.svelte} | 170 ++------ .../charts/ArcChart/ArcChart.canvas.svelte | 16 + .../charts/ArcChart/ArcChart.shared.svelte.ts | 131 ++++++ .../charts/ArcChart/ArcChart.svelte | 16 + .../charts/ArcChart/ArcChart.svg.svelte | 16 + .../charts/AreaChart.svelte.test.ts | 2 +- .../AreaChart.base.svelte} | 52 +-- .../charts/AreaChart/AreaChart.canvas.svelte | 14 + .../AreaChart/AreaChart.shared.svelte.ts | 40 ++ .../charts/AreaChart/AreaChart.svelte | 14 + .../charts/AreaChart/AreaChart.svg.svelte | 14 + .../components/charts/BarChart.svelte.test.ts | 2 +- .../BarChart.base.svelte} | 88 +--- .../charts/BarChart/BarChart.canvas.svelte | 14 + .../charts/BarChart/BarChart.shared.svelte.ts | 70 +++ .../charts/BarChart/BarChart.svelte | 14 + .../charts/BarChart/BarChart.svg.svelte | 14 + .../charts/DefaultTooltip.svelte.test.ts | 6 +- .../charts/LineChart.svelte.bench.ts | 2 +- .../charts/LineChart.svelte.test.ts | 2 +- .../LineChart.base.svelte} | 57 +-- .../charts/LineChart/LineChart.canvas.svelte | 14 + .../LineChart/LineChart.shared.svelte.ts | 41 ++ .../charts/LineChart/LineChart.svelte | 14 + .../charts/LineChart/LineChart.svg.svelte | 14 + .../components/charts/PieChart.svelte.test.ts | 2 +- .../PieChart.base.svelte} | 228 ++-------- .../charts/PieChart/PieChart.canvas.svelte | 17 + .../charts/PieChart/PieChart.shared.svelte.ts | 183 ++++++++ .../charts/PieChart/PieChart.svelte | 17 + .../charts/PieChart/PieChart.svg.svelte | 17 + .../ScatterChart.base.svelte} | 39 +- .../ScatterChart/ScatterChart.canvas.svelte | 14 + .../ScatterChart.shared.svelte.ts | 24 + .../charts/ScatterChart/ScatterChart.svelte | 14 + .../ScatterChart/ScatterChart.svg.svelte | 14 + .../__fixtures__/ArcChartTooltip.svelte | 2 +- .../src/lib/components/charts/index.ts | 24 +- .../components/tooltip/Tooltip.svelte.test.ts | 2 +- packages/layerchart/src/lib/html.ts | 12 +- packages/layerchart/src/lib/svg.ts | 12 +- 45 files changed, 1259 insertions(+), 709 deletions(-) rename packages/layerchart/src/lib/components/charts/{ArcChart.svelte => ArcChart/ArcChart.base.svelte} (68%) create mode 100644 packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.svelte create mode 100644 packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.svg.svelte rename packages/layerchart/src/lib/components/charts/{AreaChart.svelte => AreaChart/AreaChart.base.svelte} (60%) create mode 100644 packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.svelte create mode 100644 packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.svg.svelte rename packages/layerchart/src/lib/components/charts/{BarChart.svelte => BarChart/BarChart.base.svelte} (55%) create mode 100644 packages/layerchart/src/lib/components/charts/BarChart/BarChart.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/charts/BarChart/BarChart.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/charts/BarChart/BarChart.svelte create mode 100644 packages/layerchart/src/lib/components/charts/BarChart/BarChart.svg.svelte rename packages/layerchart/src/lib/components/charts/{LineChart.svelte => LineChart/LineChart.base.svelte} (64%) create mode 100644 packages/layerchart/src/lib/components/charts/LineChart/LineChart.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/charts/LineChart/LineChart.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/charts/LineChart/LineChart.svelte create mode 100644 packages/layerchart/src/lib/components/charts/LineChart/LineChart.svg.svelte rename packages/layerchart/src/lib/components/charts/{PieChart.svelte => PieChart/PieChart.base.svelte} (63%) create mode 100644 packages/layerchart/src/lib/components/charts/PieChart/PieChart.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/charts/PieChart/PieChart.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/charts/PieChart/PieChart.svelte create mode 100644 packages/layerchart/src/lib/components/charts/PieChart/PieChart.svg.svelte rename packages/layerchart/src/lib/components/charts/{ScatterChart.svelte => ScatterChart/ScatterChart.base.svelte} (66%) create mode 100644 packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.canvas.svelte create mode 100644 packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.shared.svelte.ts create mode 100644 packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.svelte create mode 100644 packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.svg.svelte diff --git a/bundle-analyzer/bundle-reports/latest.json b/bundle-analyzer/bundle-reports/latest.json index 76f06d9a1..7f43d443b 100644 --- a/bundle-analyzer/bundle-reports/latest.json +++ b/bundle-analyzer/bundle-reports/latest.json @@ -1,12 +1,12 @@ { - "timestamp": "2026-04-28T19:56:41.701Z", + "timestamp": "2026-04-28T21:04:57.162Z", "results": [ { "scenario": "core", "description": "Core charting components without rendering layer", "group": "Foundation", - "size": 374970, - "gzipSize": 86619, + "size": 379780, + "gzipSize": 88341, "imports": [ "Chart", "Svg" @@ -17,7 +17,7 @@ "description": "Svg-based rendering", "group": "Foundation", "size": 351979, - "gzipSize": 82186, + "gzipSize": 82188, "imports": [ "Chart", "Svg" @@ -28,7 +28,7 @@ "description": "Canvas-based rendering", "group": "Foundation", "size": 358506, - "gzipSize": 83935, + "gzipSize": 83936, "imports": [ "Chart", "Canvas" @@ -39,7 +39,7 @@ "description": "HTML-based rendering", "group": "Foundation", "size": 359830, - "gzipSize": 84202, + "gzipSize": 84206, "imports": [ "Chart", "Html" @@ -49,8 +49,8 @@ "scenario": "line-chart", "description": "Basic line chart with axes and grid", "group": "Cartesian charts", - "size": 375465, - "gzipSize": 86651, + "size": 380275, + "gzipSize": 88373, "imports": [ "Chart", "Svg", @@ -64,7 +64,7 @@ "description": "Line chart composed from `layerchart/svg`", "group": "Cartesian charts", "size": 352003, - "gzipSize": 82207, + "gzipSize": 82208, "imports": [ "Chart", "Layer", @@ -92,7 +92,7 @@ "description": "Line chart composed from `layerchart/html`", "group": "Cartesian charts", "size": 359854, - "gzipSize": 84220, + "gzipSize": 84223, "imports": [ "Chart", "Layer", @@ -105,8 +105,8 @@ "scenario": "line-chart-interactive", "description": "Line chart with tooltip and highlight", "group": "Cartesian charts", - "size": 390306, - "gzipSize": 90005, + "size": 395116, + "gzipSize": 91773, "imports": [ "Chart", "Svg", @@ -121,8 +121,28 @@ "scenario": "LineChart", "description": "High-level `LineChart` component", "group": "Cartesian charts", - "size": 399089, - "gzipSize": 93025, + "size": 404710, + "gzipSize": 94711, + "imports": [ + "LineChart" + ] + }, + { + "scenario": "LineChart-svg", + "description": "High-level `LineChart` from `layerchart/svg`", + "group": "Cartesian charts", + "size": 359176, + "gzipSize": 83881, + "imports": [ + "LineChart" + ] + }, + { + "scenario": "LineChart-canvas", + "description": "High-level `LineChart` from `layerchart/canvas`", + "group": "Cartesian charts", + "size": 365689, + "gzipSize": 85642, "imports": [ "LineChart" ] @@ -131,8 +151,8 @@ "scenario": "area-chart", "description": "Area chart with axes", "group": "Cartesian charts", - "size": 388981, - "gzipSize": 89730, + "size": 393791, + "gzipSize": 91485, "imports": [ "Chart", "Svg", @@ -145,8 +165,28 @@ "scenario": "AreaChart", "description": "High-level `AreaChart` component", "group": "Cartesian charts", - "size": 392569, - "gzipSize": 90589, + "size": 398193, + "gzipSize": 92477, + "imports": [ + "AreaChart" + ] + }, + { + "scenario": "AreaChart-svg", + "description": "High-level `AreaChart` from `layerchart/svg`", + "group": "Cartesian charts", + "size": 369610, + "gzipSize": 86100, + "imports": [ + "AreaChart" + ] + }, + { + "scenario": "AreaChart-canvas", + "description": "High-level `AreaChart` from `layerchart/canvas`", + "group": "Cartesian charts", + "size": 376125, + "gzipSize": 87798, "imports": [ "AreaChart" ] @@ -155,8 +195,8 @@ "scenario": "bar-chart", "description": "Bar chart with axes", "group": "Cartesian charts", - "size": 379305, - "gzipSize": 87393, + "size": 384115, + "gzipSize": 89150, "imports": [ "Chart", "Svg", @@ -169,8 +209,28 @@ "scenario": "BarChart", "description": "High-level `BarChart` component", "group": "Cartesian charts", - "size": 384438, - "gzipSize": 88531, + "size": 390134, + "gzipSize": 90442, + "imports": [ + "BarChart" + ] + }, + { + "scenario": "BarChart-svg", + "description": "High-level `BarChart` from `layerchart/svg`", + "group": "Cartesian charts", + "size": 361572, + "gzipSize": 84090, + "imports": [ + "BarChart" + ] + }, + { + "scenario": "BarChart-canvas", + "description": "High-level `BarChart` from `layerchart/canvas`", + "group": "Cartesian charts", + "size": 368079, + "gzipSize": 85821, "imports": [ "BarChart" ] @@ -179,8 +239,8 @@ "scenario": "scatter-chart", "description": "Scatter plot with points", "group": "Cartesian charts", - "size": 376247, - "gzipSize": 87110, + "size": 381057, + "gzipSize": 88782, "imports": [ "Chart", "Svg", @@ -194,8 +254,28 @@ "scenario": "ScatterChart", "description": "High-level `ScatterChart` component", "group": "Cartesian charts", - "size": 378994, - "gzipSize": 87788, + "size": 384600, + "gzipSize": 89485, + "imports": [ + "ScatterChart" + ] + }, + { + "scenario": "ScatterChart-svg", + "description": "High-level `ScatterChart` from `layerchart/svg`", + "group": "Cartesian charts", + "size": 355746, + "gzipSize": 82944, + "imports": [ + "ScatterChart" + ] + }, + { + "scenario": "ScatterChart-canvas", + "description": "High-level `ScatterChart` from `layerchart/canvas`", + "group": "Cartesian charts", + "size": 362257, + "gzipSize": 84670, "imports": [ "ScatterChart" ] @@ -204,8 +284,8 @@ "scenario": "pie-chart", "description": "Pie/donut chart with arcs", "group": "Cartesian charts", - "size": 387712, - "gzipSize": 89809, + "size": 392522, + "gzipSize": 91106, "imports": [ "Chart", "Svg", @@ -218,8 +298,28 @@ "scenario": "PieChart", "description": "High-level `PieChart` component", "group": "Cartesian charts", - "size": 413165, - "gzipSize": 95513, + "size": 420076, + "gzipSize": 97106, + "imports": [ + "PieChart" + ] + }, + { + "scenario": "PieChart-svg", + "description": "High-level `PieChart` from `layerchart/svg`", + "group": "Cartesian charts", + "size": 390063, + "gzipSize": 90247, + "imports": [ + "PieChart" + ] + }, + { + "scenario": "PieChart-canvas", + "description": "High-level `PieChart` from `layerchart/canvas`", + "group": "Cartesian charts", + "size": 396575, + "gzipSize": 92081, "imports": [ "PieChart" ] @@ -228,8 +328,28 @@ "scenario": "ArcChart", "description": "High-level `ArcChart` component", "group": "Cartesian charts", - "size": 406661, - "gzipSize": 94270, + "size": 413189, + "gzipSize": 95950, + "imports": [ + "ArcChart" + ] + }, + { + "scenario": "ArcChart-svg", + "description": "High-level `ArcChart` from `layerchart/svg`", + "group": "Cartesian charts", + "size": 383728, + "gzipSize": 89211, + "imports": [ + "ArcChart" + ] + }, + { + "scenario": "ArcChart-canvas", + "description": "High-level `ArcChart` from `layerchart/canvas`", + "group": "Cartesian charts", + "size": 390250, + "gzipSize": 90961, "imports": [ "ArcChart" ] @@ -238,8 +358,8 @@ "scenario": "geo", "description": "Geographic map with paths", "group": "Geo", - "size": 397074, - "gzipSize": 92030, + "size": 397070, + "gzipSize": 92273, "imports": [ "Chart", "Svg", @@ -252,8 +372,8 @@ "scenario": "geo-tiles", "description": "Geographic map with tile layer", "group": "Geo", - "size": 402266, - "gzipSize": 93681, + "size": 402262, + "gzipSize": 93936, "imports": [ "Chart", "Svg", @@ -267,8 +387,8 @@ "scenario": "geo-full", "description": "Full geo setup with all geo components", "group": "Geo", - "size": 455732, - "gzipSize": 107596, + "size": 455728, + "gzipSize": 107871, "imports": [ "Chart", "Svg", @@ -290,8 +410,8 @@ "scenario": "hierarchy-tree", "description": "Tree layout with links", "group": "Hierarchy", - "size": 415076, - "gzipSize": 96456, + "size": 419886, + "gzipSize": 98174, "imports": [ "Chart", "Svg", @@ -305,8 +425,8 @@ "scenario": "hierarchy-treemap", "description": "Treemap layout", "group": "Hierarchy", - "size": 393889, - "gzipSize": 91266, + "size": 398699, + "gzipSize": 93055, "imports": [ "Chart", "Svg", @@ -320,8 +440,8 @@ "scenario": "hierarchy-pack", "description": "Circle packing layout", "group": "Hierarchy", - "size": 393608, - "gzipSize": 91347, + "size": 398418, + "gzipSize": 93124, "imports": [ "Chart", "Svg", @@ -334,8 +454,8 @@ "scenario": "force", "description": "Force-directed graph layout", "group": "Graph / network", - "size": 417533, - "gzipSize": 97346, + "size": 422343, + "gzipSize": 99077, "imports": [ "Chart", "Svg", @@ -349,8 +469,8 @@ "scenario": "dagre", "description": "Dagre directed graph layout", "group": "Graph / network", - "size": 474962, - "gzipSize": 112736, + "size": 479772, + "gzipSize": 114401, "imports": [ "Chart", "Svg", @@ -364,8 +484,8 @@ "scenario": "sankey", "description": "Sankey flow diagram", "group": "Graph / network", - "size": 417268, - "gzipSize": 96693, + "size": 422078, + "gzipSize": 98409, "imports": [ "Chart", "Svg", @@ -379,8 +499,8 @@ "scenario": "chord", "description": "Chord diagram", "group": "Graph / network", - "size": 384902, - "gzipSize": 88881, + "size": 389712, + "gzipSize": 90614, "imports": [ "Chart", "Svg", @@ -392,8 +512,8 @@ "scenario": "Circle", "description": "Standalone Circle (agnostic) — baseline", "group": "Components", - "size": 69178, - "gzipSize": 17492, + "size": 69174, + "gzipSize": 17473, "imports": [ "Circle" ] @@ -432,8 +552,8 @@ "scenario": "Text", "description": "Standalone Text (agnostic) — baseline", "group": "Components", - "size": 119984, - "gzipSize": 29838, + "size": 119980, + "gzipSize": 29817, "imports": [ "Text" ] @@ -472,8 +592,8 @@ "scenario": "Rect", "description": "Standalone Rect (agnostic) — baseline", "group": "Components", - "size": 76130, - "gzipSize": 19020, + "size": 76126, + "gzipSize": 19000, "imports": [ "Rect" ] @@ -512,8 +632,8 @@ "scenario": "Line", "description": "Standalone Line (agnostic) — baseline", "group": "Components", - "size": 75988, - "gzipSize": 19090, + "size": 75984, + "gzipSize": 19053, "imports": [ "Line" ] @@ -552,8 +672,8 @@ "scenario": "Path", "description": "Standalone Path (agnostic) — baseline", "group": "Components", - "size": 86123, - "gzipSize": 21822, + "size": 86119, + "gzipSize": 21790, "imports": [ "Path" ] @@ -583,7 +703,7 @@ "description": "Standalone ClipPath (agnostic) — baseline", "group": "Components", "size": 7283, - "gzipSize": 2106, + "gzipSize": 2105, "imports": [ "ClipPath" ] @@ -622,8 +742,8 @@ "scenario": "RadialGradient", "description": "Standalone RadialGradient (agnostic) — baseline", "group": "Components", - "size": 55791, - "gzipSize": 14421, + "size": 55787, + "gzipSize": 14410, "imports": [ "RadialGradient" ] @@ -652,8 +772,8 @@ "scenario": "LinearGradient", "description": "Standalone LinearGradient (agnostic) — baseline", "group": "Components", - "size": 57184, - "gzipSize": 14725, + "size": 57180, + "gzipSize": 14718, "imports": [ "LinearGradient" ] @@ -693,7 +813,7 @@ "description": "Standalone Group (agnostic) — baseline", "group": "Components", "size": 15713, - "gzipSize": 4088, + "gzipSize": 4078, "imports": [ "Group" ] @@ -733,7 +853,7 @@ "description": "Standalone Pattern (agnostic) — baseline", "group": "Components", "size": 58891, - "gzipSize": 15162, + "gzipSize": 15157, "imports": [ "Pattern" ] @@ -772,8 +892,8 @@ "scenario": "Ellipse", "description": "Standalone Ellipse (agnostic) — baseline", "group": "Components", - "size": 69298, - "gzipSize": 17404, + "size": 69294, + "gzipSize": 17385, "imports": [ "Ellipse" ] @@ -812,8 +932,8 @@ "scenario": "Polygon", "description": "Standalone Polygon (agnostic) — baseline", "group": "Components", - "size": 77516, - "gzipSize": 20291, + "size": 77512, + "gzipSize": 20293, "imports": [ "Polygon" ] @@ -842,8 +962,8 @@ "scenario": "Image", "description": "Standalone Image (agnostic) — baseline", "group": "Components", - "size": 62636, - "gzipSize": 15314, + "size": 62632, + "gzipSize": 15256, "imports": [ "Image" ] @@ -882,8 +1002,8 @@ "scenario": "Axis", "description": "Standalone Axis (agnostic) — baseline", "group": "Components", - "size": 198533, - "gzipSize": 44626, + "size": 198525, + "gzipSize": 44216, "imports": [ "Axis" ] @@ -922,8 +1042,8 @@ "scenario": "Rule", "description": "Standalone Rule (agnostic) — baseline", "group": "Components", - "size": 103889, - "gzipSize": 23685, + "size": 103881, + "gzipSize": 23887, "imports": [ "Rule" ] @@ -962,8 +1082,8 @@ "scenario": "Grid", "description": "Standalone Grid (agnostic) — baseline", "group": "Components", - "size": 53319, - "gzipSize": 9509, + "size": 53316, + "gzipSize": 9508, "imports": [ "Grid" ] @@ -1002,8 +1122,8 @@ "scenario": "Highlight", "description": "Standalone Highlight (agnostic) — baseline", "group": "Components", - "size": 48347, - "gzipSize": 8827, + "size": 48346, + "gzipSize": 8828, "imports": [ "Highlight" ] @@ -1013,7 +1133,7 @@ "description": "Standalone Highlight from `layerchart/svg`", "group": "Components", "size": 35567, - "gzipSize": 6785, + "gzipSize": 6784, "imports": [ "Highlight" ] @@ -1023,7 +1143,7 @@ "description": "Standalone Highlight from `layerchart/canvas`", "group": "Components", "size": 32884, - "gzipSize": 6217, + "gzipSize": 6215, "imports": [ "Highlight" ] @@ -1033,7 +1153,7 @@ "description": "Standalone Highlight from `layerchart/html`", "group": "Components", "size": 37229, - "gzipSize": 7003, + "gzipSize": 7002, "imports": [ "Highlight" ] @@ -1043,7 +1163,7 @@ "description": "Standalone RectClipPath (agnostic) — baseline", "group": "Components", "size": 8799, - "gzipSize": 2383, + "gzipSize": 2386, "imports": [ "RectClipPath" ] @@ -1082,8 +1202,8 @@ "scenario": "ChartClipPath", "description": "Standalone ChartClipPath (agnostic) — baseline", "group": "Components", - "size": 52422, - "gzipSize": 12431, + "size": 52418, + "gzipSize": 12417, "imports": [ "ChartClipPath" ] @@ -1122,8 +1242,8 @@ "scenario": "Arc", "description": "Standalone Arc (agnostic) — baseline", "group": "Components", - "size": 135254, - "gzipSize": 36329, + "size": 135246, + "gzipSize": 36233, "imports": [ "Arc" ] @@ -1152,8 +1272,8 @@ "scenario": "Spline", "description": "Standalone Spline (agnostic) — baseline", "group": "Components", - "size": 108816, - "gzipSize": 27778, + "size": 108808, + "gzipSize": 27771, "imports": [ "Spline" ] @@ -1182,8 +1302,8 @@ "scenario": "Area", "description": "Standalone Area (agnostic) — baseline", "group": "Components", - "size": 119999, - "gzipSize": 29640, + "size": 119991, + "gzipSize": 29617, "imports": [ "Area" ] @@ -1212,8 +1332,8 @@ "scenario": "Pie", "description": "Standalone Pie (agnostic) — baseline", "group": "Components", - "size": 140628, - "gzipSize": 37499, + "size": 140620, + "gzipSize": 37308, "imports": [ "Pie" ] @@ -1242,8 +1362,8 @@ "scenario": "ArcLabel", "description": "Standalone ArcLabel (agnostic) — baseline", "group": "Components", - "size": 150476, - "gzipSize": 37198, + "size": 150472, + "gzipSize": 36779, "imports": [ "ArcLabel" ] @@ -1272,8 +1392,8 @@ "scenario": "Bar", "description": "Standalone Bar (agnostic) — baseline", "group": "Components", - "size": 165007, - "gzipSize": 42342, + "size": 164999, + "gzipSize": 41847, "imports": [ "Bar" ] @@ -1302,8 +1422,8 @@ "scenario": "Bars", "description": "Standalone Bars (agnostic) — baseline", "group": "Components", - "size": 168863, - "gzipSize": 42988, + "size": 168855, + "gzipSize": 42482, "imports": [ "Bars" ] @@ -1332,8 +1452,8 @@ "scenario": "Points", "description": "Standalone Points (agnostic) — baseline", "group": "Components", - "size": 75396, - "gzipSize": 18769, + "size": 75388, + "gzipSize": 18756, "imports": [ "Points" ] @@ -1372,8 +1492,8 @@ "scenario": "Labels", "description": "Standalone Labels (agnostic) — baseline", "group": "Components", - "size": 156676, - "gzipSize": 36456, + "size": 156668, + "gzipSize": 36334, "imports": [ "Labels" ] @@ -1412,8 +1532,8 @@ "scenario": "Frame", "description": "Standalone Frame (agnostic) — baseline", "group": "Components", - "size": 77940, - "gzipSize": 19344, + "size": 77932, + "gzipSize": 19362, "imports": [ "Frame" ] @@ -1452,8 +1572,8 @@ "scenario": "Cell", "description": "Standalone Cell (agnostic) — baseline", "group": "Components", - "size": 97795, - "gzipSize": 22000, + "size": 97791, + "gzipSize": 21920, "imports": [ "Cell" ] @@ -1492,8 +1612,8 @@ "scenario": "Threshold", "description": "Standalone Threshold (agnostic) — baseline", "group": "Components", - "size": 126571, - "gzipSize": 30882, + "size": 126563, + "gzipSize": 30802, "imports": [ "Threshold" ] @@ -1522,8 +1642,8 @@ "scenario": "AnnotationLine", "description": "Standalone AnnotationLine (agnostic) — baseline", "group": "Components", - "size": 134549, - "gzipSize": 32610, + "size": 134545, + "gzipSize": 32637, "imports": [ "AnnotationLine" ] @@ -1552,8 +1672,8 @@ "scenario": "AnnotationPoint", "description": "Standalone AnnotationPoint (agnostic) — baseline", "group": "Components", - "size": 187586, - "gzipSize": 45105, + "size": 187578, + "gzipSize": 44677, "imports": [ "AnnotationPoint" ] @@ -1582,8 +1702,8 @@ "scenario": "Trail", "description": "Standalone Trail (agnostic) — baseline", "group": "Components", - "size": 95748, - "gzipSize": 24551, + "size": 95744, + "gzipSize": 24522, "imports": [ "Trail" ] @@ -1612,8 +1732,8 @@ "scenario": "Vector", "description": "Standalone Vector (agnostic) — baseline", "group": "Components", - "size": 95271, - "gzipSize": 24081, + "size": 95267, + "gzipSize": 23983, "imports": [ "Vector" ] @@ -1642,8 +1762,8 @@ "scenario": "Link", "description": "Standalone Link (agnostic) — baseline", "group": "Components", - "size": 109775, - "gzipSize": 27590, + "size": 109767, + "gzipSize": 27587, "imports": [ "Link" ] @@ -1672,8 +1792,8 @@ "scenario": "AnnotationRange", "description": "Standalone AnnotationRange (agnostic) — baseline", "group": "Components", - "size": 145645, - "gzipSize": 34805, + "size": 145641, + "gzipSize": 34828, "imports": [ "AnnotationRange" ] @@ -1702,8 +1822,8 @@ "scenario": "Hull", "description": "Standalone Hull (agnostic) — baseline", "group": "Components", - "size": 181180, - "gzipSize": 47930, + "size": 181172, + "gzipSize": 47939, "imports": [ "Hull" ] @@ -1732,8 +1852,8 @@ "scenario": "Density", "description": "Standalone Density (agnostic) — baseline", "group": "Components", - "size": 133598, - "gzipSize": 36407, + "size": 133590, + "gzipSize": 36397, "imports": [ "Density" ] @@ -1762,8 +1882,8 @@ "scenario": "Calendar", "description": "Standalone Calendar (agnostic) — baseline", "group": "Components", - "size": 167349, - "gzipSize": 40293, + "size": 167341, + "gzipSize": 39831, "imports": [ "Calendar" ] @@ -1793,7 +1913,7 @@ "description": "Standalone CircleClipPath (agnostic) — baseline", "group": "Components", "size": 8570, - "gzipSize": 2301, + "gzipSize": 2302, "imports": [ "CircleClipPath" ] @@ -1832,8 +1952,8 @@ "scenario": "Voronoi", "description": "Standalone Voronoi (agnostic) — baseline", "group": "Components", - "size": 177536, - "gzipSize": 46642, + "size": 177532, + "gzipSize": 46615, "imports": [ "Voronoi" ] @@ -1862,8 +1982,8 @@ "scenario": "Contour", "description": "Standalone Contour (agnostic) — baseline", "group": "Components", - "size": 165261, - "gzipSize": 45091, + "size": 165253, + "gzipSize": 45066, "imports": [ "Contour" ] @@ -1892,8 +2012,8 @@ "scenario": "Month", "description": "Standalone Month (agnostic) — baseline", "group": "Components", - "size": 145395, - "gzipSize": 34818, + "size": 145387, + "gzipSize": 34710, "imports": [ "Month" ] @@ -1922,8 +2042,8 @@ "scenario": "Raster", "description": "Standalone Raster (agnostic) — baseline", "group": "Components", - "size": 124893, - "gzipSize": 33929, + "size": 124885, + "gzipSize": 33810, "imports": [ "Raster" ] @@ -1962,8 +2082,8 @@ "scenario": "Violin", "description": "Standalone Violin (agnostic) — baseline", "group": "Components", - "size": 134196, - "gzipSize": 31522, + "size": 134192, + "gzipSize": 31175, "imports": [ "Violin" ] @@ -1992,8 +2112,8 @@ "scenario": "BoxPlot", "description": "Standalone BoxPlot (agnostic) — baseline", "group": "Components", - "size": 122645, - "gzipSize": 25706, + "size": 122641, + "gzipSize": 25593, "imports": [ "BoxPlot" ] @@ -2292,8 +2412,8 @@ "scenario": "all", "description": "Everything from layerchart (worst case)", "group": "Worst case", - "size": 1007001, - "gzipSize": 235893, + "size": 1014066, + "gzipSize": 237011, "imports": [ "*" ] diff --git a/bundle-analyzer/bundle-scenarios.ts b/bundle-analyzer/bundle-scenarios.ts index 42a60f359..a1b7a9afe 100644 --- a/bundle-analyzer/bundle-scenarios.ts +++ b/bundle-analyzer/bundle-scenarios.ts @@ -132,6 +132,20 @@ export const scenarios: Scenario[] = [ description: 'High-level `LineChart` component', imports: ['LineChart'], }, + { + name: 'LineChart-svg', + group: 'Cartesian charts', + description: 'High-level `LineChart` from `layerchart/svg`', + imports: ['LineChart'], + layers: { LineChart: 'svg' }, + }, + { + name: 'LineChart-canvas', + group: 'Cartesian charts', + description: 'High-level `LineChart` from `layerchart/canvas`', + imports: ['LineChart'], + layers: { LineChart: 'canvas' }, + }, { name: 'area-chart', group: 'Cartesian charts', @@ -144,6 +158,20 @@ export const scenarios: Scenario[] = [ description: 'High-level `AreaChart` component', imports: ['AreaChart'], }, + { + name: 'AreaChart-svg', + group: 'Cartesian charts', + description: 'High-level `AreaChart` from `layerchart/svg`', + imports: ['AreaChart'], + layers: { AreaChart: 'svg' }, + }, + { + name: 'AreaChart-canvas', + group: 'Cartesian charts', + description: 'High-level `AreaChart` from `layerchart/canvas`', + imports: ['AreaChart'], + layers: { AreaChart: 'canvas' }, + }, { name: 'bar-chart', group: 'Cartesian charts', @@ -156,6 +184,20 @@ export const scenarios: Scenario[] = [ description: 'High-level `BarChart` component', imports: ['BarChart'], }, + { + name: 'BarChart-svg', + group: 'Cartesian charts', + description: 'High-level `BarChart` from `layerchart/svg`', + imports: ['BarChart'], + layers: { BarChart: 'svg' }, + }, + { + name: 'BarChart-canvas', + group: 'Cartesian charts', + description: 'High-level `BarChart` from `layerchart/canvas`', + imports: ['BarChart'], + layers: { BarChart: 'canvas' }, + }, { name: 'scatter-chart', group: 'Cartesian charts', @@ -168,6 +210,20 @@ export const scenarios: Scenario[] = [ description: 'High-level `ScatterChart` component', imports: ['ScatterChart'], }, + { + name: 'ScatterChart-svg', + group: 'Cartesian charts', + description: 'High-level `ScatterChart` from `layerchart/svg`', + imports: ['ScatterChart'], + layers: { ScatterChart: 'svg' }, + }, + { + name: 'ScatterChart-canvas', + group: 'Cartesian charts', + description: 'High-level `ScatterChart` from `layerchart/canvas`', + imports: ['ScatterChart'], + layers: { ScatterChart: 'canvas' }, + }, { name: 'pie-chart', group: 'Cartesian charts', @@ -180,12 +236,40 @@ export const scenarios: Scenario[] = [ description: 'High-level `PieChart` component', imports: ['PieChart'], }, + { + name: 'PieChart-svg', + group: 'Cartesian charts', + description: 'High-level `PieChart` from `layerchart/svg`', + imports: ['PieChart'], + layers: { PieChart: 'svg' }, + }, + { + name: 'PieChart-canvas', + group: 'Cartesian charts', + description: 'High-level `PieChart` from `layerchart/canvas`', + imports: ['PieChart'], + layers: { PieChart: 'canvas' }, + }, { name: 'ArcChart', group: 'Cartesian charts', description: 'High-level `ArcChart` component', imports: ['ArcChart'], }, + { + name: 'ArcChart-svg', + group: 'Cartesian charts', + description: 'High-level `ArcChart` from `layerchart/svg`', + imports: ['ArcChart'], + layers: { ArcChart: 'svg' }, + }, + { + name: 'ArcChart-canvas', + group: 'Cartesian charts', + description: 'High-level `ArcChart` from `layerchart/canvas`', + imports: ['ArcChart'], + layers: { ArcChart: 'canvas' }, + }, // --- Geo --- { diff --git a/packages/layerchart/src/lib/bench/composable-vs-linechart.svelte.bench.ts b/packages/layerchart/src/lib/bench/composable-vs-linechart.svelte.bench.ts index 3f21f88aa..626c1f6b6 100644 --- a/packages/layerchart/src/lib/bench/composable-vs-linechart.svelte.bench.ts +++ b/packages/layerchart/src/lib/bench/composable-vs-linechart.svelte.bench.ts @@ -1,7 +1,7 @@ import { describe, bench, afterEach } from 'vitest'; import { render, cleanup } from 'vitest-browser-svelte'; -import LineChart from '$lib/components/charts/LineChart.svelte'; +import LineChart from '$lib/components/charts/LineChart/LineChart.svelte'; import ComposableLineChart from './ComposableLineChart.svelte'; import { generateTimeSeriesData, generateMultiSeriesData } from './generateData.js'; diff --git a/packages/layerchart/src/lib/canvas.ts b/packages/layerchart/src/lib/canvas.ts index c59da8d82..b4a23a745 100644 --- a/packages/layerchart/src/lib/canvas.ts +++ b/packages/layerchart/src/lib/canvas.ts @@ -310,12 +310,12 @@ export * as Tooltip from './components/tooltip/index.js'; export * from './components/tooltip/TooltipContext.svelte'; // High-level chart wrappers -export { default as LineChart } from './components/charts/LineChart.svelte'; -export { default as AreaChart } from './components/charts/AreaChart.svelte'; -export { default as BarChart } from './components/charts/BarChart.svelte'; -export { default as PieChart } from './components/charts/PieChart.svelte'; -export { default as ScatterChart } from './components/charts/ScatterChart.svelte'; -export { default as ArcChart } from './components/charts/ArcChart.svelte'; +export { default as LineChart } from './components/charts/LineChart/LineChart.canvas.svelte'; +export { default as AreaChart } from './components/charts/AreaChart/AreaChart.canvas.svelte'; +export { default as BarChart } from './components/charts/BarChart/BarChart.canvas.svelte'; +export { default as PieChart } from './components/charts/PieChart/PieChart.canvas.svelte'; +export { default as ScatterChart } from './components/charts/ScatterChart/ScatterChart.canvas.svelte'; +export { default as ArcChart } from './components/charts/ArcChart/ArcChart.canvas.svelte'; // Layout components export { default as Tree } from './components/hierarchy/Tree.svelte'; diff --git a/packages/layerchart/src/lib/components/charts/ArcChart.svelte b/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.base.svelte similarity index 68% rename from packages/layerchart/src/lib/components/charts/ArcChart.svelte rename to packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.base.svelte index 317749919..c584c7cde 100644 --- a/packages/layerchart/src/lib/components/charts/ArcChart.svelte +++ b/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.base.svelte @@ -1,136 +1,15 @@ + + + + diff --git a/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.shared.svelte.ts b/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.shared.svelte.ts new file mode 100644 index 000000000..388a78ec5 --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.shared.svelte.ts @@ -0,0 +1,131 @@ +import type { Component, Snippet } from 'svelte'; + +import type { ChartProps } from '../../Chart/Chart.shared.svelte.js'; +import type { ChartState } from '$lib/contexts/chart.js'; +import type { ArcProps, ArcPropsWithoutHTML } from '../../Arc/Arc.shared.svelte.js'; +import type { ArcLabelConfig } from '../../ArcLabel/ArcLabel.shared.svelte.js'; +import type { GroupProps } from '../../Group/Group.shared.svelte.js'; +import type { Accessor } from '$lib/utils/common.js'; +import type { SeriesData } from '../types.js'; + +export type ArcChartExtraSnippetProps = { + key: Accessor; + label: Accessor; + value: Accessor; + visibleData: TData[]; + getGroupProps: () => GroupProps; + getArcProps: (s: SeriesData>, i: number) => ArcProps; +}; + +// Use explicit data prop for TData inference, with rest from ChartPropsWithoutHTML +export type ArcChartProps = { + /** + * The data for the chart + */ + data?: TData[] | readonly TData[]; +} & Omit< + ChartProps, + // Props that don't apply to ArcChart + 'data' | 'axis' | 'brush' | 'grid' | 'highlight' | 'labels' | 'points' | 'rule' +> & { + /** + * Render text labels on each arc. + * + * Pass `true` to enable with default placement (`centroid`), or an object + * to customize via `ArcLabel` props (placement, format, value accessor, etc). + */ + labels?: boolean | (ArcLabelConfig & { value?: Accessor }); + } & Pick< + ArcPropsWithoutHTML, + | 'cornerRadius' + | 'trackCornerRadius' + | 'padAngle' + | 'trackPadAngle' + | 'trackStartAngle' + | 'trackEndAngle' + | 'trackInnerRadius' + | 'trackOuterRadius' + | 'innerRadius' + | 'outerRadius' + | 'range' + > & { + /** + * The series data to be used for the chart. + */ + series?: SeriesData>[]; + + /** + * Key accessor + * + * @default 'key' + */ + key?: Accessor; + + /** + * Label accessor + * + * @default 'label' + */ + label?: Accessor; + + /** + * Value accessor + * + * @default 'value' + */ + value?: Accessor; + + /** + * Color accessor + * + * @default key + */ + c?: Accessor; + + /** + * Maximum possible value, useful when `data` is single item + */ + maxValue?: number; + + /** + * Placement of the ArcChart + * + * @default 'center' + */ + placement?: 'left' | 'center' | 'right'; + + /** + * Center the chart. + * + * Override and use `props.group` for more control. + * + * @default placement === 'center' + */ + center?: boolean; + + /** + * A callback function triggered when the arc is clicked. + */ + onArcClick?: ( + e: MouseEvent, + detail: { data: any; series: SeriesData> } + ) => void; + + arc?: Snippet< + [ + { context: ChartState } & ArcChartExtraSnippetProps & { + props: ArcProps; + /** + * The index of the series currently being iterated over. + */ + seriesIndex: number; + }, + ] + >; + + /** + * Enable profiling to measure render time. + * @default false + */ + profile?: boolean; + }; diff --git a/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.svelte b/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.svelte new file mode 100644 index 000000000..d067c6e05 --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.svg.svelte b/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.svg.svelte new file mode 100644 index 000000000..646c53779 --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.svg.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/AreaChart.svelte.test.ts b/packages/layerchart/src/lib/components/charts/AreaChart.svelte.test.ts index 89ade7263..aec2f7e3a 100644 --- a/packages/layerchart/src/lib/components/charts/AreaChart.svelte.test.ts +++ b/packages/layerchart/src/lib/components/charts/AreaChart.svelte.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { render } from 'vitest-browser-svelte'; -import AreaChart from './AreaChart.svelte'; +import AreaChart from './AreaChart/AreaChart.svelte'; const seriesData = [ { date: new Date('2024-01-01'), one: 10, two: 20, three: 15 }, diff --git a/packages/layerchart/src/lib/components/charts/AreaChart.svelte b/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.base.svelte similarity index 60% rename from packages/layerchart/src/lib/components/charts/AreaChart.svelte rename to packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.base.svelte index 6f4ef1819..ab975bcdf 100644 --- a/packages/layerchart/src/lib/components/charts/AreaChart.svelte +++ b/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.base.svelte @@ -1,53 +1,23 @@ + + + + diff --git a/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.shared.svelte.ts b/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.shared.svelte.ts new file mode 100644 index 000000000..fe3112b04 --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.shared.svelte.ts @@ -0,0 +1,40 @@ +import type { Component } from 'svelte'; + +import type { ChartProps } from '../../Chart/Chart.shared.svelte.js'; +import type { HighlightPoint } from '../../Highlight/Highlight.shared.svelte.js'; +import type { AreaProps } from '../../Area/Area.shared.svelte.js'; +import type { SeriesData } from '../types.js'; + +// Use explicit data prop for TData inference, with rest from ChartPropsWithoutHTML +export type AreaChartProps = { + /** + * The data for the chart + */ + data?: TData[] | readonly TData[]; +} & Omit, 'data'> & { + /** + * The series data to be used for the chart. + * @default [{ key: 'default', value: y, color: 'var(--color-primary)' }] + */ + series?: SeriesData>[]; + + /** + * The layout of the series. + * @default 'overlap' + */ + seriesLayout?: 'overlap' | 'stack' | 'stackExpand' | 'stackDiverging'; + + /** + * A callback function called when a point in the chart is clicked. + * + * @param e - the original event that triggered the `onPointClick` + * @param details - an object containing the highlighted point and data + */ + onPointClick?: (e: MouseEvent, details: { point: HighlightPoint; data: any }) => void; + + /** + * Enable profiling to measure render time. + * @default false + */ + profile?: boolean; + }; diff --git a/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.svelte b/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.svelte new file mode 100644 index 000000000..59bbc0b73 --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.svg.svelte b/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.svg.svelte new file mode 100644 index 000000000..41446fa74 --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/BarChart.svelte.test.ts b/packages/layerchart/src/lib/components/charts/BarChart.svelte.test.ts index 5ddf4da17..6a2f343ec 100644 --- a/packages/layerchart/src/lib/components/charts/BarChart.svelte.test.ts +++ b/packages/layerchart/src/lib/components/charts/BarChart.svelte.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import { render } from 'vitest-browser-svelte'; -import BarChart from './BarChart.svelte'; +import BarChart from './BarChart/BarChart.svelte'; import BarChartFixedWidthTest from './BarChartFixedWidthTest.svelte'; const wideData = [ diff --git a/packages/layerchart/src/lib/components/charts/BarChart.svelte b/packages/layerchart/src/lib/components/charts/BarChart/BarChart.base.svelte similarity index 55% rename from packages/layerchart/src/lib/components/charts/BarChart.svelte rename to packages/layerchart/src/lib/components/charts/BarChart/BarChart.base.svelte index a49ce4fc8..490e5e5a9 100644 --- a/packages/layerchart/src/lib/components/charts/BarChart.svelte +++ b/packages/layerchart/src/lib/components/charts/BarChart/BarChart.base.svelte @@ -1,81 +1,21 @@ + + + + diff --git a/packages/layerchart/src/lib/components/charts/BarChart/BarChart.shared.svelte.ts b/packages/layerchart/src/lib/components/charts/BarChart/BarChart.shared.svelte.ts new file mode 100644 index 000000000..1579ae5a4 --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/BarChart/BarChart.shared.svelte.ts @@ -0,0 +1,70 @@ +import type { Component } from 'svelte'; + +import type { ChartProps } from '../../Chart/Chart.shared.svelte.js'; +import type { BarsProps } from '../../Bars/Bars.shared.svelte.js'; +import type { SeriesData } from '../types.js'; + +// Use explicit data prop for TData inference, with rest from ChartPropsWithoutHTML +export type BarChartProps = { + /** + * The data for the chart + */ + data?: TData[] | readonly TData[]; +} & Omit, 'data'> & { + /** + * The orientation of the chart. Sets which axis is the value axis. + * + * @default 'vertical' + */ + orientation?: 'horizontal' | 'vertical'; + + /** + * The series data to be used for the chart. + * @default [{ key: 'default', value: y, color: 'var(--color-primary)' }] + */ + series?: SeriesData>[]; + + /** + * The layout of the series. + * + * @default 'overlap' + */ + seriesLayout?: 'overlap' | 'stack' | 'stackExpand' | 'stackDiverging' | 'group'; + + /** + * Padding between primary x or y bands/bars, applied to scaleBand().padding() + * + * @default 0.4 + */ + bandPadding?: number; + + /** + * Padding between group/series items when using 'seriesLayout="group"', applied to scaleBand().padding() + * + * @default 0 + */ + groupPadding?: number; + + /** + * Padding between series items within bars when using 'seriesLayout="stack"' + * + * @default 0 + */ + stackPadding?: number; + + /** + * A callback function that is called when a bar is clicked. + * @param e - The original event that triggered the callback + * @param detail - An object containing the bar's data and series information + */ + onBarClick?: ( + event: MouseEvent, + detail: { data: any; series: SeriesData> } + ) => void; + + /** + * Enable profiling to measure render time. + * @default false + */ + profile?: boolean; + }; diff --git a/packages/layerchart/src/lib/components/charts/BarChart/BarChart.svelte b/packages/layerchart/src/lib/components/charts/BarChart/BarChart.svelte new file mode 100644 index 000000000..fb5305123 --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/BarChart/BarChart.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/BarChart/BarChart.svg.svelte b/packages/layerchart/src/lib/components/charts/BarChart/BarChart.svg.svelte new file mode 100644 index 000000000..b126505ca --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/BarChart/BarChart.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/DefaultTooltip.svelte.test.ts b/packages/layerchart/src/lib/components/charts/DefaultTooltip.svelte.test.ts index e748ffa54..6fe288bcb 100644 --- a/packages/layerchart/src/lib/components/charts/DefaultTooltip.svelte.test.ts +++ b/packages/layerchart/src/lib/components/charts/DefaultTooltip.svelte.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it, vi } from 'vitest'; import { render } from 'vitest-browser-svelte'; -import AreaChart from './AreaChart.svelte'; -import LineChart from './LineChart.svelte'; -import ScatterChart from './ScatterChart.svelte'; +import AreaChart from './AreaChart/AreaChart.svelte'; +import LineChart from './LineChart/LineChart.svelte'; +import ScatterChart from './ScatterChart/ScatterChart.svelte'; // Shared test data const timeSeriesData = [ diff --git a/packages/layerchart/src/lib/components/charts/LineChart.svelte.bench.ts b/packages/layerchart/src/lib/components/charts/LineChart.svelte.bench.ts index 549bfd8cc..9e16c2ded 100644 --- a/packages/layerchart/src/lib/components/charts/LineChart.svelte.bench.ts +++ b/packages/layerchart/src/lib/components/charts/LineChart.svelte.bench.ts @@ -1,7 +1,7 @@ import { describe, bench, afterEach } from 'vitest'; import { render, cleanup } from 'vitest-browser-svelte'; -import LineChart from './LineChart.svelte'; +import LineChart from './LineChart/LineChart.svelte'; import { generateTimeSeriesData, generateMultiSeriesData, diff --git a/packages/layerchart/src/lib/components/charts/LineChart.svelte.test.ts b/packages/layerchart/src/lib/components/charts/LineChart.svelte.test.ts index 75c8fc4bb..2234852c4 100644 --- a/packages/layerchart/src/lib/components/charts/LineChart.svelte.test.ts +++ b/packages/layerchart/src/lib/components/charts/LineChart.svelte.test.ts @@ -3,7 +3,7 @@ import { render } from 'vitest-browser-svelte'; import { scaleSequential } from 'd3-scale'; import { interpolateTurbo } from 'd3-scale-chromatic'; -import LineChart from './LineChart.svelte'; +import LineChart from './LineChart/LineChart.svelte'; const data = [ { date: 0, value: 10 }, diff --git a/packages/layerchart/src/lib/components/charts/LineChart.svelte b/packages/layerchart/src/lib/components/charts/LineChart/LineChart.base.svelte similarity index 64% rename from packages/layerchart/src/lib/components/charts/LineChart.svelte rename to packages/layerchart/src/lib/components/charts/LineChart/LineChart.base.svelte index e485baf1e..677683462 100644 --- a/packages/layerchart/src/lib/components/charts/LineChart.svelte +++ b/packages/layerchart/src/lib/components/charts/LineChart/LineChart.base.svelte @@ -1,55 +1,24 @@ + + + + diff --git a/packages/layerchart/src/lib/components/charts/LineChart/LineChart.shared.svelte.ts b/packages/layerchart/src/lib/components/charts/LineChart/LineChart.shared.svelte.ts new file mode 100644 index 000000000..50b718b32 --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/LineChart/LineChart.shared.svelte.ts @@ -0,0 +1,41 @@ +import type { Component } from 'svelte'; + +import type { ChartProps } from '../../Chart/Chart.shared.svelte.js'; +import type { HighlightPointData } from '../../Highlight/Highlight.shared.svelte.js'; +import type { SplineProps } from '../../Spline/Spline.shared.svelte.js'; +import type { SeriesData } from '../types.js'; + +// Use explicit data prop for TData inference, with rest from ChartPropsWithoutHTML +export type LineChartProps = { + /** + * The data for the chart + */ + data?: TData[] | readonly TData[]; +} & Omit, 'data'> & { + /** + * The orientation of the chart. Sets which axis is the value axis. + * + * @default 'horizontal' + */ + orientation?: 'horizontal' | 'vertical'; + + /** + * The series data to be used for the chart. + * @default [{ key: 'default', value: y, color: 'var(--color-primary)' }] + */ + series?: SeriesData>[]; + + /** + * The event to be dispatched when the point is clicked. + */ + onPointClick?: ( + e: MouseEvent, + details: { data: HighlightPointData; series: SeriesData> } + ) => void; + + /** + * Enable profiling to measure render time. + * @default false + */ + profile?: boolean; + }; diff --git a/packages/layerchart/src/lib/components/charts/LineChart/LineChart.svelte b/packages/layerchart/src/lib/components/charts/LineChart/LineChart.svelte new file mode 100644 index 000000000..76caf532f --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/LineChart/LineChart.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/LineChart/LineChart.svg.svelte b/packages/layerchart/src/lib/components/charts/LineChart/LineChart.svg.svelte new file mode 100644 index 000000000..fbbfe13d0 --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/LineChart/LineChart.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/PieChart.svelte.test.ts b/packages/layerchart/src/lib/components/charts/PieChart.svelte.test.ts index f99558e9e..75d2f72d4 100644 --- a/packages/layerchart/src/lib/components/charts/PieChart.svelte.test.ts +++ b/packages/layerchart/src/lib/components/charts/PieChart.svelte.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import { render } from 'vitest-browser-svelte'; -import PieChart from './PieChart.svelte'; +import PieChart from './PieChart/PieChart.svelte'; const data = [ { browser: 'chrome', visitors: 275, color: 'red' }, diff --git a/packages/layerchart/src/lib/components/charts/PieChart.svelte b/packages/layerchart/src/lib/components/charts/PieChart/PieChart.base.svelte similarity index 63% rename from packages/layerchart/src/lib/components/charts/PieChart.svelte rename to packages/layerchart/src/lib/components/charts/PieChart/PieChart.base.svelte index a83491e74..ca6d18881 100644 --- a/packages/layerchart/src/lib/components/charts/PieChart.svelte +++ b/packages/layerchart/src/lib/components/charts/PieChart/PieChart.base.svelte @@ -1,187 +1,16 @@ + + + + diff --git a/packages/layerchart/src/lib/components/charts/PieChart/PieChart.shared.svelte.ts b/packages/layerchart/src/lib/components/charts/PieChart/PieChart.shared.svelte.ts new file mode 100644 index 000000000..69cad873e --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/PieChart/PieChart.shared.svelte.ts @@ -0,0 +1,183 @@ +import type { Component, Snippet } from 'svelte'; + +import type { ChartProps } from '../../Chart/Chart.shared.svelte.js'; +import type { ChartState } from '$lib/contexts/chart.js'; +import type { Accessor } from '$lib/utils/common.js'; +import type { ArcProps } from '../../Arc/Arc.shared.svelte.js'; +import type { ArcLabelConfig } from '../../ArcLabel/ArcLabel.shared.svelte.js'; +import type { GroupProps } from '../../Group/Group.shared.svelte.js'; +import type { PieProps } from '../../Pie/Pie.shared.svelte.js'; +import type { SeriesData } from '../types.js'; + +export type PieChartExtraSnippetProps = { + key: Accessor; + label: Accessor; + value: Accessor; + visibleData: TData[]; + getGroupProps: () => GroupProps; +}; + +// Use explicit data prop for TData inference, with rest from ChartPropsWithoutHTML +export type PieChartProps = { + /** + * The data for the chart + */ + data?: TData[] | readonly TData[]; +} & Omit< + ChartProps, + // Props that don't apply to PieChart + 'data' | 'axis' | 'brush' | 'grid' | 'highlight' | 'labels' | 'points' | 'rule' +> & { + /** + * Render text labels on each arc. + * + * Pass `true` to enable with default placement (`centroid`), or an object + * to customize via `ArcLabel` props (placement, format, value accessor, etc). + */ + labels?: boolean | (ArcLabelConfig & { value?: Accessor }); + /** + * The series data to be used for the chart. + */ + series?: SeriesData>[]; + + /** + * Key accessor + * + * @default 'key' + */ + key?: Accessor; + + /** + * Label accessor + * + * @default 'label' + */ + label?: Accessor; + + /** + * Value accessor + * + * @default 'value' + */ + value?: Accessor; + + /** + * Color accessor + * + * @default key + */ + c?: Accessor; + + /** + * Maximum possible value, useful when `data` is single item + */ + maxValue?: number; + + /** + * Range [min, max] in degrees. + * + * See also `startAngle`/`endAngle` + * + * @default [0, 360] + */ + range?: [number, number]; + + /** + * Inner radius of the arc. + * value >= 1: discrete value + * value > 0: percent of `outerRadius` + * value < 0: offset of `outerRadius` + */ + innerRadius?: number; + + /** + * Outer radius of the arc. + */ + outerRadius?: number; + + /** + * Corner radius of the arc + * + * @default 0 + */ + cornerRadius?: number; + + /** + * Angle between the arcs + * + * @default 0 + */ + padAngle?: number; + + /** + * Placement of the PieChart + * + * @default 'center' + */ + placement?: 'left' | 'center' | 'right'; + + /** + * Center the chart. + * + * Override and use `props.group` for more control. + * + * @default placement === 'center' + */ + center?: boolean; + + /** + * Replace the default rendering of the `` component internally with your own. + * + * Use the `props` snippet prop to access the default props. + */ + pie?: Snippet< + [ + { context: ChartState } & PieChartExtraSnippetProps & { + /** + * Default props to apply to the Pie component. + */ + props: PieProps; + /** + * The index of the pie series currently being iterated over. + */ + index: number; + }, + ] + >; + + /** + * Replace the default rendering of the `` component internally with your own. + * + * Use the `props` snippet prop to access the default props. + */ + arc?: Snippet< + [ + { context: ChartState } & PieChartExtraSnippetProps & { + props: ArcProps; + /** + * The index of the arc currently being iterated over + */ + index: number; + + /** + * The index of the series currently being iterated over. + */ + seriesIndex: number; + }, + ] + >; + + /** + * A callback function triggered when the arc is clicked. + */ + onArcClick?: ( + e: MouseEvent, + detail: { data: any; series: SeriesData> } + ) => void; + + /** + * Enable profiling to measure render time. + * @default false + */ + profile?: boolean; + }; diff --git a/packages/layerchart/src/lib/components/charts/PieChart/PieChart.svelte b/packages/layerchart/src/lib/components/charts/PieChart/PieChart.svelte new file mode 100644 index 000000000..c034a5a7f --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/PieChart/PieChart.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/PieChart/PieChart.svg.svelte b/packages/layerchart/src/lib/components/charts/PieChart/PieChart.svg.svelte new file mode 100644 index 000000000..de9d89992 --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/PieChart/PieChart.svg.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte b/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.base.svelte similarity index 66% rename from packages/layerchart/src/lib/components/charts/ScatterChart.svelte rename to packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.base.svelte index d3562b0f1..049c44487 100644 --- a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte +++ b/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.base.svelte @@ -1,37 +1,24 @@ + + + + diff --git a/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.shared.svelte.ts b/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.shared.svelte.ts new file mode 100644 index 000000000..c48d3fbac --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.shared.svelte.ts @@ -0,0 +1,24 @@ +import type { Component } from 'svelte'; + +import type { ChartProps } from '../../Chart/Chart.shared.svelte.js'; +import type { PointsProps } from '../../Points/Points.shared.svelte.js'; +import type { SeriesData } from '../types.js'; + +// Use explicit data prop for TData inference, with rest from ChartPropsWithoutHTML +export type ScatterChartProps = { + /** + * The data for the chart + */ + data?: TData[] | readonly TData[]; +} & Omit, 'data'> & { + /** + * The series data to be used for the chart. + */ + series?: SeriesData>[]; + + /** + * Enable profiling to measure render time. + * @default false + */ + profile?: boolean; + }; diff --git a/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.svelte b/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.svelte new file mode 100644 index 000000000..5a6455fc3 --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.svg.svelte b/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.svg.svelte new file mode 100644 index 000000000..00f1cc6ac --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.svg.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/charts/__fixtures__/ArcChartTooltip.svelte b/packages/layerchart/src/lib/components/charts/__fixtures__/ArcChartTooltip.svelte index 20b6c27fe..dbbfe1d4a 100644 --- a/packages/layerchart/src/lib/components/charts/__fixtures__/ArcChartTooltip.svelte +++ b/packages/layerchart/src/lib/components/charts/__fixtures__/ArcChartTooltip.svelte @@ -1,5 +1,5 @@ + + + {#snippet children({ context })} + + + + + + {/snippet} + +``` + +Measured savings (bundle scenarios): +- `base` (``) → `core` (``): 83.42 → 50.93 KB gz (**−39%**) +- `geo` (`` + `GeoPath`/`GeoPoint`) → `core-geo` (`` + `GeoProjection` + `GeoPath`): 87.23 → 54.67 KB gz (**−37%**) +- `base-svg` (per-layer) → `core-svg` (per-layer): 77.37 → 50.88 KB gz (**−34%**) + ### Behavior Identical to the agnostic versions: visual output, props, types, and bindable refs all match. The dispatcher pattern adds ~0.2 KB per primitive to `core` for users on the agnostic API (transitive cost from `Highlight` / `Axis` / `Chart`) — a worthwhile tradeoff for the opt-in per-layer savings. diff --git a/bundle-analyzer/README.md b/bundle-analyzer/README.md index 3892bd7d2..7ba9dbb14 100644 --- a/bundle-analyzer/README.md +++ b/bundle-analyzer/README.md @@ -32,14 +32,18 @@ Scenarios are defined in [`bundle-scenarios.ts`](./bundle-scenarios.ts) and repr | Scenario | Description | |----------|-------------| -| `core` | Bare minimum: `Chart` + `Svg` | +| `base` | Full `Chart` (with the cartesian frame: Axis, Grid, Rule, Highlight) | +| `base-svg` / `base-canvas` / `base-html` | `Chart` from `layerchart/svg`, etc. | +| `core` | Bare-bones `ChartCore` (no Axis/Grid/Rule/Highlight) | +| `core-svg` / `core-canvas` / `core-html` | `ChartCore` from `layerchart/svg`, etc. | +| `core-geo` / `core-line` / `core-scatter` | `ChartCore` + manual primitives (geo / spline / points) | | `line-chart` | Line chart with axes and grid | | `line-chart-interactive` | Line chart with tooltip and highlight | | `area-chart` | Area chart with axes | | `bar-chart` | Bar chart with axes | | `scatter-chart` | Scatter plot with points | | `pie-chart` | Pie/donut chart with arcs | -| `high-level-charts` | All high-level chart components | +| `LineChart` / `AreaChart` / `BarChart` / etc. | High-level chart wrappers | | `geo` | Geographic map with paths | | `geo-tiles` | Geographic map with tile layer | | `geo-full` | All geo components | @@ -50,7 +54,6 @@ Scenarios are defined in [`bundle-scenarios.ts`](./bundle-scenarios.ts) and repr | `dagre` | Dagre directed graph | | `sankey` | Sankey flow diagram | | `chord` | Chord diagram | -| `canvas` | Canvas-based rendering | | `all` | Everything from layerchart | ## CLI options diff --git a/bundle-analyzer/bundle-reports/latest.json b/bundle-analyzer/bundle-reports/latest.json index d2ce746ce..7d0150693 100644 --- a/bundle-analyzer/bundle-reports/latest.json +++ b/bundle-analyzer/bundle-reports/latest.json @@ -1,88 +1,168 @@ { - "timestamp": "2026-04-29T14:50:30.271Z", + "timestamp": "2026-04-29T15:57:54.893Z", "results": [ { - "scenario": "core", - "description": "Core charting components without rendering layer", - "group": "Core (agnostic)", - "size": 366118, - "gzipSize": 85509, + "scenario": "base", + "description": "`Chart` — full charting frame without rendering layer", + "group": "Base (agnostic)", + "size": 365532, + "gzipSize": 85425, "imports": [ "Chart" ] }, { - "scenario": "core-svg-agnostic", + "scenario": "base-svg-agnostic", "description": "`Chart` + `Svg` from `layerchart` (agnostic dispatcher)", - "group": "Core (agnostic)", - "size": 366122, - "gzipSize": 85514, + "group": "Base (agnostic)", + "size": 365536, + "gzipSize": 85428, "imports": [ "Chart", "Svg" ] }, { - "scenario": "core-canvas-agnostic", + "scenario": "base-canvas-agnostic", "description": "`Chart` + `Canvas` from `layerchart` (agnostic dispatcher)", - "group": "Core (agnostic)", - "size": 366122, - "gzipSize": 85510, + "group": "Base (agnostic)", + "size": 365536, + "gzipSize": 85425, "imports": [ "Chart", "Canvas" ] }, { - "scenario": "core-html-agnostic", + "scenario": "base-html-agnostic", "description": "`Chart` + `Html` from `layerchart` (agnostic dispatcher)", - "group": "Core (agnostic)", - "size": 366122, - "gzipSize": 85507, + "group": "Base (agnostic)", + "size": 365536, + "gzipSize": 85422, "imports": [ "Chart", "Html" ] }, { - "scenario": "core-svg", - "description": "Svg-based rendering", - "group": "Core (layer-specific)", - "size": 338299, - "gzipSize": 79343, + "scenario": "base-svg", + "description": "`Chart` + `Svg` from `layerchart/svg`", + "group": "Base (layer-specific)", + "size": 337713, + "gzipSize": 79225, "imports": [ "Chart", "Svg" ] }, { - "scenario": "core-canvas", - "description": "Canvas-based rendering", - "group": "Core (layer-specific)", - "size": 344848, - "gzipSize": 81030, + "scenario": "base-canvas", + "description": "`Chart` + `Canvas` from `layerchart/canvas`", + "group": "Base (layer-specific)", + "size": 344259, + "gzipSize": 80925, "imports": [ "Chart", "Canvas" ] }, { - "scenario": "core-html", - "description": "HTML-based rendering", - "group": "Core (layer-specific)", - "size": 346151, - "gzipSize": 81345, + "scenario": "base-html", + "description": "`Chart` + `Html` from `layerchart/html`", + "group": "Base (layer-specific)", + "size": 345563, + "gzipSize": 81246, "imports": [ "Chart", "Html" ] }, + { + "scenario": "core", + "description": "`ChartCore` — bare-bones chart without `ChartChildren`", + "group": "Core", + "size": 199234, + "gzipSize": 52157, + "imports": [ + "ChartCore" + ] + }, + { + "scenario": "core-svg", + "description": "`ChartCore` + `Svg` from `layerchart/svg`", + "group": "Core", + "size": 199243, + "gzipSize": 52102, + "imports": [ + "ChartCore", + "Svg" + ] + }, + { + "scenario": "core-canvas", + "description": "`ChartCore` + `Canvas` from `layerchart/canvas`", + "group": "Core", + "size": 217532, + "gzipSize": 57609, + "imports": [ + "ChartCore", + "Canvas" + ] + }, + { + "scenario": "core-html", + "description": "`ChartCore` + `Html` from `layerchart/html`", + "group": "Core", + "size": 201174, + "gzipSize": 52537, + "imports": [ + "ChartCore", + "Html" + ] + }, + { + "scenario": "core-geo", + "description": "`ChartCore`-based geo map (`GeoProjection` + `GeoPath`)", + "group": "Core", + "size": 213765, + "gzipSize": 55979, + "imports": [ + "ChartCore", + "Svg", + "GeoProjection", + "GeoPath" + ] + }, + { + "scenario": "core-line", + "description": "`ChartCore` + manual `Spline` line (no Axis/Grid)", + "group": "Core", + "size": 220340, + "gzipSize": 57523, + "imports": [ + "ChartCore", + "Svg", + "Spline" + ] + }, + { + "scenario": "core-scatter", + "description": "`ChartCore` + manual `Points` scatter (no Axis/Grid)", + "group": "Core", + "size": 217331, + "gzipSize": 56169, + "imports": [ + "ChartCore", + "Svg", + "Points" + ] + }, { "scenario": "line-chart", "description": "Basic line chart with axes and grid", "group": "Cartesian charts", - "size": 366617, - "gzipSize": 85542, + "size": 366031, + "gzipSize": 85452, "imports": [ "Chart", "Svg", @@ -95,8 +175,8 @@ "scenario": "line-chart-svg", "description": "Line chart composed from `layerchart/svg`", "group": "Cartesian charts", - "size": 338323, - "gzipSize": 79345, + "size": 337737, + "gzipSize": 79229, "imports": [ "Chart", "Layer", @@ -109,8 +189,8 @@ "scenario": "line-chart-canvas", "description": "Line chart composed from `layerchart/canvas`", "group": "Cartesian charts", - "size": 344872, - "gzipSize": 81053, + "size": 344283, + "gzipSize": 80944, "imports": [ "Chart", "Layer", @@ -123,8 +203,8 @@ "scenario": "line-chart-html", "description": "Line chart composed from `layerchart/html`", "group": "Cartesian charts", - "size": 346175, - "gzipSize": 81352, + "size": 345587, + "gzipSize": 81259, "imports": [ "Chart", "Layer", @@ -137,8 +217,8 @@ "scenario": "line-chart-interactive", "description": "Line chart with tooltip and highlight", "group": "Cartesian charts", - "size": 381496, - "gzipSize": 88854, + "size": 380910, + "gzipSize": 88751, "imports": [ "Chart", "Svg", @@ -153,8 +233,8 @@ "scenario": "LineChart", "description": "High-level `LineChart` component", "group": "Cartesian charts", - "size": 391087, - "gzipSize": 91801, + "size": 390503, + "gzipSize": 91714, "imports": [ "LineChart" ] @@ -163,8 +243,8 @@ "scenario": "LineChart-svg", "description": "High-level `LineChart` from `layerchart/svg`", "group": "Cartesian charts", - "size": 345496, - "gzipSize": 81065, + "size": 344908, + "gzipSize": 80940, "imports": [ "LineChart" ] @@ -173,8 +253,8 @@ "scenario": "LineChart-canvas", "description": "High-level `LineChart` from `layerchart/canvas`", "group": "Cartesian charts", - "size": 352029, - "gzipSize": 82745, + "size": 351437, + "gzipSize": 82657, "imports": [ "LineChart" ] @@ -183,8 +263,8 @@ "scenario": "area-chart", "description": "Area chart with axes", "group": "Cartesian charts", - "size": 380136, - "gzipSize": 88535, + "size": 379547, + "gzipSize": 88434, "imports": [ "Chart", "Svg", @@ -197,8 +277,8 @@ "scenario": "AreaChart", "description": "High-level `AreaChart` component", "group": "Cartesian charts", - "size": 384537, - "gzipSize": 89529, + "size": 383949, + "gzipSize": 89410, "imports": [ "AreaChart" ] @@ -207,8 +287,8 @@ "scenario": "AreaChart-svg", "description": "High-level `AreaChart` from `layerchart/svg`", "group": "Cartesian charts", - "size": 355930, - "gzipSize": 83247, + "size": 355341, + "gzipSize": 83143, "imports": [ "AreaChart" ] @@ -217,8 +297,8 @@ "scenario": "AreaChart-canvas", "description": "High-level `AreaChart` from `layerchart/canvas`", "group": "Cartesian charts", - "size": 362463, - "gzipSize": 84921, + "size": 361875, + "gzipSize": 84810, "imports": [ "AreaChart" ] @@ -227,8 +307,8 @@ "scenario": "bar-chart", "description": "Bar chart with axes", "group": "Cartesian charts", - "size": 370458, - "gzipSize": 86272, + "size": 369875, + "gzipSize": 86210, "imports": [ "Chart", "Svg", @@ -241,8 +321,8 @@ "scenario": "BarChart", "description": "High-level `BarChart` component", "group": "Cartesian charts", - "size": 376479, - "gzipSize": 87533, + "size": 375897, + "gzipSize": 87442, "imports": [ "BarChart" ] @@ -251,8 +331,8 @@ "scenario": "BarChart-svg", "description": "High-level `BarChart` from `layerchart/svg`", "group": "Cartesian charts", - "size": 347894, - "gzipSize": 81274, + "size": 347307, + "gzipSize": 81133, "imports": [ "BarChart" ] @@ -261,8 +341,8 @@ "scenario": "BarChart-canvas", "description": "High-level `BarChart` from `layerchart/canvas`", "group": "Cartesian charts", - "size": 354423, - "gzipSize": 82915, + "size": 353834, + "gzipSize": 82808, "imports": [ "BarChart" ] @@ -271,8 +351,8 @@ "scenario": "scatter-chart", "description": "Scatter plot with points", "group": "Cartesian charts", - "size": 367399, - "gzipSize": 85928, + "size": 366813, + "gzipSize": 85837, "imports": [ "Chart", "Svg", @@ -286,8 +366,8 @@ "scenario": "ScatterChart", "description": "High-level `ScatterChart` component", "group": "Cartesian charts", - "size": 370943, - "gzipSize": 86616, + "size": 370358, + "gzipSize": 86538, "imports": [ "ScatterChart" ] @@ -296,8 +376,8 @@ "scenario": "ScatterChart-svg", "description": "High-level `ScatterChart` from `layerchart/svg`", "group": "Cartesian charts", - "size": 342066, - "gzipSize": 80119, + "size": 341478, + "gzipSize": 80000, "imports": [ "ScatterChart" ] @@ -306,8 +386,8 @@ "scenario": "ScatterChart-canvas", "description": "High-level `ScatterChart` from `layerchart/canvas`", "group": "Cartesian charts", - "size": 348599, - "gzipSize": 81800, + "size": 348007, + "gzipSize": 81679, "imports": [ "ScatterChart" ] @@ -316,8 +396,8 @@ "scenario": "pie-chart", "description": "Pie/donut chart with arcs", "group": "Cartesian charts", - "size": 378865, - "gzipSize": 88124, + "size": 378280, + "gzipSize": 88073, "imports": [ "Chart", "Svg", @@ -330,8 +410,8 @@ "scenario": "PieChart", "description": "High-level `PieChart` component", "group": "Cartesian charts", - "size": 406450, - "gzipSize": 94161, + "size": 405863, + "gzipSize": 94060, "imports": [ "PieChart" ] @@ -340,8 +420,8 @@ "scenario": "PieChart-svg", "description": "High-level `PieChart` from `layerchart/svg`", "group": "Cartesian charts", - "size": 376415, - "gzipSize": 87392, + "size": 375829, + "gzipSize": 87292, "imports": [ "PieChart" ] @@ -350,8 +430,8 @@ "scenario": "PieChart-canvas", "description": "High-level `PieChart` from `layerchart/canvas`", "group": "Cartesian charts", - "size": 382949, - "gzipSize": 89108, + "size": 382358, + "gzipSize": 88986, "imports": [ "PieChart" ] @@ -360,8 +440,8 @@ "scenario": "ArcChart", "description": "High-level `ArcChart` component", "group": "Cartesian charts", - "size": 399567, - "gzipSize": 93021, + "size": 398981, + "gzipSize": 92901, "imports": [ "ArcChart" ] @@ -370,8 +450,8 @@ "scenario": "ArcChart-svg", "description": "High-level `ArcChart` from `layerchart/svg`", "group": "Cartesian charts", - "size": 370085, - "gzipSize": 86327, + "size": 369500, + "gzipSize": 86221, "imports": [ "ArcChart" ] @@ -380,8 +460,8 @@ "scenario": "ArcChart-canvas", "description": "High-level `ArcChart` from `layerchart/canvas`", "group": "Cartesian charts", - "size": 376619, - "gzipSize": 88080, + "size": 376033, + "gzipSize": 87964, "imports": [ "ArcChart" ] @@ -390,8 +470,8 @@ "scenario": "geo", "description": "Geographic map with paths", "group": "Geo", - "size": 383452, - "gzipSize": 89417, + "size": 382870, + "gzipSize": 89322, "imports": [ "Chart", "Svg", @@ -404,8 +484,8 @@ "scenario": "geo-tiles", "description": "Geographic map with tile layer", "group": "Geo", - "size": 388646, - "gzipSize": 91033, + "size": 388059, + "gzipSize": 90957, "imports": [ "Chart", "Svg", @@ -419,8 +499,8 @@ "scenario": "geo-full", "description": "Full geo setup with all geo components", "group": "Geo", - "size": 442123, - "gzipSize": 105026, + "size": 441539, + "gzipSize": 104917, "imports": [ "Chart", "Svg", @@ -442,8 +522,8 @@ "scenario": "hierarchy-tree", "description": "Tree layout with links", "group": "Hierarchy", - "size": 406266, - "gzipSize": 95216, + "size": 405680, + "gzipSize": 95137, "imports": [ "Chart", "Svg", @@ -457,8 +537,8 @@ "scenario": "hierarchy-treemap", "description": "Treemap layout", "group": "Hierarchy", - "size": 385077, - "gzipSize": 90193, + "size": 384492, + "gzipSize": 90077, "imports": [ "Chart", "Svg", @@ -472,8 +552,8 @@ "scenario": "hierarchy-pack", "description": "Circle packing layout", "group": "Hierarchy", - "size": 384802, - "gzipSize": 90257, + "size": 384215, + "gzipSize": 90176, "imports": [ "Chart", "Svg", @@ -486,8 +566,8 @@ "scenario": "force", "description": "Force-directed graph layout", "group": "Graph / network", - "size": 408724, - "gzipSize": 96072, + "size": 408136, + "gzipSize": 95979, "imports": [ "Chart", "Svg", @@ -501,8 +581,8 @@ "scenario": "dagre", "description": "Dagre directed graph layout", "group": "Graph / network", - "size": 466234, - "gzipSize": 111615, + "size": 465647, + "gzipSize": 111506, "imports": [ "Chart", "Svg", @@ -516,8 +596,8 @@ "scenario": "sankey", "description": "Sankey flow diagram", "group": "Graph / network", - "size": 408454, - "gzipSize": 95460, + "size": 407868, + "gzipSize": 95361, "imports": [ "Chart", "Svg", @@ -531,8 +611,8 @@ "scenario": "chord", "description": "Chord diagram", "group": "Graph / network", - "size": 376055, - "gzipSize": 87651, + "size": 375470, + "gzipSize": 87565, "imports": [ "Chart", "Svg", @@ -2444,8 +2524,8 @@ "scenario": "all", "description": "Everything from layerchart (worst case)", "group": "Worst case", - "size": 1015104, - "gzipSize": 237529, + "size": 1014956, + "gzipSize": 237523, "imports": [ "*" ] diff --git a/bundle-analyzer/bundle-scenarios.ts b/bundle-analyzer/bundle-scenarios.ts index bda70f2c2..1ad18921d 100644 --- a/bundle-analyzer/bundle-scenarios.ts +++ b/bundle-analyzer/bundle-scenarios.ts @@ -35,37 +35,38 @@ export interface ComponentInfo { * Each scenario includes the minimum set of components for that chart type. */ export const scenarios: Scenario[] = [ - // --- Core (agnostic) --- + // --- Base (agnostic) --- + // The full `` (with Axis/Grid/Rule/Highlight/Layer/ChartClipPath baked in). { - name: 'core', - group: 'Core (agnostic)', - description: 'Core charting components without rendering layer', + name: 'base', + group: 'Base (agnostic)', + description: '`Chart` — full charting frame without rendering layer', imports: ['Chart'], }, { - name: 'core-svg-agnostic', - group: 'Core (agnostic)', + name: 'base-svg-agnostic', + group: 'Base (agnostic)', description: '`Chart` + `Svg` from `layerchart` (agnostic dispatcher)', imports: ['Chart', 'Svg'], }, { - name: 'core-canvas-agnostic', - group: 'Core (agnostic)', + name: 'base-canvas-agnostic', + group: 'Base (agnostic)', description: '`Chart` + `Canvas` from `layerchart` (agnostic dispatcher)', imports: ['Chart', 'Canvas'], }, { - name: 'core-html-agnostic', - group: 'Core (agnostic)', + name: 'base-html-agnostic', + group: 'Base (agnostic)', description: '`Chart` + `Html` from `layerchart` (agnostic dispatcher)', imports: ['Chart', 'Html'], }, - // --- Core (layer-specific) --- + // --- Base (layer-specific) --- { - name: 'core-svg', - group: 'Core (layer-specific)', - description: 'Svg-based rendering', + name: 'base-svg', + group: 'Base (layer-specific)', + description: '`Chart` + `Svg` from `layerchart/svg`', imports: ['Chart', 'Svg'], layers: { Chart: 'svg', @@ -73,9 +74,9 @@ export const scenarios: Scenario[] = [ }, }, { - name: 'core-canvas', - group: 'Core (layer-specific)', - description: 'Canvas-based rendering', + name: 'base-canvas', + group: 'Base (layer-specific)', + description: '`Chart` + `Canvas` from `layerchart/canvas`', imports: ['Chart', 'Canvas'], layers: { Chart: 'canvas', @@ -83,9 +84,9 @@ export const scenarios: Scenario[] = [ }, }, { - name: 'core-html', - group: 'Core (layer-specific)', - description: 'HTML-based rendering', + name: 'base-html', + group: 'Base (layer-specific)', + description: '`Chart` + `Html` from `layerchart/html`', imports: ['Chart', 'Html'], layers: { Chart: 'html', @@ -93,6 +94,55 @@ export const scenarios: Scenario[] = [ }, }, + // --- Core --- + // The bare-bones `` (no `` — no Axis/Grid/Rule/Highlight/Layer). + // Use cases: geo maps, custom layouts, or anything that doesn't need the cartesian frame. + { + name: 'core', + group: 'Core', + description: '`ChartCore` — bare-bones chart without `ChartChildren`', + imports: ['ChartCore'], + }, + { + name: 'core-svg', + group: 'Core', + description: '`ChartCore` + `Svg` from `layerchart/svg`', + imports: ['ChartCore', 'Svg'], + layers: { ChartCore: 'svg', Svg: 'svg' }, + }, + { + name: 'core-canvas', + group: 'Core', + description: '`ChartCore` + `Canvas` from `layerchart/canvas`', + imports: ['ChartCore', 'Canvas'], + layers: { ChartCore: 'canvas', Canvas: 'canvas' }, + }, + { + name: 'core-html', + group: 'Core', + description: '`ChartCore` + `Html` from `layerchart/html`', + imports: ['ChartCore', 'Html'], + layers: { ChartCore: 'html', Html: 'html' }, + }, + { + name: 'core-geo', + group: 'Core', + description: '`ChartCore`-based geo map (`GeoProjection` + `GeoPath`)', + imports: ['ChartCore', 'Svg', 'GeoProjection', 'GeoPath'], + }, + { + name: 'core-line', + group: 'Core', + description: '`ChartCore` + manual `Spline` line (no Axis/Grid)', + imports: ['ChartCore', 'Svg', 'Spline'], + }, + { + name: 'core-scatter', + group: 'Core', + description: '`ChartCore` + manual `Points` scatter (no Axis/Grid)', + imports: ['ChartCore', 'Svg', 'Points'], + }, + // --- Cartesian charts --- { name: 'line-chart', @@ -1712,6 +1762,7 @@ const INDIVIDUAL_COMPONENTS: string[] = [ 'Canvas', 'Cell', 'Chart', + 'ChartCore', 'Chord', 'ChartClipPath', 'Circle', diff --git a/bundle-analyzer/generate-pr-comment.js b/bundle-analyzer/generate-pr-comment.js index 16da75122..2de8c4a08 100644 --- a/bundle-analyzer/generate-pr-comment.js +++ b/bundle-analyzer/generate-pr-comment.js @@ -163,7 +163,7 @@ function generateComment(changes, hasBaseline = true) { byGroup.get(g).push(s); } - const expandedGroups = new Set(["Core (agnostic)", "Core (layer-specific)"]); + const expandedGroups = new Set(["Base (agnostic)", "Base (layer-specific)", "Core"]); for (const [groupName, rows] of byGroup) { const open = expandedGroups.has(groupName) ? " open" : ""; @@ -228,9 +228,9 @@ function generateComment(changes, hasBaseline = true) { return `| ${icon} \`${s.scenario}\` | ${current} | ${newSize} | ${change} |\n`; }; - // Core groups expanded by default; other groups collapsed to keep the - // comment scannable. Each group shows the count of changed scenarios. - const expandedGroups = new Set(["Core (agnostic)", "Core (layer-specific)"]); + // Base + Core groups expanded by default; other groups collapsed to keep + // the comment scannable. Each group shows the count of changed scenarios. + const expandedGroups = new Set(["Base (agnostic)", "Base (layer-specific)", "Core"]); for (const [groupName, rows] of byGroup) { const open = expandedGroups.has(groupName) ? " open" : ""; diff --git a/docs/src/content/guides/bundle-size.md b/docs/src/content/guides/bundle-size.md index 3dfe4fb6e..31ea78730 100644 --- a/docs/src/content/guides/bundle-size.md +++ b/docs/src/content/guides/bundle-size.md @@ -21,6 +21,7 @@ The following heavy features are loaded only when you use them, with no code cha | Feature | When it loads | | --- | --- | | `` (and brush state) | When `` is set | +| `` (and transform state) | When `` is set | | `` | When `tooltipContext` is set and you don't provide a custom `tooltip` snippet | | `Voronoi` hit-detection | When `` is used | | `Arc` (radial tooltip rects) | When `` or `mode="band"` is used inside a radial chart | @@ -55,6 +56,38 @@ If you don't use them, you don't pay for them — the agnostic root export simpl Each sub-path also re-exports the layer-agnostic helpers you'd need alongside its specialty components (e.g. `Chart`, `Tooltip`, `Axis`, `Highlight`, scales, layouts). For typical usage you can stay on a single sub-path import line for an entire chart. +## `` for non-cartesian charts (opt-in) + +`` includes the standard cartesian frame: ``, ``, ``, ``, ``, and ``. Those are baked into `Chart`'s import graph so cartesian users get them automatically. But if you're rendering a geo map, a custom layout, or any chart that doesn't need axes/grids, that import chain is dead weight. + +`` is a bare-bones variant that provides the chart context, sizing, brush, transform, and tooltip plumbing — but skips `ChartChildren` and everything it pulls in. You provide your own primitives directly via the `children` snippet: + +```svelte + + + + {#snippet children({ context })} + + + + + + {/snippet} + +``` + +A typical geo chart drops from ~90 KB gz with `` to ~70 KB gz with `` (a ~20 KB gz / ~22% saving) because the entire `Axis` / `Grid` / `Rule` / `Highlight` / `ChartClipPath` / `Layer` chain is no longer in the import graph. + +`` is exported from each layer sub-path (`layerchart`, `layerchart/svg`, `layerchart/canvas`, `layerchart/html`). The component itself is layer-agnostic — the layer is whatever you put inside the `children` snippet — so the sub-path choice only affects what other components (like `` or ``) tree-shake to. + +When to use ``: +- ✅ Geo maps (you'll render `` + `` directly, no axes needed) +- ✅ Custom force-directed or hierarchy layouts that compose their own primitives +- ✅ Pre-rendered SVG/canvas content that just needs chart context for sizing +- 🤷 Cartesian charts — keep using ``; you'd just have to re-add Axis/Grid/etc. yourself + ## Per-layer variants (opt-in) Almost every layer-agnostic component — primitives like `` / `` / ``, compound marks like `` / `` / ``, geo components like ``, and the high-level chart wrappers like `` — auto-detects the surrounding layer (``, ``, or ``) and renders appropriately. To do this they bundle every rendering path. @@ -192,10 +225,13 @@ The numbers below are gzipped totals from LayerChart's own bundle analyzer. They | Scenario | Imports | Gzipped | | --- | --- | --- | -| `core` | `Chart`, `Svg` | ~86 KB | -| `core-svg` (per-layer) | `Chart`, `Svg` from `layerchart/svg` | ~80 KB | -| `core-canvas` (per-layer) | `Chart`, `Canvas` from `layerchart/canvas` | ~82 KB | -| `core-html` (per-layer) | `Chart`, `Html` from `layerchart/html` | ~82 KB | +| `base` | `Chart` | ~83 KB | +| `base-svg` (per-layer) | `Chart`, `Svg` from `layerchart/svg` | ~77 KB | +| `base-canvas` (per-layer) | `Chart`, `Canvas` from `layerchart/canvas` | ~79 KB | +| `base-html` (per-layer) | `Chart`, `Html` from `layerchart/html` | ~79 KB | +| `core` | `ChartCore` (no Axis/Grid/Highlight) | ~51 KB | +| `core-svg` (per-layer) | `ChartCore`, `Svg` from `layerchart/svg` | ~51 KB | +| `core-geo` | `ChartCore`, `Svg`, `GeoProjection`, `GeoPath` | ~58 KB | | `line-chart` | `Chart`, `Svg`, `Line`, `Axis`, `Grid` | ~86 KB | | `LineChart` | high-level `LineChart` | ~92 KB | | `LineChart-svg` (per-layer) | high-level `LineChart` from `layerchart/svg` | ~82 KB | @@ -207,7 +243,7 @@ The numbers below are gzipped totals from LayerChart's own bundle analyzer. They | `text-svg` (per-layer) | `Text` from `layerchart/svg` | ~16 KB | | `text-agnostic` | `Text` from `layerchart` | ~29 KB | -`core` is what every chart pays. The other rows show what specific feature additions cost on top. +`base` is the cost of `` (with the cartesian frame baked in) — what every cartesian chart pays. `core` is the cost of `` — what geo and custom-layout charts pay instead. ## Background: how LayerChart minimizes baseline cost diff --git a/packages/layerchart/src/lib/canvas.ts b/packages/layerchart/src/lib/canvas.ts index b4a23a745..d1272d8a3 100644 --- a/packages/layerchart/src/lib/canvas.ts +++ b/packages/layerchart/src/lib/canvas.ts @@ -12,6 +12,7 @@ export { default as Canvas, default as Layer } from './components/layers/Canvas.svelte'; export type { CanvasProps } from './components/layers/Canvas.svelte'; export { default as Chart } from './components/Chart/Chart.canvas.svelte'; +export { default as ChartCore } from './components/Chart/ChartCore.svelte'; export type { ChartProps, ChartPropsWithoutHTML, diff --git a/packages/layerchart/src/lib/components/Chart/Chart.base.svelte b/packages/layerchart/src/lib/components/Chart/Chart.base.svelte index af0cdcff5..8a02d746d 100644 --- a/packages/layerchart/src/lib/components/Chart/Chart.base.svelte +++ b/packages/layerchart/src/lib/components/Chart/Chart.base.svelte @@ -12,8 +12,12 @@ * The `ChartChildren` component to render inside the chart. Per-layer * variants (`Chart.svg.svelte` etc.) inject the matching per-layer * `ChartChildren` so the underlying primitive imports are layer-specific. + * + * When undefined (used by `ChartCore`), the `children` snippet is rendered + * directly without `ChartChildren` — skipping the `Axis` / `Grid` / `Rule` / + * `Highlight` / `ChartClipPath` / `Layer` import chain. */ - ChartChildren: Component; + ChartChildren?: Component; }; @@ -476,6 +480,14 @@
{/if} +{#snippet body()} + {#if ChartChildren} + + {:else} + {@render children?.({ context: chartState })} + {/if} +{/snippet} + {#snippet inner()} {#if brush} {#await import('../BrushContext.svelte')} @@ -485,7 +497,7 @@ {...getObjectOrNull(tooltipContext)} bind:state={chartState.tooltipState} > - + {@render body()} {:then { default: BrushContext }} @@ -496,7 +508,7 @@ {...getObjectOrNull(tooltipContext)} bind:state={chartState.tooltipState} > - + {@render body()} {/await} @@ -507,7 +519,7 @@ {...getObjectOrNull(tooltipContext)} bind:state={chartState.tooltipState} > - + {@render body()} {/if} {/snippet} diff --git a/packages/layerchart/src/lib/components/Chart/ChartCore.svelte b/packages/layerchart/src/lib/components/Chart/ChartCore.svelte new file mode 100644 index 000000000..6f35ed658 --- /dev/null +++ b/packages/layerchart/src/lib/components/Chart/ChartCore.svelte @@ -0,0 +1,36 @@ + + + + + + diff --git a/packages/layerchart/src/lib/components/ChartCore.svelte.test.ts b/packages/layerchart/src/lib/components/ChartCore.svelte.test.ts new file mode 100644 index 000000000..b171828d1 --- /dev/null +++ b/packages/layerchart/src/lib/components/ChartCore.svelte.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest'; +import { render } from 'vitest-browser-svelte'; +import { tick } from 'svelte'; + +import ChartCoreTestHarness from './tests/ChartCoreTestHarness.svelte'; + +const data = [ + { x: 0, y: 10 }, + { x: 1, y: 20 }, + { x: 2, y: 30 }, +]; + +describe('ChartCore', () => { + it('should render the children snippet content', async () => { + const { container } = render(ChartCoreTestHarness, { + chartProps: { data, x: 'x', y: 'y', height: 200 }, + }); + + await tick(); + + // Children snippet renders a
+ expect(container.querySelector('[data-testid="children-content"]')).not.toBeNull(); + }); + + it('should NOT render ChartChildren-injected marks (Axis, Grid, Rule, Highlight)', async () => { + const { container } = render(ChartCoreTestHarness, { + chartProps: { data, x: 'x', y: 'y', height: 200 }, + }); + + await tick(); + + // Compare to : those classes would be present. ChartCore skips them. + expect(container.querySelector('.lc-axis')).toBeNull(); + expect(container.querySelector('.lc-grid')).toBeNull(); + expect(container.querySelector('.lc-rule')).toBeNull(); + expect(container.querySelector('.lc-highlight-area')).toBeNull(); + }); + + it('should expose the chart context to children', async () => { + let chartContext: any; + + render(ChartCoreTestHarness, { + chartProps: { data, x: 'x', y: 'y', height: 200 }, + oncontext: (ctx: any) => { + chartContext = ctx; + }, + }); + + await tick(); + + expect(chartContext).toBeDefined(); + expect(chartContext.width).toBeGreaterThan(0); + expect(chartContext.height).toBeGreaterThan(0); + }); + + it('should render the root container', async () => { + const { container } = render(ChartCoreTestHarness, { + chartProps: { data, x: 'x', y: 'y', height: 200 }, + }); + + await tick(); + + expect(container.querySelector('.lc-root-container')).not.toBeNull(); + }); +}); diff --git a/packages/layerchart/src/lib/components/TransformContext.svelte.test.ts b/packages/layerchart/src/lib/components/TransformContext.svelte.test.ts index 7f4b5a51e..90c4375e8 100644 --- a/packages/layerchart/src/lib/components/TransformContext.svelte.test.ts +++ b/packages/layerchart/src/lib/components/TransformContext.svelte.test.ts @@ -70,7 +70,10 @@ describe('TransformContext', () => { await vi.waitFor(() => expect(chartContext).toBeDefined()); // Orthographic is a globe — processTranslate should be a function - expect(chartContext.transform.processTranslate).toBeTypeOf('function'); + // (TransformContext is lazy-loaded, so wait for it to resolve) + await vi.waitFor(() => { + expect(chartContext.transform.processTranslate).toBeTypeOf('function'); + }); // Switch to Mercator (flat) chartProps.geo = { @@ -107,8 +110,9 @@ describe('TransformContext', () => { await vi.waitFor(() => expect(chartContext).toBeDefined()); + // TransformContext is lazy-loaded, so wait for fitSize to apply + await vi.waitFor(() => expect(chartContext.transform.scale).toBeGreaterThan(1)); const initialScale = chartContext.transform.scale; - expect(initialScale).toBeGreaterThan(1); // fitSize should give a scale > 1 // Simulate zoom in chartContext.transform.setScale(initialScale * 2, { instant: true }); @@ -145,8 +149,9 @@ describe('TransformContext', () => { await vi.waitFor(() => expect(chartContext).toBeDefined()); + // TransformContext is lazy-loaded, so wait for fitSize to apply + await vi.waitFor(() => expect(chartContext.transform.scale).toBeGreaterThan(10)); const initialScale = chartContext.transform.scale; - expect(initialScale).toBeGreaterThan(10); // Mercator fitSize scale is typically large // Try to zoom way beyond 2x — should be clamped to 2x initial chartContext.transform.setScale(initialScale * 5, { instant: true }); @@ -186,7 +191,10 @@ describe('TransformContext', () => { await vi.waitFor(() => expect(chartContext).toBeDefined()); - expect(chartContext.transform.disablePointer).toBe(false); + // TransformContext is lazy-loaded, so wait for it to resolve + await vi.waitFor(() => { + expect(chartContext.transform.disablePointer).toBe(false); + }); // Enable disablePointer chartProps.transform = { diff --git a/packages/layerchart/src/lib/components/index.ts b/packages/layerchart/src/lib/components/index.ts index 57b363124..fd668257a 100644 --- a/packages/layerchart/src/lib/components/index.ts +++ b/packages/layerchart/src/lib/components/index.ts @@ -35,6 +35,7 @@ export { default as Canvas } from './layers/Canvas.svelte'; export * from './layers/Canvas.svelte'; export { default as Chart } from "./Chart/Chart.svelte"; export * from "./Chart/Chart.svelte"; +export { default as ChartCore } from './Chart/ChartCore.svelte'; export { default as ChartClipPath } from './ChartClipPath/ChartClipPath.svelte'; export * from './ChartClipPath/ChartClipPath.svelte'; export { default as Circle } from './Circle/Circle.svelte'; diff --git a/packages/layerchart/src/lib/components/tests/ChartCoreTestHarness.svelte b/packages/layerchart/src/lib/components/tests/ChartCoreTestHarness.svelte new file mode 100644 index 000000000..371a7378b --- /dev/null +++ b/packages/layerchart/src/lib/components/tests/ChartCoreTestHarness.svelte @@ -0,0 +1,31 @@ + + + + {#snippet children()} +
ChartCore children
+ {/snippet} +
diff --git a/packages/layerchart/src/lib/html.ts b/packages/layerchart/src/lib/html.ts index 1189836d4..f054d8656 100644 --- a/packages/layerchart/src/lib/html.ts +++ b/packages/layerchart/src/lib/html.ts @@ -12,6 +12,7 @@ export { default as Html, default as Layer } from './components/layers/Html.svelte'; export type { HTMLProps } from './components/layers/Html.svelte'; export { default as Chart } from './components/Chart/Chart.html.svelte'; +export { default as ChartCore } from './components/Chart/ChartCore.svelte'; export type { ChartProps, ChartPropsWithoutHTML, diff --git a/packages/layerchart/src/lib/svg.ts b/packages/layerchart/src/lib/svg.ts index 4aab352b0..933e43ec0 100644 --- a/packages/layerchart/src/lib/svg.ts +++ b/packages/layerchart/src/lib/svg.ts @@ -14,6 +14,7 @@ export { default as Svg, default as Layer } from './components/layers/Svg.svelte'; export type { SVGProps } from './components/layers/Svg.svelte'; export { default as Chart } from './components/Chart/Chart.svg.svelte'; +export { default as ChartCore } from './components/Chart/ChartCore.svelte'; export type { ChartProps, ChartPropsWithoutHTML, From 32296dab35c2fc4497abcb4d0a4c9f0f98c333ec Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Wed, 29 Apr 2026 12:20:36 -0400 Subject: [PATCH 31/36] update bundle sizes in docs --- .changeset/per-layer-primitive-variants.md | 2 +- docs/src/content/guides/bundle-size.md | 30 +++++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.changeset/per-layer-primitive-variants.md b/.changeset/per-layer-primitive-variants.md index 88297a4e8..efb828b08 100644 --- a/.changeset/per-layer-primitive-variants.md +++ b/.changeset/per-layer-primitive-variants.md @@ -52,7 +52,7 @@ The geo, graph, hierarchy, and force sub-paths also re-export every layer-agnost **Compound marks:** typically 8–15% gz savings per-layer; outliers like `Highlight` (-30% canvas) and `Cell` (-22% svg) are larger because their HTML/canvas vs. SVG paths diverge significantly. -**High-level charts:** ~10–12% gz savings (~5–11 KB) when imported from `layerchart/svg` or `layerchart/canvas`. A single-layer LineChart drops from 92.5 KB → 81.9 KB gz on the SVG path. +**High-level charts:** ~5–12% gz savings (~5–11 KB) when imported from `layerchart/svg` or `layerchart/canvas`. A single-layer LineChart drops from 89.6 KB → 79.0 KB gz on the SVG path. For a consumer who migrates all imports to a single layer, cumulative savings across primitives and compound marks are 60–80 KB gz. diff --git a/docs/src/content/guides/bundle-size.md b/docs/src/content/guides/bundle-size.md index 31ea78730..97f4edcab 100644 --- a/docs/src/content/guides/bundle-size.md +++ b/docs/src/content/guides/bundle-size.md @@ -78,7 +78,7 @@ Each sub-path also re-exports the layer-agnostic helpers you'd need alongside it ``` -A typical geo chart drops from ~90 KB gz with `` to ~70 KB gz with `` (a ~20 KB gz / ~22% saving) because the entire `Axis` / `Grid` / `Rule` / `Highlight` / `ChartClipPath` / `Layer` chain is no longer in the import graph. +A typical geo chart drops from ~87 KB gz with `` to ~55 KB gz with `` (a ~32 KB gz / ~37% saving) because the entire `Axis` / `Grid` / `Rule` / `Highlight` / `ChartClipPath` / `Layer` chain is no longer in the import graph. `` is exported from each layer sub-path (`layerchart`, `layerchart/svg`, `layerchart/canvas`, `layerchart/html`). The component itself is layer-agnostic — the layer is whatever you put inside the `children` snippet — so the sub-path choice only affects what other components (like `` or ``) tree-shake to. @@ -193,12 +193,12 @@ Geo marks live on `layerchart/geo` (and `layerchart/geo`-prefixed variants like | Chart | Agnostic | `from 'layerchart/svg'` | `from 'layerchart/canvas'` | | --- | --- | --- | --- | -| `LineChart` | 92.5 KB gz | 81.9 KB gz (-11%) | 83.6 KB gz (-10%) | -| `AreaChart` | 90.3 KB gz | 84.1 KB gz (-7%) | 85.7 KB gz (-5%) | -| `BarChart` | 88.3 KB gz | 82.1 KB gz (-7%) | 83.8 KB gz (-5%) | -| `ScatterChart` | 87.4 KB gz | 81.0 KB gz (-7%) | 82.7 KB gz (-5%) | -| `PieChart` | 94.8 KB gz | 88.1 KB gz (-7%) | 89.9 KB gz (-5%) | -| `ArcChart` | 93.7 KB gz | 87.1 KB gz (-7%) | 88.8 KB gz (-5%) | +| `LineChart` | 89.6 KB gz | 79.0 KB gz (-12%) | 80.7 KB gz (-10%) | +| `AreaChart` | 87.3 KB gz | 81.2 KB gz (-7%) | 82.8 KB gz (-5%) | +| `BarChart` | 85.4 KB gz | 79.2 KB gz (-7%) | 80.9 KB gz (-5%) | +| `ScatterChart` | 84.5 KB gz | 78.1 KB gz (-8%) | 79.8 KB gz (-6%) | +| `PieChart` | 91.9 KB gz | 85.3 KB gz (-7%) | 86.9 KB gz (-5%) | +| `ArcChart` | 90.7 KB gz | 84.2 KB gz (-7%) | 85.9 KB gz (-5%) | ```ts // Agnostic — supports Svg / Canvas / Html @@ -215,7 +215,7 @@ There is no `layerchart/html` variant for the high-level charts because the mark ## Worst case: importing everything -If you `import * as LayerChart from 'layerchart'` (or your bundler can't tree-shake at all), you'll pay for the entire surface area of the root barrel — currently around 240 KB gz across all components. The strategies above exist precisely to keep this from happening for typical consumers. +If you `import * as LayerChart from 'layerchart'` (or your bundler can't tree-shake at all), you'll pay for the entire surface area of the root barrel — currently around 232 KB gz across all components. The strategies above exist precisely to keep this from happening for typical consumers. If you're not sure what your bundle looks like in practice, run a tool like [`rollup-plugin-visualizer`](https://github.com/btd/rollup-plugin-visualizer) or `vite build --mode=production` with source maps and inspect the output. @@ -231,13 +231,13 @@ The numbers below are gzipped totals from LayerChart's own bundle analyzer. They | `base-html` (per-layer) | `Chart`, `Html` from `layerchart/html` | ~79 KB | | `core` | `ChartCore` (no Axis/Grid/Highlight) | ~51 KB | | `core-svg` (per-layer) | `ChartCore`, `Svg` from `layerchart/svg` | ~51 KB | -| `core-geo` | `ChartCore`, `Svg`, `GeoProjection`, `GeoPath` | ~58 KB | -| `line-chart` | `Chart`, `Svg`, `Line`, `Axis`, `Grid` | ~86 KB | -| `LineChart` | high-level `LineChart` | ~92 KB | -| `LineChart-svg` (per-layer) | high-level `LineChart` from `layerchart/svg` | ~82 KB | -| `geo` (sub-path) | `Chart`, `Svg`, `GeoProjection`, `GeoPath`, `GeoPoint` | ~90 KB | -| `force` (sub-path) | `Chart`, `Svg`, `ForceSimulation`, `Link`, `Circle`, `Text` | ~97 KB | -| `dagre` (sub-path) | `Chart`, `Svg`, `Dagre`, `Link`, `Circle`, `Text` | ~112 KB | +| `core-geo` | `ChartCore`, `Svg`, `GeoProjection`, `GeoPath` | ~55 KB | +| `line-chart` | `Chart`, `Svg`, `Line`, `Axis`, `Grid` | ~83 KB | +| `LineChart` | high-level `LineChart` | ~90 KB | +| `LineChart-svg` (per-layer) | high-level `LineChart` from `layerchart/svg` | ~79 KB | +| `geo` (sub-path) | `Chart`, `Svg`, `GeoProjection`, `GeoPath`, `GeoPoint` | ~87 KB | +| `force` (sub-path) | `Chart`, `Svg`, `ForceSimulation`, `Link`, `Circle`, `Text` | ~94 KB | +| `dagre` (sub-path) | `Chart`, `Svg`, `Dagre`, `Link`, `Circle`, `Text` | ~109 KB | | `circle-svg` (per-layer) | `Circle` from `layerchart/svg` | ~13 KB | | `circle-agnostic` | `Circle` from `layerchart` | ~17 KB | | `text-svg` (per-layer) | `Text` from `layerchart/svg` | ~16 KB | From c8ba50ad97d1f7aaace7d146ce33ae80340a8cf0 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Wed, 29 Apr 2026 12:24:01 -0400 Subject: [PATCH 32/36] move core section above base --- bundle-analyzer/bundle-scenarios.ts | 98 ++++++++++++++--------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/bundle-analyzer/bundle-scenarios.ts b/bundle-analyzer/bundle-scenarios.ts index 1ad18921d..43e032454 100644 --- a/bundle-analyzer/bundle-scenarios.ts +++ b/bundle-analyzer/bundle-scenarios.ts @@ -35,6 +35,55 @@ export interface ComponentInfo { * Each scenario includes the minimum set of components for that chart type. */ export const scenarios: Scenario[] = [ + // --- Core --- + // The bare-bones `` (no `` — no Axis/Grid/Rule/Highlight/Layer). + // Use cases: geo maps, custom layouts, or anything that doesn't need the cartesian frame. + { + name: 'core', + group: 'Core', + description: '`ChartCore` — bare-bones chart without `ChartChildren`', + imports: ['ChartCore'], + }, + { + name: 'core-svg', + group: 'Core', + description: '`ChartCore` + `Svg` from `layerchart/svg`', + imports: ['ChartCore', 'Svg'], + layers: { ChartCore: 'svg', Svg: 'svg' }, + }, + { + name: 'core-canvas', + group: 'Core', + description: '`ChartCore` + `Canvas` from `layerchart/canvas`', + imports: ['ChartCore', 'Canvas'], + layers: { ChartCore: 'canvas', Canvas: 'canvas' }, + }, + { + name: 'core-html', + group: 'Core', + description: '`ChartCore` + `Html` from `layerchart/html`', + imports: ['ChartCore', 'Html'], + layers: { ChartCore: 'html', Html: 'html' }, + }, + { + name: 'core-geo', + group: 'Core', + description: '`ChartCore`-based geo map (`GeoProjection` + `GeoPath`)', + imports: ['ChartCore', 'Svg', 'GeoProjection', 'GeoPath'], + }, + { + name: 'core-line', + group: 'Core', + description: '`ChartCore` + manual `Spline` line (no Axis/Grid)', + imports: ['ChartCore', 'Svg', 'Spline'], + }, + { + name: 'core-scatter', + group: 'Core', + description: '`ChartCore` + manual `Points` scatter (no Axis/Grid)', + imports: ['ChartCore', 'Svg', 'Points'], + }, + // --- Base (agnostic) --- // The full `` (with Axis/Grid/Rule/Highlight/Layer/ChartClipPath baked in). { @@ -94,55 +143,6 @@ export const scenarios: Scenario[] = [ }, }, - // --- Core --- - // The bare-bones `` (no `` — no Axis/Grid/Rule/Highlight/Layer). - // Use cases: geo maps, custom layouts, or anything that doesn't need the cartesian frame. - { - name: 'core', - group: 'Core', - description: '`ChartCore` — bare-bones chart without `ChartChildren`', - imports: ['ChartCore'], - }, - { - name: 'core-svg', - group: 'Core', - description: '`ChartCore` + `Svg` from `layerchart/svg`', - imports: ['ChartCore', 'Svg'], - layers: { ChartCore: 'svg', Svg: 'svg' }, - }, - { - name: 'core-canvas', - group: 'Core', - description: '`ChartCore` + `Canvas` from `layerchart/canvas`', - imports: ['ChartCore', 'Canvas'], - layers: { ChartCore: 'canvas', Canvas: 'canvas' }, - }, - { - name: 'core-html', - group: 'Core', - description: '`ChartCore` + `Html` from `layerchart/html`', - imports: ['ChartCore', 'Html'], - layers: { ChartCore: 'html', Html: 'html' }, - }, - { - name: 'core-geo', - group: 'Core', - description: '`ChartCore`-based geo map (`GeoProjection` + `GeoPath`)', - imports: ['ChartCore', 'Svg', 'GeoProjection', 'GeoPath'], - }, - { - name: 'core-line', - group: 'Core', - description: '`ChartCore` + manual `Spline` line (no Axis/Grid)', - imports: ['ChartCore', 'Svg', 'Spline'], - }, - { - name: 'core-scatter', - group: 'Core', - description: '`ChartCore` + manual `Points` scatter (no Axis/Grid)', - imports: ['ChartCore', 'Svg', 'Points'], - }, - // --- Cartesian charts --- { name: 'line-chart', From f8397b67d163ef9a2d2b36c05396c6719a9a35e2 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Wed, 29 Apr 2026 13:03:53 -0400 Subject: [PATCH 33/36] Fix primitive fill/stroke for canvas primitives --- bundle-analyzer/bundle-reports/latest.json | 154 +++++++++--------- .../components/Circle/Circle.canvas.svelte | 6 +- .../components/Ellipse/Ellipse.canvas.svelte | 6 +- .../lib/components/Line/Line.canvas.svelte | 6 +- .../components/Polygon/Polygon.canvas.svelte | 6 +- .../lib/components/Rect/Rect.canvas.svelte | 8 +- .../lib/components/Text/Text.canvas.svelte | 6 +- 7 files changed, 103 insertions(+), 89 deletions(-) diff --git a/bundle-analyzer/bundle-reports/latest.json b/bundle-analyzer/bundle-reports/latest.json index 7d0150693..b7d0ff8c4 100644 --- a/bundle-analyzer/bundle-reports/latest.json +++ b/bundle-analyzer/bundle-reports/latest.json @@ -1,82 +1,6 @@ { - "timestamp": "2026-04-29T15:57:54.893Z", + "timestamp": "2026-04-29T16:24:54.584Z", "results": [ - { - "scenario": "base", - "description": "`Chart` — full charting frame without rendering layer", - "group": "Base (agnostic)", - "size": 365532, - "gzipSize": 85425, - "imports": [ - "Chart" - ] - }, - { - "scenario": "base-svg-agnostic", - "description": "`Chart` + `Svg` from `layerchart` (agnostic dispatcher)", - "group": "Base (agnostic)", - "size": 365536, - "gzipSize": 85428, - "imports": [ - "Chart", - "Svg" - ] - }, - { - "scenario": "base-canvas-agnostic", - "description": "`Chart` + `Canvas` from `layerchart` (agnostic dispatcher)", - "group": "Base (agnostic)", - "size": 365536, - "gzipSize": 85425, - "imports": [ - "Chart", - "Canvas" - ] - }, - { - "scenario": "base-html-agnostic", - "description": "`Chart` + `Html` from `layerchart` (agnostic dispatcher)", - "group": "Base (agnostic)", - "size": 365536, - "gzipSize": 85422, - "imports": [ - "Chart", - "Html" - ] - }, - { - "scenario": "base-svg", - "description": "`Chart` + `Svg` from `layerchart/svg`", - "group": "Base (layer-specific)", - "size": 337713, - "gzipSize": 79225, - "imports": [ - "Chart", - "Svg" - ] - }, - { - "scenario": "base-canvas", - "description": "`Chart` + `Canvas` from `layerchart/canvas`", - "group": "Base (layer-specific)", - "size": 344259, - "gzipSize": 80925, - "imports": [ - "Chart", - "Canvas" - ] - }, - { - "scenario": "base-html", - "description": "`Chart` + `Html` from `layerchart/html`", - "group": "Base (layer-specific)", - "size": 345563, - "gzipSize": 81246, - "imports": [ - "Chart", - "Html" - ] - }, { "scenario": "core", "description": "`ChartCore` — bare-bones chart without `ChartChildren`", @@ -157,6 +81,82 @@ "Points" ] }, + { + "scenario": "base", + "description": "`Chart` — full charting frame without rendering layer", + "group": "Base (agnostic)", + "size": 365532, + "gzipSize": 85425, + "imports": [ + "Chart" + ] + }, + { + "scenario": "base-svg-agnostic", + "description": "`Chart` + `Svg` from `layerchart` (agnostic dispatcher)", + "group": "Base (agnostic)", + "size": 365536, + "gzipSize": 85428, + "imports": [ + "Chart", + "Svg" + ] + }, + { + "scenario": "base-canvas-agnostic", + "description": "`Chart` + `Canvas` from `layerchart` (agnostic dispatcher)", + "group": "Base (agnostic)", + "size": 365536, + "gzipSize": 85425, + "imports": [ + "Chart", + "Canvas" + ] + }, + { + "scenario": "base-html-agnostic", + "description": "`Chart` + `Html` from `layerchart` (agnostic dispatcher)", + "group": "Base (agnostic)", + "size": 365536, + "gzipSize": 85422, + "imports": [ + "Chart", + "Html" + ] + }, + { + "scenario": "base-svg", + "description": "`Chart` + `Svg` from `layerchart/svg`", + "group": "Base (layer-specific)", + "size": 337713, + "gzipSize": 79225, + "imports": [ + "Chart", + "Svg" + ] + }, + { + "scenario": "base-canvas", + "description": "`Chart` + `Canvas` from `layerchart/canvas`", + "group": "Base (layer-specific)", + "size": 344259, + "gzipSize": 80925, + "imports": [ + "Chart", + "Canvas" + ] + }, + { + "scenario": "base-html", + "description": "`Chart` + `Html` from `layerchart/html`", + "group": "Base (layer-specific)", + "size": 345563, + "gzipSize": 81246, + "imports": [ + "Chart", + "Html" + ] + }, { "scenario": "line-chart", "description": "Basic line chart with axes and grid", diff --git a/packages/layerchart/src/lib/components/Circle/Circle.canvas.svelte b/packages/layerchart/src/lib/components/Circle/Circle.canvas.svelte index 6ccb8ae25..522362950 100644 --- a/packages/layerchart/src/lib/components/Circle/Circle.canvas.svelte +++ b/packages/layerchart/src/lib/components/Circle/Circle.canvas.svelte @@ -38,10 +38,12 @@ styleOverrides ) : { + // Use raw `rest.fill` / `rest.stroke` (not `staticFill`) so canvas + // accepts non-string values like `CanvasPattern` / `CanvasGradient`. styles: { - fill: itemFill ?? c.staticFill, + fill: itemFill ?? (rest.fill as any), fillOpacity: itemFillOpacity ?? c.staticFillOpacity, - stroke: itemStroke ?? c.staticStroke, + stroke: itemStroke ?? (rest.stroke as any), strokeWidth: itemStrokeWidth ?? c.staticStrokeWidth, opacity: itemOpacity ?? c.staticOpacity, }, diff --git a/packages/layerchart/src/lib/components/Ellipse/Ellipse.canvas.svelte b/packages/layerchart/src/lib/components/Ellipse/Ellipse.canvas.svelte index a79caf2ff..e083ee1d3 100644 --- a/packages/layerchart/src/lib/components/Ellipse/Ellipse.canvas.svelte +++ b/packages/layerchart/src/lib/components/Ellipse/Ellipse.canvas.svelte @@ -32,10 +32,12 @@ styleOverrides ) : { + // Use raw `rest.fill` / `rest.stroke` (not `staticFill`) so canvas + // accepts non-string values like `CanvasPattern` / `CanvasGradient`. styles: { - fill: itemFill ?? c.staticFill, + fill: itemFill ?? (rest.fill as any), fillOpacity: itemFillOpacity ?? c.staticFillOpacity, - stroke: itemStroke ?? c.staticStroke, + stroke: itemStroke ?? (rest.stroke as any), strokeWidth: itemStrokeWidth ?? c.staticStrokeWidth, opacity: itemOpacity ?? c.staticOpacity, }, diff --git a/packages/layerchart/src/lib/components/Line/Line.canvas.svelte b/packages/layerchart/src/lib/components/Line/Line.canvas.svelte index af0d3dd28..2d9484171 100644 --- a/packages/layerchart/src/lib/components/Line/Line.canvas.svelte +++ b/packages/layerchart/src/lib/components/Line/Line.canvas.svelte @@ -34,10 +34,12 @@ styleOverrides ) : { + // Use raw `rest.fill` / `rest.stroke` (not `staticFill`) so canvas + // accepts non-string values like `CanvasPattern` / `CanvasGradient`. styles: { - fill: itemFill ?? c.staticFill, + fill: itemFill ?? (rest.fill as any), fillOpacity: itemFillOpacity ?? c.staticFillOpacity, - stroke: itemStroke ?? c.staticStroke, + stroke: itemStroke ?? (rest.stroke as any), strokeWidth: itemStrokeWidth ?? c.staticStrokeWidth, opacity: itemOpacity ?? c.staticOpacity, }, diff --git a/packages/layerchart/src/lib/components/Polygon/Polygon.canvas.svelte b/packages/layerchart/src/lib/components/Polygon/Polygon.canvas.svelte index 172f7f25c..08508646d 100644 --- a/packages/layerchart/src/lib/components/Polygon/Polygon.canvas.svelte +++ b/packages/layerchart/src/lib/components/Polygon/Polygon.canvas.svelte @@ -32,10 +32,12 @@ styleOverrides ) : { + // Use raw `rest.fill` / `rest.stroke` (not `staticFill`) so canvas + // accepts non-string values like `CanvasPattern` / `CanvasGradient`. styles: { - fill: itemFill ?? c.staticFill, + fill: itemFill ?? (rest.fill as any), fillOpacity: itemFillOpacity ?? c.staticFillOpacity, - stroke: itemStroke ?? c.staticStroke, + stroke: itemStroke ?? (rest.stroke as any), strokeWidth: itemStrokeWidth ?? c.staticStrokeWidth, opacity: itemOpacity ?? c.staticOpacity, }, diff --git a/packages/layerchart/src/lib/components/Rect/Rect.canvas.svelte b/packages/layerchart/src/lib/components/Rect/Rect.canvas.svelte index 508ebb699..f5df99395 100644 --- a/packages/layerchart/src/lib/components/Rect/Rect.canvas.svelte +++ b/packages/layerchart/src/lib/components/Rect/Rect.canvas.svelte @@ -35,10 +35,14 @@ styleOverrides ) : { + // Use raw `rest.fill` / `rest.stroke` (not `staticFill`) so canvas + // accepts non-string values like `CanvasPattern` / `CanvasGradient` + // produced by `` / ``. `staticFill` is + // string-only for the SVG/HTML pixel-mode templates. styles: { - fill: itemFill ?? c.staticFill, + fill: itemFill ?? (rest.fill as any), fillOpacity: itemFillOpacity ?? c.staticFillOpacity, - stroke: itemStroke ?? c.staticStroke, + stroke: itemStroke ?? (rest.stroke as any), strokeOpacity: itemStrokeOpacity ?? c.staticStrokeOpacity, strokeWidth: itemStrokeWidth ?? c.staticStrokeWidth, opacity: itemOpacity ?? c.staticOpacity, diff --git a/packages/layerchart/src/lib/components/Text/Text.canvas.svelte b/packages/layerchart/src/lib/components/Text/Text.canvas.svelte index 4e90a2ade..5f4d860fd 100644 --- a/packages/layerchart/src/lib/components/Text/Text.canvas.svelte +++ b/packages/layerchart/src/lib/components/Text/Text.canvas.svelte @@ -41,10 +41,12 @@ styleOverrides ) : { + // Use raw `rest.fill` / `rest.stroke` (not `staticFill`) so canvas + // accepts non-string values like `CanvasPattern` / `CanvasGradient`. styles: { - fill: itemFill ?? c.staticFill, + fill: itemFill ?? (rest.fill as any), fillOpacity: itemFillOpacity ?? c.staticFillOpacity, - stroke: itemStroke ?? c.staticStroke, + stroke: itemStroke ?? (rest.stroke as any), strokeWidth: itemStrokeWidth ?? c.staticStrokeWidth, opacity: itemOpacity ?? c.staticOpacity, paintOrder: 'stroke', From 98a40a8c128560d1fda4afdbaa30a7d25e84ae99 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Wed, 29 Apr 2026 13:12:58 -0400 Subject: [PATCH 34/36] fix(Text): Render on `` layer when only one of `x`/`y` is set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The static-mode render guard required both `x` and `y` to be explicit, so `` inside a positioned `` (e.g. tooltip labels in the GeoPoint world-capitals example) was skipped on the Svg layer — Canvas worked because it doesn't gate on coordinate validity. Change `&&` to `||` so Text renders when either coordinate is set; the missing one falls through to the existing `motionX`/`motionY` default of 0, matching SVG's natural "missing coord = 0" behavior and the Canvas variant. --- packages/layerchart/src/lib/components/Text/Text.svg.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/layerchart/src/lib/components/Text/Text.svg.svelte b/packages/layerchart/src/lib/components/Text/Text.svg.svelte index 405c3d986..4eaae1ab4 100644 --- a/packages/layerchart/src/lib/components/Text/Text.svg.svelte +++ b/packages/layerchart/src/lib/components/Text/Text.svg.svelte @@ -128,7 +128,7 @@ {c.wordsByLines.map((line) => line.words.join(' ')).join()} - {:else if isValidXOrY(typeof rest.x === 'function' ? undefined : rest.x) && isValidXOrY(typeof rest.y === 'function' ? undefined : rest.y)} + {:else if isValidXOrY(typeof rest.x === 'function' ? undefined : rest.x) || isValidXOrY(typeof rest.y === 'function' ? undefined : rest.y)} Date: Wed, 29 Apr 2026 13:26:55 -0400 Subject: [PATCH 35/36] fix components missing html impls (Calendar, Bars, Annotation*, etc) --- .../AnnotationLine/AnnotationLine.html.svelte | 17 ++++++++++ .../AnnotationLine/AnnotationLine.svelte | 3 ++ .../AnnotationPoint.base.svelte | 9 ++++-- .../AnnotationPoint.html.svelte | 18 +++++++++++ .../AnnotationRange.html.svelte | 19 ++++++++++++ .../AnnotationRange/AnnotationRange.svelte | 3 ++ .../src/lib/components/Bar/Bar.base.svelte | 8 +++-- .../src/lib/components/Bar/Bar.html.svelte | 15 +++++++++ .../src/lib/components/Bar/Bar.svelte | 3 ++ .../src/lib/components/Bars/Bars.html.svelte | 15 +++++++++ .../src/lib/components/Bars/Bars.svelte | 3 ++ .../components/Calendar/Calendar.html.svelte | 18 +++++++++++ .../lib/components/Calendar/Calendar.svelte | 3 ++ .../lib/components/Month/Month.html.svelte | 19 ++++++++++++ .../src/lib/components/Month/Month.svelte | 3 ++ packages/layerchart/src/lib/html.ts | 31 +++++++++++++++++++ 16 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.html.svelte create mode 100644 packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.html.svelte create mode 100644 packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.html.svelte create mode 100644 packages/layerchart/src/lib/components/Bar/Bar.html.svelte create mode 100644 packages/layerchart/src/lib/components/Bars/Bars.html.svelte create mode 100644 packages/layerchart/src/lib/components/Calendar/Calendar.html.svelte create mode 100644 packages/layerchart/src/lib/components/Month/Month.html.svelte diff --git a/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.html.svelte b/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.html.svelte new file mode 100644 index 000000000..b840e5b02 --- /dev/null +++ b/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.html.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.svelte b/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.svelte index 8b44a10ce..743c068a0 100644 --- a/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.svelte +++ b/packages/layerchart/src/lib/components/AnnotationLine/AnnotationLine.svelte @@ -9,6 +9,7 @@ import { getLayerContext } from '$lib/contexts/layer.js'; import AnnotationLineSvg from './AnnotationLine.svg.svelte'; import AnnotationLineCanvas from './AnnotationLine.canvas.svelte'; + import AnnotationLineHtml from './AnnotationLine.html.svelte'; import type { AnnotationLineProps } from './AnnotationLine.shared.svelte.js'; const layerCtx = getLayerContext(); @@ -20,4 +21,6 @@ {:else if layerCtx === 'canvas'} +{:else if layerCtx === 'html'} + {/if} diff --git a/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.base.svelte b/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.base.svelte index 53b32cf4a..a7f3276e8 100644 --- a/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.base.svelte +++ b/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.base.svelte @@ -4,7 +4,12 @@ export type AnnotationPointBaseLayerComponents = { Circle: Component; - Link: Component; + /** + * Used for callout link rendering. Optional because the HTML layer + * doesn't have a `Link` variant; HTML annotation points without callouts + * still render correctly. + */ + Link?: Component; Text: Component; }; @@ -119,7 +124,7 @@ class={cls('lc-annotation-point', link && 'lc-annotation-point-ring', props?.circle?.class)} /> -{#if linkEndpoints} +{#if linkEndpoints && Link} + export type { + AnnotationPointProps, + AnnotationPointPropsWithoutHTML, + } from './AnnotationPoint.shared.svelte.js'; + + + + + + diff --git a/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.html.svelte b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.html.svelte new file mode 100644 index 000000000..1a4d7fc00 --- /dev/null +++ b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.html.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svelte b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svelte index d1282d4ce..28b75a750 100644 --- a/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svelte +++ b/packages/layerchart/src/lib/components/AnnotationRange/AnnotationRange.svelte @@ -9,6 +9,7 @@ import { getLayerContext } from '$lib/contexts/layer.js'; import AnnotationRangeSvg from './AnnotationRange.svg.svelte'; import AnnotationRangeCanvas from './AnnotationRange.canvas.svelte'; + import AnnotationRangeHtml from './AnnotationRange.html.svelte'; import type { AnnotationRangeProps } from './AnnotationRange.shared.svelte.js'; const layerCtx = getLayerContext(); @@ -20,4 +21,6 @@ {:else if layerCtx === 'canvas'} +{:else if layerCtx === 'html'} + {/if} diff --git a/packages/layerchart/src/lib/components/Bar/Bar.base.svelte b/packages/layerchart/src/lib/components/Bar/Bar.base.svelte index 6a0da54e7..030c65d96 100644 --- a/packages/layerchart/src/lib/components/Bar/Bar.base.svelte +++ b/packages/layerchart/src/lib/components/Bar/Bar.base.svelte @@ -4,7 +4,11 @@ export type BarBaseLayerComponents = { Rect: Component; - Arc: Component; + /** + * Used only when the chart is radial. Optional because the HTML layer + * doesn't support radial charts and therefore doesn't need to bundle Arc. + */ + Arc?: Component; }; export type BarBaseProps = BarProps & BarBaseLayerComponents; @@ -89,7 +93,7 @@ }; -{#if c.ctx.radial} +{#if c.ctx.radial && Arc} + export type { BarProps, BarPropsWithoutHTML } from './Bar.shared.svelte.js'; + + + + + + diff --git a/packages/layerchart/src/lib/components/Bar/Bar.svelte b/packages/layerchart/src/lib/components/Bar/Bar.svelte index 375a903e7..202df682b 100644 --- a/packages/layerchart/src/lib/components/Bar/Bar.svelte +++ b/packages/layerchart/src/lib/components/Bar/Bar.svelte @@ -6,6 +6,7 @@ import { getLayerContext } from '$lib/contexts/layer.js'; import BarSvg from './Bar.svg.svelte'; import BarCanvas from './Bar.canvas.svelte'; + import BarHtml from './Bar.html.svelte'; import type { BarProps } from './Bar.shared.svelte.js'; const layerCtx = getLayerContext(); @@ -17,4 +18,6 @@ {:else if layerCtx === 'canvas'} +{:else if layerCtx === 'html'} + {/if} diff --git a/packages/layerchart/src/lib/components/Bars/Bars.html.svelte b/packages/layerchart/src/lib/components/Bars/Bars.html.svelte new file mode 100644 index 000000000..e2215dcda --- /dev/null +++ b/packages/layerchart/src/lib/components/Bars/Bars.html.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Bars/Bars.svelte b/packages/layerchart/src/lib/components/Bars/Bars.svelte index f81ada97c..273de8836 100644 --- a/packages/layerchart/src/lib/components/Bars/Bars.svelte +++ b/packages/layerchart/src/lib/components/Bars/Bars.svelte @@ -6,6 +6,7 @@ import { getLayerContext } from '$lib/contexts/layer.js'; import BarsSvg from './Bars.svg.svelte'; import BarsCanvas from './Bars.canvas.svelte'; + import BarsHtml from './Bars.html.svelte'; import type { BarsProps } from './Bars.shared.svelte.js'; const layerCtx = getLayerContext(); @@ -17,4 +18,6 @@ {:else if layerCtx === 'canvas'} +{:else if layerCtx === 'html'} + {/if} diff --git a/packages/layerchart/src/lib/components/Calendar/Calendar.html.svelte b/packages/layerchart/src/lib/components/Calendar/Calendar.html.svelte new file mode 100644 index 000000000..7c41e21bb --- /dev/null +++ b/packages/layerchart/src/lib/components/Calendar/Calendar.html.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Calendar/Calendar.svelte b/packages/layerchart/src/lib/components/Calendar/Calendar.svelte index 95a5752a2..8e045a764 100644 --- a/packages/layerchart/src/lib/components/Calendar/Calendar.svelte +++ b/packages/layerchart/src/lib/components/Calendar/Calendar.svelte @@ -10,6 +10,7 @@ import { getLayerContext } from '$lib/contexts/layer.js'; import CalendarSvg from './Calendar.svg.svelte'; import CalendarCanvas from './Calendar.canvas.svelte'; + import CalendarHtml from './Calendar.html.svelte'; import type { CalendarPropsWithoutHTML } from './Calendar.shared.svelte.js'; const layerCtx = getLayerContext(); @@ -21,4 +22,6 @@ {:else if layerCtx === 'canvas'} +{:else if layerCtx === 'html'} + {/if} diff --git a/packages/layerchart/src/lib/components/Month/Month.html.svelte b/packages/layerchart/src/lib/components/Month/Month.html.svelte new file mode 100644 index 000000000..e4093ce38 --- /dev/null +++ b/packages/layerchart/src/lib/components/Month/Month.html.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/layerchart/src/lib/components/Month/Month.svelte b/packages/layerchart/src/lib/components/Month/Month.svelte index 8d46bd578..445bc2cf5 100644 --- a/packages/layerchart/src/lib/components/Month/Month.svelte +++ b/packages/layerchart/src/lib/components/Month/Month.svelte @@ -10,6 +10,7 @@ import { getLayerContext } from '$lib/contexts/layer.js'; import MonthSvg from './Month.svg.svelte'; import MonthCanvas from './Month.canvas.svelte'; + import MonthHtml from './Month.html.svelte'; import type { MonthPropsWithoutHTML } from './Month.shared.svelte.js'; const layerCtx = getLayerContext(); @@ -21,4 +22,6 @@ {:else if layerCtx === 'canvas'} +{:else if layerCtx === 'html'} + {/if} diff --git a/packages/layerchart/src/lib/html.ts b/packages/layerchart/src/lib/html.ts index f054d8656..28fc5559f 100644 --- a/packages/layerchart/src/lib/html.ts +++ b/packages/layerchart/src/lib/html.ts @@ -127,6 +127,37 @@ export type { RasterProps, RasterPropsWithoutHTML, } from './components/Raster/Raster.shared.svelte.js'; +export { default as Bar } from './components/Bar/Bar.html.svelte'; +export type { BarProps, BarPropsWithoutHTML } from './components/Bar/Bar.shared.svelte.js'; +export { default as Bars } from './components/Bars/Bars.html.svelte'; +export type { BarsProps, BarsPropsWithoutHTML } from './components/Bars/Bars.shared.svelte.js'; +export { default as AnnotationLine } from './components/AnnotationLine/AnnotationLine.html.svelte'; +export type { + AnnotationLineProps, + AnnotationLinePropsWithoutHTML, +} from './components/AnnotationLine/AnnotationLine.shared.svelte.js'; +export { default as AnnotationPoint } from './components/AnnotationPoint/AnnotationPoint.html.svelte'; +export type { + AnnotationPointProps, + AnnotationPointPropsWithoutHTML, +} from './components/AnnotationPoint/AnnotationPoint.shared.svelte.js'; +export { default as AnnotationRange } from './components/AnnotationRange/AnnotationRange.html.svelte'; +export type { + AnnotationRangeProps, + AnnotationRangePropsWithoutHTML, +} from './components/AnnotationRange/AnnotationRange.shared.svelte.js'; +export { default as Calendar } from './components/Calendar/Calendar.html.svelte'; +export type { + CalendarProps, + CalendarPropsWithoutHTML, + CalendarCell, +} from './components/Calendar/Calendar.shared.svelte.js'; +export { default as Month } from './components/Month/Month.html.svelte'; +export type { + MonthProps, + MonthPropsWithoutHTML, + MonthCell, +} from './components/Month/Month.shared.svelte.js'; // --- Layer-agnostic re-exports --- // These components don't render layer-specific elements (pure logic, layout From 3a67a8a15878209814a48d5303d83073f2eb2068 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Wed, 29 Apr 2026 14:16:46 -0400 Subject: [PATCH 36/36] Fix Text within group --- .../layerchart/src/lib/components/Text/Text.svg.svelte | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/layerchart/src/lib/components/Text/Text.svg.svelte b/packages/layerchart/src/lib/components/Text/Text.svg.svelte index 4eaae1ab4..8ce83bb32 100644 --- a/packages/layerchart/src/lib/components/Text/Text.svg.svelte +++ b/packages/layerchart/src/lib/components/Text/Text.svg.svelte @@ -12,7 +12,6 @@ import { TextState, textMarkInfo, - isValidXOrY, getPixelValue, type TextProps, } from './Text.shared.svelte.js'; @@ -128,7 +127,11 @@ {c.wordsByLines.map((line) => line.words.join(' ')).join()} - {:else if isValidXOrY(typeof rest.x === 'function' ? undefined : rest.x) || isValidXOrY(typeof rest.y === 'function' ? undefined : rest.y)} + {:else} +