diff --git a/.changeset/beige-bears-joke.md b/.changeset/beige-bears-joke.md new file mode 100644 index 000000000..09d83d2de --- /dev/null +++ b/.changeset/beige-bears-joke.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat: New Connector component (issue #11) diff --git a/.changeset/beige-doodles-shout.md b/.changeset/beige-doodles-shout.md new file mode 100644 index 000000000..8908ed852 --- /dev/null +++ b/.changeset/beige-doodles-shout.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(ScatterChart): Change default tooltip mode from `voronoi` to `quadtree` diff --git a/.changeset/better-eagles-scream.md b/.changeset/better-eagles-scream.md new file mode 100644 index 000000000..e9ffbecdb --- /dev/null +++ b/.changeset/better-eagles-scream.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Bars): Fix inverted rect when rendered top-to-bottom or right-to-left. Fixes #540 diff --git a/.changeset/better-pets-divide.md b/.changeset/better-pets-divide.md new file mode 100644 index 000000000..4412f18e1 --- /dev/null +++ b/.changeset/better-pets-divide.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(AreaChart|BarChar|LineChart): Use value axis (typically y) property name/accessor for tooltip label if defined as string (ex. `` will use `visitors` instead of `value`) diff --git a/.changeset/big-boxes-shout.md b/.changeset/big-boxes-shout.md new file mode 100644 index 000000000..ce3d215d4 --- /dev/null +++ b/.changeset/big-boxes-shout.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(TooltipContext): Handle chart padding when using `quadtree` mode diff --git a/.changeset/blue-doodles-chew.md b/.changeset/blue-doodles-chew.md new file mode 100644 index 000000000..430f02acf --- /dev/null +++ b/.changeset/blue-doodles-chew.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +breaking(AnnotationLine|AnnotationPoint): Change `labelOffset` into explicit `labelXOffset` and `labelYOffset` for greater control (aligns with AnnotationRange) diff --git a/.changeset/brave-spies-give.md b/.changeset/brave-spies-give.md new file mode 100644 index 000000000..c75aa5d15 --- /dev/null +++ b/.changeset/brave-spies-give.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(GeoPath): Fix reactivity with `curve` when using Canvas context diff --git a/.changeset/breezy-donuts-sniff.md b/.changeset/breezy-donuts-sniff.md new file mode 100644 index 000000000..bb4c789da --- /dev/null +++ b/.changeset/breezy-donuts-sniff.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(TooltipContext): Support `quadtree-x` and `quadtree-y` modes. Resolves #525 diff --git a/.changeset/brown-terms-tie.md b/.changeset/brown-terms-tie.md new file mode 100644 index 000000000..0c066f649 --- /dev/null +++ b/.changeset/brown-terms-tie.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Tooltip): Correctly set tooltip position on chart enter and exit diff --git a/.changeset/bumpy-breads-rhyme.md b/.changeset/bumpy-breads-rhyme.md new file mode 100644 index 000000000..13f10d948 --- /dev/null +++ b/.changeset/bumpy-breads-rhyme.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(GeoPath): Do not register with hit canavs unless pointer events (onclick, onpointermove, etc) or tooltipContext are defined diff --git a/.changeset/calm-jars-mix.md b/.changeset/calm-jars-mix.md new file mode 100644 index 000000000..76386fbfc --- /dev/null +++ b/.changeset/calm-jars-mix.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat: Integrate `annotations` into simplified charts diff --git a/.changeset/chatty-flies-bet.md b/.changeset/chatty-flies-bet.md new file mode 100644 index 000000000..36669a999 --- /dev/null +++ b/.changeset/chatty-flies-bet.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(HighlightKey): Define `set()` with arrow function to solve `current` access when passed directly diff --git a/.changeset/chatty-shirts-rule.md b/.changeset/chatty-shirts-rule.md new file mode 100644 index 000000000..c6d9377c9 --- /dev/null +++ b/.changeset/chatty-shirts-rule.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(Canvas): Support disabling the hit canavs (useful when animations are playing) diff --git a/.changeset/chilly-games-hide.md b/.changeset/chilly-games-hide.md new file mode 100644 index 000000000..6a20d7b9f --- /dev/null +++ b/.changeset/chilly-games-hide.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(TooltipContext): Revert back to pointer events (instead of mouse/touch) but with `touch-action: pan-y`. Provides simplified events while allowing horizontal scrubbing with vertical scrolling. diff --git a/.changeset/chubby-ties-play.md b/.changeset/chubby-ties-play.md new file mode 100644 index 000000000..05bbcd313 --- /dev/null +++ b/.changeset/chubby-ties-play.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(PieChart): Do not pass `y` accessor to use linear scale fallback diff --git a/.changeset/clean-nights-jog.md b/.changeset/clean-nights-jog.md new file mode 100644 index 000000000..7aa215bae --- /dev/null +++ b/.changeset/clean-nights-jog.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(AnnotationPoint): Do not propagate mouse/touch move/leave events to TooltipContext after switching from pointer events. Fixes #598 diff --git a/.changeset/clear-ghosts-arrive.md b/.changeset/clear-ghosts-arrive.md new file mode 100644 index 000000000..7745f181a --- /dev/null +++ b/.changeset/clear-ghosts-arrive.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(LineChart): Restore passing xScale / yScale overrides diff --git a/.changeset/clear-points-care.md b/.changeset/clear-points-care.md new file mode 100644 index 000000000..fc7da17d8 --- /dev/null +++ b/.changeset/clear-points-care.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(TooltipContext): Fix touch scrolling on mobile. Fixes #255 diff --git a/.changeset/cozy-moments-work.md b/.changeset/cozy-moments-work.md new file mode 100644 index 000000000..ea1aab111 --- /dev/null +++ b/.changeset/cozy-moments-work.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Highlight|TooltipContext): Support xInterval / yInterval diff --git a/.changeset/crazy-ads-appear.md b/.changeset/crazy-ads-appear.md new file mode 100644 index 000000000..2b29de706 --- /dev/null +++ b/.changeset/crazy-ads-appear.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(TooltipContext): Fix `band` mode regression when both x/y are scaleBand (ex. punchcard chart) diff --git a/.changeset/crazy-friends-talk.md b/.changeset/crazy-friends-talk.md new file mode 100644 index 000000000..86d37d715 --- /dev/null +++ b/.changeset/crazy-friends-talk.md @@ -0,0 +1,6 @@ +--- +'layerchart': minor +--- + +- Made `ForceSimulation` generic over its nodes and links, i.e. `ForceSimulation.` +- Exposed `links` via `children` snippet of `ForceSimulation`. diff --git a/.changeset/cruel-cameras-begin.md b/.changeset/cruel-cameras-begin.md new file mode 100644 index 000000000..5b14ebd06 --- /dev/null +++ b/.changeset/cruel-cameras-begin.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +breaking(Treemap): Remove `selected` prop diff --git a/.changeset/cruel-rats-taste.md b/.changeset/cruel-rats-taste.md new file mode 100644 index 000000000..076131b31 --- /dev/null +++ b/.changeset/cruel-rats-taste.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(Spline): Add `value` to `startContent` and `endContent` snippets to easily access the `x` and `y` data values diff --git a/.changeset/curly-lies-write.md b/.changeset/curly-lies-write.md new file mode 100644 index 000000000..4c9009fbe --- /dev/null +++ b/.changeset/curly-lies-write.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Canvas): Only apply text/font properties to canvas to improve performance diff --git a/.changeset/cute-donkeys-greet.md b/.changeset/cute-donkeys-greet.md new file mode 100644 index 000000000..68b3ba32a --- /dev/null +++ b/.changeset/cute-donkeys-greet.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(SimplifiedCharts): Properly handle `legend` prop as object when determining bottom padding diff --git a/.changeset/cyan-cougars-occur.md b/.changeset/cyan-cougars-occur.md new file mode 100644 index 000000000..cc2bfe564 --- /dev/null +++ b/.changeset/cyan-cougars-occur.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Axis): Fix display of axis labels diff --git a/.changeset/dark-pandas-start.md b/.changeset/dark-pandas-start.md new file mode 100644 index 000000000..1e6cec44b --- /dev/null +++ b/.changeset/dark-pandas-start.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +Decoupled `ForceSimulation` from `ChartContext`, by taking nodes and links via `data` prop. diff --git a/.changeset/deep-signs-listen.md b/.changeset/deep-signs-listen.md new file mode 100644 index 000000000..516facbb0 --- /dev/null +++ b/.changeset/deep-signs-listen.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Text): Apply `fill: currentColor` to support more straightforward way of changing color (ex. `class="text-red-500"` or `style="color:red"`) diff --git a/.changeset/dirty-kings-send.md b/.changeset/dirty-kings-send.md new file mode 100644 index 000000000..e05461c25 --- /dev/null +++ b/.changeset/dirty-kings-send.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat: Support passing `FormatConfig` (ex. `{ type: '...', options: { ... } }`) anywhere `FormatType` is supported to simplify custom formatting (ex. `variant`) diff --git a/.changeset/early-peaches-accept.md b/.changeset/early-peaches-accept.md new file mode 100644 index 000000000..08fd61a41 --- /dev/null +++ b/.changeset/early-peaches-accept.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Treemap): Add `maintainAspectRatio` (default: false) to opt into tiling function adjustment (primarily for zoom) diff --git a/.changeset/easy-candies-wait.md b/.changeset/easy-candies-wait.md new file mode 100644 index 000000000..cedf5a0c4 --- /dev/null +++ b/.changeset/easy-candies-wait.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(ForceSimulation): Restore performance to at/near Svelte 4 performance (issue #451) diff --git a/.changeset/eight-shirts-cover.md b/.changeset/eight-shirts-cover.md new file mode 100644 index 000000000..3d5ee8795 --- /dev/null +++ b/.changeset/eight-shirts-cover.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +breaking(Legend): Rename `classes.swatches` to `classes.item` diff --git a/.changeset/eighty-islands-jam.md b/.changeset/eighty-islands-jam.md new file mode 100644 index 000000000..4ba875d87 --- /dev/null +++ b/.changeset/eighty-islands-jam.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix: Improve memory leak caused by detached DOM increase when using Canvas rendering due to sometimes still rendering Svg components (ex. `` vs ``) (#490) diff --git a/.changeset/eleven-corners-float.md b/.changeset/eleven-corners-float.md new file mode 100644 index 000000000..f299720bc --- /dev/null +++ b/.changeset/eleven-corners-float.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Highlight): Properly handle area highlights with y-axis time scales diff --git a/.changeset/eleven-crabs-switch.md b/.changeset/eleven-crabs-switch.md new file mode 100644 index 000000000..463b9c74e --- /dev/null +++ b/.changeset/eleven-crabs-switch.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Group): Default `opacity` to `undefined` instead of `1` to allow overriding via class (ex. `opacity-20`) diff --git a/.changeset/eleven-trains-make.md b/.changeset/eleven-trains-make.md new file mode 100644 index 000000000..9b870746c --- /dev/null +++ b/.changeset/eleven-trains-make.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat: Support passing `PeriodTypeCode` strings for simplified date formatting and reduce imports. Example: `format={PeriodType.Day}` is now `format="day"`. Also supported with config object for passing additional options (ex. `format={{ type: 'day', options: { variant: 'long' } }}`). Supported for all `format` props include Axis, Labels, Legend and Tooltip. diff --git a/.changeset/empty-bats-stop.md b/.changeset/empty-bats-stop.md new file mode 100644 index 000000000..d430b42ae --- /dev/null +++ b/.changeset/empty-bats-stop.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Legend): Improve / simplify responsive use cases with additional default classes (center, shrink, truncate) diff --git a/.changeset/empty-buses-flash.md b/.changeset/empty-buses-flash.md new file mode 100644 index 000000000..f1820437d --- /dev/null +++ b/.changeset/empty-buses-flash.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +docs: Add examples for standalone, daisyUI v5, shadcn-svelte v1, Skeleton v3, and Svelte UX v2 (next) (including light/dark theming) diff --git a/.changeset/every-sheep-rush.md b/.changeset/every-sheep-rush.md new file mode 100644 index 000000000..605d85ae3 --- /dev/null +++ b/.changeset/every-sheep-rush.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +Update dependencies diff --git a/.changeset/evil-bags-dance.md b/.changeset/evil-bags-dance.md new file mode 100644 index 000000000..9f170d31d --- /dev/null +++ b/.changeset/evil-bags-dance.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(autoScale): Ignore `null` domain values, fixing some brush examples diff --git a/.changeset/evil-flowers-float.md b/.changeset/evil-flowers-float.md new file mode 100644 index 000000000..424829661 --- /dev/null +++ b/.changeset/evil-flowers-float.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Pattern): Canvas support diff --git a/.changeset/evil-hoops-return.md b/.changeset/evil-hoops-return.md new file mode 100644 index 000000000..eb0bff64f --- /dev/null +++ b/.changeset/evil-hoops-return.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +breaking(Axis): Rename `x="left|right"` and `y="top|bottom"` props with `$` prefix (ex. ``) diff --git a/.changeset/fast-insects-deny.md b/.changeset/fast-insects-deny.md new file mode 100644 index 000000000..146f3f252 --- /dev/null +++ b/.changeset/fast-insects-deny.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(LineChart): Support `orientation="vertical"`. Resolves #640 diff --git a/.changeset/four-taxes-beam.md b/.changeset/four-taxes-beam.md new file mode 100644 index 000000000..cf4a1af96 --- /dev/null +++ b/.changeset/four-taxes-beam.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Calendar): Pass `cellSize` to children snippet (useful when responsive) diff --git a/.changeset/free-teeth-live.md b/.changeset/free-teeth-live.md new file mode 100644 index 000000000..72f6b9a3c --- /dev/null +++ b/.changeset/free-teeth-live.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Axis): Correctly place multiline parts based on placement diff --git a/.changeset/fruity-pillows-agree.md b/.changeset/fruity-pillows-agree.md new file mode 100644 index 000000000..301a63461 --- /dev/null +++ b/.changeset/fruity-pillows-agree.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Axis): Key using tick value instead string representation to support millisecond precision diff --git a/.changeset/full-pens-cheat.md b/.changeset/full-pens-cheat.md new file mode 100644 index 000000000..5b530b4e9 --- /dev/null +++ b/.changeset/full-pens-cheat.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Axis): Filter distinct tick values (useful when manually injecting extra values) diff --git a/.changeset/funny-otters-kick.md b/.changeset/funny-otters-kick.md new file mode 100644 index 000000000..b86a03647 --- /dev/null +++ b/.changeset/funny-otters-kick.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(TooltipContext): Support `band` mode with time scale (similar to band scale) diff --git a/.changeset/funny-wasps-heal.md b/.changeset/funny-wasps-heal.md new file mode 100644 index 000000000..21134b30f --- /dev/null +++ b/.changeset/funny-wasps-heal.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Axis): Add time duration aware tick value/format support diff --git a/.changeset/giant-donuts-yell.md b/.changeset/giant-donuts-yell.md new file mode 100644 index 000000000..70f5ca405 --- /dev/null +++ b/.changeset/giant-donuts-yell.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(BarChart): Radial support (vertical and horizontal) (issue #469) diff --git a/.changeset/green-mirrors-retire.md b/.changeset/green-mirrors-retire.md new file mode 100644 index 000000000..534f0cdd5 --- /dev/null +++ b/.changeset/green-mirrors-retire.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Arc/Text): Arc path labels with inner/outer/middle placement and smart flipping (issue #7) diff --git a/.changeset/grumpy-ties-mix.md b/.changeset/grumpy-ties-mix.md new file mode 100644 index 000000000..3ab91e3a4 --- /dev/null +++ b/.changeset/grumpy-ties-mix.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(AreaChart|LineChart|DefaultTooltip): Handle per-series data with different length diff --git a/.changeset/happy-bats-eat.md b/.changeset/happy-bats-eat.md new file mode 100644 index 000000000..0be3a510e --- /dev/null +++ b/.changeset/happy-bats-eat.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Calendar): Respect `start` instead of always start of year diff --git a/.changeset/heavy-signs-kick.md b/.changeset/heavy-signs-kick.md new file mode 100644 index 000000000..ca1922f39 --- /dev/null +++ b/.changeset/heavy-signs-kick.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(Highlight): Support passing `opacity` diff --git a/.changeset/honest-hoops-peel.md b/.changeset/honest-hoops-peel.md new file mode 100644 index 000000000..a1ef67816 --- /dev/null +++ b/.changeset/honest-hoops-peel.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(LineChart): Change default tooltip mode from `bisect-x` to `quadtree-x` (works with catagorical data and does not require data to be sorted) diff --git a/.changeset/hot-pigs-push.md b/.changeset/hot-pigs-push.md new file mode 100644 index 000000000..a9b288c4c --- /dev/null +++ b/.changeset/hot-pigs-push.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(SimplifiedChart): Still add selected legend item opacity when item classes are also applied diff --git a/.changeset/huge-boats-fix.md b/.changeset/huge-boats-fix.md new file mode 100644 index 000000000..b9c90cb80 --- /dev/null +++ b/.changeset/huge-boats-fix.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +docs: Document each component's context support (svg, canvas, html) with interactive toggle diff --git a/.changeset/huge-regions-live.md b/.changeset/huge-regions-live.md new file mode 100644 index 000000000..b77d54599 --- /dev/null +++ b/.changeset/huge-regions-live.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(Legend): Add `selected` prop to fade out unselected items (if passed and non-empty) diff --git a/.changeset/huge-rocks-sip.md b/.changeset/huge-rocks-sip.md new file mode 100644 index 000000000..919cb606a --- /dev/null +++ b/.changeset/huge-rocks-sip.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +docs: Add non-radial BarChart duration example and improve radial example diff --git a/.changeset/khaki-pugs-hammer.md b/.changeset/khaki-pugs-hammer.md new file mode 100644 index 000000000..948dc5c34 --- /dev/null +++ b/.changeset/khaki-pugs-hammer.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(force-simulation): Fixed a bug that would sometimes keep a simulation running, when its inputs change, even if `alpha < alphaMin` diff --git a/.changeset/kind-shirts-sniff.md b/.changeset/kind-shirts-sniff.md new file mode 100644 index 000000000..7c94ec6de --- /dev/null +++ b/.changeset/kind-shirts-sniff.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat: Add `applyLanes()` array util to support densely packing timelines diff --git a/.changeset/large-spiders-stay.md b/.changeset/large-spiders-stay.md new file mode 100644 index 000000000..3be5f9b84 --- /dev/null +++ b/.changeset/large-spiders-stay.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Rule): Support using as data-driven mark (ex. candlestick, lollipop) by default (`` using Chart accessors) or passing explicit `x`/`y` accessors (ex. ``). Resolves #64 diff --git a/.changeset/late-glasses-itch.md b/.changeset/late-glasses-itch.md new file mode 100644 index 000000000..899395fb8 --- /dev/null +++ b/.changeset/late-glasses-itch.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(TooltipContext): Add `touchEvents` to control touch event behavior. Defaults to `pan-y` to allow vertical scrolling but horizontal scrubbing. diff --git a/.changeset/legal-parrots-beam.md b/.changeset/legal-parrots-beam.md new file mode 100644 index 000000000..9e6df5056 --- /dev/null +++ b/.changeset/legal-parrots-beam.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(Voronoi): Support passing `r` to define a max radius (clip path) diff --git a/.changeset/lemon-bats-change.md b/.changeset/lemon-bats-change.md new file mode 100644 index 000000000..88f901c66 --- /dev/null +++ b/.changeset/lemon-bats-change.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(ForceSimulation): Added `onNodesChange` callback to `ForceSimulation` diff --git a/.changeset/loud-lies-film.md b/.changeset/loud-lies-film.md new file mode 100644 index 000000000..ca568813f --- /dev/null +++ b/.changeset/loud-lies-film.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(Axis): Use `format` to filter ticks (integer and date/time). Helpful to keep ticks above a threshold for wide charts or short durations. diff --git a/.changeset/loud-paws-allow.md b/.changeset/loud-paws-allow.md new file mode 100644 index 000000000..c0e401590 --- /dev/null +++ b/.changeset/loud-paws-allow.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(GeoPath): Improve performance by only using custom geoCurvePath when `curve` overridden diff --git a/.changeset/lovely-loops-ring.md b/.changeset/lovely-loops-ring.md new file mode 100644 index 000000000..51f6c3320 --- /dev/null +++ b/.changeset/lovely-loops-ring.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Highlight): Fix display of lines for first values (`0` coord). Fixes #568 diff --git a/.changeset/lucky-pianos-count.md b/.changeset/lucky-pianos-count.md new file mode 100644 index 000000000..d84e3c5f7 --- /dev/null +++ b/.changeset/lucky-pianos-count.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Tooltip): Use standard CSS classes (non-tailwind) for root element to simplify some usage (including shadcn-svelte) diff --git a/.changeset/mean-flies-play.md b/.changeset/mean-flies-play.md new file mode 100644 index 000000000..0179d960b --- /dev/null +++ b/.changeset/mean-flies-play.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Points): Update `point.x` / `point.y` based on `ctx.radial` to simplify children snippet usage diff --git a/.changeset/mean-loops-peel.md b/.changeset/mean-loops-peel.md new file mode 100644 index 000000000..0b9be2e2c --- /dev/null +++ b/.changeset/mean-loops-peel.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(SeriesState): Add `isHighlighted(seriesKey)` to easy check if series is hightlight (or should be faded) diff --git a/.changeset/mighty-weeks-try.md b/.changeset/mighty-weeks-try.md new file mode 100644 index 000000000..a328ec3c3 --- /dev/null +++ b/.changeset/mighty-weeks-try.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(Axis): Support multiline ticks for time intervals diff --git a/.changeset/modern-nails-kiss.md b/.changeset/modern-nails-kiss.md new file mode 100644 index 000000000..e218d5071 --- /dev/null +++ b/.changeset/modern-nails-kiss.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Treemap): Fix reactivity of props (tile, padding, etc) diff --git a/.changeset/new-turtles-clean.md b/.changeset/new-turtles-clean.md new file mode 100644 index 000000000..b470884ce --- /dev/null +++ b/.changeset/new-turtles-clean.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(BarChart): Support time scale with `xInterval` / `yInterval` props diff --git a/.changeset/nine-badgers-teach.md b/.changeset/nine-badgers-teach.md new file mode 100644 index 000000000..82e551bc9 --- /dev/null +++ b/.changeset/nine-badgers-teach.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(TooltipList): Align label to top (start) instead of center by default diff --git a/.changeset/nine-carrots-grin.md b/.changeset/nine-carrots-grin.md new file mode 100644 index 000000000..2e23d4b92 --- /dev/null +++ b/.changeset/nine-carrots-grin.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Bar): Clamp radius to width/height to not cause artifacts with small values (including `0`) when rounding a single edge. Fixes #383 diff --git a/.changeset/nine-pens-design.md b/.changeset/nine-pens-design.md new file mode 100644 index 000000000..b37e68316 --- /dev/null +++ b/.changeset/nine-pens-design.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat: Add Polygon primitive diff --git a/.changeset/ninety-ghosts-taste.md b/.changeset/ninety-ghosts-taste.md new file mode 100644 index 000000000..a9770cbda --- /dev/null +++ b/.changeset/ninety-ghosts-taste.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +breaking(Blur): Remove children snippet props (not needed and easier to support canvas in the future) diff --git a/.changeset/odd-pears-float.md b/.changeset/odd-pears-float.md new file mode 100644 index 000000000..cbeb7ea96 --- /dev/null +++ b/.changeset/odd-pears-float.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat: New ArcChart component diff --git a/.changeset/old-lions-hide.md b/.changeset/old-lions-hide.md new file mode 100644 index 000000000..16224e791 --- /dev/null +++ b/.changeset/old-lions-hide.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Axis): Fix memory leak and improve performance when tick values are `Date` instances diff --git a/.changeset/open-bushes-run.md b/.changeset/open-bushes-run.md new file mode 100644 index 000000000..68503d128 --- /dev/null +++ b/.changeset/open-bushes-run.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(BarChart): Improve handling time scale for value axis (ex. xScale for horizontal orientation) diff --git a/.changeset/open-houses-vanish.md b/.changeset/open-houses-vanish.md new file mode 100644 index 000000000..607767ff0 --- /dev/null +++ b/.changeset/open-houses-vanish.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Calendar): Support showing month labels without path via `monthLabel` prop (true by default) diff --git a/.changeset/pink-flies-worry.md b/.changeset/pink-flies-worry.md new file mode 100644 index 000000000..2d6e97a4c --- /dev/null +++ b/.changeset/pink-flies-worry.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Calendar|MonthPath): Support canvas by using `Spline` instead of `path` diff --git a/.changeset/pink-hornets-rest.md b/.changeset/pink-hornets-rest.md new file mode 100644 index 000000000..b5bbcd94c --- /dev/null +++ b/.changeset/pink-hornets-rest.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +refactor: Replace `date-fns` usage with existing `d3-time` to reduce bundle size diff --git a/.changeset/pink-showers-hunt.md b/.changeset/pink-showers-hunt.md new file mode 100644 index 000000000..694666ef6 --- /dev/null +++ b/.changeset/pink-showers-hunt.md @@ -0,0 +1,5 @@ +--- +'layerchart': major +--- + +Tailwind 4 support diff --git a/.changeset/polite-parts-learn.md b/.changeset/polite-parts-learn.md new file mode 100644 index 000000000..3a5b58f51 --- /dev/null +++ b/.changeset/polite-parts-learn.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix: Support `opacity` prop/style when Canvas rendered for all primatives diff --git a/.changeset/poor-clocks-occur.md b/.changeset/poor-clocks-occur.md new file mode 100644 index 000000000..c3da59bfd --- /dev/null +++ b/.changeset/poor-clocks-occur.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(LinearGradient): Support Html context diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000..885d52322 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,150 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "layerchart": "1.0.7", + "daisyui-5": "0.0.1", + "shadcn-svelte-1": "0.0.1", + "skeleton-3": "0.0.1", + "standalone": "0.0.1", + "svelteux-2": "0.0.1" + }, + "changesets": [ + "beige-bears-joke", + "beige-doodles-shout", + "better-eagles-scream", + "better-pets-divide", + "big-boxes-shout", + "blue-doodles-chew", + "brave-spies-give", + "breezy-donuts-sniff", + "brown-terms-tie", + "bumpy-breads-rhyme", + "calm-jars-mix", + "chatty-flies-bet", + "chatty-shirts-rule", + "chilly-games-hide", + "chubby-ties-play", + "clean-nights-jog", + "clear-ghosts-arrive", + "clear-points-care", + "cozy-moments-work", + "crazy-ads-appear", + "crazy-friends-talk", + "cruel-cameras-begin", + "cruel-rats-taste", + "curly-lies-write", + "cute-donkeys-greet", + "cyan-cougars-occur", + "dark-pandas-start", + "deep-signs-listen", + "dirty-kings-send", + "early-peaches-accept", + "easy-candies-wait", + "eight-shirts-cover", + "eighty-islands-jam", + "eleven-corners-float", + "eleven-crabs-switch", + "eleven-trains-make", + "empty-bats-stop", + "empty-buses-flash", + "every-sheep-rush", + "evil-bags-dance", + "evil-flowers-float", + "evil-hoops-return", + "fast-insects-deny", + "four-taxes-beam", + "free-teeth-live", + "fruity-pillows-agree", + "full-pens-cheat", + "funny-otters-kick", + "funny-wasps-heal", + "giant-donuts-yell", + "green-mirrors-retire", + "grumpy-ties-mix", + "happy-bats-eat", + "heavy-signs-kick", + "honest-hoops-peel", + "hot-pigs-push", + "huge-boats-fix", + "huge-regions-live", + "huge-rocks-sip", + "khaki-pugs-hammer", + "kind-shirts-sniff", + "large-spiders-stay", + "late-glasses-itch", + "legal-parrots-beam", + "lemon-bats-change", + "loud-lies-film", + "loud-paws-allow", + "lovely-loops-ring", + "lucky-pianos-count", + "mean-flies-play", + "mean-loops-peel", + "mighty-weeks-try", + "modern-nails-kiss", + "new-turtles-clean", + "nine-badgers-teach", + "nine-carrots-grin", + "nine-pens-design", + "ninety-ghosts-taste", + "odd-pears-float", + "old-lions-hide", + "open-bushes-run", + "open-houses-vanish", + "pink-flies-worry", + "pink-hornets-rest", + "pink-showers-hunt", + "polite-parts-learn", + "poor-clocks-occur", + "proud-camels-cut", + "proud-llamas-fold", + "proud-melons-warn", + "public-queens-invite", + "puny-clocks-admire", + "puny-shoes-kiss", + "quiet-insects-share", + "quiet-mangos-kneel", + "rare-hats-happen", + "rare-olives-change", + "ready-pumas-sink", + "real-badgers-say", + "red-monkeys-sleep", + "rich-keys-take", + "sad-chairs-stand", + "shaggy-dryers-make", + "shaky-animals-wave", + "shaky-dots-go", + "sharp-rockets-jam", + "slow-hounds-hide", + "slow-streets-look", + "smart-dots-rule", + "smart-paths-jog", + "social-masks-teach", + "soft-pens-invite", + "solid-badgers-tan", + "some-frogs-camp", + "sour-hounds-repeat", + "spotty-plums-invite", + "spotty-rules-taste", + "swift-gifts-flow", + "tall-mice-tap", + "tall-poems-take", + "tame-lamps-report", + "tangy-lies-strive", + "tasty-states-raise", + "thirty-glasses-pick", + "tricky-nights-mix", + "tricky-pears-help", + "true-waves-roll", + "twelve-frogs-agree", + "violet-gifts-fail", + "violet-horses-walk", + "warm-mammals-deny", + "warm-women-glow", + "weak-donuts-tan", + "whole-women-listen", + "wide-berries-invite", + "witty-clocks-divide" + ] +} diff --git a/.changeset/proud-camels-cut.md b/.changeset/proud-camels-cut.md new file mode 100644 index 000000000..5419a536c --- /dev/null +++ b/.changeset/proud-camels-cut.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(ArcChart): Do not pass y accessor to use linear scale fallback diff --git a/.changeset/proud-llamas-fold.md b/.changeset/proud-llamas-fold.md new file mode 100644 index 000000000..567e9e834 --- /dev/null +++ b/.changeset/proud-llamas-fold.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(ForceSimulation): Refined `onstart`/`ontick`/`onend` events of `ForceSimulation` diff --git a/.changeset/proud-melons-warn.md b/.changeset/proud-melons-warn.md new file mode 100644 index 000000000..1ba9c2912 --- /dev/null +++ b/.changeset/proud-melons-warn.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +breaking(Points): Remove `` prop. Use `` with x/y accessor instead diff --git a/.changeset/public-queens-invite.md b/.changeset/public-queens-invite.md new file mode 100644 index 000000000..169a1d362 --- /dev/null +++ b/.changeset/public-queens-invite.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +refactor: Update `@layerstack/svelte-state` and replace remaining `@layerstack/svelte-stores` usage (media query) (mostly docs related except Canvas) diff --git a/.changeset/puny-clocks-admire.md b/.changeset/puny-clocks-admire.md new file mode 100644 index 000000000..b6849ce31 --- /dev/null +++ b/.changeset/puny-clocks-admire.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(Axis): Support responsive tick counts via `tickSpacing` prop diff --git a/.changeset/puny-shoes-kiss.md b/.changeset/puny-shoes-kiss.md new file mode 100644 index 000000000..26d49cde0 --- /dev/null +++ b/.changeset/puny-shoes-kiss.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Primatives): Apply default classes when using Canvas context (like Svg). Resolves #544 diff --git a/.changeset/quiet-insects-share.md b/.changeset/quiet-insects-share.md new file mode 100644 index 000000000..f5fda5366 --- /dev/null +++ b/.changeset/quiet-insects-share.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Pattern): Simplified definitions via `lines`/`circles` props (issue #472) diff --git a/.changeset/quiet-mangos-kneel.md b/.changeset/quiet-mangos-kneel.md new file mode 100644 index 000000000..6ce35d7be --- /dev/null +++ b/.changeset/quiet-mangos-kneel.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +breaking(Bar): Rename `bar` prop to `data` to better represent usage diff --git a/.changeset/rare-hats-happen.md b/.changeset/rare-hats-happen.md new file mode 100644 index 000000000..1274ca35e --- /dev/null +++ b/.changeset/rare-hats-happen.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Axis): Additional multiline month fix for day ticks diff --git a/.changeset/rare-olives-change.md b/.changeset/rare-olives-change.md new file mode 100644 index 000000000..07271d4e9 --- /dev/null +++ b/.changeset/rare-olives-change.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat: Add `Layer` component to easily switch between Svg, Canavs, and Html layers diff --git a/.changeset/ready-pumas-sink.md b/.changeset/ready-pumas-sink.md new file mode 100644 index 000000000..116e5669c --- /dev/null +++ b/.changeset/ready-pumas-sink.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix: Reduce bundle size by removing culori as transitive dependency diff --git a/.changeset/real-badgers-say.md b/.changeset/real-badgers-say.md new file mode 100644 index 000000000..4741a5422 --- /dev/null +++ b/.changeset/real-badgers-say.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(ScatterChart): Support color scales based on value (such as threshold) diff --git a/.changeset/red-monkeys-sleep.md b/.changeset/red-monkeys-sleep.md new file mode 100644 index 000000000..0a4f5f693 --- /dev/null +++ b/.changeset/red-monkeys-sleep.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +refactor: Remove LayerCake dependency (issue #432) diff --git a/.changeset/rich-keys-take.md b/.changeset/rich-keys-take.md new file mode 100644 index 000000000..f6ec4b7ca --- /dev/null +++ b/.changeset/rich-keys-take.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(TooltipContext): Support `quadtree` mode for geo visualizations diff --git a/.changeset/sad-chairs-stand.md b/.changeset/sad-chairs-stand.md new file mode 100644 index 000000000..4a0a7f9d8 --- /dev/null +++ b/.changeset/sad-chairs-stand.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(Chart): Add `xInterval` / `yInterval` for time scales usage with bar charts diff --git a/.changeset/shaggy-dryers-make.md b/.changeset/shaggy-dryers-make.md new file mode 100644 index 000000000..2cdaae9bd --- /dev/null +++ b/.changeset/shaggy-dryers-make.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Highlight): Support radial area (issue #112) diff --git a/.changeset/shaky-animals-wave.md b/.changeset/shaky-animals-wave.md new file mode 100644 index 000000000..c12b44a44 --- /dev/null +++ b/.changeset/shaky-animals-wave.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix: Update dependencies, notable @layerstack/utils with improved metric number formatting diff --git a/.changeset/shaky-dots-go.md b/.changeset/shaky-dots-go.md new file mode 100644 index 000000000..343bc0d6c --- /dev/null +++ b/.changeset/shaky-dots-go.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat(Chart): Automatically determine scale based on data and domain values (instead of defaulting to scaleLinear) diff --git a/.changeset/sharp-rockets-jam.md b/.changeset/sharp-rockets-jam.md new file mode 100644 index 000000000..8f9026ba7 --- /dev/null +++ b/.changeset/sharp-rockets-jam.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(AreaChart): Change default tooltip mode from `bisect-x` to `quadtree-x` (works with catagorical data and does not require data to be sorted) diff --git a/.changeset/slow-hounds-hide.md b/.changeset/slow-hounds-hide.md new file mode 100644 index 000000000..6a372ffbb --- /dev/null +++ b/.changeset/slow-hounds-hide.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(ForceSimulation): Expose default values by exporting them as constants diff --git a/.changeset/slow-streets-look.md b/.changeset/slow-streets-look.md new file mode 100644 index 000000000..c12c6d366 --- /dev/null +++ b/.changeset/slow-streets-look.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat: Update `applyLanes()` util to support nested string key and function accessors for start/end properties diff --git a/.changeset/smart-dots-rule.md b/.changeset/smart-dots-rule.md new file mode 100644 index 000000000..21cf6b27f --- /dev/null +++ b/.changeset/smart-dots-rule.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Spline): Only re-draw on data/path changes and not other context (such as width/height). Fixes #504 diff --git a/.changeset/smart-paths-jog.md b/.changeset/smart-paths-jog.md new file mode 100644 index 000000000..7e3e15b93 --- /dev/null +++ b/.changeset/smart-paths-jog.md @@ -0,0 +1,5 @@ +--- +'layerchart': major +--- + +feat: Migrate to Svelte 5 runes/snippets (issue #159) diff --git a/.changeset/social-masks-teach.md b/.changeset/social-masks-teach.md new file mode 100644 index 000000000..600de3243 --- /dev/null +++ b/.changeset/social-masks-teach.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(Axis): Fix multiline month when day tick does not align on first of month diff --git a/.changeset/soft-pens-invite.md b/.changeset/soft-pens-invite.md new file mode 100644 index 000000000..d77cfb30a --- /dev/null +++ b/.changeset/soft-pens-invite.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix: Update `dagreAncestors()` and `dagreDescendants()` util types diff --git a/.changeset/solid-badgers-tan.md b/.changeset/solid-badgers-tan.md new file mode 100644 index 000000000..9cf44c755 --- /dev/null +++ b/.changeset/solid-badgers-tan.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix: Add `applyLanes()` as top-level export diff --git a/.changeset/some-frogs-camp.md b/.changeset/some-frogs-camp.md new file mode 100644 index 000000000..59a9b5148 --- /dev/null +++ b/.changeset/some-frogs-camp.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat: Add classes for underlying element styling diff --git a/.changeset/sour-hounds-repeat.md b/.changeset/sour-hounds-repeat.md new file mode 100644 index 000000000..ddade1f2a --- /dev/null +++ b/.changeset/sour-hounds-repeat.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix: Resolves "Target div has zero or negative height" console warning (issue #291) diff --git a/.changeset/spotty-plums-invite.md b/.changeset/spotty-plums-invite.md new file mode 100644 index 000000000..1214fb5bc --- /dev/null +++ b/.changeset/spotty-plums-invite.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +refactor: Remove use of `layerClass` and apply `lc-{name}` class directly to allow easy component diff --git a/examples/standalone/static/robots.txt b/examples/standalone/static/robots.txt new file mode 100644 index 000000000..b6dd6670c --- /dev/null +++ b/examples/standalone/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/examples/standalone/svelte.config.js b/examples/standalone/svelte.config.js new file mode 100644 index 000000000..612cde971 --- /dev/null +++ b/examples/standalone/svelte.config.js @@ -0,0 +1,12 @@ +import adapter from '@sveltejs/adapter-cloudflare'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + kit: { adapter: adapter() } +}; + +export default config; diff --git a/examples/standalone/tsconfig.json b/examples/standalone/tsconfig.json new file mode 100644 index 000000000..a5567ee6b --- /dev/null +++ b/examples/standalone/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/examples/standalone/vite.config.ts b/examples/standalone/vite.config.ts new file mode 100644 index 000000000..a5c0236d3 --- /dev/null +++ b/examples/standalone/vite.config.ts @@ -0,0 +1,7 @@ +import devtoolsJson from 'vite-plugin-devtools-json'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit(), devtoolsJson()] +}); diff --git a/examples/svelte-ux-2/.gitignore b/examples/svelte-ux-2/.gitignore new file mode 100644 index 000000000..3b462cb0c --- /dev/null +++ b/examples/svelte-ux-2/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/examples/svelte-ux-2/.npmrc b/examples/svelte-ux-2/.npmrc new file mode 100644 index 000000000..b6f27f135 --- /dev/null +++ b/examples/svelte-ux-2/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/examples/svelte-ux-2/.prettierignore b/examples/svelte-ux-2/.prettierignore new file mode 100644 index 000000000..7d74fe246 --- /dev/null +++ b/examples/svelte-ux-2/.prettierignore @@ -0,0 +1,9 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ diff --git a/examples/svelte-ux-2/.prettierrc b/examples/svelte-ux-2/.prettierrc new file mode 100644 index 000000000..8103a0b5d --- /dev/null +++ b/examples/svelte-ux-2/.prettierrc @@ -0,0 +1,16 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ], + "tailwindStylesheet": "./src/app.css" +} diff --git a/examples/svelte-ux-2/README.md b/examples/svelte-ux-2/README.md new file mode 100644 index 000000000..75842c404 --- /dev/null +++ b/examples/svelte-ux-2/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/examples/svelte-ux-2/package.json b/examples/svelte-ux-2/package.json new file mode 100644 index 000000000..22922152b --- /dev/null +++ b/examples/svelte-ux-2/package.json @@ -0,0 +1,34 @@ +{ + "name": "svelteux-2", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check ." + }, + "devDependencies": { + "@layerstack/tailwind": "2.0.0-next.18", + "@sveltejs/adapter-cloudflare": "^7.0.0", + "@sveltejs/kit": "^2.22.0", + "@sveltejs/vite-plugin-svelte": "^6.0.0", + "@tailwindcss/vite": "^4.0.0", + "layerchart": "workspace:*", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.11", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "svelte-ux": "2.0.0-next.19", + "tailwindcss": "^4.0.0", + "typescript": "^5.0.0", + "vite": "^7.0.4", + "vite-plugin-devtools-json": "^1.0.0" + } +} diff --git a/examples/svelte-ux-2/src/app.css b/examples/svelte-ux-2/src/app.css new file mode 100644 index 000000000..cc998d863 --- /dev/null +++ b/examples/svelte-ux-2/src/app.css @@ -0,0 +1,6 @@ +@import 'tailwindcss'; +@import '@layerstack/tailwind/core.css'; +@import '@layerstack/tailwind/utils.css'; +@import '@layerstack/tailwind/themes/basic.css'; + +@source '../node_modules/svelte-ux/dist'; diff --git a/examples/svelte-ux-2/src/app.d.ts b/examples/svelte-ux-2/src/app.d.ts new file mode 100644 index 000000000..da08e6da5 --- /dev/null +++ b/examples/svelte-ux-2/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/examples/svelte-ux-2/src/app.html b/examples/svelte-ux-2/src/app.html new file mode 100644 index 000000000..f273cc58f --- /dev/null +++ b/examples/svelte-ux-2/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/examples/svelte-ux-2/src/lib/assets/favicon.svg b/examples/svelte-ux-2/src/lib/assets/favicon.svg new file mode 100644 index 000000000..cc5dc66a3 --- /dev/null +++ b/examples/svelte-ux-2/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/examples/svelte-ux-2/src/lib/index.ts b/examples/svelte-ux-2/src/lib/index.ts new file mode 100644 index 000000000..856f2b6c3 --- /dev/null +++ b/examples/svelte-ux-2/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/examples/svelte-ux-2/src/routes/+layout.svelte b/examples/svelte-ux-2/src/routes/+layout.svelte new file mode 100644 index 000000000..61be2d59a --- /dev/null +++ b/examples/svelte-ux-2/src/routes/+layout.svelte @@ -0,0 +1,24 @@ + + + + + + + + +
+
+ +
+ + {@render children?.()} +
diff --git a/examples/svelte-ux-2/src/routes/+page.svelte b/examples/svelte-ux-2/src/routes/+page.svelte new file mode 100644 index 000000000..c40c98626 --- /dev/null +++ b/examples/svelte-ux-2/src/routes/+page.svelte @@ -0,0 +1,62 @@ + + +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
diff --git a/examples/svelte-ux-2/static/robots.txt b/examples/svelte-ux-2/static/robots.txt new file mode 100644 index 000000000..b6dd6670c --- /dev/null +++ b/examples/svelte-ux-2/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/examples/svelte-ux-2/svelte.config.js b/examples/svelte-ux-2/svelte.config.js new file mode 100644 index 000000000..612cde971 --- /dev/null +++ b/examples/svelte-ux-2/svelte.config.js @@ -0,0 +1,12 @@ +import adapter from '@sveltejs/adapter-cloudflare'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + kit: { adapter: adapter() } +}; + +export default config; diff --git a/examples/svelte-ux-2/tsconfig.json b/examples/svelte-ux-2/tsconfig.json new file mode 100644 index 000000000..a5567ee6b --- /dev/null +++ b/examples/svelte-ux-2/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/examples/svelte-ux-2/vite.config.ts b/examples/svelte-ux-2/vite.config.ts new file mode 100644 index 000000000..7be426bb5 --- /dev/null +++ b/examples/svelte-ux-2/vite.config.ts @@ -0,0 +1,8 @@ +import devtoolsJson from 'vite-plugin-devtools-json'; +import tailwindcss from '@tailwindcss/vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit(), devtoolsJson()] +}); diff --git a/package.json b/package.json index 1a85294bc..60059423c 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,17 @@ "author": "Sean Lynch ", "license": "MIT", "type": "module", + "homepage": "https://layerchart.com", "scripts": { + "dev": "pnpm -r dev", "test:unit": "pnpm -r test:unit", - "build": "rimraf packages/*/dist && pnpm -r build", - "package": "pnpm -r package", - "check": "pnpm -r check", - "lint": "pnpm -r lint", + "build:packages": "rimraf packages/*/dist && pnpm --filter './packages/*' build", + "build:examples": "rimraf packages/*/dist && pnpm --filter './packages/*' package && pnpm --filter './examples/*' build", + "package": "pnpm --filter './packages/*' package", + "check:packages": "pnpm --filter './packages/*' check", + "check:examples": "pnpm --filter './examples/*' check", + "lint:packages": "pnpm --filter './packages/*' lint", + "lint:examples": "pnpm --filter './examples/*' lint", "format": "pnpm -r format", "changeset": "changeset", "changeset:version": "changeset version", @@ -17,10 +22,15 @@ "up-deps": "pnpm update -r -i --latest" }, "devDependencies": { - "@changesets/cli": "2.29.4", + "@changesets/cli": "2.29.6", "@svitejs/changesets-changelog-github-compact": "^1.2.0", "rimraf": "6.0.1", - "wrangler": "^4.14.4" + "wrangler": "^4.30.0" }, - "packageManager": "pnpm@9.1.1" + "packageManager": "pnpm@9.1.1", + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild" + ] + } } diff --git a/packages/layerchart/CHANGELOG.md b/packages/layerchart/CHANGELOG.md index 9989cef5b..200e996f4 100644 --- a/packages/layerchart/CHANGELOG.md +++ b/packages/layerchart/CHANGELOG.md @@ -1,10 +1,452 @@ # LayerChart -## 1.0.12 +## 2.0.0-next.43 ### Patch Changes -- fix(Axis): Fix reactivity issue with xRange/yRange in Svelte 5.34+. Fixes #641 ([#643](https://github.com/techniq/layerchart/pull/643)) +- fix(Highlight|TooltipContext): Support xInterval / yInterval ([#449](https://github.com/techniq/layerchart/pull/449)) + +## 2.0.0-next.42 + +### Patch Changes + +- fix(Calendar): Respect `start` instead of always start of year ([#657](https://github.com/techniq/layerchart/pull/657)) + +## 2.0.0-next.41 + +### Patch Changes + +- fix(Tooltip): Correctly set tooltip position on chart enter and exit ([#655](https://github.com/techniq/layerchart/pull/655)) + +## 2.0.0-next.40 + +### Patch Changes + +- fix(LineChart): Restore passing xScale / yScale overrides ([#449](https://github.com/techniq/layerchart/pull/449)) + +## 2.0.0-next.39 + +### Minor Changes + +- feat: Support css-only usage (no Tailwind required) while retaining first-class Tailwind support ([#557](https://github.com/techniq/layerchart/pull/557)) + +### Patch Changes + +- feat: Simplify daisyUI, shadcn-svelte, and Skeleton integrations with single line `@import 'layerchart/{library}.css'` added to `app.css` ([#557](https://github.com/techniq/layerchart/pull/557)) + +- docs: Add examples for standalone, daisyUI v5, shadcn-svelte v1, Skeleton v3, and Svelte UX v2 (next) (including light/dark theming) ([#557](https://github.com/techniq/layerchart/pull/557)) + +- feat(LineChart): Support `orientation="vertical"`. Resolves #640 ([#557](https://github.com/techniq/layerchart/pull/557)) + +- feat: Add Html context support for applicable primitives such as Circle, Line, Rect, Text (and more) as well as transitively such as Axis, Grid, Labels (and more) ([#557](https://github.com/techniq/layerchart/pull/557)) + +- feat(LinearGradient): Support Html context ([#557](https://github.com/techniq/layerchart/pull/557)) + +- fix(Text): Apply `fill: currentColor` to support more straightforward way of changing color (ex. `class="text-red-500"` or `style="color:red"`) ([#557](https://github.com/techniq/layerchart/pull/557)) + +- fix(TooltipContext): Revert back to pointer events (instead of mouse/touch) but with `touch-action: pan-y`. Provides simplified events while allowing horizontal scrubbing with vertical scrolling. ([#557](https://github.com/techniq/layerchart/pull/557)) + +- feat(TooltipContext): Add `touchEvents` to control touch event behavior. Defaults to `pan-y` to allow vertical scrolling but horizontal scrubbing. ([#557](https://github.com/techniq/layerchart/pull/557)) + +- fix(TooltipContext): Fix `band` mode regression when both x/y are scaleBand (ex. punchcard chart) ([#557](https://github.com/techniq/layerchart/pull/557)) + +- fix(SimplifiedCharts): Properly handle `legend` prop as object when determining bottom padding ([#557](https://github.com/techniq/layerchart/pull/557)) + +- fix(AreaChart|LineChart|DefaultTooltip): Handle per-series data with different length ([#557](https://github.com/techniq/layerchart/pull/557)) + +- feat(Highlight): Support passing `opacity` ([#557](https://github.com/techniq/layerchart/pull/557)) + +- fix(SimplifiedChart): Still add selected legend item opacity when item classes are also applied ([#557](https://github.com/techniq/layerchart/pull/557)) + +- feat(Legend): Add `selected` prop to fade out unselected items (if passed and non-empty) ([#557](https://github.com/techniq/layerchart/pull/557)) + +- feat(SeriesState): Add `isHighlighted(seriesKey)` to easy check if series is hightlight (or should be faded) ([#557](https://github.com/techniq/layerchart/pull/557)) + +- fix(Primatives): Apply default classes when using Canvas context (like Svg). Resolves #544 ([#557](https://github.com/techniq/layerchart/pull/557)) + +- refactor: Remove use of `layerClass` and apply `lc-{name}` class directly to allow easy component diff --git a/packages/layerchart/src/lib/components/AnnotationPoint.svelte b/packages/layerchart/src/lib/components/AnnotationPoint.svelte new file mode 100644 index 000000000..ca6140a72 --- /dev/null +++ b/packages/layerchart/src/lib/components/AnnotationPoint.svelte @@ -0,0 +1,137 @@ + + + + + + +{#if label} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/AnnotationRange.svelte b/packages/layerchart/src/lib/components/AnnotationRange.svelte new file mode 100644 index 000000000..af2aeba8b --- /dev/null +++ b/packages/layerchart/src/lib/components/AnnotationRange.svelte @@ -0,0 +1,161 @@ + + + + +{#if fill || className} + +{/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/Arc.svelte b/packages/layerchart/src/lib/components/Arc.svelte index 8374c784a..0ab56c07a 100644 --- a/packages/layerchart/src/lib/components/Arc.svelte +++ b/packages/layerchart/src/lib/components/Arc.svelte @@ -1,3 +1,170 @@ + + {#if track} {/if} { - if (tooltip) { - // Prevent touch to not interfer with pointer when using tooltip - e.preventDefault(); - } + ontouchmove?.(e); + if (!tooltipContext) return; + // Prevent touch to not interfere with pointer when using tooltip + e.preventDefault(); }} - on:touchmove /> - +{@render children?.({ + centroid: trackArcCentroid, + boundingBox, + value: motionEndAngle.current, + getTrackTextProps: getTrackTextProps, + getArcTextProps: getArcTextProps, +})} diff --git a/packages/layerchart/src/lib/components/Area.svelte b/packages/layerchart/src/lib/components/Area.svelte index 3defc551f..456ead7ab 100644 --- a/packages/layerchart/src/lib/components/Area.svelte +++ b/packages/layerchart/src/lib/components/Area.svelte @@ -1,160 +1,199 @@ - - $: xOffset = isScaleBand($xScale) ? $xScale.bandwidth() / 2 : 0; - $: yOffset = isScaleBand($yScale) ? $yScale.bandwidth() / 2 : 0; + {#if line} @@ -214,25 +245,20 @@ y={y1} {curve} {defined} - {tweened} - {...typeof line === 'object' ? line : null} + {motion} + {...extractLayerProps(line, 'lc-area-line')} /> {/if} -{#if renderContext === 'svg'} - +{#if renderCtx === 'svg'} {/if} diff --git a/packages/layerchart/src/lib/components/Axis.svelte b/packages/layerchart/src/lib/components/Axis.svelte index 74d7f8522..e057eb93e 100644 --- a/packages/layerchart/src/lib/components/Axis.svelte +++ b/packages/layerchart/src/lib/components/Axis.svelte @@ -1,161 +1,320 @@ - + + - + {#if rule !== false} - {@const ruleProps = typeof rule === 'object' ? rule : null} {/if} - {#if label} + {#if typeof label === 'function'} + {@render label({ props: resolvedLabelProps })} + {:else if label} {/if} - {#each tickVals as tick, index (tick)} - {@const tickCoords = getCoords(tick, $xRange, $yRange)} + {#each tickVals as tick, index (tick.valueOf())} + {@const tickCoords = getCoords(tick)} {@const [radialTickCoordsX, radialTickCoordsY] = pointRadial(tickCoords.x, tickCoords.y)} {@const [radialTickMarkCoordsX, radialTickMarkCoordsY] = pointRadial( tickCoords.x, @@ -266,68 +448,98 @@ {@const resolvedTickLabelProps = { x: orientation === 'angle' ? radialTickCoordsX : tickCoords.x, y: orientation === 'angle' ? radialTickCoordsY : tickCoords.y, - value: formatValue(tick, format ?? _scale.tickFormat?.() ?? ((v) => v)), + value: tickFormat(tick, index), ...getDefaultTickLabelProps(tick), - tweened, - spring, + motion, + // complement 10px text (until Text supports custom styles) + capHeight: '7px', + lineHeight: '11px', ...tickLabelProps, - class: cls( - 'tickLabel text-[10px] stroke-surface-100 [stroke-width:2px] font-light', - classes.tickLabel, - tickLabelProps?.class - ), + class: cls('lc-axis-tick-label', classes.tickLabel, tickLabelProps?.class), }} - + {#if grid !== false} - {@const ruleProps = typeof grid === 'object' ? grid : null} {/if} - - {#if orientation === 'horizontal'} - - {:else if orientation === 'vertical'} - - {:else if orientation === 'angle'} - + {#if tickMarks} + {@const tickClasses = cls('lc-axis-tick', classes.tick)} + {#if orientation === 'horizontal'} + + {:else if orientation === 'vertical'} + + {:else if orientation === 'angle'} + + {/if} {/if} - - + {#if tickLabel} + {@render tickLabel({ props: resolvedTickLabelProps, index })} + {:else} - - + {/if} + {/each} - +
+ + diff --git a/packages/layerchart/src/lib/components/Bar.svelte b/packages/layerchart/src/lib/components/Bar.svelte index 8b765bc85..f9cb0f318 100644 --- a/packages/layerchart/src/lib/components/Bar.svelte +++ b/packages/layerchart/src/lib/components/Bar.svelte @@ -1,152 +1,217 @@ + + -{#if _rounded === 'all' || _rounded === 'none' || radius === 0} +{#if ctx.radial} + +{:else if rounded === 'all' || rounded === 'none' || radius === 0} {:else} + {@const tweenMotion = extractTweenConfig(motion)} {/if} diff --git a/packages/layerchart/src/lib/components/Bars.svelte b/packages/layerchart/src/lib/components/Bars.svelte index 681bdb1df..66b0f693b 100644 --- a/packages/layerchart/src/lib/components/Bars.svelte +++ b/packages/layerchart/src/lib/components/Bars.svelte @@ -1,78 +1,71 @@ - - /** Inset the rect for amount of padding. Useful with multiple bars (bullet, overlap, etc) */ - export let insets: Insets | undefined = undefined; + - - - {#each _data as d, i (key(d, i))} + + {#if children} + {@render children()} + {:else} + {#each data as d, i (key(d, i))} onbarclick(e, { data: d })} - {...$$restProps} + {strokeWidth} + {stroke} + fill={fill ?? (ctx.config.c ? ctx.cGet(d) : null)} + onclick={(e) => onBarClick(e, { data: d })} + {...extractLayerProps(restProps, 'lc-bars-bar')} /> {/each} - - + {/if} + diff --git a/packages/layerchart/src/lib/components/Blur.svelte b/packages/layerchart/src/lib/components/Blur.svelte index 868ed9ad3..d7485adb4 100644 --- a/packages/layerchart/src/lib/components/Blur.svelte +++ b/packages/layerchart/src/lib/components/Blur.svelte @@ -1,18 +1,49 @@ + + - - - - - +{#if renderContext === 'svg'} + + + + + -{#if $$slots.default} - - - + {#if children} + + {@render children()} + + {/if} +{:else if children} + {@render children()} {/if} diff --git a/packages/layerchart/src/lib/components/Bounds.svelte b/packages/layerchart/src/lib/components/Bounds.svelte index c309eff31..321e4870e 100644 --- a/packages/layerchart/src/lib/components/Bounds.svelte +++ b/packages/layerchart/src/lib/components/Bounds.svelte @@ -1,26 +1,38 @@ + + - +{@render children?.({ xScale: xScale.current, yScale: yScale.current })} diff --git a/packages/layerchart/src/lib/components/BrushContext.svelte b/packages/layerchart/src/lib/components/BrushContext.svelte index ac62c7a4b..1eaee348e 100644 --- a/packages/layerchart/src/lib/components/BrushContext.svelte +++ b/packages/layerchart/src/lib/components/BrushContext.svelte @@ -1,25 +1,24 @@ - {#if disabled} - + {@render children?.({ brushContext })} {:else}
selectAll()} bind:this={rootEl} + style:top="{ctx.padding.top}px" + style:left="{ctx.padding.left}px" + style:width="{ctx.width}px" + style:height="{ctx.height}px" + class={cls('lc-brush-context')} + onpointerdown={createRange} + ondblclick={() => selectAll()} >
- + {@render children?.({ brushContext })}
- {#if isActive} + {#if brushContext.isActive}
reset()} + class={cls('lc-brush-range', classes.range, range?.class)} + onpointerdown={adjustRange} + ondblclick={() => reset()} >
{#if axis === 'both' || axis === 'y'} @@ -387,20 +502,14 @@ style:top="{_range.y}px" style:width="{_range.width}px" style:height="{handleSize}px" - class={cls( - 'handle top', - 'cursor-ns-resize select-none', - 'range absolute', - 'z-10', - classes.handle, - handle?.class - )} - on:pointerdown={adjustTop} - on:dblclick={(e) => { + data-position="top" + class={cls('lc-brush-handle', classes.handle, handle?.class)} + onpointerdown={adjustTop} + ondblclick={(e) => { e.stopPropagation(); if (yDomain) { yDomain[0] = yDomainMin; - onchange({ xDomain, yDomain }); + onChange({ xDomain, yDomain }); } }} >
@@ -411,20 +520,14 @@ style:top="{bottom - handleSize}px" style:width="{_range.width}px" style:height="{handleSize}px" - class={cls( - 'handle bottom', - 'cursor-ns-resize select-none', - 'range absolute', - 'z-10', - classes.handle, - handle?.class - )} - on:pointerdown={adjustBottom} - on:dblclick={(e) => { + data-position="bottom" + class={cls('lc-brush-handle', classes.handle, handle?.class)} + onpointerdown={adjustBottom} + ondblclick={(e) => { e.stopPropagation(); if (yDomain) { yDomain[1] = yDomainMax; - onchange({ xDomain, yDomain }); + onChange({ xDomain, yDomain }); } }} > @@ -437,20 +540,14 @@ style:top="{_range.y}px" style:width="{handleSize}px" style:height="{_range.height}px" - class={cls( - 'handle left', - 'cursor-ew-resize select-none', - 'range absolute', - 'z-10', - classes.handle, - handle?.class - )} - on:pointerdown={adjustLeft} - on:dblclick={(e) => { + data-position="left" + class={cls('lc-brush-handle', classes.handle, handle?.class)} + onpointerdown={adjustLeft} + ondblclick={(e) => { e.stopPropagation(); if (xDomain) { xDomain[0] = xDomainMin; - onchange({ xDomain, yDomain }); + onChange({ xDomain, yDomain }); } }} > @@ -461,20 +558,14 @@ style:top="{_range.y}px" style:width="{handleSize}px" style:height="{_range.height}px" - class={cls( - 'handle right', - 'cursor-ew-resize select-none', - 'range absolute', - 'z-10', - classes.handle, - handle?.class - )} - on:pointerdown={adjustRight} - on:dblclick={(e) => { + data-position="right" + class={cls('lc-brush-handle', classes.handle, handle?.class)} + onpointerdown={adjustRight} + ondblclick={(e) => { e.stopPropagation(); if (xDomain) { xDomain[1] = xDomainMax; - onchange({ xDomain, yDomain }); + onChange({ xDomain: xDomain, yDomain: yDomain }); } }} > @@ -482,3 +573,40 @@ {/if} {/if} + + diff --git a/packages/layerchart/src/lib/components/Calendar.svelte b/packages/layerchart/src/lib/components/Calendar.svelte index 10fc52146..8d79a7759 100644 --- a/packages/layerchart/src/lib/components/Calendar.svelte +++ b/packages/layerchart/src/lib/components/Calendar.svelte @@ -1,90 +1,173 @@ + + - +{#if children} + {@render children({ cells, cellSize })} +{:else} {#each cells as cell} tooltip?.show(e, cell.data)} onpointerleave={(e) => tooltip?.hide()} - class="stroke-surface-content/5" - {...$$restProps} + strokeWidth={1} + {...extractLayerProps(restProps, 'lc-calendar-cell')} /> {/each} - +{/if} {#if monthPath} {#each yearMonths as date} + {/each} +{/if} +{#if monthLabel} + {#each yearMonths as date} {/each} {/if} + + diff --git a/packages/layerchart/src/lib/components/Chart.svelte b/packages/layerchart/src/lib/components/Chart.svelte index ae07c565f..cce083856 100644 --- a/packages/layerchart/src/lib/components/Chart.svelte +++ b/packages/layerchart/src/lib/components/Chart.svelte @@ -1,497 +1,1437 @@ - - - /** Props passed to BrushContext */ - brush?: typeof brush; + + $effect(() => { + if (!isMounted) return; + onResize?.({ + width: context.width, + height: context.height, + containerWidth: context.containerWidth, + containerHeight: context.containerHeight, + }); + }); - - - - {@const initialTransform = + const initialTransform = $derived( geo?.applyTransform?.includes('translate') && geo?.fitGeojson && geo?.projection ? geoFitObjectTransform(geo.projection(), [width, height], geo.fitGeojson) - : undefined} - - { + if (!geo) return undefined; + return (x: number, y: number, deltaX: number, deltaY: number) => { + if (geo.applyTransform?.includes('rotate') && geoContext?.projection) { + // When applying transform to rotate, invert `y` values and reduce sensitivity based on projection scale + // see: https://observablehq.com/@benoldenburg/simple-globe and https://observablehq.com/@michael-keith/draggable-globe-in-d3 + const projectionScale = geoContext.projection.scale() ?? 0; + const sensitivity = 75; + return { + x: x + deltaX * (sensitivity / projectionScale), + y: y + deltaY * (sensitivity / projectionScale) * -1, + }; + } else { + // Apply default TransformContext.processTransform (passing `undefined` below appears to not work when checking for `geo?.applyTransform` exists) + return { x: x + deltaX, y: y + deltaY }; + } + }; + }); + + const brushProps = $derived(typeof brush === 'object' ? brush : { disabled: !brush }); + const tooltipProps = $derived(typeof tooltip === 'object' ? tooltip : {}); + + +{#if ssr === true || typeof window !== 'undefined'} +
{#key isMounted} + { - if (geo.applyTransform?.includes('rotate')) { - // When applying transform to rotate, invert `y` values and reduce sensitivity based on projection scale - // see: https://observablehq.com/@benoldenburg/simple-globe and https://observablehq.com/@michael-keith/draggable-globe-in-d3 - // @ts-expect-error - const projectionScale = $geoProjection.scale(); - const sensitivity = 75; - return { - x: x + deltaX * (sensitivity / projectionScale), - y: y + deltaY * (sensitivity / projectionScale) * -1, - }; - } else { - // Apply default TransformContext.processTransform (passing `undefined` below appears to not work when checking for `geo?.applyTransform` exists) - return { x: x + deltaX, y: y + deltaY }; - } - } - : undefined} + {processTranslate} {...transform} - let:transform={_transform} {ondragstart} - {ontransform} + {onTransform} {ondragend} > - - {@const brushProps = typeof brush === 'object' ? brush : { disabled: !brush }} - - {@const tooltipProps = typeof tooltip === 'object' ? tooltip : {}} - - + + + + + + + {@render _children?.({ + context, + })} {/key} - - +
+{/if} + + diff --git a/packages/layerchart/src/lib/components/ChartClipPath.svelte b/packages/layerchart/src/lib/components/ChartClipPath.svelte index f00952a22..e888e7d1f 100644 --- a/packages/layerchart/src/lib/components/ChartClipPath.svelte +++ b/packages/layerchart/src/lib/components/ChartClipPath.svelte @@ -1,23 +1,45 @@ + + - - + height={ctx.height + (full ? (ctx.padding?.top ?? 0) + (ctx.padding?.bottom ?? 0) : 0)} + width={ctx.width + (full ? (ctx.padding?.left ?? 0) + (ctx.padding?.right ?? 0) : 0)} + {...extractLayerProps(restProps, 'lc-chart-clip-path')} +/> diff --git a/packages/layerchart/src/lib/components/ChartContext.svelte b/packages/layerchart/src/lib/components/ChartContext.svelte deleted file mode 100644 index b74f0abcf..000000000 --- a/packages/layerchart/src/lib/components/ChartContext.svelte +++ /dev/null @@ -1,295 +0,0 @@ - - - - - - diff --git a/packages/layerchart/src/lib/components/Circle.svelte b/packages/layerchart/src/lib/components/Circle.svelte index 681ebc003..eab4c0542 100644 --- a/packages/layerchart/src/lib/components/Circle.svelte +++ b/packages/layerchart/src/lib/components/Circle.svelte @@ -1,53 +1,107 @@ + + -{#if renderContext === 'svg'} - +{#if renderCtx === 'svg'} +{:else if renderCtx === 'html'} +
{/if} + + diff --git a/packages/layerchart/src/lib/components/CircleClipPath.svelte b/packages/layerchart/src/lib/components/CircleClipPath.svelte index bd8f8832e..144a2627f 100644 --- a/packages/layerchart/src/lib/components/CircleClipPath.svelte +++ b/packages/layerchart/src/lib/components/CircleClipPath.svelte @@ -1,25 +1,92 @@ - + + - - - + + {#snippet clip()} + + {/snippet} diff --git a/packages/layerchart/src/lib/components/ClipPath.svelte b/packages/layerchart/src/lib/components/ClipPath.svelte index 90cd83dbe..bd19a6891 100644 --- a/packages/layerchart/src/lib/components/ClipPath.svelte +++ b/packages/layerchart/src/lib/components/ClipPath.svelte @@ -1,33 +1,82 @@ + + - - - +{#if renderContext === 'svg'} + + + {@render clip?.({ id })} - {#if useId} - - {/if} - - + {#if useId} + + {/if} + + +{/if} -{#if $$slots.default} - {#if disabled} - +{#if children} + {#if disabled || renderContext !== 'svg'} + {@render children({ id, url, useId })} {:else} - - - + + {@render children({ id, url, useId })} {/if} {/if} diff --git a/packages/layerchart/src/lib/components/ColorRamp.svelte b/packages/layerchart/src/lib/components/ColorRamp.svelte index 834b4d780..0a50a4c5f 100644 --- a/packages/layerchart/src/lib/components/ColorRamp.svelte +++ b/packages/layerchart/src/lib/components/ColorRamp.svelte @@ -1,21 +1,87 @@ + + - + diff --git a/packages/layerchart/src/lib/components/ComputedStyles.svelte b/packages/layerchart/src/lib/components/ComputedStyles.svelte index 2c8fb4324..f0c441cd9 100644 --- a/packages/layerchart/src/lib/components/ComputedStyles.svelte +++ b/packages/layerchart/src/lib/components/ComputedStyles.svelte @@ -1,16 +1,35 @@ + +
(styles = _styles)} >
- +{@render children?.({ styles })} + + diff --git a/packages/layerchart/src/lib/components/Connector.svelte b/packages/layerchart/src/lib/components/Connector.svelte new file mode 100644 index 000000000..8c3dc2b2a --- /dev/null +++ b/packages/layerchart/src/lib/components/Connector.svelte @@ -0,0 +1,149 @@ + + + + + + + + diff --git a/packages/layerchart/src/lib/components/Dagre.svelte b/packages/layerchart/src/lib/components/Dagre.svelte index 05a48b5d8..e2ac7cbcb 100644 --- a/packages/layerchart/src/lib/components/Dagre.svelte +++ b/packages/layerchart/src/lib/components/Dagre.svelte @@ -1,4 +1,9 @@ - - + +{@render children?.({ nodes: graphNodes, edges: graphEdges, graph: graph! })} diff --git a/packages/layerchart/src/lib/components/Ellipse.svelte b/packages/layerchart/src/lib/components/Ellipse.svelte new file mode 100644 index 000000000..e3947e2ea --- /dev/null +++ b/packages/layerchart/src/lib/components/Ellipse.svelte @@ -0,0 +1,228 @@ + + + + +{#if renderCtx === 'svg'} + +{:else if renderCtx === 'html'} +
+{/if} + + diff --git a/packages/layerchart/src/lib/components/ForceSimulation.svelte b/packages/layerchart/src/lib/components/ForceSimulation.svelte index 28a5d8ef9..1aded4a0a 100644 --- a/packages/layerchart/src/lib/components/ForceSimulation.svelte +++ b/packages/layerchart/src/lib/components/ForceSimulation.svelte @@ -1,101 +1,302 @@ - - const { data } = chartContext(); + - +{@render children?.({ + nodes: simulatedNodes, + links: simulatedLinks, + simulation, + linkPositions, +})} diff --git a/packages/layerchart/src/lib/components/Frame.svelte b/packages/layerchart/src/lib/components/Frame.svelte index 806a42f4e..112ffa0ce 100644 --- a/packages/layerchart/src/lib/components/Frame.svelte +++ b/packages/layerchart/src/lib/components/Frame.svelte @@ -1,21 +1,41 @@ + + diff --git a/packages/layerchart/src/lib/components/GeoCircle.svelte b/packages/layerchart/src/lib/components/GeoCircle.svelte index ddac401a4..6b4cf7b5a 100644 --- a/packages/layerchart/src/lib/components/GeoCircle.svelte +++ b/packages/layerchart/src/lib/components/GeoCircle.svelte @@ -1,24 +1,37 @@ - - /** sets the circle precision to the specified angle in degrees */ - export let precision = 6; + - + diff --git a/packages/layerchart/src/lib/components/GeoContext.svelte b/packages/layerchart/src/lib/components/GeoContext.svelte index b99dbec42..d9baf6f4b 100644 --- a/packages/layerchart/src/lib/components/GeoContext.svelte +++ b/packages/layerchart/src/lib/components/GeoContext.svelte @@ -1,74 +1,113 @@ - - +{@render children({ + geoContext, +})} diff --git a/packages/layerchart/src/lib/components/GeoEdgeFade.svelte b/packages/layerchart/src/lib/components/GeoEdgeFade.svelte index 50bb85033..ea1c848ab 100644 --- a/packages/layerchart/src/lib/components/GeoEdgeFade.svelte +++ b/packages/layerchart/src/lib/components/GeoEdgeFade.svelte @@ -1,25 +1,61 @@ + + - - - + + {@render children?.()} + diff --git a/packages/layerchart/src/lib/components/GeoPath.svelte b/packages/layerchart/src/lib/components/GeoPath.svelte index af59a0b98..9df491200 100644 --- a/packages/layerchart/src/lib/components/GeoPath.svelte +++ b/packages/layerchart/src/lib/components/GeoPath.svelte @@ -1,165 +1,219 @@ - + + - onDestroy(() => { - if (renderContext === 'canvas') { - canvasUnregister(); +{#if children} + {@render children({ geoPath })} +{:else if renderCtx === 'svg'} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/GeoPoint.svelte b/packages/layerchart/src/lib/components/GeoPoint.svelte index d3fd70c96..cf949f9ef 100644 --- a/packages/layerchart/src/lib/components/GeoPoint.svelte +++ b/packages/layerchart/src/lib/components/GeoPoint.svelte @@ -1,42 +1,73 @@ - + + {#if renderContext === 'svg'} - {#if $$slots.default} - - + {#if children} + + {@render children({ x, y })} {:else} - + {/if} {/if} {#if renderContext === 'canvas'} - {#if $$slots.default} - + {#if children} + - + {@render children({ x, y })} {:else} - + {/if} {/if} diff --git a/packages/layerchart/src/lib/components/GeoSpline.svelte b/packages/layerchart/src/lib/components/GeoSpline.svelte index d3de6b287..dce51d7ba 100644 --- a/packages/layerchart/src/lib/components/GeoSpline.svelte +++ b/packages/layerchart/src/lib/components/GeoSpline.svelte @@ -1,36 +1,85 @@ - + + - d[0]} y={(d) => d[1]} {curve} {...$$restProps} /> + d[0]} + y={(d) => d[1]} + {curve} + {...extractLayerProps(restProps, 'lc-geo-spline')} +/> diff --git a/packages/layerchart/src/lib/components/GeoTile.svelte b/packages/layerchart/src/lib/components/GeoTile.svelte index 1e5a968ed..bdcbbebaa 100644 --- a/packages/layerchart/src/lib/components/GeoTile.svelte +++ b/packages/layerchart/src/lib/components/GeoTile.svelte @@ -1,75 +1,126 @@ + + -{#if renderContext === 'svg' && url} - - +{#if renderCtx === 'svg' && url} + {#if children} + {@render children({ tiles })} + {:else} + {#each tiles as [x, y, z] (url(x, y, z))} - + {/each} - + {/if} {/if} diff --git a/packages/layerchart/src/lib/components/GeoVisible.svelte b/packages/layerchart/src/lib/components/GeoVisible.svelte index afc937ae1..7258b2aa5 100644 --- a/packages/layerchart/src/lib/components/GeoVisible.svelte +++ b/packages/layerchart/src/lib/components/GeoVisible.svelte @@ -1,15 +1,23 @@ + + -{#if isVisible($geo)([long, lat])} - +{#if geoCtx.projection && isVisible(geoCtx.projection)([long, lat])} + {@render children?.()} {/if} diff --git a/packages/layerchart/src/lib/components/Graticule.svelte b/packages/layerchart/src/lib/components/Graticule.svelte index 6cd0aaa4b..e29f4ca43 100644 --- a/packages/layerchart/src/lib/components/Graticule.svelte +++ b/packages/layerchart/src/lib/components/Graticule.svelte @@ -1,32 +1,48 @@ - + + - + {#if !lines && !outline} - + {/if} {#if lines} {#each graticule.lines() as line} - + {/each} {/if} {#if outline} - + {/if} - + diff --git a/packages/layerchart/src/lib/components/Grid.svelte b/packages/layerchart/src/lib/components/Grid.svelte index 98e71ebe2..eec7034be 100644 --- a/packages/layerchart/src/lib/components/Grid.svelte +++ b/packages/layerchart/src/lib/components/Grid.svelte @@ -1,178 +1,288 @@ - + + - + {#if x} - {@const splineProps = typeof x === 'object' ? x : null} - + {@const splineProps = extractLayerProps(x, 'lc-grid-x-line')} + + {#each xTickVals as x (x)} - {#if $radial} - ({ x, y }))} - x="x" - y="y" - xOffset={xBandOffset} - curve={curveLinearClosed} - {tweened} - {spring} + {#if ctx.radial} + {@const [x1, y1] = pointRadial(ctx.xScale(x), ctx.yRange[0])} + {@const [x2, y2] = pointRadial(ctx.xScale(x), ctx.yRange[1])} + {:else} {/if} {/each} - {#if isScaleBand($xScale) && bandAlign === 'between' && !$radial && xTickVals.length} + {#if isScaleBand(ctx.xScale) && bandAlign === 'between' && !ctx.radial && xTickVals.length} + {@const x = ctx.xScale(xTickVals[xTickVals.length - 1])! + ctx.xScale.step() + xBandOffset} {/if} - + {/if} {#if y} - {@const splineProps = typeof y === 'object' ? y : null} - + {@const splineProps = extractLayerProps(y, 'lc-grid-y-line')} + {#each yTickVals as y (y)} - {#if $radial} + {#if ctx.radial} {#if radialY === 'circle'} {:else} ({ x, y }))} x="x" y="y" - yOffset={yBandOffset} - {tweened} - {spring} + motion={tweenConfig} curve={curveLinearClosed} {...splineProps} - class={cls('stroke-surface-content/10', classes.line, splineProps?.class)} + class={cls('lc-grid-y-radial-line', classes.line, splineProps?.class)} /> {/if} {:else} - {/if} {/each} - {#if isScaleBand($yScale) && bandAlign === 'between' && !$radial && yTickVals.length} - + {#if isScaleBand(ctx.yScale) && bandAlign === 'between' && yTickVals.length} + {#if ctx.radial} + + {:else} + {@const y = + ctx.yScale(yTickVals[yTickVals.length - 1])! + ctx.yScale.step() + yBandOffset} + + {/if} {/if} - + {/if} - + + + diff --git a/packages/layerchart/src/lib/components/Group.svelte b/packages/layerchart/src/lib/components/Group.svelte index b95e337d7..cbfbdaaa8 100644 --- a/packages/layerchart/src/lib/components/Group.svelte +++ b/packages/layerchart/src/lib/components/Group.svelte @@ -1,135 +1,207 @@ - + + -{#if renderContext === 'canvas'} - -{:else if renderContext === 'svg'} - +{#if renderCtx === 'canvas'} + {@render children?.()} +{:else if renderCtx === 'svg'} { - if (preventTouchMove) { - // Prevent touch to not interfer with pointer - e.preventDefault(); - } - }} + class={['lc-group-g', className]} + in:transitionIn={transitionInParams} + {opacity} + {...restProps} + ontouchmove={handleTouchMove} + bind:this={ref} > - + {@render children?.()} -{:else} +{:else if renderCtx === 'html'}
{ - if (preventTouchMove) { - // Prevent touch to not interfer with pointer - e.preventDefault(); - } - }} + style:opacity + in:transitionIn={transitionInParams} + {...restProps} + class={['lc-group-div', className]} + ontouchmove={handleTouchMove} > - + {@render children?.()}
{/if} + + diff --git a/packages/layerchart/src/lib/components/Highlight.svelte b/packages/layerchart/src/lib/components/Highlight.svelte index 299c37940..e9efe9db5 100644 --- a/packages/layerchart/src/lib/components/Highlight.svelte +++ b/packages/layerchart/src/lib/components/Highlight.svelte @@ -1,443 +1,582 @@ - - - - $: highlightData = data ?? $tooltip.data; - - $: if (highlightData) { - const xValue = _x(highlightData); - const xCoord = Array.isArray(xValue) ? xValue.map((v) => $xScale(v)) : $xScale(xValue); - const xOffset = isScaleBand($xScale) && !$radial ? $xScale.bandwidth() / 2 : 0; - - const yValue = _y(highlightData); - const yCoord = Array.isArray(yValue) ? yValue.map((v) => $yScale(v)) : $yScale(yValue); - const yOffset = isScaleBand($yScale) && !$radial ? $yScale.bandwidth() / 2 : 0; - - // Reset lines - _lines = []; - - const defaultAxis = isScaleBand($yScale) ? 'y' : 'x'; - if (axis == null) { - axis = defaultAxis; - } + {#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 }))} + {...extractLayerProps(area, 'lc-highlight-area')} + onclick={onAreaClick && ((e) => onAreaClick(e, { data: highlightData }))} /> - + {/if} {/if} {#if bar} - + {#if typeof bar === 'function'} + {@render bar()} + {:else} onbarclick(e, { data: highlightData }))} + motion={motion === 'spring' ? 'spring' : undefined} + data={highlightData} + {opacity} + {...extractLayerProps(bar, 'lc-highlight-bar')} + onclick={onBarClick && ((e) => onBarClick(e, { data: highlightData }))} /> - + {/if} {/if} - {#if lines} - + {#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} { // Do not propagate `pointerdown` event to `BrushContext` if `onclick` is provided e.stopPropagation(); })} - onclick={onpointclick && ((e) => onpointclick(e, { point, data: highlightData }))} - onpointerenter={onpointenter && + onclick={onPointClick && ((e) => onPointClick(e, { point, data: highlightData }))} + onpointerenter={onPointEnter && ((e) => { - if (onpointclick) { + if (onPointClick) { asAny(e.target).style.cursor = 'pointer'; } - onpointenter(e, { point, data: highlightData }); + onPointEnter(e, { point, data: highlightData }); })} - onpointerleave={onpointleave && + onpointerleave={onPointLeave && ((e) => { - if (onpointclick) { + if (onPointClick) { asAny(e.target).style.cursor = 'default'; } - onpointleave(e, { point, data: highlightData }); + onPointLeave(e, { point, data: highlightData }); })} /> {/each} - + {/if} {/if} {/if} + + diff --git a/packages/layerchart/src/lib/components/Hull.svelte b/packages/layerchart/src/lib/components/Hull.svelte index b7661aa83..516a21247 100644 --- a/packages/layerchart/src/lib/components/Hull.svelte +++ b/packages/layerchart/src/lib/components/Hull.svelte @@ -1,65 +1,115 @@ + + - - {#if $geo} + + {#if geoCtx.projection} {@const polygon = geoVoronoi().hull(points)} onclick?.(e, { points, polygon })} onpointermove={(e) => onpointermove?.(e, { points, polygon })} {onpointerleave} @@ -72,10 +122,18 @@ x={(d) => d[0]} y={(d) => d[1]} {curve} - class={cls('fill-transparent', classes.path)} + class={['lc-hull-class', classes.path]} onclick={(e) => onclick?.(e, { points, polygon })} onpointermove={(e) => onpointermove?.(e, { points, polygon })} {onpointerleave} /> {/if} - + + + diff --git a/packages/layerchart/src/lib/components/Labels.svelte b/packages/layerchart/src/lib/components/Labels.svelte index 0cc52d42c..404ae1b44 100644 --- a/packages/layerchart/src/lib/components/Labels.svelte +++ b/packages/layerchart/src/lib/components/Labels.svelte @@ -1,56 +1,130 @@ - + + - - - {#each points as point, i (key(point.data, i))} - {@const textProps = getTextProps(point)} - - - - {/each} + + + {#snippet children({ points })} + {#each points as point, i (key(point.data, i))} + {@const textProps = extractLayerProps(getTextProps(point), 'lc-labels-text')} + {#if childrenProp} + {@render childrenProp({ data: point, textProps })} + {:else} + + {/if} + {/each} + {/snippet} - + + + diff --git a/packages/layerchart/src/lib/components/Legend.svelte b/packages/layerchart/src/lib/components/Legend.svelte index 4b00be0bd..998b78360 100644 --- a/packages/layerchart/src/lib/components/Legend.svelte +++ b/packages/layerchart/src/lib/components/Legend.svelte @@ -1,234 +1,491 @@ + +
-
{title}
- - {#if variant === 'ramp'} - - - {#if interpolator} - - {:else if swatches} - {#each swatches as swatch, i} - - {/each} - {/if} - - - - {#each tickValues ?? xScale?.ticks?.(ticks) ?? [] as tick, i} - - {tickFormat ? format(tick, tickFormat) : tick} - - - {#if tickLine} - - {/if} +
+ {title} +
+ {#if children} + {@render children({ + values: scaleConfig.tickValues ?? scaleConfig.xScale?.ticks?.(ticks) ?? [], + scale, + })} + {:else if variant === 'ramp'} + + + {#if scaleConfig.interpolator} + + {:else if scaleConfig.swatches} + {#each scaleConfig.swatches as swatch, i} + {/each} - - - {:else if variant === 'swatches'} -
- {#each tickValues ?? xScale?.ticks?.(ticks) ?? [] as tick} - {@const color = _scale(tick)} - {@const item = { value: tick, color }} - + + {tickFormatProp ? format(tick, asAny(tickFormatProp)) : tick} + + + {#if scaleConfig.tickLine} + + {/if} {/each} -
- {/if} -
+ + + {:else if variant === 'swatches'} +
+ {#each scaleConfig.tickValues ?? scaleConfig.xScale?.ticks?.(ticks) ?? [] as tick} + {@const color = scale?.(tick) ?? ''} + {@const item = { value: tick, color }} + + {/each} +
+ {/if}
+ + diff --git a/packages/layerchart/src/lib/components/Line.svelte b/packages/layerchart/src/lib/components/Line.svelte index b6ab0baa3..66c185915 100644 --- a/packages/layerchart/src/lib/components/Line.svelte +++ b/packages/layerchart/src/lib/components/Line.svelte @@ -1,163 +1,241 @@ + + -{#if renderContext === 'svg'} - +{#if renderCtx === 'svg'} - - - {#if markerStart} - - {/if} - - - - - + + + +{:else if renderCtx === 'html'} + {@const { angle, length } = pointsToAngleAndLength( + { x: motionX1.current, y: motionY1.current }, + { x: motionX2.current, y: motionY2.current } + )} + +
{/if} + + diff --git a/packages/layerchart/src/lib/components/LinearGradient.svelte b/packages/layerchart/src/lib/components/LinearGradient.svelte index 64882cffd..1e1068a05 100644 --- a/packages/layerchart/src/lib/components/LinearGradient.svelte +++ b/packages/layerchart/src/lib/components/LinearGradient.svelte @@ -1,95 +1,197 @@ - - import { getRenderContext } from './Chart.svelte'; - import { chartContext } from './ChartContext.svelte'; - import { getCanvasContext } from './layout/Canvas.svelte'; + -{#if renderContext === 'canvas'} - -{:else if renderContext === 'svg'} +{#if renderCtx === 'canvas'} + + {@render children?.({ id, gradient: asAny(canvasGradient) })} +{:else if renderCtx === 'svg'} - - {#if stops} - {#each stops as stop, i} - {#if Array.isArray(stop)} - - {:else} - - {/if} - {/each} - {/if} - + {#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 renderCtx === 'html'} + {@render children?.({ id, gradient: createCSSGradient() })} {/if} diff --git a/packages/layerchart/src/lib/components/Link.svelte b/packages/layerchart/src/lib/components/Link.svelte index 665c089fc..3b88987d0 100644 --- a/packages/layerchart/src/lib/components/Link.svelte +++ b/packages/layerchart/src/lib/components/Link.svelte @@ -1,6 +1,73 @@ + + - - - - {#if markerStart} - - {/if} - - - - - - - - - diff --git a/packages/layerchart/src/lib/components/Marker.svelte b/packages/layerchart/src/lib/components/Marker.svelte index be4509aa2..2efb45266 100644 --- a/packages/layerchart/src/lib/components/Marker.svelte +++ b/packages/layerchart/src/lib/components/Marker.svelte @@ -1,37 +1,96 @@ - - /** The x coordinate for the reference point of the marker */ - export let refX: string | number = ['arrow', 'triangle'].includes(type ?? '') ? 9 : 5; + @@ -44,38 +103,51 @@ {refX} {refY} {viewBox} - {...$$restProps} - class={cls( - 'overflow-visible', - // stroke - $$props.stroke == null && - (['arrow', 'circle-stroke', 'line'].includes(type ?? '') - ? 'stroke-[context-stroke]' - : type === 'circle' - ? 'stroke-surface-100' - : 'stroke-none'), - // extra stroke attrs - '[stroke-linecap:round] [stroke-linejoin:round]', - //fill - $$props.fill == null && - (['triangle', 'dot', 'circle'].includes(type ?? '') - ? 'fill-[context-stroke]' - : type === 'circle-stroke' - ? 'fill-surface-100' - : 'fill-none'), - $$props.class - )} + data-type={type} + {...restProps} + class={cls('lc-marker', className)} > - - {#if type === 'triangle'} - - {:else if type === 'arrow'} - - {:else if type === 'circle' || type === 'circle-stroke' || type === 'dot'} - - {:else if type === 'line'} - - {/if} - + {#if children} + {@render children()} + {:else if type === 'triangle'} + + {:else if type === 'arrow'} + + {:else if type === 'circle' || type === 'circle-stroke' || type === 'dot'} + + {:else if type === 'line'} + + {/if} + + diff --git a/packages/layerchart/src/lib/components/MarkerWrapper.svelte b/packages/layerchart/src/lib/components/MarkerWrapper.svelte new file mode 100644 index 000000000..2966a5fb1 --- /dev/null +++ b/packages/layerchart/src/lib/components/MarkerWrapper.svelte @@ -0,0 +1,35 @@ + + + + +{#if typeof marker === 'function'} + {@render marker({ id })} +{:else if marker} + +{/if} diff --git a/packages/layerchart/src/lib/components/MonthPath.svelte b/packages/layerchart/src/lib/components/MonthPath.svelte index 514e59d6a..f57ca1931 100644 --- a/packages/layerchart/src/lib/components/MonthPath.svelte +++ b/packages/layerchart/src/lib/components/MonthPath.svelte @@ -1,32 +1,97 @@ + + - + + + diff --git a/packages/layerchart/src/lib/components/MotionPath.svelte b/packages/layerchart/src/lib/components/MotionPath.svelte index 24f7ce237..0799e15f7 100644 --- a/packages/layerchart/src/lib/components/MotionPath.svelte +++ b/packages/layerchart/src/lib/components/MotionPath.svelte @@ -1,26 +1,82 @@ - + + @@ -42,12 +99,11 @@ {repeatCount} {fill} {rotate} - bind:this={animateEl} + bind:this={ref} + {...extractLayerProps(restProps, 'lc-motion-path')} > -{#if $$slots.default} - -{/if} +{@render children?.({ pathId, objectId })} diff --git a/packages/layerchart/src/lib/components/Pack.svelte b/packages/layerchart/src/lib/components/Pack.svelte index 7ff83ab30..842a6f54b 100644 --- a/packages/layerchart/src/lib/components/Pack.svelte +++ b/packages/layerchart/src/lib/components/Pack.svelte @@ -1,27 +1,63 @@ - + + - +{@render children?.({ + nodes: packedData, +})} diff --git a/packages/layerchart/src/lib/components/Partition.svelte b/packages/layerchart/src/lib/components/Partition.svelte index 18b910072..984b6aa65 100644 --- a/packages/layerchart/src/lib/components/Partition.svelte +++ b/packages/layerchart/src/lib/components/Partition.svelte @@ -1,42 +1,84 @@ - + + - +{@render children?.({ nodes: partitionData })} diff --git a/packages/layerchart/src/lib/components/Pattern.svelte b/packages/layerchart/src/lib/components/Pattern.svelte index cd11ee2bf..0354e95ef 100644 --- a/packages/layerchart/src/lib/components/Pattern.svelte +++ b/packages/layerchart/src/lib/components/Pattern.svelte @@ -1,15 +1,301 @@ + + - - - - - +{#if renderCtx === 'canvas'} + {@render children?.({ id, pattern: asAny(canvasPattern) })} +{:else if renderCtx === '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})` })} +{/if} diff --git a/packages/layerchart/src/lib/components/Pie.svelte b/packages/layerchart/src/lib/components/Pie.svelte index 962a69dc8..727a4d18f 100644 --- a/packages/layerchart/src/lib/components/Pie.svelte +++ b/packages/layerchart/src/lib/components/Pie.svelte @@ -1,13 +1,86 @@ + + - +{#if children} + {@render children({ arcs })} +{:else} {#each arcs as arc} {/each} - +{/if} diff --git a/packages/layerchart/src/lib/components/Point.svelte b/packages/layerchart/src/lib/components/Point.svelte index cddf43410..375e218bd 100644 --- a/packages/layerchart/src/lib/components/Point.svelte +++ b/packages/layerchart/src/lib/components/Point.svelte @@ -1,14 +1,25 @@ - + + - +{@render children?.({ x: ctx.xGet(d), y: ctx.yGet(d) })} diff --git a/packages/layerchart/src/lib/components/Points.svelte b/packages/layerchart/src/lib/components/Points.svelte index 0df38600a..26347e903 100644 --- a/packages/layerchart/src/lib/components/Points.svelte +++ b/packages/layerchart/src/lib/components/Points.svelte @@ -1,195 +1,147 @@ - - - {#if links} - {#each _links as link} - - {/each} - {/if} + return []; + }) as Point[] + ); + +{#if children} + {@render children({ points })} +{:else} {#each points as point} - {@const radialPoint = pointRadial(point.x, point.y)} {/each} - +{/if} diff --git a/packages/layerchart/src/lib/components/Polygon.svelte b/packages/layerchart/src/lib/components/Polygon.svelte new file mode 100644 index 000000000..24b5dd159 --- /dev/null +++ b/packages/layerchart/src/lib/components/Polygon.svelte @@ -0,0 +1,309 @@ + + + + +{#if renderCtx === 'svg'} + +{/if} + + diff --git a/packages/layerchart/src/lib/components/RadialGradient.svelte b/packages/layerchart/src/lib/components/RadialGradient.svelte index 62d4df792..371977a57 100644 --- a/packages/layerchart/src/lib/components/RadialGradient.svelte +++ b/packages/layerchart/src/lib/components/RadialGradient.svelte @@ -1,91 +1,161 @@ - + -{#if renderContext === 'canvas'} - -{:else if renderContext === 'svg'} +{#if renderCtx === 'canvas'} + {@render children?.({ id, gradient: canvasGradient as any })} +{:else if renderCtx === 'svg'} - - {#if stops} - {#each stops as stop, i} - {#if Array.isArray(stop)} - - {:else} - - {/if} - {/each} - {/if} - + {#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/Rect.svelte b/packages/layerchart/src/lib/components/Rect.svelte index b951dfa61..88153923e 100644 --- a/packages/layerchart/src/lib/components/Rect.svelte +++ b/packages/layerchart/src/lib/components/Rect.svelte @@ -1,68 +1,95 @@ + + -{#if renderContext === 'svg'} - - +{#if renderCtx === 'svg'} +{:else if renderCtx === 'html'} + + +
{/if} + + diff --git a/packages/layerchart/src/lib/components/RectClipPath.svelte b/packages/layerchart/src/lib/components/RectClipPath.svelte index 8b9c6254c..e9c7d7f09 100644 --- a/packages/layerchart/src/lib/components/RectClipPath.svelte +++ b/packages/layerchart/src/lib/components/RectClipPath.svelte @@ -1,25 +1,90 @@ - + + - - - + + {#snippet clip()} + + {/snippet} + {#snippet children({ url })} + {@render childrenProp?.({ id, url })} + {/snippet} diff --git a/packages/layerchart/src/lib/components/Rule.svelte b/packages/layerchart/src/lib/components/Rule.svelte index 3c5ad1edc..1361cdde1 100644 --- a/packages/layerchart/src/lib/components/Rule.svelte +++ b/packages/layerchart/src/lib/components/Rule.svelte @@ -1,113 +1,247 @@ + + - - {#if showRule(x, 'x')} - {@const xCoord = - x === true || x === 'left' ? xRangeMin : x === 'right' ? xRangeMax : $xScale(x) + xOffset} + // Single y line + if (singleY) { + const _y = + y === true || y === '$bottom' + ? yRangeMinMax[1]! + : y === '$top' + ? yRangeMinMax[0]! + : ctx.yScale(y) + yOffset; + + result.push({ + x1: ctx.xRange[0] || 0, + y1: _y, + x2: ctx.xRange[1] || 0, + y2: _y, + axis: 'y', + }); + } - {#if $radial} - {@const [x1, y1] = pointRadial(xCoord, Number(yRangeMin))} - {@const [x2, y2] = pointRadial(xCoord, Number(yRangeMax))} + // Data driven lines + if (!singleX && !singleY) { + const xAccessor = x !== false ? accessor(x as Accessor) : ctx.x; + const yAccessor = y !== false ? accessor(y as Accessor) : ctx.y; - - {:else} - - {/if} - {/if} - - {#if showRule(y, 'y')} - {#if $radial} - + const xBandOffset = isScaleBand(ctx.xScale) ? ctx.xScale.bandwidth() / 2 : 0; + const yBandOffset = isScaleBand(ctx.yScale) ? ctx.yScale.bandwidth() / 2 : 0; + + for (const d of data) { + const xValue = xAccessor(d); + const yValue = yAccessor(d); + + const x1Value = Array.isArray(xValue) ? xValue[0] : isScaleNumeric(ctx.xScale) ? 0 : xValue; + const x2Value = Array.isArray(xValue) ? xValue[1] : xValue; + const y1Value = Array.isArray(yValue) ? yValue[0] : isScaleNumeric(ctx.yScale) ? 0 : yValue; + const y2Value = Array.isArray(yValue) ? yValue[1] : yValue; + + result.push({ + x1: ctx.xScale(x1Value) + xBandOffset + xOffset, + y1: ctx.yScale(y1Value) + yBandOffset + yOffset, + x2: ctx.xScale(x2Value) + xBandOffset + xOffset, + y2: ctx.yScale(y2Value) + yBandOffset + yOffset, + axis: Array.isArray(yValue) || isScaleBand(ctx.xScale) ? 'x' : 'y', // TODO: what about single prop like lollipop? + stroke: (strokeProp ?? ctx.config.c) ? ctx.cGet(d) : null, // use color scale, if available + }); + } + } + + // Remove lines if out of range of chart (non-0 baseline, brushing, etc) + return result.filter((line) => { + return ( + line.x1 >= xRangeMinMax[0]! && + line.x2 <= xRangeMinMax[1]! && + line.y1 >= yRangeMinMax[0]! && + line.y2 <= yRangeMinMax[1]! + ); + }); + }); + + // $inspect({ lines }); + + + + {#each lines as line} + {@const stroke = line.stroke ?? strokeProp} + + {#if ctx.radial} + {#if line.axis === 'x'} + {@const [x1, y1] = pointRadial(line.x1, line.y1)} + {@const [x2, y2] = pointRadial(line.x2, line.y2)} + + {:else if line.axis === 'y'} + + {/if} {:else} {/if} - {/if} - + {/each} + + + diff --git a/packages/layerchart/src/lib/components/Sankey.svelte b/packages/layerchart/src/lib/components/Sankey.svelte index 7a74fb776..6a011ce15 100644 --- a/packages/layerchart/src/lib/components/Sankey.svelte +++ b/packages/layerchart/src/lib/components/Sankey.svelte @@ -1,3 +1,81 @@ + + - +{@render children?.({ + nodes: sankeyData.nodes, + links: sankeyData.links, +})} diff --git a/packages/layerchart/src/lib/components/Spline.svelte b/packages/layerchart/src/lib/components/Spline.svelte index 94227c844..c5fb4cf0e 100644 --- a/packages/layerchart/src/lib/components/Spline.svelte +++ b/packages/layerchart/src/lib/components/Spline.svelte @@ -1,106 +1,186 @@ + + -{#if renderContext === 'svg'} +{#if renderCtx === 'svg'} {#key key} - - - - {#if markerStart} - - {/if} - - - - {#if markerMid} - - {/if} - - - - {#if markerEnd} - - {/if} - - - {#if $$slots.start && $startPoint} - - + + + + + {#if startContent && startPoint} + + {@render startContent({ + point: startPoint, + value: { + x: ctx.xScale?.invert?.(startPoint.x), + y: ctx.yScale?.invert?.(startPoint.y), + }, + })} {/if} - {#if $$slots.end && $endPoint} - - + {#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/Text.svelte b/packages/layerchart/src/lib/components/Text.svelte index 2b718c153..fb09924db 100644 --- a/packages/layerchart/src/lib/components/Text.svelte +++ b/packages/layerchart/src/lib/components/Text.svelte @@ -1,111 +1,317 @@ + + -{#if renderContext === 'svg'} +{#if renderCtx === 'svg'} - - {#if isValidXOrY(x) && isValidXOrY(y)} + + {#if path} + + {#key path} + + {/key} + + + {wordsByLines.map((line) => line.words.join(' ')).join()} + + + {:else if isValidXOrY(x) && isValidXOrY(y)} + {#each wordsByLines as line, index} - + {line.words.join(' ')} {/each} {/if} +{:else if renderCtx === 'html'} + {@const translateX = textAnchor === 'middle' ? '-50%' : textAnchor === 'end' ? '-100%' : '0%'} + {@const translateY = + verticalAnchor === 'middle' ? '-50%' : verticalAnchor === 'end' ? '-100%' : '0%'} + + + +
+ {textValue} +
{/if} + + diff --git a/packages/layerchart/src/lib/components/Threshold.svelte b/packages/layerchart/src/lib/components/Threshold.svelte index dbca3220a..1932c47f4 100644 --- a/packages/layerchart/src/lib/components/Threshold.svelte +++ b/packages/layerchart/src/lib/components/Threshold.svelte @@ -1,40 +1,72 @@ + + {#key curve} - - $y(d)[0]} y1={(d) => min($yDomain)} {curve} {defined} /> - - - + {#snippet clip()} + ctx.y(d)[0]} y1={(d) => min(ctx.yDomain)} {curve} {defined} /> + {/snippet} + {@render above?.({ curve, defined })} - - min($yDomain)} y1={(d) => $y(d)[1]} {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/TileImage.svelte b/packages/layerchart/src/lib/components/TileImage.svelte index 4abff4eac..62899cdcb 100644 --- a/packages/layerchart/src/lib/components/TileImage.svelte +++ b/packages/layerchart/src/lib/components/TileImage.svelte @@ -1,35 +1,98 @@ - - - +{#key href} + + +{/key} {#if debug} {/if} + + diff --git a/packages/layerchart/src/lib/components/TransformContext.svelte b/packages/layerchart/src/lib/components/TransformContext.svelte index c393a55e8..d5502c8f2 100644 --- a/packages/layerchart/src/lib/components/TransformContext.svelte +++ b/packages/layerchart/src/lib/components/TransformContext.svelte @@ -1,126 +1,324 @@ - - - - /** Action to take during wheel scroll */ - export let initialScrollMode: TransformScrollMode = 'none'; + -
{ + onwheel={onWheel} + onpointerdown={onPointerDown} + onpointermove={onPointerMove} + ontouchmove={(e) => { + ontouchmove?.(e); // Touch events cause pointer events to be interrupted. // Typically `touch-action: none` works, but doesn't appear to with SVG, but `preventDefault()` works here // https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events#touch-action_css_property @@ -311,23 +514,20 @@ e.preventDefault(); } }} - on:pointerup={onPointerUp} - on:dblclick={onDoubleClick} - on:click|capture={onClick} - on:click - on:keydown - on:keyup - on:keypress - class="h-full" + onpointerup={onPointerUp} + ondblclick={onDoubleClick} + onclickcapture={onClick} + class={['lc-transform-context', className]} + bind:this={ref} + {...restProps} > - + {@render children?.({ transformContext: transformContext })}
+ + diff --git a/packages/layerchart/src/lib/components/TransformControls.svelte b/packages/layerchart/src/lib/components/TransformControls.svelte index 05951d0d2..74dba7937 100644 --- a/packages/layerchart/src/lib/components/TransformControls.svelte +++ b/packages/layerchart/src/lib/components/TransformControls.svelte @@ -1,40 +1,60 @@ + + - +
{ + class={['lc-transform-controls', className]} + data-orientation={orientation} + data-placement={placement} + ondblclick={(e) => { // Stop from propagating to TransformContext e.stopPropagation(); }} @@ -89,7 +95,7 @@ {#if show.includes('zoomIn')}
+ + diff --git a/packages/layerchart/src/lib/components/Tree.svelte b/packages/layerchart/src/lib/components/Tree.svelte index 56c4690fc..8a2d451c0 100644 --- a/packages/layerchart/src/lib/components/Tree.svelte +++ b/packages/layerchart/src/lib/components/Tree.svelte @@ -1,41 +1,82 @@ - + + - +{@render children?.({ + nodes: treeData.nodes, + links: treeData.links, +})} diff --git a/packages/layerchart/src/lib/components/Treemap.svelte b/packages/layerchart/src/lib/components/Treemap.svelte index b2987e12b..3d92d0b54 100644 --- a/packages/layerchart/src/lib/components/Treemap.svelte +++ b/packages/layerchart/src/lib/components/Treemap.svelte @@ -1,4 +1,78 @@ - + + - +{@render children?.({ nodes: treemapData.nodes })} diff --git a/packages/layerchart/src/lib/components/Voronoi.svelte b/packages/layerchart/src/lib/components/Voronoi.svelte index 0e59cbc54..349085c9d 100644 --- a/packages/layerchart/src/lib/components/Voronoi.svelte +++ b/packages/layerchart/src/lib/components/Voronoi.svelte @@ -1,109 +1,147 @@ + + - - {#if $geo} + + {#if geo.projection} {@const polygons = geoVoronoi().polygons(points)} {#each polygons.features as feature} - 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 interfer with pointer - e.preventDefault(); - }} - /> + {@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])} @@ -111,20 +149,31 @@ {@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 interfer with pointer - e.preventDefault(); - }} - /> + + 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} - +
+ + diff --git a/packages/layerchart/src/lib/components/charts/ArcChart.svelte b/packages/layerchart/src/lib/components/charts/ArcChart.svelte new file mode 100644 index 000000000..a23b2b55f --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/ArcChart.svelte @@ -0,0 +1,436 @@ + + + + + + cAccessor(d)) + : [ + 'var(--color-primary, currentColor)', + 'var(--color-secondary, currentColor)', + 'var(--color-info, currentColor)', + 'var(--color-success, currentColor)', + 'var(--color-warning, currentColor)', + 'var(--color-danger, currentColor)', + ]} + padding={{ bottom: legend ? 32 : 0 }} + {...restProps} + tooltip={tooltip === false + ? false + : { ...props.tooltip?.context, ...(typeof tooltip === 'object' ? tooltip : null) }} +> + {#snippet children({ context })} + {@const snippetProps = { + label: labelAccessor, + key: keyAccessor, + value: valueAccessor, + color: cAccessor, + context, + series, + visibleSeries: seriesState.visibleSeries, + visibleData, + highlightKey: seriesState.highlightKey.current, + setHighlightKey: seriesState.highlightKey.set, + getLegendProps, + getGroupProps, + getArcProps, + }} + {#if childrenProp} + {@render childrenProp(snippetProps)} + {:else} + {@render belowContext?.(snippetProps)} + + + {@render belowMarks?.(snippetProps)} + + {#if typeof marks === 'function'} + {@render marks(snippetProps)} + {:else} + + {#each series as s, i (s.key)} + {#if typeof arc === 'function'} + {@render arc({ ...snippetProps, seriesIndex: i, props: getArcProps(s, i) })} + {:else} + + {/if} + {/each} + + {/if} + + {@render aboveMarks?.(snippetProps)} + + + {@render aboveContext?.(snippetProps)} + + {#if typeof legend === 'function'} + {@render legend(snippetProps)} + {:else if legend} + + {/if} + + {#if typeof tooltip === 'function'} + {@render tooltip(snippetProps)} + {:else if tooltip} + + {#snippet children({ data })} + + (seriesState.highlightKey.current = keyAccessor(data))} + onpointerleave={() => (seriesState.highlightKey.current = null)} + {...props.tooltip?.item} + /> + + {/snippet} + + {/if} + {/if} + {/snippet} + diff --git a/packages/layerchart/src/lib/components/charts/AreaChart.svelte b/packages/layerchart/src/lib/components/charts/AreaChart.svelte index 01b92a749..61d1e4c9c 100644 --- a/packages/layerchart/src/lib/components/charts/AreaChart.svelte +++ b/packages/layerchart/src/lib/components/charts/AreaChart.svelte @@ -1,27 +1,86 @@ + + + visibleSeries.flatMap((s, i) => d.stackData[i]) - : visibleSeries.map((s) => s.value ?? s.key))} + y={resolveAccessor(y)} yBaseline={0} yNice {radial} padding={radial ? undefined : defaultChartPadding(axis, legend)} - {...$$restProps} - tooltip={$$props.tooltip === false + {...restProps} + tooltip={tooltip === false ? false : { - mode: 'bisect-x', - onclick: ontooltipclick, + mode: 'quadtree-x', + onclick: onTooltipClick, debug, ...props.tooltip?.context, - ...$$props.tooltip, + ...(typeof tooltip === 'object' ? tooltip : null), }} - bind:tooltipContext brush={brush && (brush === true || brush.mode == undefined || brush.mode === 'integrated') ? { axis: 'x', resetOnEnd: true, xDomain, ...brushProps, - onbrushend: (e) => { + onBrushEnd: (e) => { xDomain = e.xDomain; - brushProps.onbrushend?.(e); + brushProps.onBrushEnd?.(e); }, } : false} - let:x - let:xScale - let:y - let:yScale - let:c - let:cScale - let:width - let:height - let:padding - let:tooltip > - {@const slotProps = { - x, - xScale, - y, - yScale, - c, - cScale, - width, - height, - padding, - tooltip, - series, - visibleSeries, - getAreaProps, - getLabelsProps, - getPointsProps, - highlightSeriesKey, - setHighlightSeriesKey, - }} - - - - - - - {#if grid} - + {#snippet children({ context })} + {@const snippetProps = { + context, + series, + visibleSeries: seriesState.visibleSeries, + getAreaProps, + getLabelsProps, + getPointsProps, + getHighlightProps, + getLegendProps, + getGridProps, + getAxisProps, + getRuleProps, + highlightKey: seriesState.highlightKey.current, + setHighlightKey: seriesState.highlightKey.set, + }} + + {#if childrenProp} + {@render childrenProp(snippetProps)} + {:else} + {@render belowContext?.(snippetProps)} + + {#if typeof grid === 'function'} + {@render grid(snippetProps)} + {:else if grid} + {/if} - - - - - - {#each visibleSeries as s, i (s.key)} - - {/each} - - - - - - - {#if axis} + + + + {@render belowMarks?.(snippetProps)} + + {#if marks} + {@render marks(snippetProps)} + {:else} + {#each seriesState.visibleSeries as s, i (s.key)} + + {/each} + {/if} + + + {@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 (seriesLayout === 'stackExpand') { - return format(value, 'percentRound'); - } else { - return format(value, undefined, { variant: 'short' }); - } - }} - {...typeof axis === 'object' ? axis : null} - {...props.yAxis} - /> + {/if} {#if axis !== 'y'} - format(value, undefined, { variant: 'short' })} - {...typeof axis === 'object' ? axis : null} - {...props.xAxis} - /> + {/if} - {#if rule} - + {#if typeof rule === 'function'} + {@render rule(snippetProps)} + {:else if rule} + {/if} {/if} - - - - - {#if points} - {#each visibleSeries as s, i (s.key)} - - {/each} - {/if} - - {#each visibleSeries as s, i (s.key)} - {@const seriesTooltipData = - s.data && tooltip.data ? findRelatedData(s.data, tooltip.data, x) : null} - {@const highlightPointsProps = - typeof props.highlight?.points === 'object' ? props.highlight.points : null} - - d.stackData[i][1] : (s.value ?? (s.data ? undefined : s.key))} - lines={i == 0} - onpointclick={onpointclick - ? (e, detail) => onpointclick(e, { ...detail, series: s }) - : undefined} - onpointenter={() => (highlightSeriesKey = s.key)} - onpointleave={() => (highlightSeriesKey = null)} - {...props.highlight} - points={props.highlight?.points == false - ? false - : { - ...highlightPointsProps, - fill: s.color, - class: cls( - 'transition-opacity', - highlightSeriesKey && highlightSeriesKey !== s.key && 'opacity-10', - highlightPointsProps?.class - ), - }} - /> - {/each} - - - {#if labels} - {#each visibleSeries as s, i (s.key)} - - {/each} - {/if} - - - - - - - {#if legend} - s.key), - series.map((s) => s.color) - )} - tickFormat={(key) => series.find((s) => s.key === key)?.label ?? key} - placement="bottom" - variant="swatches" - onclick={(e, item) => $selectedSeries.toggleSelected(item.value)} - onpointerenter={(e, item) => (highlightSeriesKey = item.value)} - onpointerleave={(e) => (highlightSeriesKey = null)} - {...props.legend} - {...typeof legend === 'object' ? legend : null} - classes={{ - item: (item) => - visibleSeries.length && !visibleSeries.some((s) => s.key === item.value) - ? 'opacity-50' - : '', - ...props.legend?.classes, - ...(typeof legend === 'object' ? legend.classes : null), - }} - /> - {/if} - - - - - - - - - {@const seriesItems = stackSeries ? [...visibleSeries].reverse() : visibleSeries} - {#each seriesItems as s} - {@const seriesTooltipData = s.data ? findRelatedData(s.data, data, x) : data} - {@const valueAccessor = accessor(s.value ?? (s.data ? asAny(y) : s.key))} - - (highlightSeriesKey = s.key)} - onpointerleave={() => (highlightSeriesKey = null)} - {...props.tooltip?.item} - /> - {/each} - - {#if stackSeries && visibleSeries.length > 1} - - - { - const seriesTooltipData = s.data ? s.data.find((d) => x(d) === x(data)) : data; - const valueAccessor = accessor(s.value ?? (s.data ? asAny(y) : s.key)); - - return valueAccessor(seriesTooltipData); - })} - format="integer" - valueAlign="right" - {...props.tooltip?.root} - /> + + + {#if typeof points === 'function'} + {@render points(snippetProps)} + {:else if points} + {#each seriesState.visibleSeries as s, i (s.key)} + + {/each} {/if} - - - - + + {#if typeof highlight === 'function'} + {@render highlight(snippetProps)} + {:else if highlight} + {#each seriesState.visibleSeries as s, i (s.key)} + + {/each} + {/if} + + {#if typeof labels === 'function'} + {@render labels(snippetProps)} + {:else if labels} + {#each seriesState.visibleSeries as s, i (s.key)} + + {/each} + {/if} + + + + + + {@render aboveContext?.(snippetProps)} + + {#if typeof legend === 'function'} + {@render legend(snippetProps)} + {:else if legend} + + {/if} + + {#if typeof tooltip === 'function'} + {@render tooltip(snippetProps)} + {:else if tooltip} + + {/if} + {/if} + {/snippet} diff --git a/packages/layerchart/src/lib/components/charts/BarChart.svelte b/packages/layerchart/src/lib/components/charts/BarChart.svelte index a0cf324c9..e035095e9 100644 --- a/packages/layerchart/src/lib/components/charts/BarChart.svelte +++ b/packages/layerchart/src/lib/components/charts/BarChart.svelte @@ -1,213 +1,277 @@ + + + visibleSeries.flatMap((s, i) => d.stackData[i]) - : visibleSeries.map((s) => s.value ?? s.key))} + x={resolveAccessor(xProp)} + {xDomain} {xScale} {xBaseline} xNice={orientation === 'horizontal'} {x1Scale} {x1Domain} {x1Range} - y={y ?? - (stackSeries - ? (d) => visibleSeries.flatMap((s, i) => d.stackData[i]) - : visibleSeries.map((s) => s.value ?? s.key))} + {xInterval} + y={resolveAccessor(yProp)} {yScale} {yBaseline} yNice={orientation === 'vertical'} {y1Scale} {y1Domain} {y1Range} - c={isVertical ? y : x} - cRange={['hsl(var(--color-primary))']} - padding={defaultChartPadding(axis, legend)} - {...$$restProps} - tooltip={$$props.tooltip === false + {yInterval} + c={isVertical ? yProp : xProp} + cRange={['var(--color-primary, currentColor)']} + {radial} + padding={radial ? undefined : defaultChartPadding(axis, legend)} + {...restProps} + tooltip={tooltip === false ? false : { mode: 'band', - onclick: ontooltipclick, + onclick: onTooltipClick, debug, ...props.tooltip?.context, - ...$$props.tooltip, + ...(typeof tooltip === 'object' ? tooltip : null), }} - bind:tooltipContext - let:x - let:xScale - let:x1 - let:x1Scale - let:y1 - let:y - let:yScale - let:y1Scale - let:c - let:cScale - let:width - let:height - let:padding - let:tooltip + brush={brush && (brush === true || brush.mode == undefined || brush.mode === 'integrated') + ? { + axis: 'x', + resetOnEnd: true, + xDomain, + ...brushProps, + onBrushEnd: (e) => { + // TOOD: This should set xRange instead of xDomain, and/or xDomain should be all values, not just bounds of brush range + xDomain = e.xDomain; + brushProps.onBrushEnd?.(e); + }, + } + : false} > - {@const slotProps = { - x, - xScale, - x1, - x1Scale, - y, - yScale, - y1, - y1Scale, - c, - cScale, - width, - height, - padding, - tooltip, - series, - visibleSeries, - getBarsProps, - getLabelsProps, - highlightSeriesKey, - setHighlightSeriesKey, - }} - - - - - - {#if grid} - + {#snippet children({ context })} + {@const snippetProps = { + context, + series, + visibleSeries: seriesState.visibleSeries, + getBarsProps, + getLabelsProps, + getLegendProps, + getGridProps, + getHighlightProps, + getAxisProps, + getRuleProps, + highlightKey: seriesState.highlightKey.current, + setHighlightKey: seriesState.highlightKey.set, + }} + {#if childrenProp} + {@render childrenProp(snippetProps)} + {:else} + {@render belowContext?.(snippetProps)} + + + {#if typeof grid === 'function'} + {@render grid(snippetProps)} + {:else if grid} + {/if} - - + + - - {#each visibleSeries as s, i (s.key)} - - {/each} - + {@render belowMarks?.(snippetProps)} - + {#if typeof marks === 'function'} + {@render marks(snippetProps)} + {:else} + {#each seriesState.visibleSeries as s, i (s.key)} + + {/each} + {/if} + + + {@render aboveMarks?.(snippetProps)} - - {#if axis} + {#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 (isVertical && seriesLayout === 'stackExpand') { - return format(value, 'percentRound'); - } else { - return format(value, undefined, { variant: 'short' }); - } - }} - {...typeof axis === 'object' ? axis : null} - {...props.yAxis} - /> + {/if} {#if axis !== 'y'} - { - if (!isVertical && seriesLayout === 'stackExpand') { - return format(value, 'percentRound'); - } else { - return format(value, undefined, { variant: 'short' }); - } - }} - {...typeof axis === 'object' ? axis : null} - {...props.xAxis} - /> + {/if} - {#if rule} - + {#if typeof rule === 'function'} + {@render rule(snippetProps)} + {:else if rule} + {/if} {/if} - - - - + + + {#if typeof highlight === 'function'} + {@render highlight(snippetProps)} + {:else if highlight} + + {/if} + + {#if typeof labels === 'function'} + {@render labels(snippetProps)} + {:else if labels} + {#each seriesState.visibleSeries as s, i (s.key)} + + {/each} + {/if} + + + + + + {@render aboveContext?.(snippetProps)} - {#if labels} - {#each visibleSeries as s, i (s.key)} - - {/each} + {#if typeof legend === 'function'} + {@render legend(snippetProps)} + {:else if legend} + {/if} - - - - - - {#if legend} - s.key), - series.map((s) => s.color) - )} - tickFormat={(key) => series.find((s) => s.key === key)?.label ?? key} - placement="bottom" - variant="swatches" - onclick={(e, item) => $selectedSeries.toggleSelected(item.value)} - onpointerenter={(e, item) => (highlightSeriesKey = item.value)} - onpointerleave={(e) => (highlightSeriesKey = null)} - {...props.legend} - {...typeof legend === 'object' ? legend : null} - classes={{ - item: (item) => - visibleSeries.length && !visibleSeries.some((s) => s.key === item.value) - ? 'opacity-50' - : '', - ...props.legend?.classes, - ...(typeof legend === 'object' ? legend.classes : null), - }} + + {#if typeof tooltip === 'function'} + {@render tooltip(snippetProps)} + {:else if tooltip} + {/if} - - - - - - - - - {@const seriesItems = stackSeries ? [...visibleSeries].reverse() : visibleSeries} - {#each seriesItems as s} - {@const seriesTooltipData = s.data ? findRelatedData(s.data, data, x) : data} - {@const valueAccessor = accessor(s.value ?? (s.data ? asAny(y) : s.key))} - (highlightSeriesKey = s.key)} - onpointerleave={() => (highlightSeriesKey = null)} - {...props.tooltip?.item} - /> - {/each} - - {#if (stackSeries || groupSeries) && visibleSeries.length > 1 && !props.tooltip?.hideTotal} - - - { - const seriesTooltipData = s.data ? findRelatedData(s.data, data, x) : data; - const valueAccessor = accessor(s.value ?? (s.data ? asAny(y) : s.key)); - return valueAccessor(seriesTooltipData); - })} - format="integer" - valueAlign="right" - {...props.tooltip?.item} - /> - {/if} - - - - + {/if} + {/snippet} diff --git a/packages/layerchart/src/lib/components/charts/ChartAnnotations.svelte b/packages/layerchart/src/lib/components/charts/ChartAnnotations.svelte new file mode 100644 index 000000000..080082b6a --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/ChartAnnotations.svelte @@ -0,0 +1,37 @@ + + +{#each visibleAnnotations as annotation} + {#if annotation.type === 'point'} + + {:else if annotation.type === 'line'} + + {:else if annotation.type === 'range'} + + {/if} +{/each} diff --git a/packages/layerchart/src/lib/components/charts/DefaultTooltip.svelte b/packages/layerchart/src/lib/components/charts/DefaultTooltip.svelte new file mode 100644 index 000000000..9ea980a7b --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/DefaultTooltip.svelte @@ -0,0 +1,60 @@ + + + + {#snippet children({ data, payload })} + + + + + {#each payload as p, i (p.key ?? i)} + (seriesState.highlightKey.current = p.key)} + onpointerleave={() => (seriesState.highlightKey.current = null)} + {...tooltipProps?.item} + /> + {/each} + + {#if canHaveTotal && payload.length > 1 && !tooltipProps?.hideTotal} + + + { + const seriesTooltipData = s.data ? findRelatedData(s.data, data, context.x) : data; + const valueAccessor = accessor(s.value ?? (s.data ? context.y : s.key)); + return seriesTooltipData ? valueAccessor(seriesTooltipData) : 0; + })} + format="integer" + valueAlign="right" + {...tooltipProps?.item} + /> + {/if} + + {/snippet} + diff --git a/packages/layerchart/src/lib/components/charts/LineChart.svelte b/packages/layerchart/src/lib/components/charts/LineChart.svelte index 2f0aac4d9..4c296de25 100644 --- a/packages/layerchart/src/lib/components/charts/LineChart.svelte +++ b/packages/layerchart/src/lib/components/charts/LineChart.svelte @@ -1,213 +1,302 @@ + + + s.value ?? s.key)} - yBaseline={0} - yNice + x={xProp ?? (isVertical ? series.map((s) => s.value ?? s.key) : undefined)} + {xDomain} + xBaseline={!isVertical || (xScale && isScaleTime(xScale)) ? undefined : 0} + xNice={orientation === 'vertical'} + {yScale} + y={yProp ?? (isVertical ? undefined : series.map((s) => s.value ?? s.key))} + yBaseline={isVertical || (yScale && isScaleTime(yScale)) ? undefined : 0} + yNice={orientation === 'horizontal'} {radial} padding={radial ? undefined : defaultChartPadding(axis, legend)} - {...$$restProps} - tooltip={$$props.tooltip === false + {...restProps} + tooltip={tooltip === false ? false : { - mode: 'bisect-x', - onclick: ontooltipclick, + mode: isVertical ? 'quadtree-y' : 'quadtree-x', + onclick: onTooltipClick, debug, ...props.tooltip?.context, - ...$$props.tooltip, + ...(typeof tooltip === 'object' ? tooltip : null), }} - bind:tooltipContext brush={brush && (brush === true || brush.mode == undefined || brush.mode === 'integrated') ? { axis: 'x', resetOnEnd: true, xDomain, ...brushProps, - onbrushend: (e) => { + onBrushEnd: (e) => { xDomain = e.xDomain; - brushProps.onbrushend?.(e); + brushProps.onBrushEnd?.(e); }, } : false} - let:x - let:xScale - let:y - let:yScale - let:c - let:cScale - let:width - let:height - let:padding - let:tooltip > - {@const slotProps = { - x, - xScale, - y, - yScale, - c, - cScale, - width, - height, - padding, - tooltip, - series, - visibleSeries, - getLabelsProps, - getPointsProps, - getSplineProps, - highlightSeriesKey, - setHighlightSeriesKey, - }} - - - - - - {#if grid} - + {#snippet children({ context })} + {@const snippetProps = { + context, + series, + visibleSeries: seriesState.visibleSeries, + getLabelsProps, + getPointsProps, + getSplineProps, + getHighlightProps, + getLegendProps, + getGridProps, + getAxisProps, + getRuleProps, + highlightKey: seriesState.highlightKey.current, + setHighlightKey: seriesState.highlightKey.set, + }} + {#if childrenProp} + {@render childrenProp(snippetProps)} + {:else} + {@render belowContext?.(snippetProps)} + + + {#if typeof grid === 'function'} + {@render grid(snippetProps)} + {:else if grid} + {/if} - - - + + + + {@render belowMarks?.(snippetProps)} + {#if marks} + {@render marks(snippetProps)} + {:else} + {#each seriesState.visibleSeries as s, i (s.key)} + {#if typeof spline === 'function'} + {@render spline({ ...snippetProps, props: getSplineProps(s, i), seriesIndex: i })} + {:else} + + {/if} + {/each} + {/if} - - {#each visibleSeries as s, i (s.key)} - - {/each} - + {@render aboveMarks?.(snippetProps)} + - - + {#if typeof axis === 'function'} + {@render axis(snippetProps)} - - {#if axis} + {#if typeof rule === 'function'} + {@render rule(snippetProps)} + {:else if rule} + + {/if} + {:else if axis} {#if axis !== 'x'} - format(value, undefined, { variant: 'short' })} - {...typeof axis === 'object' ? axis : null} - {...props.yAxis} - /> + {/if} {#if axis !== 'y'} - format(value, undefined, { variant: 'short' })} - {...typeof axis === 'object' ? axis : null} - {...props.xAxis} - /> + {/if} - {#if rule} - + {#if typeof rule === 'function'} + {@render rule(snippetProps)} + {:else if rule} + {/if} {/if} - - - - - {#if points} - {#each visibleSeries as s, i (s.key)} - - {/each} - {/if} - {#if labels} - {#each visibleSeries as s, i (s.key)} - - {/each} - {/if} + + + {#if typeof points === 'function'} + {@render points(snippetProps)} + {:else if points} + {#each seriesState.visibleSeries as s, i (s.key)} + + {/each} + {/if} + + {#if typeof labels === 'function'} + {@render labels(snippetProps)} + {:else if labels} + {#each seriesState.visibleSeries as s, i (s.key)} + + {/each} + {/if} + + {#if typeof highlight === 'function'} + {@render highlight(snippetProps)} + {:else if highlight} + {#each seriesState.visibleSeries as s, i (s.key)} + + {/each} + {/if} + + + + + + {@render aboveContext?.(snippetProps)} + + {#if typeof legend === 'function'} + {@render legend(snippetProps)} + {:else if legend} + + {/if} - - {#each visibleSeries as s, i (s.key)} - {@const seriesTooltipData = - s.data && tooltip.data ? findRelatedData(s.data, tooltip.data, x) : null} - {@const highlightPointsProps = - typeof props.highlight?.points === 'object' ? props.highlight.points : null} - - onpointclick(e, { ...detail, series: s }) - : undefined} - onpointenter={() => (highlightSeriesKey = s.key)} - onpointleave={() => (highlightSeriesKey = null)} - {...props.highlight} - points={props.highlight?.points == false - ? false - : { - ...highlightPointsProps, - fill: s.color, - class: cls( - 'transition-opacity', - highlightSeriesKey && highlightSeriesKey !== s.key && 'opacity-10', - highlightPointsProps?.class - ), - }} - /> - {/each} - - - - - - - - {#if legend} - s.key), - series.map((s) => s.color) - )} - tickFormat={(key) => series.find((s) => s.key === key)?.label ?? key} - placement="bottom" - variant="swatches" - onclick={(e, item) => $selectedSeries.toggleSelected(item.value)} - onpointerenter={(e, item) => (highlightSeriesKey = item.value)} - onpointerleave={(e) => (highlightSeriesKey = null)} - {...props.legend} - {...typeof legend === 'object' ? legend : null} - classes={{ - item: (item) => - visibleSeries.length && !visibleSeries.some((s) => s.key === item.value) - ? 'opacity-50' - : '', - ...props.legend?.classes, - ...(typeof legend === 'object' ? legend.classes : null), - }} - /> + {#if typeof tooltip === 'function'} + {@render tooltip(snippetProps)} + {:else if tooltip} + {/if} - - - - - - - {#each visibleSeries as s} - {@const seriesTooltipData = s.data ? findRelatedData(s.data, data, x) : data} - {@const valueAccessor = accessor(s.value ?? (s.data ? asAny(y) : s.key))} - - (highlightSeriesKey = s.key)} - onpointerleave={() => (highlightSeriesKey = null)} - {...props.tooltip?.item} - /> - {/each} - - - - + {/if} + {/snippet} diff --git a/packages/layerchart/src/lib/components/charts/PieChart.svelte b/packages/layerchart/src/lib/components/charts/PieChart.svelte index 5300ee260..201647a37 100644 --- a/packages/layerchart/src/lib/components/charts/PieChart.svelte +++ b/packages/layerchart/src/lib/components/charts/PieChart.svelte @@ -1,168 +1,333 @@ + + + cAccessor(d)) : [ - 'hsl(var(--color-primary))', - 'hsl(var(--color-secondary))', - 'hsl(var(--color-info))', - 'hsl(var(--color-success))', - 'hsl(var(--color-warning))', - 'hsl(var(--color-danger))', + `var(--color-primary, ${schemeObservable10[0]})`, + `var(--color-secondary, ${schemeObservable10[1]})`, + `var(--color-info, ${schemeObservable10[2]})`, + `var(--color-success, ${schemeObservable10[3]})`, + `var(--color-warning, ${schemeObservable10[4]})`, + `var(--color-danger, ${schemeObservable10[5]})`, ]} - padding={{ bottom: legend === true ? 32 : 0 }} - {...$$restProps} - tooltip={props.tooltip?.context} - bind:tooltipContext - let:x - let:xScale - let:y - let:c - let:cScale - let:yScale - let:width - let:height - let:padding - let:tooltip -> - {@const slotProps = { - key, - label, - value, - x, - xScale, - y, - yScale, - c, - cScale, - width, - height, - padding, - tooltip, - series, - visibleData, - highlightKey, - setHighlightKey, + padding={{ + bottom: legend === true || getObjectOrNull(legend)?.placement?.includes('bottom') ? 32 : 0, }} - - - - - - - - - {#each series as s, i (s.key)} - {@const singleArc = s.data?.length === 1 || chartData.length === 1} - {#if singleArc} - {@const d = s.data?.[0] || chartData[0]} - { - onarcclick(e, { data: d, series: s }); - // Workaround for `tooltip={{ mode: 'manual' }} - ontooltipclick(e, { data: d }); - }} - {...props.arc} - {...s.props} - class={cls( - 'transition-opacity', - highlightKey && highlightKey !== keyAccessor(d) && 'opacity-50', - props.arc?.class, - s.props?.class - )} + {...restProps} + tooltip={tooltip === false + ? false + : { ...props.tooltip?.context, ...(typeof tooltip === 'object' ? tooltip : null) }} +> + {#snippet children({ context })} + {@const snippetProps = { + label: labelAccessor, + key: keyAccessor, + value: valueAccessor, + color: cAccessor, + context, + series, + visibleSeries: seriesState.visibleSeries, + visibleData, + highlightKey: seriesState.highlightKey.current, + setHighlightKey: seriesState.highlightKey.set, + getLegendProps, + getGroupProps, + }} + {#if childrenProp} + {@render childrenProp(snippetProps)} + {:else} + {@render belowContext?.(snippetProps)} + + + {@render belowMarks?.(snippetProps)} + + {#if typeof marks === 'function'} + {@render marks(snippetProps)} + {:else} + + + {#each series as s, seriesIdx (s.key)} + {#if typeof pie === 'function'} + {@render pie({ + ...snippetProps, + props: getPieProps(s, seriesIdx), + index: seriesIdx, + })} + {:else} + + {#snippet children({ arcs })} + {#each arcs as arcData, arcIdx (`${seriesIdx}-${arcIdx}`)} + {@const arcProps = getArcProps(s, seriesIdx, arcData, arcIdx)} + {#if typeof arc === 'function'} + {@render arc({ + ...snippetProps, + props: arcProps, + index: arcIdx, + seriesIndex: seriesIdx, + })} + {:else} + + {/if} + {/each} + {/snippet} + + {/if} + {/each} + + {/if} + + {@render aboveMarks?.(snippetProps)} + + + {@render aboveContext?.(snippetProps)} + + {#if typeof legend === 'function'} + {@render legend(snippetProps)} + {:else if legend} + + {/if} + + {#if typeof tooltip === 'function'} + {@render tooltip(snippetProps)} + {:else if tooltip} + + {#snippet children({ data })} + + (seriesState.highlightKey.current = keyAccessor(data))} + onpointerleave={() => (seriesState.highlightKey.current = null)} + {...props.tooltip?.item} /> - {:else} - - {#each arcs as arc} - 1 ? i * (outerRadius ?? 0) : outerRadius} - {innerRadius} - {cornerRadius} - {padAngle} - fill={cScale?.(c(arc.data))} - data={arc.data} - {tooltip} - onclick={(e) => { - onarcclick(e, { data: arc.data, series: s }); - // Workaround for `tooltip={{ mode: 'manual' }} - ontooltipclick(e, { data: arc.data }); - }} - class={cls( - 'transition-opacity', - highlightKey && highlightKey !== keyAccessor(arc.data) && 'opacity-50' - )} - {...props.arc} - {...s.props} - /> - {/each} - - {/if} - {/each} - - - - - - - - - - {#if legend} - { - const item = chartData.find((d) => keyAccessor(d) === tick); - return item ? (labelAccessor(item) ?? tick) : tick; - }} - placement="bottom" - variant="swatches" - onclick={(e, item) => $selectedKeys.toggleSelected(item.value)} - onpointerenter={(e, item) => (highlightKey = item.value)} - onpointerleave={(e) => (highlightKey = null)} - {...props.legend} - {...typeof legend === 'object' ? legend : null} - classes={{ - item: (item) => - visibleData.length && !visibleData.some((d) => keyAccessor(d) === item.value) - ? 'opacity-50' - : '', - ...props.legend?.classes, - ...(typeof legend === 'object' ? legend.classes : null), - }} - /> + + {/snippet} + {/if} - - - - - - (highlightKey = keyAccessor(data))} - onpointerleave={() => (highlightKey = null)} - {...props.tooltip?.item} - /> - - - - + {/if} + {/snippet} diff --git a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte b/packages/layerchart/src/lib/components/charts/ScatterChart.svelte index e54d17ab1..c15a46ed9 100644 --- a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte +++ b/packages/layerchart/src/lib/components/charts/ScatterChart.svelte @@ -1,173 +1,205 @@ - + + + { + onBrushEnd: (e) => { xDomain = e.xDomain; yDomain = e.yDomain; - brushProps.onbrushend?.(e); + brushProps.onBrushEnd?.(e); }, } : false} - let:x - let:xScale - let:y - let:yScale - let:c - let:cScale - let:r - let:width - let:height - let:padding - let:tooltip - let:config > - {@const slotProps = { - x, - xScale, - y, - yScale, - c, - cScale, - width, - height, - padding, - tooltip, - series, - visibleSeries, - getLabelsProps, - getPointsProps, - highlightSeriesKey, - setHighlightSeriesKey, - }} - {@const activeSeries = tooltip.data - ? (series.find((s) => s.key === tooltip.data.seriesKey) ?? series[0]) - : null} - - - - - - - {#if grid} - + {#snippet children({ context })} + {@const snippetProps = { + context, + series, + visibleSeries: seriesState.visibleSeries, + getLabelsProps, + getPointsProps, + getLegendProps, + getHighlightProps, + getAxisProps, + getRuleProps, + highlightKey: seriesState.highlightKey.current, + setHighlightKey: seriesState.highlightKey.set, + }} + + {#if childrenProp} + {@render childrenProp(snippetProps)} + {:else} + {@render belowContext?.(snippetProps)} + + {#if typeof grid === 'function'} + {@render grid(snippetProps)} + {:else if grid} + {/if} - - - + + - - {#each visibleSeries as s, i (s.key)} - - {/each} - + {@render belowMarks?.(snippetProps)} - - + {#if typeof marks === 'function'} + {@render marks(snippetProps)} + {:else} + {#each seriesState.visibleSeries as s, i (s.key)} + + {/each} + {/if} + + {@render aboveMarks?.(snippetProps)} + - - {#if axis} + {#if typeof axis === 'function'} + {@render axis(snippetProps)} + {#if typeof rule === 'function'} + {@render rule(snippetProps)} + {:else if rule} + + {/if} + {:else if axis} {#if axis !== 'x'} - format(value, undefined, { variant: 'short' })} - {...typeof axis === 'object' ? axis : null} - {...props.yAxis} - /> + {/if} {#if axis !== 'y'} - format(value, undefined, { variant: 'short' })} - {...typeof axis === 'object' ? axis : null} - {...props.xAxis} - /> + {/if} - {#if rule} - + {#if typeof rule === 'function'} + {@render rule(snippetProps)} + {:else if rule} + {/if} {/if} - - - - - - + + {#if typeof highlight === 'function'} + {@render highlight(snippetProps)} + {:else if highlight} + + {/if} + + {#if typeof labels === 'function'} + {@render labels(snippetProps)} + {:else if labels} + {#each seriesState.visibleSeries as s, i (s.key)} + + {/each} + {/if} + + - + + - {#if labels} - {#each visibleSeries as s, i (s.key)} - - {/each} - {/if} - - - - - - - {#if legend} - s.key), - series.map((s) => s.color) - )} - tickFormat={(key) => series.find((s) => s.key === key)?.label ?? key} - placement="bottom" - variant="swatches" - onclick={(e, item) => $selectedSeries.toggleSelected(item.value)} - onpointerenter={(e, item) => (highlightSeriesKey = item.value)} - onpointerleave={(e) => (highlightSeriesKey = null)} - {...props.legend} - {...typeof legend === 'object' ? legend : null} - classes={{ - item: (item) => - visibleSeries.length && !visibleSeries.some((s) => s.key === item.value) - ? 'opacity-50' - : '', - ...props.legend?.classes, - ...(typeof legend === 'object' ? legend.classes : null), - }} - /> + {@render aboveContext?.(snippetProps)} + + {#if typeof legend === 'function'} + {@render legend(snippetProps)} + {:else if legend} + {/if} - - - - - {#if activeSeries?.key !== 'default'} - - {/if} - - (highlightSeriesKey = activeSeries?.key ?? null)} - onpointerleave={() => (highlightSeriesKey = null)} - {...props.tooltip?.item} - /> - (highlightSeriesKey = activeSeries?.key ?? null)} - onpointerleave={() => (highlightSeriesKey = null)} - {...props.tooltip?.item} - /> - {#if config.r} - (highlightSeriesKey = activeSeries?.key ?? null)} - onpointerleave={() => (highlightSeriesKey = null)} - {...props.tooltip?.item} - /> - {/if} - - - - + + {#if typeof tooltip === 'function'} + {@render tooltip(snippetProps)} + {:else if tooltip} + + {#snippet children({ data })} + {#if activeSeries?.key !== 'default'} + + {/if} + + + (seriesState.highlightKey.current = activeSeries?.key ?? null)} + onpointerleave={() => (seriesState.highlightKey.current = null)} + {...props.tooltip?.item} + /> + + (seriesState.highlightKey.current = activeSeries?.key ?? null)} + onpointerleave={() => (seriesState.highlightKey.current = null)} + {...props.tooltip?.item} + /> + {#if context.config.r} + + (seriesState.highlightKey.current = activeSeries?.key ?? null)} + onpointerleave={() => (seriesState.highlightKey.current = null)} + {...props.tooltip?.item} + /> + {/if} + + {/snippet} + + {/if} + {/if} + {/snippet} diff --git a/packages/layerchart/src/lib/components/charts/index.ts b/packages/layerchart/src/lib/components/charts/index.ts index d2f2c6bd3..774620db1 100644 --- a/packages/layerchart/src/lib/components/charts/index.ts +++ b/packages/layerchart/src/lib/components/charts/index.ts @@ -1,5 +1,13 @@ +export { default as ArcChart } from './ArcChart.svelte'; +export * from './ArcChart.svelte'; export { default as AreaChart } from './AreaChart.svelte'; +export * from './AreaChart.svelte'; export { default as BarChart } from './BarChart.svelte'; +export * from './BarChart.svelte'; export { default as LineChart } from './LineChart.svelte'; +export * from './LineChart.svelte'; export { default as PieChart } from './PieChart.svelte'; +export * from './PieChart.svelte'; export { default as ScatterChart } from './ScatterChart.svelte'; +export * from './ScatterChart.svelte'; +export type * from './types.js'; diff --git a/packages/layerchart/src/lib/components/charts/types.ts b/packages/layerchart/src/lib/components/charts/types.ts new file mode 100644 index 000000000..e3120fc1f --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/types.ts @@ -0,0 +1,321 @@ +import type { Accessor } from '$lib/utils/common.js'; +import type { Component, ComponentProps, Snippet } from 'svelte'; +import type BrushContext from '../BrushContext.svelte'; +import type { AnyScale } from '$lib/utils/scales.svelte.js'; +import type { + AnnotationPoint, + AnnotationLine, + AnnotationRange, + Area, + Axis, + Group, + Labels, + Legend, + Points, + Rule, + Spline, +} from '../index.js'; +import type TooltipContext from '../tooltip/TooltipContext.svelte'; +import type { TooltipContextValue } from '../tooltip/TooltipContext.svelte'; +import type Highlight from '../Highlight.svelte'; +import type Line from '../Line.svelte'; +import type Svg from '../layout/Svg.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 { ChartContextValue, ChartPropsWithoutHTML } from '../Chart.svelte'; +import type Grid from '../Grid.svelte'; +import type Bars from '../Bars.svelte'; +import type Pie from '../Pie.svelte'; +import type Arc from '../Arc.svelte'; +import type Canvas from '../layout/Canvas.svelte'; +import type { Without } from '$lib/utils/types.js'; + +export type SeriesData = { + key: string; + label?: string; + value?: Accessor; + /** + * Maximum possible value. Useful when `data` is a single item + */ + maxValue?: number; + data?: TData[]; + color?: string; + props?: Partial>; +}; + +export type SimplifiedChartSnippetProps = { + /** + * The chart context + */ + context: ChartContextValue; + + /** + * The series of data for the chart. + */ + series: SeriesData[]; + + /** + * The visible series of data for the chart. + */ + visibleSeries: SeriesData[]; + + /** + * The current highlight series key for the chart. + */ + highlightKey: SeriesData['key'] | null; + + /** + * A function to set the highlight series key for the chart. + */ + setHighlightKey: (seriesKey: SeriesData['key'] | null) => void; + + /** + * Get the default props for the legend component. + */ + getLegendProps: () => ComponentProps; +} & TSnippetProps; + +export type SimplifiedChartSnippet = Snippet< + [SimplifiedChartSnippetProps] +>; + +export type SimplifiedChartPropsObject = { + 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>; +}; + +export type BaseChartProps< + TData, + TComponent extends Component, + TSnippetProps = {}, + ChartSnippet = SimplifiedChartSnippet, +> = { + /** + * The data to be rendered in the chart. + * + * @default [] + */ + data?: TData[]; + + /** + * The x accessor function to be used for the chart. + * + */ + x?: Accessor; + + /** + * The y accessor function to be used for the chart. + * + */ + y?: Accessor; + + xScale?: AnyScale; + /** + * The x domain to be used for the chart. + * + */ + xDomain?: ComponentProps['xDomain']; + /** + * Use radial instead of cartesian coordinates, mapping `x` to `angle` and `y`` to + * radial. Radial lines are positioned relative to the origin, use transform + * (ex. ``) to change the origin + * + * @default false + */ + radial?: boolean; + /** + * 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'; + /** + * The axis to be used for the chart. + * + * @default true + */ + axis?: + | ComponentProps + | 'x' + | 'y' + | boolean + | SimplifiedChartSnippet; + /** + * The brush to be used for the chart. + * + * @default false + */ + brush?: ComponentProps | boolean; + + /** + * 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 to be used for the chart. + */ + tooltip?: ComponentProps | boolean | ChartSnippet; + + highlight?: boolean | ChartSnippet; + + /** Annotations to show on chart */ + annotations?: ChartAnnotations; + + /** + * The tooltip context to be used for the chart. + */ + tooltipContext?: TooltipContextValue; + + /** + * The event to be dispatched when the tooltip is clicked. + * + */ + onTooltipClick?: (e: MouseEvent, details: { data: any }) => void; + + /** + * The render context to be used for the chart. + * + * @default 'svg' + */ + renderContext?: 'svg' | 'canvas'; + + /** + * Whether to log the initial render performance using `console.time`. + * + * @default false + */ + profile?: boolean; + + /** + * Whether to enable debug mode. + * + * @default false + */ + debug?: boolean; + + /** + * A bindable reference to the chart context. + */ + context?: ChartContextValue; + + children?: ChartSnippet; + aboveContext?: ChartSnippet; + belowContext?: ChartSnippet; + belowMarks?: ChartSnippet; + aboveMarks?: ChartSnippet; + marks?: ChartSnippet; +}; + +export type SimplifiedChartProps< + TData, + TComponent extends Component, + TSnippetProps = {}, + ChartSnippet = SimplifiedChartSnippet, +> = BaseChartProps & + Without< + ChartPropsWithoutHTML, + BaseChartProps + >; + +export type ChartAnnotations = Array< + | ({ + /** Create AnnotationPoint */ + type: 'point'; + + /** Apply `above` or `below` marks + * @default 'above' + */ + layer?: 'above' | 'below'; + + /** Related to specific series (if applicable). Will hide if set and series not highlighted */ + seriesKey?: string; + } & ComponentProps) + | ({ + /** Create AnnotationLine */ + type: 'line'; + + /** Apply `above` or `below` marks + * @default 'above' + */ + layer?: 'above' | 'below'; + + /** Related to specific series (if applicable). Will hide if set and series not highlighted */ + seriesKey?: string; + } & ComponentProps) + | ({ + /** Create AnnotationRange */ + type: 'range'; + + /** Apply `above` or `below` marks + * @default 'above' + */ + layer?: 'above' | 'below'; + + /** Related to specific series (if applicable). Will hide if set and series not highlighted */ + seriesKey?: string; + } & ComponentProps) +>; diff --git a/packages/layerchart/src/lib/components/charts/utils.svelte.ts b/packages/layerchart/src/lib/components/charts/utils.svelte.ts new file mode 100644 index 000000000..c97c97c50 --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/utils.svelte.ts @@ -0,0 +1,43 @@ +import type { Component, ComponentProps } from 'svelte'; + +import { scaleOrdinal } from 'd3-scale'; +import { cls } from '@layerstack/tailwind'; + +import type Legend from '../Legend.svelte'; +import { resolveMaybeFn } from '$lib/utils/common.js'; +import type { SeriesState } from '$lib/states/series.svelte.js'; + +type CreateLegendPropsOptions = { + seriesState: SeriesState; + props: Partial>; +}; + +/** + * A prop builder for the legend component shared between the simplified charts. + */ +export function createLegendProps( + opts: CreateLegendPropsOptions +): ComponentProps { + return { + scale: opts.seriesState.isDefaultSeries + ? undefined + : scaleOrdinal( + opts.seriesState.series.map((s) => s.key), + opts.seriesState.series.map((s) => s.color) + ), + tickFormat: (key) => opts.seriesState.series.find((s) => s.key === key)?.label ?? key, + placement: 'bottom', + variant: 'swatches', + selected: opts.seriesState.selectedKeys.current, + onclick: (_, item) => opts.seriesState.selectedKeys.toggle(item.value), + onpointerenter: (_, item) => (opts.seriesState.highlightKey.current = item.value), + onpointerleave: () => (opts.seriesState.highlightKey.current = null), + ...opts.props, + classes: { + item: (item) => { + return cls(resolveMaybeFn(opts.props?.classes?.item, item)); + }, + ...opts.props?.classes, + }, + }; +} diff --git a/packages/layerchart/src/lib/components/index.ts b/packages/layerchart/src/lib/components/index.ts index 8f5f0b8a7..48e14a6fd 100644 --- a/packages/layerchart/src/lib/components/index.ts +++ b/packages/layerchart/src/lib/components/index.ts @@ -1,66 +1,141 @@ -// Re-export for easy access (Svg and Canvas are provided by LayerChart) -export { WebGL } from 'layercake'; - export * from './charts/index.js'; +export * from './types.js'; +export { default as AnnotationLine } from './AnnotationLine.svelte'; +export * from './AnnotationLine.svelte'; +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 Area } from './Area.svelte'; +export * from './Area.svelte'; export { default as Axis } from './Axis.svelte'; +export * from './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 Blur } from './Blur.svelte'; +export * from './Blur.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 Canvas } from './layout/Canvas.svelte'; -export { default as Chart } from './Chart.svelte'; +export * from './layout/Canvas.svelte'; +export { default as Chart, getChartContext, getRenderContext } from './Chart.svelte'; +export * from './Chart.svelte'; export { default as ChartClipPath } from './ChartClipPath.svelte'; +export * from './ChartClipPath.svelte'; export { default as Circle } from './Circle.svelte'; +export * from './Circle.svelte'; export { default as CircleClipPath } from './CircleClipPath.svelte'; +export * from './CircleClipPath.svelte'; export { default as ClipPath } from './ClipPath.svelte'; +export * from './ClipPath.svelte'; export { default as ColorRamp } from './ColorRamp.svelte'; +export * from './ColorRamp.svelte'; +export { default as Connector } from './Connector.svelte'; +export * from './Connector.svelte'; export { default as Dagre } from './Dagre.svelte'; +export * from './Dagre.svelte'; +export { default as Ellipse } from './Ellipse.svelte'; +export * from './Ellipse.svelte'; export { default as Frame } from './Frame.svelte'; +export * from './Frame.svelte'; export { default as ForceSimulation } from './ForceSimulation.svelte'; +export * from './ForceSimulation.svelte'; export { default as GeoCircle } from './GeoCircle.svelte'; -export { default as GeoContext, geoContext } from './GeoContext.svelte'; +export * from './GeoCircle.svelte'; +export { default as GeoContext, getGeoContext } from './GeoContext.svelte'; +export * from './GeoContext.svelte'; export { default as GeoEdgeFade } from './GeoEdgeFade.svelte'; +export * from './GeoEdgeFade.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 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 Grid } from './Grid.svelte'; +export * from './Grid.svelte'; export { default as Group } from './Group.svelte'; +export * from './Group.svelte'; export { default as Highlight } from './Highlight.svelte'; +export * from './Highlight.svelte'; export { default as Html } from './layout/Html.svelte'; +export * from './layout/Html.svelte'; export { default as Hull } from './Hull.svelte'; +export * from './Hull.svelte'; export { default as Labels } from './Labels.svelte'; +export * from './Labels.svelte'; +export { default as Layer } from './layout/Layer.svelte'; +export * from './layout/Layer.svelte'; export { default as Legend } from './Legend.svelte'; +export * from './Legend.svelte'; export { default as Line } from './Line.svelte'; +export * from './Line.svelte'; export { default as Spline } from './Spline.svelte'; +export * from './Spline.svelte'; export { default as LinearGradient } from './LinearGradient.svelte'; +export * from './LinearGradient.svelte'; export { default as Link } from './Link.svelte'; +export * from './Link.svelte'; export { default as MotionPath } from './MotionPath.svelte'; +export * from './MotionPath.svelte'; export { default as Pack } from './Pack.svelte'; +export * from './Pack.svelte'; export { default as Partition } from './Partition.svelte'; +export * from './Partition.svelte'; export { default as Pattern } from './Pattern.svelte'; +export * from './Pattern.svelte'; export { default as Pie } from './Pie.svelte'; +export * from './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 Polygon } from './Polygon.svelte'; +export * from './Polygon.svelte'; export { default as RadialGradient } from './RadialGradient.svelte'; +export * from './RadialGradient.svelte'; export { default as Rect } from './Rect.svelte'; +export * from './Rect.svelte'; export { default as RectClipPath } from './RectClipPath.svelte'; +export * from './RectClipPath.svelte'; export { default as Rule } from './Rule.svelte'; +export * from './Rule.svelte'; export { default as Sankey } from './Sankey.svelte'; +export * from './Sankey.svelte'; export { default as Svg } from './layout/Svg.svelte'; +export * from './layout/Svg.svelte'; export { default as Text } from './Text.svelte'; +export * from './Text.svelte'; export { default as Threshold } from './Threshold.svelte'; +export * from './Threshold.svelte'; export { default as TileImage } from './TileImage.svelte'; +export * from './TileImage.svelte'; export * as Tooltip from './tooltip/index.js'; -export { default as TransformContext, transformContext } from './TransformContext.svelte'; -// export { default as TransformControls } from './TransformControls.svelte'; // TODO: Restore once no longer using `svelet-ux` or `@mdi/js` (as they are dev dependencies) +export * from './tooltip/TooltipContext.svelte'; + +export { default as TransformContext } from './TransformContext.svelte'; +export * from './TransformContext.svelte'; +// export { default as TransformControls } from './TransformControls.svelte'; // TODO: Restore once no longer using `svelte-ux` or `~icons` (as they are dev dependencies) export { default as Tree } from './Tree.svelte'; +export * from './Tree.svelte'; export { default as Treemap } from './Treemap.svelte'; +export * from './Treemap.svelte'; export { default as Voronoi } from './Voronoi.svelte'; +export * from './Voronoi.svelte'; +export { default as WebGL } from './layout/WebGL.svelte'; +export * from './layout/WebGL.svelte'; diff --git a/packages/layerchart/src/lib/components/layout/Canvas.svelte b/packages/layerchart/src/lib/components/layout/Canvas.svelte index 2bb1f5ee1..85d06efa1 100644 --- a/packages/layerchart/src/lib/components/layout/Canvas.svelte +++ b/packages/layerchart/src/lib/components/layout/Canvas.svelte @@ -1,110 +1,223 @@ - { + class={['lc-layout-canvas', className]} + class:disablePointerEvents={pointerEvents === false} + onclick={(e) => { const component = getPointerComponent(e); component?.events?.click?.(e); + onclick?.(e); }} - on:click - on:dblclick={(e) => { + ondblclick={(e) => { const component = getPointerComponent(e); component?.events?.dblclick?.(e); + ondblclick?.(e); }} - on:pointerdown={(e) => { + onpointerdown={(e) => { const component = getPointerComponent(e); component?.events?.pointerdown?.(e); + onpointerdown?.(e); + }} + onpointerenter={(e) => { + onpointerenter?.(e); + onPointerMove(e); + }} + onpointermove={(e) => { + onpointermove?.(e); + onPointerMove(e); + }} + onpointerleave={(e) => { + onpointerleave?.(e); + onPointerLeave(e); }} - on:pointerenter={onPointerMove} - on:pointerenter - on:pointermove={onPointerMove} - on:pointermove - on:pointerleave={onPointerLeave} - on:pointerleave - on:touchmove={(e) => { + ontouchmove={(e) => { // Prevent touch from interfering with pointer if over data if (lastActiveComponent) { e.preventDefault(); @@ -316,24 +489,44 @@ const component = getPointerComponent(e); component?.events?.touchmove?.(e); }} - on:touchmove + {...restProps} > - - {fallback || ''} - + {#if fallback} + {#if typeof fallback === 'function'} + {@render fallback()} + {:else} + {fallback} + {/if} + {/if} - - - + + +{@render children?.({ ref, canvasContext: context })} + + diff --git a/packages/layerchart/src/lib/components/layout/Html.svelte b/packages/layerchart/src/lib/components/layout/Html.svelte index 794ba6052..0599b9d25 100644 --- a/packages/layerchart/src/lib/components/layout/Html.svelte +++ b/packages/layerchart/src/lib/components/layout/Html.svelte @@ -1,74 +1,120 @@ - - /** - * Translate children to center (useful for radial layouts) - */ - export let center: boolean | 'x' | 'y' = false; +
- + {@render children?.({ ref })}
+ + diff --git a/packages/layerchart/src/lib/components/layout/Layer.svelte b/packages/layerchart/src/lib/components/layout/Layer.svelte new file mode 100644 index 000000000..9332170de --- /dev/null +++ b/packages/layerchart/src/lib/components/layout/Layer.svelte @@ -0,0 +1,41 @@ + + + + +{#if type === 'canvas'} + }> + {@render children?.()} + +{:else if type === 'svg'} + }> + {@render children?.()} + +{:else if type === 'html'} + }> + {@render children?.()} + +{/if} diff --git a/packages/layerchart/src/lib/components/layout/Svg.svelte b/packages/layerchart/src/lib/components/layout/Svg.svelte index c7415d9d0..696c66cbb 100644 --- a/packages/layerchart/src/lib/components/layout/Svg.svelte +++ b/packages/layerchart/src/lib/components/layout/Svg.svelte @@ -1,97 +1,157 @@ - - let transform = ''; - $: if (mode === 'canvas' && !ignoreTransform) { - transform = `translate(${$translate.x},${$translate.y}) scale(${$scale})`; - } else if (center) { - transform = `translate(${center === 'x' || center === true ? $width / 2 : 0}, ${center === 'y' || center === true ? $height / 2 : 0})`; - } + - - - - {#if title}{title}{/if} - + {#if typeof title === 'function'} + {@render title()} + {:else if title} + {title} + {/if} - + {@render defs?.()} {#if transform} - - + + {@render children?.({ ref })} {:else} - + {@render children?.({ ref })} {/if} + + diff --git a/packages/layerchart/src/lib/components/layout/WebGL.svelte b/packages/layerchart/src/lib/components/layout/WebGL.svelte new file mode 100644 index 000000000..89365fd8d --- /dev/null +++ b/packages/layerchart/src/lib/components/layout/WebGL.svelte @@ -0,0 +1,155 @@ + + + + + + {#if typeof fallback === 'function'} + {@render fallback()} + {:else if fallback} + {fallback} + {/if} + + +{@render children?.({ ref, webGLContext: context })} + + diff --git a/packages/layerchart/src/lib/components/tooltip/Tooltip.svelte b/packages/layerchart/src/lib/components/tooltip/Tooltip.svelte index 14a97f08a..22c9c0967 100644 --- a/packages/layerchart/src/lib/components/tooltip/Tooltip.svelte +++ b/packages/layerchart/src/lib/components/tooltip/Tooltip.svelte @@ -1,79 +1,221 @@ - - /** Allow pointer events. Disabled by default to reduce accidental selection, but useful to enable to allow interactdive tooltips (using `locked`) */ - export let pointerEvents = false; + -{#if $tooltip.data} +{#if tooltipCtx.data}
{ + tooltipCtx.isHoveringTooltipContent = true; + }} + onpointerleave={() => { + tooltipCtx.isHoveringTooltipContent = false; + }} >
- {#if $$slots.default} -
- + {#if children} +
+ {@render children({ data: tooltipCtx.data, payload: tooltipCtx.payload })}
{/if}
{/if} + + diff --git a/packages/layerchart/src/lib/components/tooltip/TooltipContext.svelte b/packages/layerchart/src/lib/components/tooltip/TooltipContext.svelte index 52301af34..a3622b2a1 100644 --- a/packages/layerchart/src/lib/components/tooltip/TooltipContext.svelte +++ b/packages/layerchart/src/lib/components/tooltip/TooltipContext.svelte @@ -1,137 +1,244 @@ - - + const value = ctx.xGet(d); - - -
{ - isHoveringTooltip = true; + if (Array.isArray(value)) { + // `x` accessor with multiple properties (ex. `x={['start', 'end']})`) + // Using first value. Consider using average, max, etc + // const midpoint = new Date((value[1].valueOf() + value[0].getTime()) / 2); + // return midpoint; + return min(value); + } else { + return value; + } + }) + .y((d) => { + if (mode === 'quadtree-x') { + return 0; + } + + if (geoCtx.projection) { + const lat = ctx.x(d); + const long = ctx.y(d); + const geoValue = geoCtx.projection([lat, long]) ?? [0, 0]; + return geoValue[1]; + } + + const value = ctx.yGet(d); + + if (Array.isArray(value)) { + // `x` accessor with multiple properties (ex. `x={['start', 'end']})`) + // Using first value. Consider using average, max, etc + // const midpoint = new Date((value[1].valueOf() + value[0].getTime()) / 2); + // return midpoint; + return min(value); + } else { + return value; + } + }) + .addAll(ctx.flatData as [number, number][]); + } + }); + + const rects: Array<{ x: number; y: number; width: number; height: number; data: any }> = + $derived.by(() => { + if (mode === 'bounds' || mode === 'band') { + return ctx.flatData + .map((d) => { + const xValue = ctx.xGet(d); + const yValue = ctx.yGet(d); + + const x = Array.isArray(xValue) ? xValue[0] : xValue; + const y = Array.isArray(yValue) ? yValue[0] : yValue; + + const xOffset = isScaleBand(ctx.xScale) + ? (ctx.xScale.padding() * ctx.xScale.step()) / 2 + : 0; + const yOffset = isScaleBand(ctx.yScale) + ? (ctx.yScale.padding() * ctx.yScale.step()) / 2 + : 0; + + const fullWidth = max(ctx.xRange) - min(ctx.xRange); + const fullHeight = max(ctx.yRange) - min(ctx.yRange); + + if (mode === 'band') { + if (isScaleBand(ctx.xScale)) { + // full band width/height regardless of value + return { + x: x - xOffset, + y: isScaleBand(ctx.yScale) ? y - yOffset : min(ctx.yRange), + width: ctx.xScale.step(), + height: isScaleBand(ctx.yScale) ? ctx.yScale.step() : fullHeight, + data: d, + }; + } else if (isScaleBand(ctx.yScale)) { + return { + x: isScaleBand(ctx.xScale) ? x - xOffset : min(ctx.xRange), + y: y - yOffset, + width: isScaleBand(ctx.xScale) ? ctx.xScale.step() : fullWidth, + height: ctx.yScale.step(), + data: d, + }; + } else if (ctx.xInterval) { + // x-axis time scale with interval + const xVal = ctx.x(d); + const start = ctx.xInterval.floor(xVal); + const end = ctx.xInterval.offset(start); + + return { + x: ctx.xScale(start), + y: isScaleBand(ctx.yScale) ? y - yOffset : min(ctx.yRange), + width: ctx.xScale(end) - ctx.xScale(start), + height: isScaleBand(ctx.yScale) ? ctx.yScale.step() : fullHeight, + data: d, + }; + } else if (ctx.yInterval) { + // y-axis time scale with interval + const yVal = ctx.y(d); + const start = ctx.yInterval.floor(yVal); + const end = ctx.yInterval.offset(start); + + return { + x: isScaleBand(ctx.xScale) ? x - xOffset : min(ctx.xRange), + y: ctx.yScale(start), + width: isScaleBand(ctx.xScale) ? ctx.xScale.step() : fullWidth, + height: ctx.yScale(end) - ctx.yScale(start), + data: d, + }; + } else if (isScaleTime(ctx.xScale)) { + // Find width to next data point + const index = ctx.flatData.findIndex( + (d2) => Number(ctx.x(d2)) === Number(ctx.x(d)) + ); + const isLastPoint = index + 1 === ctx.flatData.length; + const nextDataPoint = isLastPoint + ? max(ctx.xDomain) + : ctx.x(ctx.flatData[index + 1]); + + return { + x: x - xOffset, + y: isScaleBand(ctx.yScale) ? y - yOffset : min(ctx.yRange), + width: (ctx.xScale(nextDataPoint) ?? 0) - (xValue ?? 0), + height: isScaleBand(ctx.yScale) ? ctx.yScale.step() : fullHeight, + data: d, + }; + } else if (isScaleTime(ctx.yScale)) { + // Find height to next data point + const index = ctx.flatData.findIndex( + (d2) => Number(ctx.y(d2)) === Number(ctx.y(d)) + ); + const isLastPoint = index + 1 === ctx.flatData.length; + const nextDataPoint = isLastPoint + ? max(ctx.yDomain) + : ctx.y(ctx.flatData[index + 1]); + + return { + x: isScaleBand(ctx.xScale) ? x - xOffset : min(ctx.xRange), + y: y - yOffset, + width: isScaleBand(ctx.xScale) ? ctx.xScale.step() : fullWidth, + height: (ctx.yScale(nextDataPoint) ?? 0) - (yValue ?? 0), + data: d, + }; + } else { + console.warn( + '[layerchart] TooltipContext band mode requires at least one scale to be band or time.' + ); + return undefined; + } + } else if (mode === 'bounds') { + return { + x: isScaleBand(ctx.xScale) || Array.isArray(xValue) ? x - xOffset : min(ctx.xRange), + // y: isScaleBand($yScale) || Array.isArray(yValue) ? y - yOffset : min($yRange), + y: y - yOffset, + + width: Array.isArray(xValue) + ? xValue[1] - xValue[0] + : isScaleBand(ctx.xScale) + ? ctx.xScale.step() + : min(ctx.xRange) + x, + height: Array.isArray(yValue) + ? yValue[1] - yValue[0] + : isScaleBand(ctx.yScale) + ? ctx.yScale.step() + : max(ctx.yRange) - y, + data: d, + }; + } + }) + .filter((x) => x !== undefined) // make typescript happy + .sort(sortFunc('x')); + } + return []; + }); + + const triggerPointerEvents = $derived( + ['bisect-x', 'bisect-y', 'bisect-band', 'quadtree', 'quadtree-x', 'quadtree-y'].includes(mode) + ); + + function onPointerEnter(e: PointerEvent | MouseEvent | TouchEvent) { + isHoveringTooltipArea = true; if (triggerPointerEvents) { showTooltip(e); } - }} - on:pointermove={(e) => { + } + + function onPointerMove(e: PointerEvent | MouseEvent | TouchEvent) { if (triggerPointerEvents) { showTooltip(e); } - }} - on:pointerleave={(e) => { - isHoveringTooltip = false; + } + + function onPointerLeave(e: PointerEvent | MouseEvent | TouchEvent) { + isHoveringTooltipArea = false; hideTooltip(); - }} - on:click={(e) => { + } + + + +
{ // Ignore clicks without data (triggered from Legend clicks, for example) - if (triggerPointerEvents && $tooltip?.data != null) { - onclick(e, { data: $tooltip?.data }); + if (triggerPointerEvents && tooltipContext.data != null) { + onclick(e, { data: tooltipContext.data }); } }} - bind:this={tooltipContextNode} + onkeydown={() => {}} + bind:this={ref} >
- + {@render children?.({ tooltipContext: tooltipContext })} {#if mode === 'voronoi'} { showTooltip(e, data); }} onpointermove={(e, { data }) => { showTooltip(e, data); }} - onpointerleave={hideTooltip} + onpointerleave={() => hideTooltip()} onpointerdown={(e) => { // @ts-expect-error if (e.target?.hasPointerCapture(e.pointerId)) { @@ -458,52 +685,119 @@ onclick={(e, { data }) => { onclick(e, { data }); }} - classes={{ path: cls(debug && 'fill-danger/10 stroke-danger') }} + classes={{ path: cls('lc-tooltip-voronoi-path', debug && 'debug') }} /> {:else if mode === 'bounds' || mode === 'band'} - - + + {#each rects as rect} - showTooltip(e, rect.data)} - on:pointermove={(e) => showTooltip(e, rect.data)} - on:pointerleave={hideTooltip} - on:pointerdown={(e) => { - // @ts-expect-error - if (e.target?.hasPointerCapture(e.pointerId)) { - // @ts-expect-error - e.target.releasePointerCapture(e.pointerId); - } - }} - on:click={(e) => { - onclick(e, { data: rect.data }); - }} - /> + + {#if ctx.radial} + showTooltip(e, rect?.data)} + onpointermove={(e) => showTooltip(e, rect?.data)} + onpointerleave={() => hideTooltip()} + onpointerdown={(e) => { + const target = e.target as Element; + if (target?.hasPointerCapture(e.pointerId)) { + target.releasePointerCapture(e.pointerId); + } + }} + onclick={(e) => { + onclick(e, { data: rect?.data }); + }} + /> + {:else} + showTooltip(e, rect?.data)} + onpointermove={(e) => showTooltip(e, rect?.data)} + onpointerleave={() => hideTooltip()} + onpointerdown={(e) => { + const target = e.target as Element; + if (target?.hasPointerCapture(e.pointerId)) { + target.releasePointerCapture(e.pointerId); + } + }} + onclick={(e) => { + onclick(e, { data: rect?.data }); + }} + /> + {/if} {/each} - {:else if mode === 'quadtree' && debug} + {:else if ['quadtree', 'quadtree-x', 'quadtree-y'].includes(mode) && debug} - - {#each quadtreeRects(quadtree, false) as rect} - - {/each} + + {#if quadtree} + {#each quadtreeRects(quadtree, false) as rect} + + {/each} + {/if} {/if}
+ + diff --git a/packages/layerchart/src/lib/components/tooltip/TooltipHeader.svelte b/packages/layerchart/src/lib/components/tooltip/TooltipHeader.svelte index 0847dbffe..9ab2f9aab 100644 --- a/packages/layerchart/src/lib/components/tooltip/TooltipHeader.svelte +++ b/packages/layerchart/src/lib/components/tooltip/TooltipHeader.svelte @@ -1,30 +1,133 @@ + +
{#if color}
{/if} - {format ? formatUtil(value, format) : value} + {#if children} + {@render children?.()} + {:else} + + {format ? formatUtil(value, asAny(format)) : value} + {/if}
+ + diff --git a/packages/layerchart/src/lib/components/tooltip/TooltipItem.svelte b/packages/layerchart/src/lib/components/tooltip/TooltipItem.svelte index 600738da7..b67a3ca2a 100644 --- a/packages/layerchart/src/lib/components/tooltip/TooltipItem.svelte +++ b/packages/layerchart/src/lib/components/tooltip/TooltipItem.svelte @@ -1,55 +1,204 @@ + +
-
+
{#if color}
{/if} - {label} + {#if typeof label === 'function'} + {@render label()} + {:else} + {label} + {/if}
- {format ? formatUtil(value, format) : value} + {#if children} + {@render children()} + {:else} + + {format ? formatUtil(value, asAny(format)) : value} + {/if}
+ + diff --git a/packages/layerchart/src/lib/components/tooltip/TooltipList.svelte b/packages/layerchart/src/lib/components/tooltip/TooltipList.svelte index dc5c15908..c0fdf818d 100644 --- a/packages/layerchart/src/lib/components/tooltip/TooltipList.svelte +++ b/packages/layerchart/src/lib/components/tooltip/TooltipList.svelte @@ -1,13 +1,34 @@ -
- +
+ {@render children?.()}
+ + diff --git a/packages/layerchart/src/lib/components/tooltip/TooltipSeparator.svelte b/packages/layerchart/src/lib/components/tooltip/TooltipSeparator.svelte index 8ce53f18f..ce78ebb65 100644 --- a/packages/layerchart/src/lib/components/tooltip/TooltipSeparator.svelte +++ b/packages/layerchart/src/lib/components/tooltip/TooltipSeparator.svelte @@ -1,5 +1,39 @@ -
+
+ {@render children?.()} +
+ + diff --git a/packages/layerchart/src/lib/components/tooltip/index.ts b/packages/layerchart/src/lib/components/tooltip/index.ts index a3d575a8e..5f6bc2036 100644 --- a/packages/layerchart/src/lib/components/tooltip/index.ts +++ b/packages/layerchart/src/lib/components/tooltip/index.ts @@ -1,6 +1,12 @@ export { default as Context } from './TooltipContext.svelte'; +export * from './TooltipContext.svelte'; export { default as Header } from './TooltipHeader.svelte'; +export * from './TooltipHeader.svelte'; export { default as Item } from './TooltipItem.svelte'; +export * from './TooltipItem.svelte'; export { default as List } from './TooltipList.svelte'; +export * from './TooltipList.svelte'; export { default as Separator } from './TooltipSeparator.svelte'; +export * from './TooltipSeparator.svelte'; export { default as Root } from './Tooltip.svelte'; +export * from './Tooltip.svelte'; diff --git a/packages/layerchart/src/lib/components/tooltip/tooltipMetaContext.ts b/packages/layerchart/src/lib/components/tooltip/tooltipMetaContext.ts new file mode 100644 index 000000000..2074861b0 --- /dev/null +++ b/packages/layerchart/src/lib/components/tooltip/tooltipMetaContext.ts @@ -0,0 +1,263 @@ +// Additional meta data that can be set by the various simplified chart components +// to provide additional payload data to the tooltip for ease of composition. + +import { accessor, findRelatedData, type Accessor } from '$lib/utils/common.js'; +import type { SeriesData } from '../charts/index.js'; +import type { ChartContextValue } from '../Chart.svelte'; +import { asAny } from '$lib/utils/types.js'; +import { format, type FormatType, type FormatConfig } from '@layerstack/utils'; +import { Context } from 'runed'; + +export type SimplifiedChartType = 'bar' | 'area' | 'line' | 'pie' | 'scatter'; + +export type BarTooltipMetaContextValue = { + type: 'bar'; + orientation: 'horizontal' | 'vertical'; + stackSeries: boolean; + visibleSeries: SeriesData[]; +}; + +export type AreaTooltipMetaContextValue = { + type: 'area'; + stackSeries: boolean; + visibleSeries: SeriesData[]; +}; + +export type LineTooltipMetaContextValue = { + type: 'line'; + visibleSeries: SeriesData[]; +}; + +export type PieTooltipMetaContextValue = { + type: 'pie'; + visibleSeries: SeriesData[]; + key: Accessor; + label: Accessor; + value: Accessor; + color: Accessor; +}; + +export type ArcTooltipMetaContextValue = { + type: 'arc'; + visibleSeries: SeriesData[]; + key: Accessor; + label: Accessor; + value: Accessor; + color: Accessor; +}; + +export type ScatterTooltipMetaContextValue = { + type: 'scatter'; + visibleSeries: SeriesData[]; +}; + +export type TooltipMetaContextValue = + | BarTooltipMetaContextValue + | AreaTooltipMetaContextValue + | LineTooltipMetaContextValue + | PieTooltipMetaContextValue + | ScatterTooltipMetaContextValue + | ArcTooltipMetaContextValue; + +export type TooltipPayload = { + color?: string; + name?: string; + key: string; + label?: string; + value?: any; + keyAccessor?: Accessor; + valueAccessor?: Accessor; + labelAccessor?: Accessor; + colorAccessor?: Accessor; + chartType?: SimplifiedChartType; + payload: any; + rawSeriesData?: SeriesData; + formatter?: FormatType | FormatConfig; +}; + +type BasePayloadHandlerProps = { + ctx: ChartContextValue; + data: any; +}; + +function handleBarTooltipPayload({ + ctx, + data, + metaCtx, +}: BasePayloadHandlerProps & { + metaCtx: BarTooltipMetaContextValue; +}): TooltipPayload[] { + const seriesItems = metaCtx.stackSeries + ? [...metaCtx.visibleSeries].reverse() + : metaCtx.visibleSeries; + const payload: TooltipPayload[] = seriesItems.map((s) => { + const seriesTooltipData = s.data ? findRelatedData(s.data, data, ctx.x) : data; + const valueAccessor = accessor(s.value ?? (s.data ? ctx.y : s.key)); + const label = metaCtx.orientation === 'vertical' ? ctx.x(data) : ctx.y(data); + const name = s.label ?? (s.key !== 'default' ? s.key : 'value'); + const value = seriesTooltipData ? valueAccessor(seriesTooltipData) : undefined; + const color = s.color ?? ctx.cScale?.(ctx.c(data)); + + return { + ...s.data, + chartType: 'bar', + color, + label, + name, + value, + valueAccessor, + key: s.key, + payload: data, + rawSeriesData: s, + formatter: format, + }; + }); + return payload; +} + +function handleAreaTooltipPayload({ + ctx, + data, + metaCtx, +}: BasePayloadHandlerProps & { + metaCtx: AreaTooltipMetaContextValue; +}): TooltipPayload[] { + const seriesItems = metaCtx.stackSeries + ? [...metaCtx.visibleSeries].reverse() + : metaCtx.visibleSeries; + + const payload: TooltipPayload[] = seriesItems.map((s) => { + const seriesTooltipData = s.data ? findRelatedData(s.data, data, ctx.x) : data; + const valueAccessor = accessor(s.value ?? (s.data ? asAny(ctx.y) : s.key)); + const label = ctx.x(data); + const name = s.label ?? (s.key !== 'default' ? s.key : 'value'); + const value = seriesTooltipData ? valueAccessor(seriesTooltipData) : undefined; + const color = s.color ?? ctx.cScale?.(ctx.c(data)); + return { + ...s.data, + chartType: 'area', + color, + label, + name, + value, + valueAccessor, + key: s.key, + payload: data, + rawSeriesData: s, + formatter: format, + }; + }); + return payload; +} + +function handleLineTooltipPayload({ + ctx, + data, + metaCtx, +}: BasePayloadHandlerProps & { + metaCtx: LineTooltipMetaContextValue; +}): TooltipPayload[] { + return metaCtx.visibleSeries.map((s) => { + const seriesTooltipData = s.data ? findRelatedData(s.data, data, ctx.x) : data; + const label = ctx.x(data); + const valueAccessor = accessor(s.value ?? (s.data ? asAny(ctx.y) : s.key)); + const name = s.label ?? (s.key !== 'default' ? s.key : 'value'); + const value = seriesTooltipData ? valueAccessor(seriesTooltipData) : undefined; + const color = s.color ?? ctx.cScale?.(ctx.c(data)); + + return { + ...s.data, + chartType: 'line', + color, + label, + name, + value, + valueAccessor, + key: s.key, + payload: data, + rawSeriesData: s, + formatter: format, + }; + }); +} + +function handlePieOrArcTooltipPayload({ + ctx, + data, + metaCtx, +}: BasePayloadHandlerProps & { + metaCtx: PieTooltipMetaContextValue | ArcTooltipMetaContextValue; +}): TooltipPayload[] { + const keyAccessor = accessor(metaCtx.key); + const labelAccessor = accessor(metaCtx.label); + const valueAccessor = accessor(metaCtx.value); + const colorAccessor = accessor(metaCtx.color); + return [ + { + key: keyAccessor(data), + label: labelAccessor(data) || keyAccessor(data), + value: valueAccessor(data), + color: colorAccessor(data) ?? ctx.cScale?.(ctx.c(data)), + payload: data, + chartType: 'pie', + labelAccessor, + keyAccessor, + valueAccessor, + colorAccessor, + }, + ]; +} + +export function handleScatterTooltipPayload({ + ctx, + data, + metaCtx, +}: BasePayloadHandlerProps & { + metaCtx: ScatterTooltipMetaContextValue; +}): TooltipPayload[] { + // TODO: implement scatter tooltip payload handling + return [{ payload: data, key: '' }]; +} + +const _TooltipMetaContext = new Context('TooltipMetaContext'); + +/** + * Retrieves the current tooltip meta context value, or null if not set. + */ +export function getTooltipMetaContext() { + return _TooltipMetaContext.getOr(null); +} + +/** + * Sets the tooltip meta context value, used to provide additional payload data to the tooltip. + * This is typically set by the various simplified chart components, such as BarChart, AreaChart, + * etc. + */ +export function setTooltipMetaContext(v: TooltipMetaContextValue | null) { + return _TooltipMetaContext.set(v); +} + +export function getTooltipPayload({ + ctx, + tooltipData, + metaCtx, +}: { + ctx: ChartContextValue; + tooltipData: any; + metaCtx: TooltipMetaContextValue | null; +}): TooltipPayload[] { + if (!metaCtx) return [{ payload: tooltipData, key: '' }]; + switch (metaCtx.type) { + case 'bar': + return handleBarTooltipPayload({ ctx, data: tooltipData, metaCtx }); + case 'area': + return handleAreaTooltipPayload({ ctx, data: tooltipData, metaCtx }); + case 'line': + return handleLineTooltipPayload({ ctx, data: tooltipData, metaCtx }); + case 'pie': + case 'arc': + return handlePieOrArcTooltipPayload({ ctx, data: tooltipData, metaCtx }); + case 'scatter': + return handleScatterTooltipPayload({ ctx, data: tooltipData, metaCtx }); + } +} diff --git a/packages/layerchart/src/lib/components/types.ts b/packages/layerchart/src/lib/components/types.ts new file mode 100644 index 000000000..3f49f1b84 --- /dev/null +++ b/packages/layerchart/src/lib/components/types.ts @@ -0,0 +1,10 @@ +export type Placement = + | 'top-left' + | 'top' + | 'top-right' + | 'left' + | 'center' + | 'right' + | 'bottom-left' + | 'bottom' + | 'bottom-right'; diff --git a/packages/layerchart/src/lib/docs/Blockquote.svelte b/packages/layerchart/src/lib/docs/Blockquote.svelte index adde7486c..00c1a4bd5 100644 --- a/packages/layerchart/src/lib/docs/Blockquote.svelte +++ b/packages/layerchart/src/lib/docs/Blockquote.svelte @@ -1,15 +1,17 @@
a]:font-medium [&>a]:underline [&>a]:decoration-dashed [&>a]:decoration-primary/50 [&>a]:underline-offset-2' )} > - - + + {@render children()}
diff --git a/packages/layerchart/src/lib/docs/Code.svelte b/packages/layerchart/src/lib/docs/Code.svelte index c843968f9..5651de976 100644 --- a/packages/layerchart/src/lib/docs/Code.svelte +++ b/packages/layerchart/src/lib/docs/Code.svelte @@ -1,38 +1,80 @@ + + -
+
{#if source} -
-
-          {@html highlightedSource}
-      
- -
- -
+
+      
+        {#await highlighter}
+          
Loading...
+ {:then h} + {@html h.codeToHtml(source, { + lang: language, + themes: { + light: 'github-light-default', + dark: 'github-dark-default', + }, + })} + {:catch error} +
Error loading code highlighting: {error.message}
+ {/await} + +
+
+ +
+
{/if}
+ + diff --git a/packages/layerchart/src/lib/docs/ConnectorSweepMenuField.svelte b/packages/layerchart/src/lib/docs/ConnectorSweepMenuField.svelte new file mode 100644 index 000000000..035d22367 --- /dev/null +++ b/packages/layerchart/src/lib/docs/ConnectorSweepMenuField.svelte @@ -0,0 +1,17 @@ + + + diff --git a/packages/layerchart/src/lib/docs/ConnectorTypeMenuField.svelte b/packages/layerchart/src/lib/docs/ConnectorTypeMenuField.svelte new file mode 100644 index 000000000..b20f5bcdd --- /dev/null +++ b/packages/layerchart/src/lib/docs/ConnectorTypeMenuField.svelte @@ -0,0 +1,17 @@ + + + diff --git a/packages/layerchart/src/lib/docs/CurveMenuField.svelte b/packages/layerchart/src/lib/docs/CurveMenuField.svelte index 2c01e5e71..6f2510626 100644 --- a/packages/layerchart/src/lib/docs/CurveMenuField.svelte +++ b/packages/layerchart/src/lib/docs/CurveMenuField.svelte @@ -3,9 +3,20 @@ import { MenuField } from 'svelte-ux'; import { entries } from '@layerstack/utils'; + import type { ComponentProps } from 'svelte'; - export let value: any | undefined = d3shapes['curveLinear']; - export let showOpenClosed = false; + let { + value = $bindable(), + showOpenClosed = false, + ...restProps + }: { + value?: any; + showOpenClosed?: boolean; + } & ComponentProps = $props(); + + if (value === undefined) { + value = d3shapes['curveLinear']; + } const options = entries(d3shapes) .filter(([key]) => { @@ -29,5 +40,5 @@ bind:value stepper classes={{ menuIcon: 'hidden' }} - {...$$restProps} + {...restProps} /> diff --git a/packages/layerchart/src/lib/docs/GeoDebug.svelte b/packages/layerchart/src/lib/docs/GeoDebug.svelte index 829831a09..6c8a7b260 100644 --- a/packages/layerchart/src/lib/docs/GeoDebug.svelte +++ b/packages/layerchart/src/lib/docs/GeoDebug.svelte @@ -2,52 +2,59 @@ import { Checkbox } from 'svelte-ux'; import { cls } from '@layerstack/tailwind'; import { format } from '@layerstack/utils'; + import { getChartContext } from '$lib/components/Chart.svelte'; + import { getGeoContext } from '$lib/components/GeoContext.svelte'; + import type { HTMLAttributes } from 'svelte/elements'; - import { chartContext } from '$lib/components/ChartContext.svelte'; - import { geoContext } from '$lib/components/GeoContext.svelte'; + const ctx = getChartContext(); + const geoCtx = getGeoContext(); - const { width, height } = chartContext(); - const geo = geoContext(); + let { class: className }: HTMLAttributes = $props(); - let showCenter = false; + let showCenter = $state(false); -
-
-
scale: {format($geo.scale(), 'decimal')}
- -
- translate: - {#each $geo.translate() as coord} -
{format(coord, 'decimal')}
- {/each} -
- -
- rotate: - {#each $geo.rotate() as angle} -
{format(angle, 'decimal')}
- {/each} -
- -
- center: - - {$geo.center?.()} - -
- -
- long/lat: - {#each $geo.invert?.([$width / 2, $height / 2]) ?? [] as coord} -
{format(coord, 'decimal')}
- {/each} +{#if geoCtx.projection} +
+
+
+ scale: + {format(geoCtx.projection.scale(), 'decimal')} +
+ +
+ translate: + {#each geoCtx.projection.translate() as coord} +
{format(coord, 'decimal')}
+ {/each} +
+ +
+ rotate: + {#each geoCtx.projection.rotate() as angle} +
{format(angle, 'decimal')}
+ {/each} +
+ +
+ center: + + {geoCtx.projection.center?.()} + +
+ +
+ long/lat: + {#each geoCtx.projection.invert?.([ctx.width / 2, ctx.height / 2]) ?? [] as coord} +
{format(coord, 'decimal')}
+ {/each} +
-
-{#if showCenter} -
+ {#if showCenter} +
+ {/if} {/if} diff --git a/packages/layerchart/src/lib/docs/Header1.svelte b/packages/layerchart/src/lib/docs/Header1.svelte index ff25855a4..b2168f079 100644 --- a/packages/layerchart/src/lib/docs/Header1.svelte +++ b/packages/layerchart/src/lib/docs/Header1.svelte @@ -1,5 +1,7 @@ - - + + {@render children()} diff --git a/packages/layerchart/src/lib/docs/Json.svelte b/packages/layerchart/src/lib/docs/Json.svelte index 4ff157abc..2bacd9360 100644 --- a/packages/layerchart/src/lib/docs/Json.svelte +++ b/packages/layerchart/src/lib/docs/Json.svelte @@ -1,12 +1,20 @@ -
+
+ - +{@render children()} diff --git a/packages/layerchart/src/lib/docs/Link.svelte b/packages/layerchart/src/lib/docs/Link.svelte index e607ded31..2bbc54c55 100644 --- a/packages/layerchart/src/lib/docs/Link.svelte +++ b/packages/layerchart/src/lib/docs/Link.svelte @@ -1,5 +1,9 @@ + + - - - + + + {@render children()} diff --git a/packages/layerchart/src/lib/docs/PathDataMenuField.svelte b/packages/layerchart/src/lib/docs/PathDataMenuField.svelte index f4f540ee0..68c2a7d22 100644 --- a/packages/layerchart/src/lib/docs/PathDataMenuField.svelte +++ b/packages/layerchart/src/lib/docs/PathDataMenuField.svelte @@ -1,17 +1,21 @@ -
+
- + {@render children()}
{#if code && showCode} -
- +
+
{/if}
{#if code} View data
diff --git a/packages/layerchart/src/lib/docs/TilesetField.svelte b/packages/layerchart/src/lib/docs/TilesetField.svelte index 6f5958125..19e11f1ed 100644 --- a/packages/layerchart/src/lib/docs/TilesetField.svelte +++ b/packages/layerchart/src/lib/docs/TilesetField.svelte @@ -1,26 +1,26 @@ -
+
e.stopPropagation()} role="none">
2x
diff --git a/packages/layerchart/src/lib/docs/TransformDebug.svelte b/packages/layerchart/src/lib/docs/TransformDebug.svelte index 7c1d85301..79011e539 100644 --- a/packages/layerchart/src/lib/docs/TransformDebug.svelte +++ b/packages/layerchart/src/lib/docs/TransformDebug.svelte @@ -2,20 +2,21 @@ import { cls } from '@layerstack/tailwind'; import { format } from '@layerstack/utils'; - import { transformContext } from '$lib/components/TransformContext.svelte'; + import { getTransformContext } from '$lib/components/TransformContext.svelte'; - const transform = transformContext(); - const { translate, scale } = transform; + const transform = getTransformContext(); -
+
-
scale: {format($scale, 'decimal')}
+
scale: {format(transform.scale, 'decimal')}
translate: -
{format($translate.x, 'decimal')}
-
{format($translate.y, 'decimal')}
+
{format(transform.translate.x, 'decimal')}
+
{format(transform.translate.y, 'decimal')}
diff --git a/packages/layerchart/src/lib/docs/ViewSourceButton.svelte b/packages/layerchart/src/lib/docs/ViewSourceButton.svelte index 3190a93c3..4cc05e09f 100644 --- a/packages/layerchart/src/lib/docs/ViewSourceButton.svelte +++ b/packages/layerchart/src/lib/docs/ViewSourceButton.svelte @@ -1,14 +1,17 @@ {#if source} @@ -17,7 +20,7 @@
@@ -26,13 +29,13 @@
{#if href} - {/if}
-
+
diff --git a/packages/layerchart/src/lib/states/series.svelte.ts b/packages/layerchart/src/lib/states/series.svelte.ts new file mode 100644 index 000000000..8cbe491be --- /dev/null +++ b/packages/layerchart/src/lib/states/series.svelte.ts @@ -0,0 +1,70 @@ +import type { Component } from 'svelte'; +import type { SeriesData } from '../components/charts/types.js'; + +import { SelectionState } from '@layerstack/svelte-state'; + +class HighlightKey { + current = $state['key'] | null>(null); + + set = (seriesKey: typeof this.current) => { + this.current = seriesKey; + }; +} + +export class SeriesState { + #series = $state.raw[]>([]); + selectedKeys = new SelectionState(); + highlightKey = new HighlightKey(); + + constructor(getSeries: () => SeriesData[]) { + this.#series = getSeries(); + + $effect.pre(() => { + // keep series state in sync with the prop + this.#series = getSeries(); + }); + } + + get series() { + return this.#series; + } + + get isDefaultSeries() { + return this.#series.length === 1 && this.#series[0].key === 'default'; + } + + get visibleSeries() { + return this.#series.filter((s) => this.isVisible(s.key)); + } + + /** + * Check if series is visible + */ + isVisible(seriesKey: SeriesData['key']) { + return this.selectedKeys.isEmpty() || this.selectedKeys.isSelected(seriesKey); + } + + /** + * Check if series is highlighted + * Changing default to `true` is useful to determine if series should be faded + */ + isHighlighted(seriesKey: SeriesData['key'], defaultValue = false) { + if (this.highlightKey.current === null) { + return defaultValue; + } else { + return this.highlightKey.current === seriesKey; + } + } + + get allSeriesData() { + return this.#series + .flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d }))) + .filter((d) => d) as Array; + } + + get allSeriesColors() { + return this.#series.map((s) => s.color).filter((c) => c != null) as Array< + NonNullable['color']> + >; + } +} diff --git a/packages/layerchart/src/lib/stores/motionStore.ts b/packages/layerchart/src/lib/stores/motionStore.ts deleted file mode 100644 index adf0b612f..000000000 --- a/packages/layerchart/src/lib/stores/motionStore.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { writable } from 'svelte/store'; -import { spring, tweened } from 'svelte/motion'; - -export type SpringOptions = Parameters>[1]; -export type TweenedOptions = Parameters>[1]; - -export type MotionOptions = { - spring?: boolean | SpringOptions; - tweened?: boolean | TweenedOptions; -}; - -export type PropMotionOptions = { - spring?: boolean | SpringOptions | { [prop: string]: SpringOptions }; - tweened?: boolean | TweenedOptions | { [prop: string]: TweenedOptions }; -}; - -/** - * Convenient wrapper to create a motion store (spring(), tweened()) based on properties, or fall back to basic writable() store - */ -export function motionStore(value: T, options: MotionOptions) { - if (options.spring) { - return spring(value, options.spring === true ? undefined : options.spring); - } else if (options.tweened) { - return tweened(value, options.tweened === true ? undefined : options.tweened); - } else { - return writable(value); - } -} - -/** - * Helper to resolve motion options with specific property option (useful for specifying per-prop delays) - */ -export function resolveOptions(prop: string, options: PropMotionOptions) { - return { - spring: - typeof options.spring === 'boolean' || options.spring == null - ? options.spring - : prop in options.spring - ? //@ts-expect-error - options.spring[prop] - : Object.keys(options.spring).some((key) => - ['precision', 'damping', 'stiffness'].includes(key) - ) - ? options.tweened - : false, - tweened: - typeof options.tweened === 'boolean' || options.tweened == null - ? options.tweened - : prop in options.tweened - ? //@ts-expect-error - options.tweened[prop] - : Object.keys(options.tweened).some((key) => - ['delay', 'duration', 'easing'].includes(key) - ) - ? options.tweened - : false, - }; -} - -export function motionFinishHandler() { - let latestIndex = 0; - const moving = writable(false); - const handle = function (promise: Promise | void) { - latestIndex += 1; - if (!promise) { - // The store returned nothing, which means that the motion store is immediate. - moving.set(false); - return; - } - - let thisIndex = latestIndex; - moving.set(true); - promise.then(() => { - if (thisIndex === latestIndex) { - moving.set(false); - } - }); - }; - - return { - subscribe: moving.subscribe, - handle, - }; -} diff --git a/packages/layerchart/src/lib/styles/daisyui-5.css b/packages/layerchart/src/lib/styles/daisyui-5.css new file mode 100644 index 000000000..fc917f070 --- /dev/null +++ b/packages/layerchart/src/lib/styles/daisyui-5.css @@ -0,0 +1,6 @@ +.lc-root-container { + --color-surface-100: var(--color-base-100); + --color-surface-200: var(--color-base-200); + --color-surface-300: var(--color-base-300); + --color-surface-content: var(--color-base-content); +} diff --git a/packages/layerchart/src/lib/styles/shadcn-svelte.css b/packages/layerchart/src/lib/styles/shadcn-svelte.css new file mode 100644 index 000000000..1f9f94a80 --- /dev/null +++ b/packages/layerchart/src/lib/styles/shadcn-svelte.css @@ -0,0 +1,11 @@ +/* + When NOT using shadcn-svelte Chart component. + Not typically needed even when using built-in Chart, as defaults typically are sufficient +*/ +.lc-root-container { + --color-primary: var(--primary); + --color-surface-100: var(--card-background); + --color-surface-200: var(--card-muted); + /* No direct mapping, should add explicit color (light and dark mode) */ + --color-surface-content: var(--card-foreground); +} diff --git a/packages/layerchart/src/lib/styles/skeleton-3.css b/packages/layerchart/src/lib/styles/skeleton-3.css new file mode 100644 index 000000000..4b2fadac4 --- /dev/null +++ b/packages/layerchart/src/lib/styles/skeleton-3.css @@ -0,0 +1,15 @@ +.lc-root-container { + --color-primary: var(--color-primary-500); + + --color-surface-100: var(--color-surface-50); + --color-surface-200: var(--color-surface-100); + --color-surface-300: var(--color-surface-200); + --color-surface-content: var(--base-font-color); + + html.dark & { + --color-surface-100: var(--color-surface-700); + --color-surface-200: var(--color-surface-800); + --color-surface-300: var(--color-surface-900); + --color-surface-content: var(--base-font-color-dark); + } +} diff --git a/packages/layerchart/src/lib/styles/skeleton-4.css b/packages/layerchart/src/lib/styles/skeleton-4.css new file mode 100644 index 000000000..4b2fadac4 --- /dev/null +++ b/packages/layerchart/src/lib/styles/skeleton-4.css @@ -0,0 +1,15 @@ +.lc-root-container { + --color-primary: var(--color-primary-500); + + --color-surface-100: var(--color-surface-50); + --color-surface-200: var(--color-surface-100); + --color-surface-300: var(--color-surface-200); + --color-surface-content: var(--base-font-color); + + html.dark & { + --color-surface-100: var(--color-surface-700); + --color-surface-200: var(--color-surface-800); + --color-surface-300: var(--color-surface-900); + --color-surface-content: var(--base-font-color-dark); + } +} diff --git a/packages/layerchart/src/lib/types/d3-shape-extentions.d.ts b/packages/layerchart/src/lib/types/d3-shape-extentions.d.ts new file mode 100644 index 000000000..b6d42e71d --- /dev/null +++ b/packages/layerchart/src/lib/types/d3-shape-extentions.d.ts @@ -0,0 +1,7 @@ +import * as d3 from 'd3-shape'; + +declare module 'd3-shape' { + interface Arc { + (): string | null; + } +} diff --git a/packages/layerchart/src/lib/utils/afterTick.ts b/packages/layerchart/src/lib/utils/afterTick.ts new file mode 100644 index 000000000..717ef849a --- /dev/null +++ b/packages/layerchart/src/lib/utils/afterTick.ts @@ -0,0 +1,9 @@ +import { tick } from 'svelte'; + +/** + * Executes a callback function after the next DOM update. + * @param cb Callback function to be executed after the next DOM update + */ +export function afterTick(cb: () => void) { + tick().then(cb); +} diff --git a/packages/layerchart/src/lib/utils/arcText.svelte.ts b/packages/layerchart/src/lib/utils/arcText.svelte.ts new file mode 100644 index 000000000..99b70ae29 --- /dev/null +++ b/packages/layerchart/src/lib/utils/arcText.svelte.ts @@ -0,0 +1,366 @@ +/** + * Reactive utilities to create and position text for Arc-based labels/annotations. + * + * TODO: we can probably simplify / pull some of these pieces out to not do a bunch + * of extra work when we don't need to. But for now while we work on the API, this is fine :) + */ + +import { arc as d3arc } from 'd3-shape'; +import type { Getter, GetterValues } from './types.js'; +import { radiansToDegrees } from './math.js'; +import type { ComponentProps } from 'svelte'; +import type { Text } from '$lib/components/index.js'; + +function extractOutsideArc(arcPath: string) { + // Extract first arc until straight line to innerRadius (L) or close path (Z) + const matches = arcPath.match(/(^.+?)(L|Z)/); + if (!matches || !matches[1]) return arcPath; + return matches[1]; +} + +// Normalize angles to [0, 360) range +function normalizeAngle(angle: number): number { + return ((angle % 360) + 360) % 360; +} + +export type ArcTextProps = GetterValues<{ + innerRadius: number; + outerRadius: number; + cornerRadius: number; + startAngle: number; + endAngle: number; + centroid: [number, number]; +}>; + +export type InternalArcTextProps = ArcTextProps & { + /** + * Whether the corner radius should be inverted or not. + * This changes when text is rotated/flipped to ensure + * the corner offset is applied correctly. + */ + invertCorner: Getter; +}; + +export type ArcPathResult = { current: string }; + +/** + * Calculates and generates a path in the middle/medial line of an arc. + */ +function getArcPathMiddle(props: InternalArcTextProps): ArcPathResult { + const centerRadius = $derived((props.innerRadius() + props.outerRadius()) / 2); + const cornerAngleOffset = $derived.by(() => { + if (props.cornerRadius() <= 0 || centerRadius <= 0) return 0; + + const effectiveCornerRadius = Math.min(props.cornerRadius(), centerRadius); + return (effectiveCornerRadius * 0.5) / centerRadius; + }); + + const effectiveStartAngle = $derived.by(() => { + if (props.invertCorner()) { + return props.startAngle() - cornerAngleOffset; + } + return props.startAngle() + cornerAngleOffset; + }); + + const effectiveEndAngle = $derived.by(() => { + if (props.invertCorner()) { + return props.endAngle() + cornerAngleOffset; + } + return props.endAngle() - cornerAngleOffset; + }); + + const path = $derived( + extractOutsideArc( + d3arc() + .outerRadius(centerRadius) + .innerRadius(centerRadius - 0.5) + .startAngle(effectiveStartAngle) + .endAngle(effectiveEndAngle)() ?? '' + ) + ); + + return { + get current() { + return path; + }, + }; +} + +function getArcPathInner(props: InternalArcTextProps): ArcPathResult { + const cornerAngleOffset = $derived.by(() => { + if (props.cornerRadius() <= 0 || props.innerRadius() <= 0) return 0; + + if (props.cornerRadius() >= props.innerRadius()) return Math.PI / 4; + return (props.cornerRadius() * 0.5) / props.innerRadius(); + }); + + const effectiveStartAngle = $derived.by(() => { + if (props.invertCorner()) { + return props.startAngle() - cornerAngleOffset; + } + return props.startAngle() + cornerAngleOffset; + }); + + const effectiveEndAngle = $derived.by(() => { + if (props.invertCorner()) { + return props.endAngle() + cornerAngleOffset; + } + return props.endAngle() - cornerAngleOffset; + }); + + const path = $derived( + extractOutsideArc( + d3arc() + .innerRadius(props.innerRadius()) + .outerRadius(props.innerRadius() + 0.5) + .startAngle(effectiveStartAngle) + .endAngle(effectiveEndAngle)() ?? '' + ) + ); + + return { + get current() { + return path; + }, + }; +} + +function getArcPathOuter(props: InternalArcTextProps): ArcPathResult { + const cornerAngleOffset = $derived.by(() => { + if (props.cornerRadius() <= 0 || props.outerRadius() <= 0) return 0; + return (props.cornerRadius() * 0.5) / props.outerRadius(); + }); + + const effectiveStartAngle = $derived.by(() => { + if (props.invertCorner()) { + return props.startAngle() - cornerAngleOffset; + } + return props.startAngle() + cornerAngleOffset; + }); + + const effectiveEndAngle = $derived.by(() => { + if (props.invertCorner()) { + return props.endAngle() + cornerAngleOffset; + } + return props.endAngle() - cornerAngleOffset; + }); + + const path = $derived( + extractOutsideArc( + d3arc() + .innerRadius(props.outerRadius() - 0.5) + .outerRadius(props.outerRadius()) + .startAngle(effectiveStartAngle) + .endAngle(effectiveEndAngle)() ?? '' + ) + ); + + return { + get current() { + return path; + }, + }; +} + +export type ArcTextPosition = 'inner' | 'outer' | 'middle' | 'centroid' | 'outer-radial'; + +export type ArcTextOptions = { + /** + * A percentage string (e.g., '50%') to offset the start angle of the text path. + * If a specific offset is needed, you should use option rather than passing it + * directly to the `` component to ensure it is taken into account when + * calculating the path the text should follow. + * + * This has no effect if the position is `'centroid'`. + */ + startOffset?: string; + + /** + * An amount of padding to add to the outer radius of the path to add space + * between the text and the arc. + */ + outerPadding?: number; + + /** + * Optional offset specifically for 'outer-radial' position from the outer arc edge. + * If not provided, 'outerPadding' will be used. + * + * // TODO: does 23 even make sense? It looks good but is it too arbitrary? needs sean's attention + * Defaults to 23 if neither is set. + */ + radialOffset?: number; +}; + +function pointOnCircle(radius: number, angle: number): [number, number] { + const adjustedAngle = angle - Math.PI / 2; + return [radius * Math.cos(adjustedAngle), radius * Math.sin(adjustedAngle)]; +} + +export function createArcTextProps( + props: ArcTextProps, + opts: ArcTextOptions = {}, + position: ArcTextPosition +): { current: ComponentProps } { + const effectiveStartAngleRadians = $derived.by(() => { + const start = props.startAngle(); + const end = props.endAngle(); + const offset = opts.startOffset; + + if (offset) { + try { + const percentage = parseFloat(offset.slice(0, -1)) / 100; + if (!isNaN(percentage) && percentage >= 0 && percentage <= 1) { + const span = end - start; + return start + span * percentage; + } else { + console.warn('Invalid percentage for startOffset:', offset); + } + } catch (e) { + console.warn('Could not parse startOffset percentage:', offset, e); + } + } + + return start; + }); + + // Convert the effective start angle to degrees for orientation checks + const effectiveStartDegrees = $derived(radiansToDegrees(effectiveStartAngleRadians)); + // Normalize the effective angle to the [0, 360) range + const normalizedStartDegrees = $derived(normalizeAngle(effectiveStartDegrees)); + + const startDegrees = $derived(radiansToDegrees(props.startAngle())); + const endDegrees = $derived(radiansToDegrees(props.endAngle())); + + const isClockwise = $derived(startDegrees < endDegrees); + + // Reverse direction of arc when text is on top going counterclockwise or bottom going clockwise + const isTopCw = $derived( + isClockwise && (normalizedStartDegrees >= 270 || normalizedStartDegrees <= 90) + ); + const isTopCcw = $derived( + !isClockwise && (normalizedStartDegrees > 270 || normalizedStartDegrees <= 90) + ); + const isBottomCw = $derived( + isClockwise && normalizedStartDegrees < 270 && normalizedStartDegrees >= 90 + ); + const isBottomCcw = $derived( + !isClockwise && normalizedStartDegrees <= 270 && normalizedStartDegrees > 90 + ); + + const reverseText = $derived(isTopCcw || isBottomCw); + + const pathGenProps = { + ...props, + startAngle: () => (reverseText ? props.endAngle() : props.startAngle()), + endAngle: () => (reverseText ? props.startAngle() : props.endAngle()), + invertCorner: () => isBottomCw || isBottomCcw, + }; + + const innerPath = getArcPathInner(pathGenProps); + const middlePath = getArcPathMiddle(pathGenProps); + const outerPath = getArcPathOuter(pathGenProps); + + const innerDominantBaseline = $derived.by(() => { + if (isBottomCw || isBottomCcw) return 'auto' as const; + if (isTopCw || isTopCcw) return 'hanging' as const; + return 'auto' as const; + }); + + const outerDominantBaseline = $derived.by(() => { + if (isBottomCw || isBottomCcw) return 'hanging' as const; + return undefined; + }); + + const sharedProps = $derived.by(() => { + if (reverseText) { + return { + startOffset: opts.startOffset ?? '100%', + textAnchor: 'end' as const, + }; + } + return { + startOffset: opts.startOffset ?? undefined, + }; + }); + + const radialPositionProps = $derived.by(() => { + if (position !== 'outer-radial') return {}; + + const midAngle = (props.startAngle() + props.endAngle()) / 2; + const basePadding = opts.radialOffset ?? opts.outerPadding ?? 23; + const midAngleDegrees = normalizeAngle(radiansToDegrees(midAngle)); + + let textAnchor: 'start' | 'end' | 'middle' = 'middle'; + let effectivePadding = basePadding; + + const isBottomZone = midAngleDegrees > 45 && midAngleDegrees < 135; + const isTopZone = midAngleDegrees > 225 && midAngleDegrees < 315; + const isRightZone = midAngleDegrees <= 45 || midAngleDegrees >= 315; + const isLeftZone = midAngleDegrees >= 135 && midAngleDegrees <= 225; + + const positionRadius = props.outerRadius() + effectivePadding; + const [x, y] = pointOnCircle(positionRadius, midAngle); + + if (isRightZone) { + textAnchor = 'start'; + if (midAngleDegrees > 350 || midAngleDegrees < 10) textAnchor = 'start'; + } else if (isLeftZone) { + textAnchor = 'end'; + if (midAngleDegrees > 170 && midAngleDegrees < 190) textAnchor = 'end'; + } else if (isBottomZone) { + textAnchor = 'middle'; + } else if (isTopZone) { + textAnchor = 'middle'; + } + + return { + x: x, + y: y, + textAnchor, + dominantBaseline: 'middle' as const, + }; + }); + + const current = $derived.by(() => { + if (position === 'inner') { + return { + path: innerPath.current, + ...sharedProps, + dominantBaseline: innerDominantBaseline, + }; + } else if (position === 'outer') { + return { + path: outerPath.current, + ...sharedProps, + dominantBaseline: outerDominantBaseline, + }; + } else if (position === 'middle') { + return { + path: middlePath.current, + ...sharedProps, + dominantBaseline: 'middle' as const, + }; + } else if (position === 'centroid') { + const centroid = props.centroid(); + return { + x: centroid[0], + y: centroid[1], + textAnchor: 'middle' as const, + verticalAnchor: 'middle' as const, + }; + } else { + return radialPositionProps; + } + }); + + return { + get current() { + return current; + }, + }; +} + +export type GetArcTextProps = ( + position: ArcTextPosition, + opts?: ArcTextOptions +) => ComponentProps; diff --git a/packages/layerchart/src/lib/utils/array.test.ts b/packages/layerchart/src/lib/utils/array.test.ts new file mode 100644 index 000000000..b73807090 --- /dev/null +++ b/packages/layerchart/src/lib/utils/array.test.ts @@ -0,0 +1,233 @@ +import { describe, it, expect } from 'vitest'; +import { applyLanes } from './array.js'; + +describe('applyLanes', () => { + it('should assign same lane to non-overlapping events', () => { + const data = [ + { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02') }, + { id: 2, start: new Date('2023-01-03'), end: new Date('2023-01-05') }, + { id: 3, start: new Date('2023-01-06'), end: new Date('2023-01-08') }, + ]; + + const result = applyLanes(data); + + expect(result).toEqual([ + { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02'), lane: 0 }, + { id: 2, start: new Date('2023-01-03'), end: new Date('2023-01-05'), lane: 0 }, + { id: 3, start: new Date('2023-01-06'), end: new Date('2023-01-08'), lane: 0 }, + ]); + }); + + it('should assign different lanes to overlapping events', () => { + const data = [ + { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-03') }, + { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-04') }, + { id: 3, start: new Date('2023-01-02T12:00:00'), end: new Date('2023-01-05') }, + ]; + + const result = applyLanes(data); + + expect(result).toEqual([ + { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-03'), lane: 0 }, + { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-04'), lane: 1 }, + { id: 3, start: new Date('2023-01-02T12:00:00'), end: new Date('2023-01-05'), lane: 2 }, + ]); + }); + + it('should reuse lanes when events no longer overlap', () => { + const data = [ + { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02') }, + { id: 2, start: new Date('2023-01-01T12:00:00'), end: new Date('2023-01-03') }, + { id: 3, start: new Date('2023-01-04'), end: new Date('2023-01-06') }, // starts after id: 1 ends + { id: 4, start: new Date('2023-01-05'), end: new Date('2023-01-07') }, // starts after id: 2 ends + ]; + + const result = applyLanes(data); + + expect(result).toEqual([ + { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02'), lane: 0 }, + { id: 2, start: new Date('2023-01-01T12:00:00'), end: new Date('2023-01-03'), lane: 1 }, + { id: 3, start: new Date('2023-01-04'), end: new Date('2023-01-06'), lane: 0 }, // reuses lane 0 + { id: 4, start: new Date('2023-01-05'), end: new Date('2023-01-07'), lane: 1 }, // reuses lane 1 + ]); + }); + + it('should handle events that start exactly when another ends', () => { + const data = [ + { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02') }, + { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-04') }, // starts exactly when id: 1 ends + { id: 3, start: new Date('2023-01-01T12:00:00'), end: new Date('2023-01-03') }, // overlaps with both + ]; + + const result = applyLanes(data); + + expect(result).toEqual([ + { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02'), lane: 0 }, + { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-04'), lane: 0 }, // can reuse lane 0 + { id: 3, start: new Date('2023-01-01T12:00:00'), end: new Date('2023-01-03'), lane: 1 }, // overlaps, needs new lane + ]); + }); + + it('should work with string keys for start and end', () => { + const data = [ + { name: 'Task 1', startTime: new Date('2023-01-01'), endTime: new Date('2023-01-03') }, + { name: 'Task 2', startTime: new Date('2023-01-02'), endTime: new Date('2023-01-04') }, + ]; + + const result = applyLanes(data, { start: 'startTime', end: 'endTime' }); + + expect(result).toEqual([ + { + name: 'Task 1', + startTime: new Date('2023-01-01'), + endTime: new Date('2023-01-03'), + lane: 0, + }, + { + name: 'Task 2', + startTime: new Date('2023-01-02'), + endTime: new Date('2023-01-04'), + lane: 1, + }, + ]); + }); + + it('should work with nested string keys for start and end', () => { + const data = [ + { name: 'Task 1', duration: { start: new Date('2023-01-01'), end: new Date('2023-01-02') } }, + { name: 'Task 2', duration: { start: new Date('2023-01-03'), end: new Date('2023-01-04') } }, + ]; + + const result = applyLanes(data, { start: 'duration.start', end: 'duration.end' }); + + expect(result).toEqual([ + { + name: 'Task 1', + duration: { start: new Date('2023-01-01'), end: new Date('2023-01-02') }, + lane: 0, + }, + { + name: 'Task 2', + duration: { start: new Date('2023-01-03'), end: new Date('2023-01-04') }, + lane: 0, + }, + ]); + }); + + it('should work with function accessors for start and end', () => { + const data = [ + { name: 'Task 1', duration: { start: new Date('2023-01-01'), end: new Date('2023-01-02') } }, + { name: 'Task 2', duration: { start: new Date('2023-01-03'), end: new Date('2023-01-04') } }, + ]; + + const result = applyLanes(data, { start: (d) => d.duration.start, end: (d) => d.duration.end }); + + expect(result).toEqual([ + { + name: 'Task 1', + duration: { start: new Date('2023-01-01'), end: new Date('2023-01-02') }, + lane: 0, + }, + { + name: 'Task 2', + duration: { start: new Date('2023-01-03'), end: new Date('2023-01-04') }, + lane: 0, + }, + ]); + }); + + it('should handle empty array', () => { + const data: any[] = []; + const result = applyLanes(data); + expect(result).toEqual([]); + }); + + it('should handle single event', () => { + const data = [{ id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02') }]; + const result = applyLanes(data); + + expect(result).toEqual([ + { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02'), lane: 0 }, + ]); + }); + + it('should handle complex overlapping scenario', () => { + const data = [ + { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-05') }, // long event + { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-03') }, // short event inside + { id: 3, start: new Date('2023-01-02T12:00:00'), end: new Date('2023-01-04') }, // overlaps with both + { id: 4, start: new Date('2023-01-03'), end: new Date('2023-01-04T12:00:00') }, // overlaps with 1 and 3 + { id: 5, start: new Date('2023-01-06'), end: new Date('2023-01-08') }, // separate event + ]; + + const result = applyLanes(data); + + expect(result).toEqual([ + { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-05'), lane: 0 }, + { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-03'), lane: 1 }, + { id: 3, start: new Date('2023-01-02T12:00:00'), end: new Date('2023-01-04'), lane: 2 }, + { id: 4, start: new Date('2023-01-03'), end: new Date('2023-01-04T12:00:00'), lane: 1 }, // can reuse lane 1 since id: 2 ended + { id: 5, start: new Date('2023-01-06'), end: new Date('2023-01-08'), lane: 0 }, // can reuse lane 0 since id: 1 ended + ]); + }); + + it('should preserve all original properties', () => { + const data = [ + { + id: 1, + start: new Date('2023-01-01'), + end: new Date('2023-01-02'), + name: 'First', + priority: 'high', + metadata: { foo: 'bar' }, + }, + { + id: 2, + start: new Date('2023-01-01T12:00:00'), + end: new Date('2023-01-03'), + name: 'Second', + priority: 'low', + metadata: { baz: 'qux' }, + }, + ]; + + const result = applyLanes(data); + + expect(result).toEqual([ + { + id: 1, + start: new Date('2023-01-01'), + end: new Date('2023-01-02'), + name: 'First', + priority: 'high', + metadata: { foo: 'bar' }, + lane: 0, + }, + { + id: 2, + start: new Date('2023-01-01T12:00:00'), + end: new Date('2023-01-03'), + name: 'Second', + priority: 'low', + metadata: { baz: 'qux' }, + lane: 1, + }, + ]); + }); + + it('should work with numeric values', () => { + const data = [ + { id: 1, start: 0, end: 3 }, + { id: 2, start: 1, end: 4 }, + { id: 3, start: 5, end: 7 }, + ]; + + const result = applyLanes(data); + + expect(result).toEqual([ + { id: 1, start: 0, end: 3, lane: 0 }, + { id: 2, start: 1, end: 4, lane: 1 }, + { id: 3, start: 5, end: 7, lane: 0 }, // can reuse lane 0 since id: 1 ended + ]); + }); +}); diff --git a/packages/layerchart/src/lib/utils/array.ts b/packages/layerchart/src/lib/utils/array.ts index 54208d5b6..86ae845bc 100644 --- a/packages/layerchart/src/lib/utils/array.ts +++ b/packages/layerchart/src/lib/utils/array.ts @@ -1,5 +1,6 @@ import type { Numeric } from 'd3-array'; import { extent as d3extent } from 'd3-array'; +import { accessor, type Accessor } from './common.js'; /** * Wrapper around d3-array's `extent()` but remove [undefined, undefined] return type @@ -7,3 +8,48 @@ import { extent as d3extent } from 'd3-array'; export function extent(iterable: Parameters>[0]) { return d3extent(iterable) as [T, T]; } + +/** + * Determine whether two arrays equal one another, order not important. + * This uses includes instead of converting to a set because this is only + * used internally on a small array size and it's not worth the cost + * of making a set + */ +export function arraysEqual(arr1: unknown[], arr2: unknown[]) { + if (arr1.length !== arr2.length) return false; + return arr1.every((k) => { + return arr2.includes(k); + }); +} + +/** + * Add `lanes` property to each element in the data array support densely packing. + * This is useful for visualizing overlapping events in a timeline / Gantt chart. + */ +export function applyLanes>( + data: T[], + options: { start: Accessor; end: Accessor } = { + start: 'start', + end: 'end', + } +) { + const result: (T & { lane: number })[] = []; + let stack: T[] = []; + + const startAccessor = accessor(options.start as any); + const endAccessor = accessor(options.end as any); + + for (const d of data) { + let lane = stack.findIndex( + (s) => endAccessor(s) <= startAccessor(d) && startAccessor(s) < startAccessor(d) + ); + if (lane === -1) { + lane = stack.length; + } + + result.push({ ...d, lane }); + stack[lane] = d; + } + + return result; +} diff --git a/packages/layerchart/src/lib/utils/attributes.ts b/packages/layerchart/src/lib/utils/attributes.ts new file mode 100644 index 000000000..b0f696cc4 --- /dev/null +++ b/packages/layerchart/src/lib/utils/attributes.ts @@ -0,0 +1,38 @@ +import { cls } from '@layerstack/tailwind'; +import type { ClassValue } from 'svelte/elements'; + +type ExtractObjectType = T extends object ? (T extends Function ? never : T) : never; +type WithClass = T & { class?: string }; +type DefaultProps = WithClass<{ [key: string]: any }>; + +// type guard to narrow props to an object with optional class +// for extractLayerProps +function isObjectWithClass(val: any): val is { class?: string } { + return typeof val === 'object' && val !== null && typeof val !== 'function'; +} + +/** + * Pulls out the props from an arbitrary object/function/boolean and appends + * a class name to its class property to identify the layer for CSS targeting. + * + * @param props The props to be extracted, can be an object, function or any other type + * @param className The class name to be applied to the layer for targeting styling (e.g. 'lc-layer') + * @param extraClasses Additional classes to be applied to the layer if they don't exist in the props already + * @returns a typed spreadable object with props for the layer + */ +export function extractLayerProps( + props: T, + className: string, + ...extraClasses: ClassValue[] +): WithClass extends never ? DefaultProps : ExtractObjectType> { + if (isObjectWithClass(props)) { + return { + ...props, + class: cls(className, ...extraClasses, props.class), + } as WithClass>; + } + + return { + class: cls(className, ...extraClasses), + } as WithClass extends never ? DefaultProps : ExtractObjectType>; +} diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts index ed79eb815..f164c357a 100644 --- a/packages/layerchart/src/lib/utils/canvas.ts +++ b/packages/layerchart/src/lib/utils/canvas.ts @@ -1,29 +1,47 @@ -import { cls } from '@layerstack/tailwind'; -import { memoize } from 'lodash-es'; import type { ClassValue } from 'svelte/elements'; +import memoize from 'memoize'; +import { cls } from '@layerstack/tailwind'; +import type { PatternShape } from '$lib/components/Pattern.svelte'; export const DEFAULT_FILL = 'rgb(0, 0, 0)'; const CANVAS_STYLES_ELEMENT_ID = '__layerchart_canvas_styles_id'; +type StyleOptions = Partial< + Omit & { + fillOpacity?: number | string; + strokeWidth?: number | string; + opacity?: number | string; + } +>; + export type ComputedStylesOptions = { - styles?: Partial< - Omit & { - fillOpacity?: number | string; - strokeWidth?: number | string; - opacity?: number | string; - } - >; - classes?: ClassValue; + styles?: StyleOptions; + classes?: ClassValue | null; }; +const supportedStyles = [ + 'fill', + 'fillOpacity', + 'stroke', + 'strokeWidth', + 'opacity', + 'fontWeight', + 'fontSize', + 'fontFamily', + 'textAnchor', + 'textAlign', + 'paintOrder', +] as const; + /** - * Appends or reuses `` element below `` to resolve CSS variables and classes (ex. `stroke: hsl(var(--color-primary))` => `stroke: rgb(...)` ) + * Appends or reuses `` element below `` to resolve CSS variables and classes (ex. `stroke: var(--color-primary)` => `stroke: rgb(...)` ) */ -export function getComputedStyles( +export function _getComputedStyles( canvas: HTMLCanvasElement, { styles, classes }: ComputedStylesOptions = {} ) { + // console.count(`getComputedStyles: ${getComputedStylesKey(canvas, { styles, classes })}`); try { // Get or create `` below `` let svg = document.getElementById(CANVAS_STYLES_ELEMENT_ID) as SVGElement | null; @@ -45,6 +63,8 @@ export function getComputedStyles( if (styles) { Object.assign(svg.style, styles); } + // Make sure `` is not visible + svg.style.display = 'none'; if (classes) { svg.setAttribute( @@ -56,7 +76,12 @@ export function getComputedStyles( ); } - const computedStyles = window.getComputedStyle(svg); + // Capture copy to enable memoization and avoid capturing all styles (which is very slow) + const computedStyles = supportedStyles.reduce((acc, style) => { + acc[style] = window.getComputedStyle(svg)[style]; + return acc; + }, {} as CSSStyleDeclaration); + return computedStyles; } catch (e) { console.error('Unable to get computed styles', e); @@ -64,6 +89,19 @@ export function getComputedStyles( } } +function getComputedStylesKey( + canvas: HTMLCanvasElement, + { styles, classes }: ComputedStylesOptions = {} +) { + return JSON.stringify({ canvasId: canvas.id, styles, classes }); +} + +export const getComputedStyles = memoize(_getComputedStyles, { + cacheKey: ([canvas, styleOptions]) => { + return getComputedStylesKey(canvas, styleOptions); + }, +}); + /** Render onto canvas context. Supports CSS variables and classes by tranferring to hidden `` element before retrieval) */ function render( ctx: CanvasRenderingContext2D, @@ -71,63 +109,103 @@ function render( stroke: (ctx: CanvasRenderingContext2D) => void; fill: (ctx: CanvasRenderingContext2D) => void; }, - styleOptions: ComputedStylesOptions = {} + styleOptions: ComputedStylesOptions = {}, + { + applyText, + }: { + applyText?: boolean; + } = {} ) { // console.count('render'); // TODO: Consider memoizing? How about reactiving to CSS variable changes (light/dark mode toggle) - const computedStyles = getComputedStyles(ctx.canvas, styleOptions); + let resolvedStyles: StyleOptions; + if ( + styleOptions.classes == null && + !Object.values(styleOptions.styles ?? {}).some( + (v) => typeof v === 'string' && v.includes('var(') + ) + ) { + // Skip resolving styles if no classes are provided and no styles are using CSS variables + resolvedStyles = styleOptions.styles ?? {}; + } else { + // Remove constant non-css variable properties (ex. `strokeWidth: 0.5`, `fill: #123456`) as not needed and improves memoization cache hit + const { constantStyles, variableStyles } = Object.entries(styleOptions.styles ?? {}).reduce<{ + constantStyles: StyleOptions; + variableStyles: StyleOptions; + }>( + (acc, [key, value]) => { + if (typeof value === 'number' || (typeof value === 'string' && !value.includes('var('))) { + (acc.constantStyles as any)[key] = value; + } else if (typeof value === 'string' && value.includes('var(')) { + (acc.variableStyles as any)[key] = value; + } + return acc; + }, + { constantStyles: {} as StyleOptions, variableStyles: {} as StyleOptions } + ); + + const computedStyles = getComputedStyles(ctx.canvas, { + styles: variableStyles, + classes: styleOptions.classes, + }); + resolvedStyles = { ...computedStyles, ...constantStyles }; + } // Adhere to CSS paint order: https://developer.mozilla.org/en-US/docs/Web/CSS/paint-order const paintOrder = - computedStyles?.paintOrder === 'stroke' ? ['stroke', 'fill'] : ['fill', 'stroke']; + resolvedStyles?.paintOrder === 'stroke' ? ['stroke', 'fill'] : ['fill', 'stroke']; - if (computedStyles?.opacity) { - ctx.globalAlpha = Number(computedStyles?.opacity); + if (resolvedStyles?.opacity) { + ctx.globalAlpha = Number(resolvedStyles?.opacity); } - // Text properties - ctx.font = `${computedStyles.fontSize} ${computedStyles.fontFamily}`; // build string instead of using `computedStyles.font` to fix/workaround `tabular-nums` returning `null` + // font/text properties can be expensive to set (not sure why), so only apply if needed (renderText()) + if (applyText) { + // Text properties + ctx.font = `${resolvedStyles.fontWeight} ${resolvedStyles.fontSize} ${resolvedStyles.fontFamily}`; // build string instead of using `computedStyles.font` to fix/workaround `tabular-nums` returning `null` + + // TODO: Hack to handle `textAnchor` with canvas. Try to find a better approach + if (resolvedStyles.textAnchor === 'middle') { + ctx.textAlign = 'center'; + } else if (resolvedStyles.textAnchor === 'end') { + ctx.textAlign = 'right'; + } else { + ctx.textAlign = resolvedStyles.textAlign as CanvasTextAlign; // TODO: Handle/map `justify` and `match-parent`? + } - // TODO: Hack to handle `textAnchor` with canvas. Try to find a better approach - if (computedStyles.textAnchor === 'middle') { - ctx.textAlign = 'center'; - } else if (computedStyles.textAnchor === 'end') { - ctx.textAlign = 'right'; - } else { - ctx.textAlign = computedStyles.textAlign as CanvasTextAlign; // TODO: Handle/map `justify` and `match-parent`? + // TODO: Handle `textBaseline` / `verticalAnchor` (Text) + // ctx.textBaseline = 'top'; + // ctx.textBaseline = 'middle'; + // ctx.textBaseline = 'bottom'; + // ctx.textBaseline = 'alphabetic'; + // ctx.textBaseline = 'hanging'; + // ctx.textBaseline = 'ideographic'; } - // TODO: Handle `textBaseline` / `verticalAnchor` (Text) - // ctx.textBaseline = 'top'; - // ctx.textBaseline = 'middle'; - // ctx.textBaseline = 'bottom'; - // ctx.textBaseline = 'alphabetic'; - // ctx.textBaseline = 'hanging'; - // ctx.textBaseline = 'ideographic'; - // Dashed lines - if (computedStyles.strokeDasharray.includes(',')) { - const dashArray = computedStyles.strokeDasharray + if (resolvedStyles.strokeDasharray?.includes(',')) { + const dashArray = resolvedStyles.strokeDasharray .split(',') .map((s) => Number(s.replace('px', ''))); ctx.setLineDash(dashArray); } - paintOrder.forEach((attr) => { + for (const attr of paintOrder) { if (attr === 'fill') { const fill = styleOptions.styles?.fill && ((styleOptions.styles?.fill as any) instanceof CanvasGradient || + (styleOptions.styles?.fill as any) instanceof CanvasPattern || !styleOptions.styles?.fill?.includes('var')) ? styleOptions.styles.fill - : computedStyles?.fill; + : resolvedStyles?.fill; if (fill && !['none', DEFAULT_FILL].includes(fill)) { const currentGlobalAlpha = ctx.globalAlpha; - const fillOpacity = Number(computedStyles?.fillOpacity); - const opacity = Number(computedStyles?.opacity); + const fillOpacity = Number(resolvedStyles?.fillOpacity); + const opacity = Number(resolvedStyles?.opacity); ctx.globalAlpha = fillOpacity * opacity; ctx.fillStyle = fill; @@ -142,19 +220,19 @@ function render( ((styleOptions.styles?.stroke as any) instanceof CanvasGradient || !styleOptions.styles?.stroke?.includes('var')) ? styleOptions.styles?.stroke - : computedStyles?.stroke; + : resolvedStyles?.stroke; if (stroke && !['none'].includes(stroke)) { ctx.lineWidth = - typeof computedStyles?.strokeWidth === 'string' - ? Number(computedStyles?.strokeWidth?.replace('px', '')) - : (computedStyles?.strokeWidth ?? 1); + typeof resolvedStyles?.strokeWidth === 'string' + ? Number(resolvedStyles?.strokeWidth?.replace('px', '')) + : (resolvedStyles?.strokeWidth ?? 1); ctx.strokeStyle = stroke; render.stroke(ctx); } } - }); + } } /** Render SVG path data onto canvas context. Supports CSS variables and classes by tranferring to hidden `` element before retrieval) */ @@ -188,7 +266,8 @@ export function renderText( fill: (ctx) => ctx.fillText(text.toString(), coords.x, coords.y), stroke: (ctx) => ctx.strokeText(text.toString(), coords.x, coords.y), }, - styleOptions + styleOptions, + { applyText: true } ); } } @@ -230,6 +309,28 @@ export function renderCircle( ctx.closePath(); } +export function renderEllipse( + ctx: CanvasRenderingContext2D, + coords: { cx: number; cy: number; rx: number; ry: number }, + styleOptions: ComputedStylesOptions = {} +) { + ctx.beginPath(); + ctx.ellipse(coords.cx, coords.cy, coords.rx, coords.ry, 0, 0, 2 * Math.PI); + render( + ctx, + { + fill: (ctx) => { + ctx.fill(); + }, + stroke: (ctx) => { + ctx.stroke(); + }, + }, + styleOptions + ); + ctx.closePath(); +} + /** Clear canvas accounting for Canvas `context.translate(...)` */ export function clearCanvasContext( ctx: CanvasRenderingContext2D, @@ -271,6 +372,14 @@ export function scaleCanvas(ctx: CanvasRenderingContext2D, width: number, height return { width: ctx.canvas.width, height: ctx.canvas.height }; } +/** Get pixel color (r,g,b,a) at canvas coordinates */ +export function getPixelColor(ctx: CanvasRenderingContext2D, x: number, y: number) { + const dpr = window.devicePixelRatio ?? 1; + const imageData = ctx.getImageData(x * dpr, y * dpr, 1, 1); + const [r, g, b, a] = imageData.data; + return { r, g, b, a }; +} + export function _createLinearGradient( ctx: CanvasRenderingContext2D, x0: number, @@ -281,32 +390,66 @@ export function _createLinearGradient( ) { const gradient = ctx.createLinearGradient(x0, y0, x1, y1); - stops.forEach(({ offset, color }) => { + for (const { offset, color } of stops) { gradient.addColorStop(offset, color); - }); + } return gradient; } /** Create linear gradient and memoize result to fix reactivity */ -export const createLinearGradient = memoize( - _createLinearGradient, - ( - ctx: CanvasRenderingContext2D, - x0: number, - y0: number, - x1: number, - y1: number, - stops: { offset: number; color: string }[] - ) => { - const key = JSON.stringify({ x0, y0, x1, y1, stops }); - return key; +export const createLinearGradient = memoize(_createLinearGradient, { + cacheKey: (args) => JSON.stringify(args.slice(1)), // Ignore `ctx` argument +}); + +export function _createPattern( + ctx: CanvasRenderingContext2D, + width: number, + height: number, + shapes: PatternShape[], + background?: string +) { + const patternCanvas = document.createElement('canvas'); + const patternCtx = patternCanvas.getContext('2d')!; + + // Add pattern canvas to DOM to allow computed styles to be read (`getComputedStyles()`) + ctx.canvas.after(patternCanvas); + + // TODO: Fix blurry pattern + // const newScale = scaleCanvas(patternCtx, width, height); + patternCanvas.width = width; + patternCanvas.height = height; + + if (background) { + patternCtx.fillStyle = background; + patternCtx.fillRect(0, 0, width, height); } -); -export function getPixelColor(ctx: CanvasRenderingContext2D, x: number, y: number) { - const dpr = window.devicePixelRatio ?? 1; - const imageData = ctx.getImageData(x * dpr, y * dpr, 1, 1); - const [r, g, b, a] = imageData.data; - return { r, g, b, a }; + for (const shape of shapes) { + patternCtx.save(); + if (shape.type === 'circle') { + renderCircle( + patternCtx, + { cx: shape.cx, cy: shape.cy, r: shape.r }, + { styles: { fill: shape.fill, opacity: shape.opacity } } + ); + } else if (shape.type === 'line') { + renderPathData(patternCtx, shape.path, { + styles: { stroke: shape.stroke, strokeWidth: shape.strokeWidth, opacity: shape.opacity }, + }); + } + patternCtx.restore(); + } + + const pattern = ctx.createPattern(patternCanvas, 'repeat'); + + // Cleanup + ctx.canvas.parentElement?.removeChild(patternCanvas); + + return pattern; } + +/** Create pattern and memoize result to fix reactivity */ +export const createPattern = memoize(_createPattern, { + cacheKey: (args) => JSON.stringify(args.slice(1)), // Ignore `ctx` argument +}); diff --git a/packages/layerchart/src/lib/utils/chart.ts b/packages/layerchart/src/lib/utils/chart.ts new file mode 100644 index 000000000..ec3302484 --- /dev/null +++ b/packages/layerchart/src/lib/utils/chart.ts @@ -0,0 +1,721 @@ +import type { DomainType } from './scales.svelte.js'; +import type { + AxisKey, + BaseRange, + DataType, + Extents, + FieldAccessors, + Nice, + PaddingArray, +} from './types.js'; +import type { AnyScale } from './scales.svelte.js'; +import { arraysEqual } from './array.js'; +import { toTitleCase } from './string.js'; +import { InternSet } from 'd3-array'; + +/** + * Creates a function to calculate a domain based on extents and a domain directive. + * @param s The key (e.g., 'x', 'y') to look up in the extents object + * @returns A function that computes the final domain from extents and a domain input + */ +export function calcDomain( + s: K, + extents: Extents, + domain: DomainType | undefined +): number[] { + // @ts-expect-error - TODO: fix these types + return extents ? partialDomain(extents[s], domain) : domain; +} + +/** + * If we have a domain from settings (the directive), fill in + * any null values with ones from our measured extents; + * otherwise, return the measured extent. + * @param domain A two-value array of numbers representing the measured extent + * @param directive A two-value array of numbers or nulls that will have any nulls filled in from the `domain` array + * @returns A two-value array of numbers representing the filled-in domain + */ +export function partialDomain( + domain: number[] | undefined = [], + directive?: Array +): number[] { + if (Array.isArray(directive) === true) { + return directive.map((d, i) => { + if (d === null) { + return domain[i]; + } + return d; + }); + } + return domain; +} + +type CreateChartScaleOpts = { + domain: number[]; + scale: AnyScale & { + interpolator?: () => { name: string }; + nice?: (nice?: number) => void; + }; + padding: PaddingArray | undefined; + nice: Nice; + reverse: boolean; + width: number; + height: number; + range: BaseRange | undefined; + percentRange: boolean; + extents: Extents; +}; + +export function createChartScale( + axis: AxisKey, + { + domain, + scale, + padding, + nice, + reverse, + width, + height, + range, + percentRange, + }: CreateChartScaleOpts +): AnyScale { + const defaultRange = getDefaultRange(axis, width, height, reverse, range, percentRange); + + const trueScale = scale.copy(); + + /* -------------------------------------------- + * Set the domain + */ + trueScale.domain(domain); + + /* -------------------------------------------- + * Set the range of the scale to our default if + * the scale doesn't have an interpolator function + * or if it does, still set the range if that function + * is the default identity function + */ + if ( + !trueScale.interpolator || + (typeof trueScale.interpolator === 'function' && + trueScale.interpolator().name.startsWith('identity')) + ) { + trueScale.range(defaultRange); + } + + if (padding) { + trueScale.domain(padScale(trueScale, padding)); + } + + if (nice === true || typeof nice === 'number') { + if (typeof trueScale.nice === 'function') { + trueScale.nice(typeof nice === 'number' ? nice : undefined); + } else { + console.error( + `[Layer Chart] You set \`${axis}Nice: true\` but the ${axis}Scale does not have a \`.nice\` method. Ignoring...` + ); + } + } + + return trueScale; +} + +// These scales have a discrete range so they can't be padded +const unpaddable = ['scaleThreshold', 'scaleQuantile', 'scaleQuantize', 'scaleSequentialQuantile']; + +function padScale(scale: AnyScale, padding: PaddingArray | undefined) { + if (typeof scale.range !== 'function') { + throw new Error('Scale method `range` must be a function'); + } + if (typeof scale.domain !== 'function') { + throw new Error('Scale method `domain` must be a function'); + } + + if (!Array.isArray(padding) || unpaddable.includes(findScaleName(scale))) { + return scale.domain(); + } + + if (isOrdinalDomain(scale) === true) return scale.domain(); + + const { lift, ground } = getPadFunctions(scale); + + const d0 = scale.domain()[0]; + const isTime = Object.prototype.toString.call(d0) === '[object Date]'; + + const [d1, d2] = scale.domain().map((d) => { + return isTime ? lift(d.getTime()) : lift(d); + }); + + const [r1, r2] = scale.range(); + const paddingLeft = padding[0] || 0; + const paddingRight = padding[1] || 0; + + const step = (d2 - d1) / (Math.abs(r2 - r1) - paddingLeft - paddingRight); + + return [d1 - paddingLeft * step, paddingRight * step + d2].map((d) => { + return isTime ? ground(new Date(d).getTime()) : ground(d); + }); +} +function f(name: string, modifier = '') { + return `scale${toTitleCase(modifier)}${toTitleCase(name)}`; +} + +/** + * Get a D3 scale name + * https://svelte.dev/repl/ec6491055208401ca41120c9c8a67737?version=3.49.0 + */ +export function findScaleName(scale: any) { + /** + * Ordinal scales + */ + // scaleBand, scalePoint + // @ts-ignore + if (typeof scale.bandwidth === 'function') { + // @ts-ignore + if (typeof scale.paddingInner === 'function') { + return f('band'); + } + return f('point'); + } + // scaleOrdinal + if (arraysEqual(Object.keys(scale), ['domain', 'range', 'unknown', 'copy'])) { + return f('ordinal'); + } + + /** + * Sequential versus diverging + */ + let modifier = ''; + // @ts-ignore + if (scale.interpolator) { + // @ts-ignore + if (scale.domain().length === 3) { + modifier = 'diverging'; + } else { + modifier = 'sequential'; + } + } + + /** + * Continuous scales + */ + // @ts-ignore + if (scale.quantiles) { + return f('quantile', modifier); + } + // @ts-ignore + if (scale.thresholds) { + return f('quantize', modifier); + } + // @ts-ignore + if (scale.constant) { + return f('symlog', modifier); + } + // @ts-ignore + if (scale.base) { + return f('log', modifier); + } + // @ts-ignore + if (scale.exponent) { + // @ts-ignore + if (scale.exponent() === 0.5) { + return f('sqrt', modifier); + } + return f('pow', modifier); + } + + if (arraysEqual(Object.keys(scale), ['domain', 'range', 'invertExtent', 'unknown', 'copy'])) { + return f('threshold'); + } + + if ( + arraysEqual(Object.keys(scale), [ + 'invert', + 'range', + 'domain', + 'unknown', + 'copy', + 'ticks', + 'tickFormat', + 'nice', + ]) + ) { + return f('identity'); + } + + if ( + arraysEqual(Object.keys(scale), [ + 'invert', + 'domain', + 'range', + 'rangeRound', + 'round', + 'clamp', + 'unknown', + 'copy', + 'ticks', + 'tickFormat', + 'nice', + ]) + ) { + return f('radial'); + } + + if (modifier) { + return f(modifier); + } + + /** + * Test for scaleTime vs scaleUtc + * https://github.com/d3/d3-scale/pull/274#issuecomment-1462935595 + */ + if (scale.domain()[0] instanceof Date) { + const d = new Date(); + let s: string = ''; + // @ts-ignore + d.getDay = () => (s = 'time'); + // @ts-ignore + d.getUTCDay = () => (s = 'utc'); + + scale.tickFormat(0, '%a')(d); + return f(s); + } + + return f('linear'); +} + +/** Determine whether a scale has an ordinal domain + * https://svelte.dev/repl/ec6491055208401ca41120c9c8a67737?version=3.49.0 + * @param scale A D3 scale + * @returns Whether the scale is an ordinal scale + */ +function isOrdinalDomain(scale: AnyScale) { + // scaleBand, scalePoint + if (typeof scale.bandwidth === 'function') return true; + + // scaleOrdinal + if (arraysEqual(Object.keys(scale), ['domain', 'range', 'unknown', 'copy'])) { + return true; + } + return false; +} + +interface ScaleInfo { + scale: AnyScale; + sort?: boolean; +} + +interface ActiveScales { + [key: string]: ScaleInfo; +} + +interface ScaleGroups { + ordinal: FieldAccessors | false; + other: FieldAccessors | false; +} + +/** + * Calculates scale extents for given data and scales + * @template T The type of data objects in the input array + * @param {T[]} flatData Array of data objects + * @param {FieldAccessors} getters Field accessor functions + * @param {ActiveScales} activeScales Object containing scale information + * @returns {Extents} Calculated extents for each scale + */ +export function calcScaleExtents( + flatData: DataType, + getters: FieldAccessors, + activeScales: ActiveScales +): UniqueResults { + // group scales by domain type (ordinal vs other) + const scaleGroups = Object.entries(activeScales).reduce>( + (groups, [key, scaleInfo]) => { + const domainType = isOrdinalDomain(scaleInfo.scale) === true ? 'ordinal' : 'other'; + + if (!groups[domainType]) { + groups[domainType] = {}; + } + + (groups[domainType] as FieldAccessors)[key as keyof FieldAccessors] = + getters[key as keyof FieldAccessors]; + + return groups; + }, + { ordinal: false, other: false } + ); + + let extents: UniqueResults = {}; + + // ordinal scales + if (scaleGroups.ordinal) { + const sortOptions = Object.fromEntries( + Object.entries(activeScales).map(([key, scaleInfo]) => [key, scaleInfo.sort]) + ); + extents = calcUniques(flatData, scaleGroups.ordinal, sortOptions); + } + + // other scales + if (scaleGroups.other) { + const otherExtents = calcExtents(flatData, scaleGroups.other); + extents = { ...extents, ...otherExtents }; + } + + return extents; +} + +interface SortOptions { + sort?: boolean; + x?: boolean; + y?: boolean; + z?: boolean; + r?: boolean; +} + +export interface UniqueResults { + x?: (number | string)[]; + y?: (number | string)[]; + z?: (number | string)[]; + r?: (number | string)[]; +} + +/** + * Calculate the unique values of desired fields + * For example, data like this: [{ x: 0, y: -10 }, { x: 10, y: 0 }, { x: 5, y: 10 }] + * and a fields object like this: {'x': d => d.x, 'y': d => d.y} + * returns an object like this: { x: [0, 10, 5], y: [-10, 0, 10] } + * + * @template T The type of data objects in the input array + * @param data A flat array of data objects + * @param fields An object containing accessor functions for fields + * @param [sortOptions={}] Sorting options for the results + * @returns An object with unique values for each specified field + * @throws {TypeError} If data is not an array or fields is not a valid object + */ +function calcUniques( + data: DataType, + fields: FieldAccessors, + sortOptions: SortOptions = {} +): UniqueResults { + if (!Array.isArray(data)) { + throw new TypeError( + `The first argument of calcUniques() must be an array. You passed in a ${typeof data}. If you got this error using the component, consider passing a flat array to the \`flatData\` prop` + ); + } + + if (Array.isArray(fields) || fields === undefined || fields === null) { + throw new TypeError( + 'The second argument of calcUniques() must be an object with field names as keys and accessor functions as values.' + ); + } + + const uniques: UniqueResults = {}; + const keys = Object.keys(fields) as (keyof FieldAccessors)[]; + + for (const key of keys) { + const set = new InternSet(); + const accessor = fields[key]; + + if (!accessor) continue; + + for (const item of data) { + const value = accessor(item); + + if (Array.isArray(value)) { + for (const val of value) { + set.add(val); + } + } else { + set.add(value); + } + } + + const results = Array.from(set); + if (sortOptions.sort === true || sortOptions[key as keyof SortOptions] === true) { + results.sort((a, b) => { + // type-safe sorting for both numbers and strings + if (typeof a === 'number' && typeof b === 'number') { + return a - b; + } + return String(a).localeCompare(String(b)); + }); + } + + uniques[key] = results; + } + + return uniques; +} + +function calcBaseRange( + s: AxisKey, + width: number, + height: number, + reverse: boolean, + percentRange: boolean +): number[] { + let min: number; + let max: number; + + if (percentRange === true) { + min = 0; + max = 100; + } else { + min = s === 'r' ? 1 : 0; + max = s === 'y' ? height : s === 'r' ? 25 : width; + } + + return reverse === true ? [max, min] : [min, max]; +} + +function getDefaultRange( + s: AxisKey, + width: number, + height: number, + reverse: boolean, + range?: BaseRange, + percentRange: boolean = false +): number[] | string[] { + return !range + ? calcBaseRange(s, width, height, reverse, percentRange) + : typeof range === 'function' + ? range({ width, height }) + : range; +} + +// Define possible scale types +type ScaleType = 'log' | 'symlog' | 'pow' | 'sqrt' | 'other'; + +export function identity(d: T): T { + return d; +} + +type ScaleWithProps = AnyScale & { + constant?: number; + base?: () => number; + exponent?: () => number; + domain: () => number[]; +}; + +function findScaleType(scale: ScaleWithProps): ScaleType { + if (scale.constant) { + return 'symlog'; + } + if (scale.base) { + return 'log'; + } + if (typeof scale.exponent === 'function') { + const expValue = scale.exponent(); + if (expValue === 0.5) { + return 'sqrt'; + } + return 'pow'; + } + return 'other'; +} + +// Type for transformation functions +interface TransformFunctions { + lift: (x: number) => number; + ground: (x: number) => number; + scaleType: ScaleType; +} + +function log(sign: number): (x: number) => number { + return (x: number): number => Math.log(sign * x); +} + +function exp(sign: number): (x: number) => number { + return (x: number): number => sign * Math.exp(x); +} + +function symlog(c: number): (x: number) => number { + return (x: number): number => Math.sign(x) * Math.log1p(Math.abs(x / c)); +} + +function symexp(c: number): (x: number) => number { + return (x: number): number => Math.sign(x) * Math.expm1(Math.abs(x)) * c; +} + +function pow(exponent: number): (x: number) => number { + return function powFn(x: number): number { + return x < 0 ? -Math.pow(-x, exponent) : Math.pow(x, exponent); + }; +} + +function getPadFunctions(scale: ScaleWithProps): TransformFunctions { + const scaleType = findScaleType(scale); + + switch (scaleType) { + case 'log': { + const domain = scale.domain(); + const sign = Math.sign(domain[0]); + return { lift: log(sign), ground: exp(sign), scaleType }; + } + case 'pow': { + const exponent = 1; + return { + lift: pow(exponent), + ground: pow(1 / exponent), + scaleType, + }; + } + case 'sqrt': { + const exponent = 0.5; + return { + lift: pow(exponent), + ground: pow(1 / exponent), + scaleType, + }; + } + case 'symlog': { + const constant = 1; + return { + lift: symlog(constant), + ground: symexp(constant), + scaleType, + }; + } + default: + return { + lift: identity, + ground: identity, + scaleType, + }; + } +} + +export function createGetter(accessor: (d: TData) => any, scale: AnyScale | null) { + return (d: TData) => { + const val = accessor(d); + if (!scale) return undefined; + if (Array.isArray(val)) { + return val.map((v) => scale(v)); + } + return scale(val); + }; +} + +/** + * Calculate the extents of desired fields, skipping `false`, `undefined`, `null` and `NaN` values + * For example, data like this: + * [{ x: 0, y: -10 }, { x: 10, y: 0 }, { x: 5, y: 10 }] + * and a fields object like this: + * `{'x': d => d.x, 'y': d => d.y}` + * returns an object like this: + * `{ x: [0, 10], y: [-10, 10] }` + * @param data A flat array of objects. + * @param fields An object containing `x`, `y`, `r` or `z` keys that equal an accessor function. + * @returns An object with the same structure as `fields` but with min/max arrays. + */ +function calcExtents(data: DataType, fields: FieldAccessors): UniqueResults { + if (!Array.isArray(data)) { + throw new TypeError( + `The first argument of calcExtents() must be an array. You passed in a ${typeof data}. If you got this error using the component, consider passing a flat array to the \`flatData\` prop.` + ); + } + + if (Array.isArray(fields) || fields === undefined || fields === null) { + throw new TypeError( + 'The second argument of calcExtents() must be an ' + + 'object with field names as keys as accessor functions as values.' + ); + } + + const extents: UniqueResults = {}; + + const keys = Object.keys(fields) as (keyof FieldAccessors)[]; + const kl = keys.length; + let i: number; + let j: number; + let k: number; + let s: keyof FieldAccessors; + let min: number | string | null; + let max: number | string | null; + let acc: ((d: T) => number | string | (number | string)[]) | undefined; + let val: number | string | (number | string)[] | undefined; + + const dl = data.length; + for (i = 0; i < kl; i += 1) { + s = keys[i]; + acc = fields[s]; + min = null; + max = null; + + if (!acc) continue; // Skip if accessor is undefined + + for (j = 0; j < dl; j += 1) { + val = acc(data[j]); + + if (Array.isArray(val)) { + const vl = val.length; + for (k = 0; k < vl; k += 1) { + if ( + val[k] !== undefined && + val[k] !== null && + (typeof val[k] === 'string' || Number.isNaN(val[k]) === false) + ) { + if (min === null || val[k] < min) { + min = val[k]; + } + if (max === null || val[k] > max) { + max = val[k]; + } + } + } + } else if ( + val !== undefined && + val !== null && + (typeof val === 'string' || Number.isNaN(val) === false) + ) { + if (min === null || val < min) { + min = val; + } + if (max === null || val > max) { + max = val; + } + } + } + extents[s] = [min as string | number, max as string | number]; + } + + return extents; +} + +/** + * Move an element to the last child of its parent. + * Adapted from d3-selection `.raise` + */ +export function raise(node: Element) { + if (node.nextSibling) { + node.parentNode?.appendChild(node); + } +} + +/** + * Flatten arrays of arrays one level deep + * @param list The list to flatten + * @param accessor An optional accessor function or string property key + * @returns Flattened array + */ +export default function flatten( + list: T[], + accessor: string | ((item: T) => U[]) = (d: T) => d as unknown as U[] +): U[] { + // type the accessor function based on input + const acc: (item: T) => U[] = + typeof accessor === 'string' ? (d: T) => d[accessor as keyof T] as U[] : accessor; + + // check if list is array and first element through accessor is array + const firstElement = list[0] && acc(list[0]); + if (Array.isArray(list) && Array.isArray(firstElement)) { + let flat: U[] = []; + const l = list.length; + for (let i = 0; i < l; i += 1) { + flat = flat.concat(acc(list[i])); + } + return flat; + } + + // type assertion here since we know list contains U[] if not flattened + return list as unknown as U[]; +} diff --git a/packages/layerchart/src/lib/utils/color.ts b/packages/layerchart/src/lib/utils/color.ts index 8c94e113c..776cd8c3e 100644 --- a/packages/layerchart/src/lib/utils/color.ts +++ b/packages/layerchart/src/lib/utils/color.ts @@ -23,3 +23,10 @@ export function getColorStr(color: RGBColor) { return `rgb(${color.r},${color.g},${color.b})`; } } + +export function getColorIfDefined(data: any): string | undefined { + if (!data || typeof data !== 'object' || Array.isArray(data)) return; + + if ('color' in data) return data.color; + if ('fill' in data) return data.fill; +} diff --git a/packages/layerchart/src/lib/utils/common.test.ts b/packages/layerchart/src/lib/utils/common.test.ts index 7f5dad0e6..4f2c3e3e0 100644 --- a/packages/layerchart/src/lib/utils/common.test.ts +++ b/packages/layerchart/src/lib/utils/common.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { accessor } from './common.js'; +import { accessor, resolveMaybeFn, getObjectOrNull } from './common.js'; export const testData = { one: 1, @@ -45,3 +45,33 @@ describe('accessor', () => { expect(actual).toEqual(testData); }); }); + +describe('getObjectOrNull', () => { + it('returns null for non-object values', () => { + expect(getObjectOrNull(5)).toBeNull(); + expect(getObjectOrNull('string')).toBeNull(); + expect(getObjectOrNull(null)).toBeNull(); + expect(getObjectOrNull(undefined)).toBeUndefined(); + }); + + it('returns null for functions', () => { + const fn = () => {}; + expect(getObjectOrNull(fn)).toBeNull(); + }); + + it('returns the object if value is an object', () => { + const obj = { a: 1 }; + expect(getObjectOrNull(obj)).toBe(obj); + }); +}); + +describe('resolveMaybeFn', () => { + it('returns value if not a function', () => { + expect(resolveMaybeFn(5)).toBe(5); + }); + + it('calls function with args', () => { + const fn = (a: number, b: number) => a + b; + expect(resolveMaybeFn(fn, 2, 3)).toBe(5); + }); +}); diff --git a/packages/layerchart/src/lib/utils/common.ts b/packages/layerchart/src/lib/utils/common.ts index 5172d06a1..6983fd93e 100644 --- a/packages/layerchart/src/lib/utils/common.ts +++ b/packages/layerchart/src/lib/utils/common.ts @@ -1,8 +1,8 @@ -import type { ComponentProps } from 'svelte'; +import type { Component, ComponentProps } from 'svelte'; import { get } from 'lodash-es'; import type Chart from '../components/Chart.svelte'; -import type LineChart from '../components/charts/LineChart.svelte'; +import type { SimplifiedChartProps } from '$lib/components/charts/types.js'; export type Accessor = | number @@ -35,17 +35,15 @@ export function chartDataArray(data: ComponentProps>[' return data; } else if ('nodes' in data) { return data.nodes; - } else { + } else if ('descendants' in data) { return data.descendants(); } + return []; } -// Using LineChart but could any simplified chart -type SimplifiedChartProps = ComponentProps>; - -export function defaultChartPadding( - axis: SimplifiedChartProps['axis'], - legend: SimplifiedChartProps['legend'] +export function defaultChartPadding( + axis: SimplifiedChartProps['axis'] = true, + legend: SimplifiedChartProps['legend'] = false ) { if (axis === false) { return undefined; @@ -53,7 +51,7 @@ export function defaultChartPadding( return { top: axis === true || axis === 'y' ? 4 : 0, left: axis === true || axis === 'y' ? 20 : 0, - bottom: (axis === true || axis === 'x' ? 20 : 0) + (legend === true ? 32 : 0), + bottom: (axis === true || axis === 'x' ? 20 : 0) + (legend ? 32 : 0), right: axis === true || axis === 'x' ? 4 : 0, }; } @@ -68,3 +66,30 @@ export function findRelatedData(data: any[], original: any, accessor: Function) return accessor(d)?.valueOf() === accessor(original)?.valueOf(); }); } + +/** + * Return the object if the value is an object, otherwise return null. + * Functions (including Snippet types) are treated as non-objects and return null. + */ +export function getObjectOrNull( + value: T +): T extends object + ? T extends Function + ? null + : T + : T extends null + ? null + : T extends undefined + ? undefined + : null { + if (typeof value === 'object') return value as any; + if (value === undefined) return undefined as any; + return null as any; +} + +/** + * Call with args if function, otherwise return the value. + */ +export function resolveMaybeFn(value: T | ((...args: any[]) => T), ...args: any[]) { + return typeof value === 'function' ? (value as Function)(...args) : value; +} diff --git a/packages/layerchart/src/lib/utils/connectorUtils.ts b/packages/layerchart/src/lib/utils/connectorUtils.ts new file mode 100644 index 000000000..553d8d3a0 --- /dev/null +++ b/packages/layerchart/src/lib/utils/connectorUtils.ts @@ -0,0 +1,167 @@ +import { type CurveFactory, line as d3Line, curveLinear } from 'd3-shape'; + +export type ConnectorCoords = { + x: number; + y: number; +}; + +export type PresetConnectorType = 'straight' | 'square' | 'beveled' | 'rounded'; + +export type ConnectorType = PresetConnectorType | 'd3'; + +export type ConnectorSweep = 'horizontal-vertical' | 'vertical-horizontal' | 'none'; + +function isSamePoint(p1: ConnectorCoords, p2: ConnectorCoords): boolean { + return Math.abs(p1.x - p2.x) < 1e-6 && Math.abs(p1.y - p2.y) < 1e-6; +} + +function createDirectPath(source: ConnectorCoords, target: ConnectorCoords): string { + if (isSamePoint(source, target)) return ''; + return `M ${source.x} ${source.y} L ${target.x} ${target.y}`; +} + +function isNearZero(value: number): boolean { + return Math.abs(value) < 1e-6; +} + +type CreateConnectorPathProps = { + source: ConnectorCoords; + target: ConnectorCoords; + radius: number; + sweep: ConnectorSweep; + dx: number; + dy: number; +}; + +function createSquarePath({ source, target, sweep }: CreateConnectorPathProps): string { + if (sweep === 'horizontal-vertical') { + return `M ${source.x} ${source.y} L ${target.x} ${source.y} L ${target.x} ${target.y}`; + } else { + return `M ${source.x} ${source.y} L ${source.x} ${target.y} L ${target.x} ${target.y}`; + } +} + +function createBeveledPath(opts: CreateConnectorPathProps): string { + const { radius, dx, dy, source, target, sweep } = opts; + const effectiveRadius = Math.max(0, Math.min(radius, Math.abs(dx), Math.abs(dy))); + + if (isNearZero(effectiveRadius)) { + return createSquarePath(opts); + } + + const signX = Math.sign(dx); + const signY = Math.sign(dy); + + if (sweep === 'horizontal-vertical') { + const pBeforeCorner = { x: target.x - effectiveRadius * signX, y: source.y }; + const pAfterCorner = { x: target.x, y: source.y + effectiveRadius * signY }; + + return `M ${source.x} ${source.y} L ${pBeforeCorner.x} ${pBeforeCorner.y} L ${pAfterCorner.x} ${pAfterCorner.y} L ${target.x} ${target.y}`; + } else { + const pBeforeCorner = { x: source.x, y: target.y - effectiveRadius * signY }; + const pAfterCorner = { x: source.x + effectiveRadius * signX, y: target.y }; + + return `M ${source.x} ${source.y} L ${pBeforeCorner.x} ${pBeforeCorner.y} L ${pAfterCorner.x} ${pAfterCorner.y} L ${target.x} ${target.y}`; + } +} + +function createRoundedPath(opts: CreateConnectorPathProps): string { + const { radius, dx, dy, source, target, sweep } = opts; + const effectiveRadius = Math.max(0, Math.min(radius, Math.abs(dx), Math.abs(dy))); + + if (isNearZero(effectiveRadius)) { + return createSquarePath(opts); + } + + const signX = Math.sign(dx); + const signY = Math.sign(dy); + + if (sweep === 'horizontal-vertical') { + const pBeforeCorner = { x: target.x - effectiveRadius * signX, y: source.y }; + const pAfterCorner = { x: target.x, y: source.y + effectiveRadius * signY }; + const sweepFlag = signX * signY > 0 ? 1 : 0; + + return `M ${source.x} ${source.y} L ${pBeforeCorner.x} ${pBeforeCorner.y} A ${effectiveRadius} ${effectiveRadius} 0 0 ${sweepFlag} ${pAfterCorner.x} ${pAfterCorner.y} L ${target.x} ${target.y}`; + } else { + const pBeforeCorner = { x: source.x, y: target.y - effectiveRadius * signY }; + const pAfterCorner = { x: source.x + effectiveRadius * signX, y: target.y }; + const sweepFlag = signX * signY > 0 ? 0 : 1; + + return `M ${source.x} ${source.y} L ${pBeforeCorner.x} ${pBeforeCorner.y} A ${effectiveRadius} ${effectiveRadius} 0 0 ${sweepFlag} ${pAfterCorner.x} ${pAfterCorner.y} L ${target.x} ${target.y}`; + } +} + +type PathStrategyMap = Record< + 'square' | 'beveled' | 'rounded', + (props: CreateConnectorPathProps) => string +>; + +const pathStrategies: PathStrategyMap = { + square: createSquarePath, + beveled: createBeveledPath, + rounded: createRoundedPath, +}; + +type GetConnectorPresetPathProps = { + source: ConnectorCoords; + target: ConnectorCoords; + radius: number; + type: PresetConnectorType; + sweep: ConnectorSweep; +}; + +export function getConnectorPresetPath(opts: GetConnectorPresetPathProps) { + const { source, target, type } = opts; + if (isSamePoint(source, target)) return ''; + const dx = target.x - source.x; + const dy = target.y - source.y; + + // straight line cases + if (type === 'straight' || isNearZero(dx) || isNearZero(dy)) { + return createDirectPath(source, target); + } + + return (pathStrategies[type] || pathStrategies.square)({ ...opts, dx, dy }); +} + +const FALLBACK_PATH = 'M0,0L0,0'; + +type GetConnectorD3PathProps = Omit & { + curve: CurveFactory; +}; + +export function getConnectorD3Path({ source, target, sweep, curve }: GetConnectorD3PathProps) { + const dx = target.x - source.x; + const dy = target.y - source.y; + const line = d3Line().curve(curve); + let points: [number, number][] = []; + + const isAligned = isNearZero(dx) || isNearZero(dy); + + if (sweep === 'none' || isAligned) { + points = [ + [source.x, source.y], + [target.x, target.y], + ]; + } else if (sweep === 'horizontal-vertical') { + points = [ + [source.x, source.y], + [target.x, source.y], + [target.x, target.y], + ]; + } else if (sweep === 'vertical-horizontal') { + points = [ + [source.x, source.y], + [source.x, target.y], + [target.x, target.y], + ]; + } + + if (points.length === 2 && isNearZero(dx) && isNearZero(dx)) return FALLBACK_PATH; + + const d = line(points); + + if (!d || d.includes('NaN')) return FALLBACK_PATH; + + return d; +} diff --git a/packages/layerchart/src/lib/utils/createId.ts b/packages/layerchart/src/lib/utils/createId.ts new file mode 100644 index 000000000..16df9d766 --- /dev/null +++ b/packages/layerchart/src/lib/utils/createId.ts @@ -0,0 +1,9 @@ +/** + * Creates a unique ID for a given prefix and uid. + * + * @param prefix - prefix to use for the id + * @param uid - the uid generated by $props.id() + */ +export function createId(prefix: string, uid: string) { + return `${prefix}-${uid}`; +} diff --git a/packages/layerchart/src/lib/utils/debug.ts b/packages/layerchart/src/lib/utils/debug.ts new file mode 100644 index 000000000..acd9a6752 --- /dev/null +++ b/packages/layerchart/src/lib/utils/debug.ts @@ -0,0 +1,103 @@ +import { rgb } from 'd3-color'; +import { toTitleCase } from './string.js'; +import { findScaleName } from './chart.js'; + +const indent = ' '; + +type RGBInput = Parameters[0]; +type RGBOutput = { r: number; g: number; b: number; o: number }; + +function printObject(obj: Record) { + Object.entries(obj).forEach(([key, value]) => { + console.log(`${indent}${key}:`, value); + }); +} + +function getRgb(clr: RGBInput) { + const { r, g, b, opacity: o } = rgb(clr); + if (![r, g, b].every((c) => c >= 0 && c <= 255)) { + return false; + } + return { r, g, b, o }; +} + +function printValues(scale: any, method: string, extraSpace = '') { + const values = scale[method](); + const colorValues = colorizeArray(values); + if (colorValues) { + printColorArray(colorValues, method, values); + } else { + console.log(`${indent}${indent}${toTitleCase(method)}:${extraSpace}`, values); + } +} + +function printColorArray(colorValues: (string[] | string)[], method: string, values: any[]) { + console.log( + `${indent}${indent}${toTitleCase(method)}: %cArray%c(${values.length}) ` + + colorValues[0] + + '%c ]', + 'color: #1377e4', + 'color: #737373', + 'color: #1478e4', + ...colorValues[1], + 'color: #1478e4' + ); +} +function colorizeArray(arr: RGBInput[]) { + const colors: RGBOutput[] = []; + const a = arr.map((d, i) => { + const rgbo = getRgb(d); + if (rgbo !== false) { + colors.push(rgbo); + // Add a space to the last item + const space = i === arr.length - 1 ? ' ' : ''; + return `%c ${d}${space}`; + } + return d; + }); + if (colors.length) { + return [ + `%c[ ${a.join(', ')}`, + colors.map( + (d) => `background-color: rgba(${d.r}, ${d.g}, ${d.b}, ${d.o}); color:${contrast(d)};` + ), + ]; + } + return null; +} + +function printScale(s: string, scale: any, acc: any) { + const scaleName = findScaleName(scale); + console.log(`${indent}${s}:`); + console.log(`${indent}${indent}Accessor: "${acc.toString()}"`); + console.log(`${indent}${indent}Type: ${scaleName}`); + printValues(scale, 'domain'); + printValues(scale, 'range', ' '); +} + +/** + * Calculate human-perceived lightness from RGB + * This doesn't take opacity into account + * https://stackoverflow.com/a/596243 + */ +function contrast({ r, g, b }: Pick) { + const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; + return luminance > 0.6 ? 'black' : 'white'; +} + +export function printDebug(obj: Record) { + console.log('/********* LayerChart Debug ************/'); + console.log('Bounding box:'); + printObject(obj.boundingBox); + console.log('Data:'); + console.log(indent, obj.data); + if (obj.flatData) { + console.log('flatData:'); + console.log(indent, obj.flatData); + } + console.log('Scales:'); + Object.keys(obj.activeGetters).forEach((g) => { + printScale(g, obj[`${g}Scale`], obj[g]); + }); + console.log('/************ End LayerChart Debug ***************/\n'); +} diff --git a/packages/layerchart/src/lib/utils/filterObject.ts b/packages/layerchart/src/lib/utils/filterObject.ts new file mode 100644 index 000000000..f71f1ffd0 --- /dev/null +++ b/packages/layerchart/src/lib/utils/filterObject.ts @@ -0,0 +1,14 @@ +/** + * Remove undefined fields from an object + * @param obj The object to filter + * @param comparisonObk An object that, for any key, if the key is not present on that object, the + * key will be filtered out. Note, this ignores the value on that object + */ +export function filterObject(obj: object, comparisonObj = {}) { + return Object.fromEntries( + Object.entries(obj).filter(([key, value]) => { + // @ts-expect-error - shh + return value !== undefined && comparisonObj[key] === undefined; + }) + ); +} diff --git a/packages/layerchart/src/lib/utils/genData.ts b/packages/layerchart/src/lib/utils/genData.ts index d489f4848..87f0177a2 100644 --- a/packages/layerchart/src/lib/utils/genData.ts +++ b/packages/layerchart/src/lib/utils/genData.ts @@ -1,4 +1,4 @@ -import { addMinutes, startOfDay, startOfToday, subDays } from 'date-fns'; +import { timeMinute, timeDay } from 'd3-time'; import { cumsum } from 'd3-array'; import { randomNormal } from 'd3-random'; @@ -58,28 +58,31 @@ export function createSeries(options: { }); } -export function createDateSeries(options: { - count?: number; - min: number; - max: number; - keys?: TKey[]; - value?: 'number' | 'integer'; -}) { - const now = startOfToday(); +export function createDateSeries( + options: { + count?: number; + min?: number; + max?: number; + keys?: TKey[]; + value?: 'number' | 'integer'; + } = {} +) { + const now = timeDay.floor(new Date()); const count = options.count ?? 10; - const min = options.min; - const max = options.max; + const min = options.min ?? 0; + const max = options.max ?? 100; const keys = options.keys ?? ['value']; + const valueType = options.value ?? 'number'; return Array.from({ length: count }).map((_, i) => { return { - date: subDays(now, count - i - 1), + date: timeDay.offset(now, -count + i), ...Object.fromEntries( keys.map((key) => { return [ key, - options.value === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max), + valueType === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max), ]; }) ), @@ -87,23 +90,26 @@ export function createDateSeries(options: { }); } -export function createTimeSeries(options: { - count?: number; - min: number; - max: number; - keys: TKey[]; - value: 'number' | 'integer'; -}) { +export function createTimeSeries( + options: { + count?: number; + min?: number; + max?: number; + keys?: TKey[]; + value?: 'number' | 'integer'; + } = {} +) { const count = options.count ?? 10; - const min = options.min; - const max = options.max; + const min = options.min ?? 0; + const max = options.max ?? 100; const keys = options.keys ?? ['value']; + const valueType = options.value ?? 'number'; - let lastStartDate = startOfDay(new Date()); + let lastStartDate = timeDay.floor(new Date()); const timeSeries = Array.from({ length: count }).map((_, i) => { - const startDate = addMinutes(lastStartDate, getRandomInteger(0, 60)); - const endDate = addMinutes(startDate, getRandomInteger(5, 60)); + const startDate = timeMinute.offset(lastStartDate, getRandomInteger(0, 60)); + const endDate = timeMinute.offset(startDate, getRandomInteger(5, 60)); lastStartDate = startDate; return { name: `item ${i + 1}`, @@ -113,7 +119,7 @@ export function createTimeSeries(options: { keys.map((key) => { return [ key, - options.value === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max), + valueType === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max), ]; }) ), @@ -190,3 +196,47 @@ export function getSpiral({ }; }); } + +interface SineWaveOptions { + numPoints: number; + frequency?: number; + amplitude?: number; + noiseLevel?: number; + phase?: number; + xMin?: number; + xMax?: number; +} + +export function generateSineWave(options: SineWaveOptions) { + const { + numPoints, + frequency = 1, + amplitude = 1, + noiseLevel = 0, + phase = 0, + xMin = 0, + xMax = 2 * Math.PI, + } = options; + + if (numPoints <= 0) { + throw new Error('Number of points must be greater than 0'); + } + + const points: { x: number; y: number }[] = []; + const xStep = (xMax - xMin) / (numPoints - 1); + + for (let i = 0; i < numPoints; i++) { + const x = xMin + i * xStep; + + // Generate base sine wave + const sineValue = amplitude * Math.sin(frequency * x + phase); + + // Add random noise if specified + const noise = noiseLevel > 0 ? (Math.random() - 0.5) * 2 * noiseLevel : 0; + const y = sineValue + noise; + + points.push({ x, y }); + } + + return points; +} diff --git a/packages/layerchart/src/lib/utils/graph.test.ts b/packages/layerchart/src/lib/utils/graph/dagre.test.ts similarity index 53% rename from packages/layerchart/src/lib/utils/graph.test.ts rename to packages/layerchart/src/lib/utils/graph/dagre.test.ts index f91db9b71..d20018a4a 100644 --- a/packages/layerchart/src/lib/utils/graph.test.ts +++ b/packages/layerchart/src/lib/utils/graph/dagre.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect } from 'vitest'; -import dagre from '@dagrejs/dagre'; -import { ancestors, descendants } from './graph.js'; +import { dagreGraph, dagreAncestors, dagreDescendants } from './dagre.js'; const exampleGraph = { nodes: [ @@ -27,72 +26,54 @@ const exampleGraph = { ], }; -function buildGraph(data: typeof exampleGraph) { - const g = new dagre.graphlib.Graph(); - - g.setGraph({}); - - data.nodes.forEach((n) => { - g.setNode(n.id, { - label: n.id, - }); - }); - - data.edges.forEach((e) => { - g.setEdge(e.source, e.target); - }); - - return g; -} - -describe('accessors', () => { +describe('dagreAncestors', () => { it('start of graph ', () => { - const graph = buildGraph(exampleGraph); - const actual = ancestors(graph, 'A'); + const graph = dagreGraph(exampleGraph); + const actual = dagreAncestors(graph, 'L'); expect(actual).length(0); }); it('middle of graph ', () => { - const graph = buildGraph(exampleGraph); - const actual = ancestors(graph, 'E'); + const graph = dagreGraph(exampleGraph); + const actual = dagreAncestors(graph, 'E'); expect(actual).to.have.members(['A', 'B', 'C', 'D']); }); it('end of graph ', () => { - const graph = buildGraph(exampleGraph); - const actual = ancestors(graph, 'I'); + const graph = dagreGraph(exampleGraph); + const actual = dagreAncestors(graph, 'I'); expect(actual).to.have.members(['A', 'B', 'C', 'D', 'E', 'G', 'H']); }); it('max depth', () => { - const graph = buildGraph(exampleGraph); - const actual = ancestors(graph, 'H', 2); + const graph = dagreGraph(exampleGraph); + const actual = dagreAncestors(graph, 'H', 2); expect(actual).to.have.members(['B', 'D', 'E', 'G']); }); }); -describe('descendants', () => { +describe('dagreDescendants', () => { it('start of graph ', () => { - const graph = buildGraph(exampleGraph); - const actual = descendants(graph, 'A'); + const graph = dagreGraph(exampleGraph); + const actual = dagreDescendants(graph, 'A'); expect(actual).to.have.members(['B', 'E', 'F', 'H', 'I']); }); it('middle of graph ', () => { - const graph = buildGraph(exampleGraph); - const actual = descendants(graph, 'E'); + const graph = dagreGraph(exampleGraph); + const actual = dagreDescendants(graph, 'E'); expect(actual).to.have.members(['H', 'I']); }); it('end of graph ', () => { - const graph = buildGraph(exampleGraph); - const actual = descendants(graph, 'I'); + const graph = dagreGraph(exampleGraph); + const actual = dagreDescendants(graph, 'I'); expect(actual).length(0); }); it('max depth', () => { - const graph = buildGraph(exampleGraph); - const actual = descendants(graph, 'B', 2); + const graph = dagreGraph(exampleGraph); + const actual = dagreDescendants(graph, 'B', 2); expect(actual).to.have.members(['E', 'F', 'H']); }); }); diff --git a/packages/layerchart/src/lib/utils/graph/dagre.ts b/packages/layerchart/src/lib/utils/graph/dagre.ts new file mode 100644 index 000000000..5b7adf70d --- /dev/null +++ b/packages/layerchart/src/lib/utils/graph/dagre.ts @@ -0,0 +1,149 @@ +import dagre from '@dagrejs/dagre'; +import { Align, EdgeLabelPosition, RankDir, type DagreProps } from '$lib/components/Dagre.svelte'; + +/** + * Build `dagre.graphlib.Graph` instance from DagreGraphData (`{ nodes, edges }`) + */ +export function dagreGraph( + data: DagreProps['data'], + { + nodes = (d: any) => d.nodes, + nodeId = (d: any) => d.id, + edges = (d: any) => d.edges, + directed = true, + multigraph = false, + compound = false, + ranker = 'network-simplex', + direction = 'top-bottom', + align, + rankSeparation = 50, + nodeSeparation = 50, + edgeSeparation = 10, + nodeWidth = 100, + nodeHeight = 50, + edgeLabelWidth = 100, + edgeLabelHeight = 20, + edgeLabelPosition = 'center', + edgeLabelOffset = 10, + filterNodes = () => true, + }: { + nodes?: DagreProps['nodes']; + nodeId?: DagreProps['nodeId']; + edges?: DagreProps['edges']; + directed?: DagreProps['directed']; + multigraph?: DagreProps['multigraph']; + compound?: DagreProps['compound']; + ranker?: DagreProps['ranker']; + direction?: DagreProps['direction']; + align?: DagreProps['align']; + rankSeparation?: DagreProps['rankSeparation']; + nodeSeparation?: DagreProps['nodeSeparation']; + edgeSeparation?: DagreProps['edgeSeparation']; + nodeWidth?: DagreProps['nodeWidth']; + nodeHeight?: DagreProps['nodeHeight']; + edgeLabelWidth?: DagreProps['edgeLabelWidth']; + edgeLabelHeight?: DagreProps['edgeLabelHeight']; + edgeLabelPosition?: DagreProps['edgeLabelPosition']; + edgeLabelOffset?: DagreProps['edgeLabelOffset']; + filterNodes?: DagreProps['filterNodes']; + } = {} +) { + let g = new dagre.graphlib.Graph({ directed, multigraph, compound }); + + g.setGraph({ + ranker: ranker, + rankdir: RankDir[direction], + align: align ? Align[align] : undefined, + ranksep: rankSeparation, + nodesep: nodeSeparation, + edgesep: edgeSeparation, + }); + + g.setDefaultEdgeLabel(() => ({})); + + const dataNodes = nodes(data); + + for (const n of dataNodes) { + const id = nodeId(n); + + g.setNode(nodeId(n), { + id, + label: typeof n.label === 'string' ? n.label : id, + width: nodeWidth, + height: nodeHeight, + ...(typeof n.label === 'object' ? n.label : null), + }); + + if (n.parent) { + g.setParent(id, n.parent); + } + } + + const nodeEdges = edges(data); + + for (const e of nodeEdges) { + const { source, target, label, ...rest } = e; + g.setEdge( + e.source, + e.target, + label + ? { + label: label, + labelpos: EdgeLabelPosition[edgeLabelPosition], + labeloffset: edgeLabelOffset, + width: edgeLabelWidth, + height: edgeLabelHeight, + ...rest, + } + : {} + ); + } + + if (filterNodes) { + g = g.filterNodes((nodeId) => filterNodes(nodeId, g)); + } + + dagre.layout(g); + + return g; +} + +/** + * Get all upstream predecessors ids for dagre nodeId + */ +export function dagreAncestors( + graph: dagre.graphlib.Graph, + nodeId: string, + maxDepth = Infinity, + currentDepth = 0 +): string[] { + if (currentDepth === maxDepth) { + return []; + } + + const predecessors = graph.predecessors(nodeId) ?? []; + return [ + ...predecessors, + ...predecessors.flatMap((pId) => dagreAncestors(graph, pId, maxDepth, currentDepth + 1)), + ]; +} + +/** + * Get all downstream descendants ids for dagre nodeId + */ +export function dagreDescendants( + graph: dagre.graphlib.Graph, + nodeId: string, + maxDepth = Infinity, + currentDepth = 0 +): string[] { + if (currentDepth === maxDepth) { + return []; + } + + const successors = graph.successors(nodeId) ?? []; + return [ + ...successors, + ...successors.flatMap((pId) => dagreDescendants(graph, pId, maxDepth, currentDepth + 1)), + ]; +} diff --git a/packages/layerchart/src/lib/utils/graph.ts b/packages/layerchart/src/lib/utils/graph/sankey.ts similarity index 50% rename from packages/layerchart/src/lib/utils/graph.ts rename to packages/layerchart/src/lib/utils/graph/sankey.ts index 0d42d3a6e..c7cb293f5 100644 --- a/packages/layerchart/src/lib/utils/graph.ts +++ b/packages/layerchart/src/lib/utils/graph/sankey.ts @@ -7,12 +7,11 @@ import type { SankeyNodeMinimal, } from 'd3-sankey'; import type { hierarchy as d3Hierarchy } from 'd3-hierarchy'; -import dagre from '@dagrejs/dagre'; /** * Convert CSV rows in format: 'source,target,value' to SankeyGraph */ -export function graphFromCsv(csv: string): SankeyGraph { +export function sankeyGraphFromCsv(csv: string): SankeyGraph { const links = csvParseRows(csv, ([source, target, value /*, linkColor = color*/]) => source && target ? { @@ -25,13 +24,13 @@ export function graphFromCsv(csv: string): SankeyGraph { : null ); - return { nodes: nodesFromLinks(links), links }; + return { nodes: sankeyNodesFromLinks(links), links }; } /** * Convert d3-hierarchy to graph (nodes/links) */ -export function graphFromHierarchy(hierarchy: ReturnType) { +export function sankeyGraphFromHierarchy(hierarchy: ReturnType) { return { nodes: hierarchy.descendants(), links: hierarchy.links().map((link) => ({ ...link, value: link.target.value })), @@ -41,31 +40,31 @@ export function graphFromHierarchy(hierarchy: ReturnType) { /** * Create graph from node (and target node/links downward) */ -export function graphFromNode(node: SankeyNodeMinimal) { +export function sankeyGraphFromNode(node: SankeyNodeMinimal) { const nodes: SankeyNode[] = [node]; const links: SankeyLink[] = []; - node.sourceLinks?.forEach((link) => { + for (const link of node.sourceLinks ?? []) { nodes.push(link.target); links.push(link); if (link.target.sourceLinks.length) { - const targetData = graphFromNode(link.target); + const targetData = sankeyGraphFromNode(link.target); // Only add new nodes - targetData.nodes.forEach((node) => { + for (const node of targetData.nodes) { if (!nodes.includes(node)) { nodes.push(node); } - }); - - targetData.links.forEach((link) => { + } + // Only add new links + for (const link of targetData.links) { if (!links.includes(link)) { links.push(link); } - }); + } } - }); + } return { nodes, links }; } @@ -73,9 +72,10 @@ export function graphFromNode(node: SankeyNodeMinimal) { /** * Get distinct nodes from link.source and link.target */ -export function nodesFromLinks( - links: Array> -) { +export function sankeyNodesFromLinks< + N extends SankeyExtraProperties, + L extends SankeyExtraProperties, +>(links: Array>) { const nodesByName = new Map(); for (const link of links) { if (!nodesByName.has(link.source)) { @@ -87,45 +87,3 @@ export function nodesFromLinks ancestors(graph, pId, maxDepth, currentDepth + 1)), - ]; -} - -/** - * Get all downstream descendants for dagre nodeId - */ -export function descendants( - graph: dagre.graphlib.Graph, - nodeId: string, - maxDepth = Infinity, - currentDepth = 0 -): dagre.Node[] { - if (currentDepth === maxDepth) { - return []; - } - - const predecessors = graph.successors(nodeId) ?? []; - return [ - ...predecessors, - // @ts-expect-error: Types from dagre appear incorrect - ...predecessors.flatMap((pId) => descendants(graph, pId, maxDepth, currentDepth + 1)), - ]; -} diff --git a/packages/layerchart/src/lib/utils/index.ts b/packages/layerchart/src/lib/utils/index.ts index 817db2b70..3e2f1d86d 100644 --- a/packages/layerchart/src/lib/utils/index.ts +++ b/packages/layerchart/src/lib/utils/index.ts @@ -1,7 +1,7 @@ +export { applyLanes } from './array.js'; export * from './canvas.js'; export * from './common.js'; export * from './geo.js'; -export * from './graph.js'; export * from './hierarchy.js'; export * from './math.js'; export * from './path.js'; @@ -9,3 +9,7 @@ export * from './pivot.js'; export * from './stack.js'; export * from './ticks.js'; export * from './threshold.js'; +export * from './types.js'; + +export * from './graph/dagre.js'; +export * from './graph/sankey.js'; diff --git a/packages/layerchart/src/lib/utils/key.svelte.ts b/packages/layerchart/src/lib/utils/key.svelte.ts new file mode 100644 index 000000000..b4552813f --- /dev/null +++ b/packages/layerchart/src/lib/utils/key.svelte.ts @@ -0,0 +1,12 @@ +import { objectId } from '@layerstack/utils/object'; + +// TODO: investigate if this is necessary with Svelte 5 +export function createKey(getValue: () => T) { + const value = $derived(getValue()); + const key = $derived(value && typeof value === 'object' ? objectId(value) : value); + return { + get current() { + return key; + }, + }; +} diff --git a/packages/layerchart/src/lib/utils/legendPayload.ts b/packages/layerchart/src/lib/utils/legendPayload.ts new file mode 100644 index 000000000..ef505fc92 --- /dev/null +++ b/packages/layerchart/src/lib/utils/legendPayload.ts @@ -0,0 +1,17 @@ +import { Context } from 'runed'; + +export type LegendPayload = { + key: string; + label?: string; + color?: string; +}; + +const _LegendPayloadContext = new Context('LegendContext'); + +export function setLegendPayloadContext(payload: LegendPayload[]) { + return _LegendPayloadContext.set(payload); +} + +export function getLegendPayloadContext() { + return _LegendPayloadContext.getOr([] as LegendPayload[]); +} diff --git a/packages/layerchart/src/lib/utils/math.ts b/packages/layerchart/src/lib/utils/math.ts index 6babcac62..9e48effac 100644 --- a/packages/layerchart/src/lib/utils/math.ts +++ b/packages/layerchart/src/lib/utils/math.ts @@ -44,6 +44,29 @@ export function cartesianToPolar(x: number, y: number) { }; } +/** + * Calculate the angle and length between two points + * @param point1 - First point + * @param point2 - Second point + * @returns Angle in degrees and length + */ +export function pointsToAngleAndLength( + point1: { x: number; y: number }, + point2: { x: number; y: number } +) { + const dx = point2.x - point1.x; + const dy = point2.y - point1.y; + + const radians = Math.atan2(dy, dx); + const length = Math.sqrt(dx * dx + dy * dy); + + return { + radians, + angle: radiansToDegrees(radians), + length, + }; +} + /** Convert celsius temperature to fahrenheit */ export function celsiusToFahrenheit(temperature: number) { return temperature * (9 / 5) + 32; diff --git a/packages/layerchart/src/lib/utils/motion.svelte.ts b/packages/layerchart/src/lib/utils/motion.svelte.ts new file mode 100644 index 000000000..628852e03 --- /dev/null +++ b/packages/layerchart/src/lib/utils/motion.svelte.ts @@ -0,0 +1,298 @@ +import { Spring, Tween } from 'svelte/motion'; +import { afterTick } from './afterTick.js'; + +/** + * Spring motion configuration options + */ +export type SpringOptions = ConstructorParameters>[1]; +export type SpringSetOptions = Parameters<(typeof Spring)['prototype']['set']>[1]; + +/** + * Tween motion configuration options + */ +export type TweenOptions = ConstructorParameters>[1]; +export type TweenSetOptions = Parameters<(typeof Tween)['prototype']['set']>[1]; + +/** + * MotionNone is a non-animating state container that provides a compatible + * interface with Spring and Tween, but updates values immediately without animation. + * This gives us consistent state management whether animations are enabled or not. + */ +export type NoneOptions = ConstructorParameters>[1]; +export type NoneSetOptions = Parameters<(typeof MotionNone)['prototype']['set']>[1]; + +/** + * Configuration object for Spring animations with additional type discriminator + */ +export type MotionSpringOption = + | ({ + type: 'spring'; + } & SpringOptions) + | 'spring'; + +/** + * Configuration object for Tween animations with additional type discriminator + */ +export type MotionTweenOption = + | ({ + type: 'tween'; + } & TweenOptions) + | 'tween'; + +/** + * Configuration object for non-animating state with additional type discriminator + */ +export type MotionNoneOption = + | { + type: 'none'; + } + | 'none'; + +/** + * Union type of all possible motion configuration options + */ +export type MotionOptions = MotionSpringOption | MotionTweenOption | MotionNoneOption; + +type IsDefault = K extends string ? (string extends K ? true : false) : never; + +/** + * Motion config that can be either a direct motion config or + * a map of property names to motion configs + */ +export type MotionProp = + IsDefault extends true ? MotionOptions : MotionOptions | { [prop in K]?: MotionOptions }; + +/** + * Extended Spring class that adds a type discriminator to help with + * type narrowing in our motion system + */ +class MotionSpring extends Spring { + type = 'spring' as const; + constructor(value: T, options?: SpringOptions) { + super(value, options); + } +} + +/** + * Extended Tween class that adds a type discriminator to help with + * type narrowing in our motion system + */ +class MotionTween extends Tween { + type = 'tween' as const; + constructor(value: T, options?: TweenOptions) { + super(value, options); + } +} + +/** + * MotionNone is a state container that provides the same interface as + * Spring and Tween but without any animation logic. Values update immediately. + * + * This allows components to use a consistent API regardless of whether + * animations are enabled or not. + */ +class MotionNone { + type = 'none' as const; + #current = $state(null!); + #target = $state(null!); + + constructor(value: T, _options: any = {}) { + this.#current = value; + this.#target = value; + } + + /** + * Updates the value immediately and returns a resolved promise + * to maintain API compatibility with animated motion classes + */ + set(value: T, _options: any = {}): Promise { + this.#current = value; + this.#target = value; + return Promise.resolve(); + } + + get current() { + return this.#current; + } + + get target() { + return this.#target; + } + + set target(v: T) { + this.set(v); + } +} + +export type ResolvedTween = { type: 'tween'; options: TweenOptions }; +export type ResolvedSpring = { type: 'spring'; options: SpringOptions }; +export type ResolvedNone = { type: 'none'; options: {} }; + +/** + * Union type of all possible resolved motion configurations + */ +export type ResolvedMotion = ResolvedSpring | ResolvedTween | ResolvedNone; + +/** + * Internal options for motion state configuration + */ +type InternalMotionOptions = { + /** + * When true, the motion state will only update when explicitly set + * rather than automatically tracking changes to the source value + */ + controlled?: boolean; +}; + +/** + * Union type of all possible motion state containers + */ +type MotionState = MotionSpring | MotionTween | MotionNone; + +/** + * Sets up automatic tracking between a source value and a motion state. + * When the `controlled` option is `true`, the motion state will not update + * automatically and will only update when explicitly set. + */ +function setupTracking( + motion: MotionState, + getValue: () => T, + options: InternalMotionOptions +) { + if (options.controlled) return; + + $effect(() => { + motion.set(getValue(), { instant: motion.target == null }); + }); +} + +export function createMotion( + initialValue: T, + getValue: () => T, + motionProp: MotionOptions | undefined, + options: InternalMotionOptions = {} +) { + const motion = parseMotionProp(motionProp); + const motionState = + motion.type === 'spring' + ? new MotionSpring(initialValue, motion.options) + : motion.type === 'tween' + ? new MotionTween(initialValue, motion.options) + : new MotionNone(initialValue); + setupTracking(motionState, getValue, options); + return motionState; +} + +/** + * Creates a controlled motion state that only updates when explicitly set + * rather than automatically tracking changes to the source value + */ +export function createControlledMotion( + initialValue: T, + motionProp: MotionOptions | undefined +) { + return createMotion(initialValue, () => initialValue, motionProp, { controlled: true }); +} + +/** + * Creates a state tracker for animation completion + * This helps track whether any motion transitions are currently in progress + * + * @returns an object with methods to handle animation promises and check current status + */ +export function createMotionTracker() { + let latestIndex = 0; + let current = $state(false); + + function handle(promise: Promise | void) { + latestIndex += 1; + if (!promise) { + current = false; + return; + } + let currIndex = latestIndex; + current = true; + promise + .then(() => { + if (currIndex === latestIndex) { + current = false; + } + }) + .catch(() => {}); + } + + return { + handle, + get current() { + return current; + }, + }; +} + +/** + * Extracts tween configuration from a motion prop + * @returns Resolved tween configuration or undefined if not a tween + */ +export function extractTweenConfig( + prop?: MotionProp | undefined +): ResolvedTween | undefined { + const resolved = parseMotionProp(prop); + if (resolved.type === 'tween') return resolved; +} + +/** + * Parses and normalizes a motion configuration into a standard format + * + * @param config - The motion configuration to parse + * @param propertyKey - Optional property key when config is a map of properties + * @returns A standardized motion configuration object + */ +export function parseMotionProp( + config: MotionProp | undefined | ResolvedMotion, + accessor?: string +): ResolvedMotion { + if (typeof config === 'object' && 'type' in config && 'options' in config) { + if (typeof config.options === 'object') return config; + return { type: config.type, options: {} }; + } + // Default to no animation if no configuration provided + if (config === undefined) return { type: 'none', options: {} }; + + // Case 1: string shorthand ('spring', 'tween', 'none') + if (typeof config === 'string') { + if (config === 'spring') { + return { type: 'spring', options: {} }; + } else if (config === 'tween') { + return { type: 'tween', options: {} }; + } + return { type: 'none', options: {} }; + } + + // Case 2: Object with explicit type property + if (typeof config === 'object' && 'type' in config) { + if (config.type === 'spring') { + const { type, ...options } = config; + return { type: 'spring', options }; + } else if (config.type === 'tween') { + const { type, ...options } = config; + return { type: 'tween', options }; + } else { + return { type: 'none', options: {} }; + } + } + + // Case 3: Property map object, lookup by property key + // We've already established config is an object at this point + if (accessor) { + const propConfig = config[accessor as keyof typeof config]; + if (propConfig !== undefined) { + return parseMotionProp(propConfig as MotionProp); + } + } + + // Fallback to no animation + return { + type: 'none', + options: {}, + }; +} diff --git a/packages/layerchart/src/lib/utils/motion.test.ts b/packages/layerchart/src/lib/utils/motion.test.ts new file mode 100644 index 000000000..04bd89022 --- /dev/null +++ b/packages/layerchart/src/lib/utils/motion.test.ts @@ -0,0 +1,255 @@ +import { describe, it, expect } from 'vitest'; +import { parseMotionProp, extractTweenConfig } from './motion.svelte.js'; +import type { + MotionProp, + MotionSpringOption, + MotionTweenOption, + MotionNoneOption, + ResolvedMotion, +} from './motion.svelte.js'; + +describe('parseMotionProp', () => { + it('should return "none" type when config is undefined', () => { + const result = parseMotionProp(undefined); + expect(result).toEqual({ type: 'none', options: {} }); + }); + + it('should return an already resolved motion object that may have been parsed earlier in the chain', () => { + const resolvedMotion: ResolvedMotion = { + type: 'spring', + options: { + stiffness: 0.5, + damping: 0.8, + }, + }; + const result = parseMotionProp(resolvedMotion); + expect(result).toEqual(resolvedMotion); + }); + + describe('string input handling', () => { + it('should handle "spring" string shorthand', () => { + const result = parseMotionProp('spring'); + expect(result).toEqual({ type: 'spring', options: {} }); + }); + + it('should handle "tween" string shorthand', () => { + const result = parseMotionProp('tween'); + expect(result).toEqual({ type: 'tween', options: {} }); + }); + + it('should handle "none" string shorthand', () => { + const result = parseMotionProp('none'); + expect(result).toEqual({ type: 'none', options: {} }); + }); + + it('should default to "none" for invalid string values', () => { + const result = parseMotionProp('invalid' as any); + expect(result).toEqual({ type: 'none', options: {} }); + }); + }); + + describe('object with type property', () => { + it('should handle spring object with options', () => { + const springConfig: MotionSpringOption = { + type: 'spring', + stiffness: 0.5, + damping: 0.8, + }; + const result = parseMotionProp(springConfig); + expect(result).toEqual({ + type: 'spring', + options: { stiffness: 0.5, damping: 0.8 }, + }); + }); + + it('should handle tween object with options', () => { + const tweenConfig: MotionTweenOption = { + type: 'tween', + duration: 300, + easing: (t) => t, + }; + const result = parseMotionProp(tweenConfig); + expect(result).toEqual({ + type: 'tween', + options: { duration: 300, easing: expect.any(Function) }, + }); + }); + + it('should handle none object', () => { + const noneConfig: MotionNoneOption = { + type: 'none', + }; + const result = parseMotionProp(noneConfig); + expect(result).toEqual({ type: 'none', options: {} }); + }); + }); + + describe('property map object with accessor', () => { + it('should extract a specific property motion config', () => { + const propMap: MotionProp<'x' | 'y' | 'scale'> = { + x: 'spring', + y: { type: 'tween', duration: 300 }, + scale: { type: 'spring', stiffness: 0.2 }, + }; + + const resultX = parseMotionProp(propMap, 'x'); + expect(resultX).toEqual({ type: 'spring', options: {} }); + + const resultY = parseMotionProp(propMap, 'y'); + expect(resultY).toEqual({ type: 'tween', options: { duration: 300 } }); + + const resultScale = parseMotionProp(propMap, 'scale'); + expect(resultScale).toEqual({ type: 'spring', options: { stiffness: 0.2 } }); + }); + + it('should return "none" type for non-existent accessor', () => { + const propMap: MotionProp<'x' | 'y'> = { + x: 'spring', + y: 'tween', + }; + + const result = parseMotionProp(propMap, 'z'); + expect(result).toEqual({ type: 'none', options: {} }); + }); + }); + + describe('edge cases', () => { + it('should handle empty object as "none"', () => { + const result = parseMotionProp({} as any); + expect(result).toEqual({ type: 'none', options: {} }); + }); + + it('should handle object without type property', () => { + const invalidObject = { + stiffness: 0.5, + damping: 0.8, + } as any; + + const result = parseMotionProp(invalidObject); + expect(result).toEqual({ type: 'none', options: {} }); + }); + }); + + // Test case 6: Type Generic + describe('type generic parameter', () => { + it('should work with explicit generic type parameter', () => { + type ValidProps = 'x' | 'y' | 'scale'; + const propMap: MotionProp = { + x: 'spring', + y: 'tween', + scale: { type: 'spring', stiffness: 0.3 }, + }; + + const result = parseMotionProp(propMap, 'x'); + expect(result).toEqual({ type: 'spring', options: {} }); + }); + }); +}); + +describe('extractTweenConfig', () => { + // Test case 1: Undefined input + it('should return undefined when config is undefined', () => { + const result = extractTweenConfig(undefined); + expect(result).toBeUndefined(); + }); + + // Test case 2: String inputs + describe('string input handling', () => { + it('should extract tween config from "tween" string shorthand', () => { + const result = extractTweenConfig('tween'); + expect(result).toEqual({ type: 'tween', options: {} }); + }); + + it('should return undefined for "spring" string shorthand', () => { + const result = extractTweenConfig('spring'); + expect(result).toBeUndefined(); + }); + + it('should return undefined for "none" string shorthand', () => { + const result = extractTweenConfig('none'); + expect(result).toBeUndefined(); + }); + }); + + // Test case 3: Object with type property + describe('object with type property', () => { + it('should extract tween object with options', () => { + const tweenConfig: MotionTweenOption = { + type: 'tween', + duration: 300, + easing: (t) => t, + }; + const result = extractTweenConfig(tweenConfig); + expect(result).toEqual({ + type: 'tween', + options: { duration: 300, easing: expect.any(Function) }, + }); + }); + + it('should return undefined for spring object', () => { + const springConfig: MotionProp = { + type: 'spring', + stiffness: 0.5, + damping: 0.8, + }; + const result = extractTweenConfig(springConfig); + expect(result).toBeUndefined(); + }); + + it('should return undefined for none object', () => { + const noneConfig: MotionProp = { + type: 'none', + }; + const result = extractTweenConfig(noneConfig); + expect(result).toBeUndefined(); + }); + }); + + // Test case 4: Property map object (without accessor) + describe('property map object without accessor', () => { + it('should return undefined for property map objects without accessor', () => { + const propMap: MotionProp<'x' | 'y' | 'scale'> = { + x: 'tween', + y: { type: 'tween', duration: 300 }, + scale: { type: 'spring', stiffness: 0.2 }, + }; + + const result = extractTweenConfig(propMap); + expect(result).toBeUndefined(); + }); + }); + + // Test case 6: Edge cases + describe('edge cases', () => { + it('should handle empty object', () => { + const result = extractTweenConfig({} as any); + expect(result).toBeUndefined(); + }); + + it('should handle object without type property', () => { + const invalidObject = { + duration: 300, + easing: (t: any) => t, + } as any; + + const result = extractTweenConfig(invalidObject); + expect(result).toBeUndefined(); + }); + }); + + describe('with generic type parameter', () => { + it('should work with explicit generic type parameter', () => { + const propMap: MotionProp<'x' | 'y' | 'scale'> = { + x: 'tween', + y: { type: 'tween', duration: 400 }, + scale: 'spring', + }; + + // The extractTweenConfig function doesn't directly support accessors, + // so for this case we manually extract the property first + const xConfig = propMap.x; + const result = extractTweenConfig(xConfig); + expect(result).toEqual({ type: 'tween', options: {} }); + }); + }); +}); diff --git a/packages/layerchart/src/lib/utils/object.ts b/packages/layerchart/src/lib/utils/object.ts deleted file mode 100644 index 5a595ed44..000000000 --- a/packages/layerchart/src/lib/utils/object.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { memoize } from 'lodash-es'; - -export const memoizeObject = memoize( - (obj) => obj, - (obj) => JSON.stringify(obj) -); diff --git a/packages/layerchart/src/lib/utils/path.ts b/packages/layerchart/src/lib/utils/path.ts index 98305a127..f8a4344c4 100644 --- a/packages/layerchart/src/lib/utils/path.ts +++ b/packages/layerchart/src/lib/utils/path.ts @@ -58,6 +58,36 @@ export function spikePath({ return pathData; } +/** Create rounded polygon path + * + * @param coords - Array of points (x, y) + * @param radius - Radius of the curve + * @returns String of path data + */ +export function roundedPolygonPath(coords: { x: number; y: number }[], radius: number) { + if (radius === 0) { + // Simple polygon with straight lines + return `M${coords[0].x},${coords[0].y}${coords + .slice(1) + .map((p) => `L${p.x},${p.y}`) + .join('')}Z`; + } + + let path = ''; + const length = coords.length + 1; + for (let i = 0; i < length; i++) { + const a = coords[i % coords.length]; + const b = coords[(i + 1) % coords.length]; + const t = Math.min(radius / Math.hypot(b.x - a.x, b.y - a.y), 0.5); + + if (i == 0) path += `M${a.x * (1 - t) + b.x * t},${a.y * (1 - t) + b.y * t}`; + if (i > 0) path += `Q${a.x},${a.y} ${a.x * (1 - t) + b.x * t},${a.y * (1 - t) + b.y * t}`; + if (i < length - 1) path += `L${a.x * t + b.x * (1 - t)},${a.y * t + b.y * (1 - t)}`; + } + path += 'Z'; + return path; +} + /** Flatten all `y` coordinates to `0` */ export function flattenPathData(pathData: string, yOverride = 0) { let result = pathData; diff --git a/packages/layerchart/src/lib/utils/pivot.ts b/packages/layerchart/src/lib/utils/pivot.ts index b417942ab..4fa5f6dc7 100644 --- a/packages/layerchart/src/lib/utils/pivot.ts +++ b/packages/layerchart/src/lib/utils/pivot.ts @@ -5,6 +5,7 @@ import { group } from 'd3-array'; * - see: https://observablehq.com/d/3ea8d446f5ba96fe * - see also: https://observablehq.com/d/ac2a320cf2b0adc4 as generator */ + export function pivotLonger(data: any[], columns: string[], name: string, value: string) { const keep = Object.keys(data[0]).filter((c) => !columns.includes(c)); return data.flatMap((d) => { diff --git a/packages/layerchart/src/lib/utils/quadtree.ts b/packages/layerchart/src/lib/utils/quadtree.ts index 352f9fa51..32e94239d 100644 --- a/packages/layerchart/src/lib/utils/quadtree.ts +++ b/packages/layerchart/src/lib/utils/quadtree.ts @@ -13,5 +13,6 @@ export function quadtreeRects(quadtree: Quadtree, showLeaves = true) { rects.push({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 }); } }); + return rects; } diff --git a/packages/layerchart/src/lib/utils/rect.svelte.ts b/packages/layerchart/src/lib/utils/rect.svelte.ts new file mode 100644 index 000000000..f0be8ee80 --- /dev/null +++ b/packages/layerchart/src/lib/utils/rect.svelte.ts @@ -0,0 +1,242 @@ +import type { ChartContextValue } from '$lib/components/Chart.svelte'; +import { accessor, type Accessor } from './common.js'; +import { isScaleBand } from './scales.svelte.js'; +import { max, min } from 'd3-array'; + +/** + * A set of inset distances, applied to a rectangle to shrink or expand + * the area represented by that rectangle. + */ +export type Insets = { + /** Applies an inset all sides of a rectangle: `left`, `right`, `bottom`, and `top` */ + all?: number; + /** Applies an inset all horizontal sides of a rectangle: `left`, and `right`, overriding `all` */ + x?: number; + /** Applies an inset all vertical sides of a rectangle: `top`, and `bottom`, overriding `all` */ + y?: number; + /** Applies an inset the left side of a rectangle, overriding `x` */ + left?: number; + /** Applies an inset the right side of a rectangle, overriding `x` */ + right?: number; + /** Applies an inset the top side of a rectangle, overriding `y` */ + top?: number; + /** Applies an inset the bottom side of a rectangle, overriding `y` */ + bottom?: number; +}; + +type DimensionGetterOptions = { + /** Override `x` accessor from context */ + x?: Accessor; + /** Override `y` accessor from context */ + y?: Accessor; + /** Override `x1` accessor from context */ + x1?: Accessor; + /** Override `y1` accessor from context */ + y1?: Accessor; + insets?: Insets; +}; + +type ResolvedInsets = { + left: number; + right: number; + top: number; + bottom: number; +}; + +function resolveInsets(insets?: Insets): ResolvedInsets { + const all = insets?.all ?? 0; + + const x = insets?.x ?? all; + const y = insets?.y ?? all; + + const left = insets?.left ?? x; + const right = insets?.right ?? x; + const top = insets?.top ?? y; + const bottom = insets?.bottom ?? y; + + return { left, right, bottom, top }; +} + +export function createDimensionGetter( + ctx: ChartContextValue, + getOptions?: () => DimensionGetterOptions +) { + const options = $derived(getOptions?.()); + + return (item: TData) => { + const insets = resolveInsets(options?.insets); + // Use `xscale.domain()` instead of `$xDomain` to include `nice()` being applied + const xDomainMinMax = ctx.xScale.domain(); + const yDomainMinMax = ctx.yScale.domain(); + + const _x = accessor(options?.x ?? ctx.x); + const _y = accessor(options?.y ?? ctx.y); + const _x1 = accessor(options?.x1 ?? ctx.x1); + const _y1 = accessor(options?.y1 ?? ctx.y1); + + if (isScaleBand(ctx.yScale)) { + // Horizontal band + const y = + firstValue(ctx.yScale(_y(item)) ?? 0) + + (ctx.y1Scale ? ctx.y1Scale(_y1(item)) : 0) + + insets.top; + + const height = Math.max( + 0, + ctx.yScale.bandwidth + ? (ctx.y1Scale ? (ctx.y1Scale.bandwidth?.() ?? 0) : ctx.yScale.bandwidth()) - + insets.bottom - + insets.top + : 0 + ); + + const xValue = _x(item); + + let left = 0; + let right = 0; + if (Array.isArray(xValue)) { + // Array contains both top and bottom values (stack, etc); + left = min(xValue); + right = max(xValue); + } else if (xValue == null) { + // null/undefined value + left = 0; + right = 0; + } else if (xValue > 0) { + // Positive value + left = max([0, xDomainMinMax[0]]); + right = xValue; + } else { + // Negative value + left = xValue; + right = min([0, xDomainMinMax[1]]); + } + + const x = ctx.xScale(left) + insets.left; + const width = Math.max(0, ctx.xScale(right) - ctx.xScale(left) - insets.left - insets.right); + + return { x, y, width, height }; + } else if (isScaleBand(ctx.xScale)) { + // Vertical band or linear + const x = + firstValue(ctx.xScale(_x(item))) + (ctx.x1Scale ? ctx.x1Scale(_x1(item)) : 0) + insets.left; + + const width = Math.max( + 0, + ctx.xScale.bandwidth + ? (ctx.x1Scale ? (ctx.x1Scale.bandwidth?.() ?? 0) : ctx.xScale.bandwidth()) - + insets.left - + insets.right + : 0 + ); + + const yValue = _y(item); + + let top = 0; + let bottom = 0; + if (Array.isArray(yValue)) { + // Array contains both top and bottom values (stack, etc); + top = max(yValue); + bottom = min(yValue); + } else if (yValue == null) { + // null/undefined value + top = 0; + bottom = 0; + } else if (yValue > 0) { + // Positive value + top = yValue; + bottom = max([0, yDomainMinMax[0]]); + } else { + // Negative value + top = min([0, yDomainMinMax[1]]); + bottom = yValue; + } + + // If yRange is inverted (drawing from top), swap top and bottom + if (ctx.yRange[0] < ctx.yRange[1]) { + [top, bottom] = [bottom, top]; + } + + const y = ctx.yScale(top) + insets.top; + const height = ctx.yScale(bottom) - ctx.yScale(top) - insets.bottom - insets.top; + + return { x, y, width, height }; + } else if (ctx.xInterval) { + // x-axis time scale with interval + const xValue = _x(item); + const start = ctx.xInterval.floor(xValue); + const end = ctx.xInterval.offset(start); + const x = ctx.xScale(start) + insets.left; + const width = ctx.xScale(end) - x - insets.right; + + const yValue = _y(item); + + let top = 0; + let bottom = 0; + if (Array.isArray(yValue)) { + // Array contains both top and bottom values (stack, etc); + top = max(yValue); + bottom = min(yValue); + } else if (yValue == null) { + // null/undefined value + top = 0; + bottom = 0; + } else if (yValue > 0) { + // Positive value + top = yValue; + bottom = max([0, yDomainMinMax[0]]); + } else { + // Negative value + top = min([0, yDomainMinMax[1]]); + bottom = yValue; + } + + const y = ctx.yScale(top) + insets.top; + const height = ctx.yScale(bottom) - ctx.yScale(top) - insets.bottom - insets.top; + + return { x, y, width, height }; + } else if (ctx.yInterval) { + // y-axis time scale with interval + const yValue = _y(item); + const start = ctx.yInterval.floor(yValue); + const end = ctx.yInterval.offset(start); + const y = ctx.yScale(start) + insets.top; + const height = ctx.yScale(end) - y - insets.bottom; + + const xValue = _x(item); + + let left = 0; + let right = 0; + if (Array.isArray(xValue)) { + // Array contains both top and bottom values (stack, etc); + left = min(xValue); + right = max(xValue); + } else if (xValue == null) { + // null/undefined value + left = 0; + right = 0; + } else if (xValue > 0) { + // Positive value + left = max([0, xDomainMinMax[0]]); + right = xValue; + } else { + // Negative value + left = xValue; + right = min([0, xDomainMinMax[1]]); + } + + const x = ctx.xScale(left) + insets.left; + const width = ctx.xScale(right) - x - insets.right; + + return { x, y, width, height }; + } + }; +} + +/** + * If value is an array, returns first item, else returns original value + * Useful when x/y getters for band scale are an array (such as for histograms) + */ +export function firstValue(value: number | number[] | undefined) { + return Array.isArray(value) ? value[0] : value; +} diff --git a/packages/layerchart/src/lib/utils/rect.ts b/packages/layerchart/src/lib/utils/rect.ts deleted file mode 100644 index cb7b3ff93..000000000 --- a/packages/layerchart/src/lib/utils/rect.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { derived } from 'svelte/store'; -import { max, min } from 'd3-array'; - -import { isScaleBand } from './scales.js'; -import type { ChartContext } from '../components/ChartContext.svelte'; -import { accessor, type Accessor } from './common.js'; - -/** A set of inset distances, applied to a rectangle to shrink or expand the area represented by that rectangle. */ -export type Insets = { - /** Applies an inset all sides of a rectangle: `left`, `right`, `bottom`, and `top` */ - all?: number; - /** Applies an inset all horizontal sides of a rectangle: `left`, and `right`, overriding `all` */ - x?: number; - /** Applies an inset all vertical sides of a rectangle: `top`, and `bottom`, overriding `all` */ - y?: number; - /** Applies an inset the left side of a rectangle, overriding `x` */ - left?: number; - /** Applies an inset the right side of a rectangle, overriding `x` */ - right?: number; - /** Applies an inset the top side of a rectangle, overriding `y` */ - top?: number; - /** Applies an inset the bottom side of a rectangle, overriding `y` */ - bottom?: number; -}; - -type ResolvedInsets = { - left: number; - right: number; - top: number; - bottom: number; -}; - -type DimensionGetterOptions = { - /** Override `x` accessor from context */ - x?: Accessor; - /** Override `y` accessor from context */ - y?: Accessor; - /** Override `x1` accessor from context */ - x1?: Accessor; - /** Override `y1` accessor from context */ - y1?: Accessor; - insets?: Insets; -}; - -export function createDimensionGetter( - context: ChartContext, - options?: DimensionGetterOptions -) { - const { - xScale, - yScale, - x: xAccessor, - y: yAccessor, - x1: x1Accessor, - y1: y1Accessor, - x1Scale, - y1Scale, - } = context; - - return derived( - [xScale, x1Scale, yScale, y1Scale, xAccessor, yAccessor, x1Accessor, y1Accessor], - ([$xScale, $x1Scale, $yScale, $y1Scale, $xAccessor, $yAccessor, $x1Accessor, $y1Accessor]) => { - const insets = resolveInsets(options?.insets); - // Use `xscale.domain()` instead of `$xDomain` to include `nice()` being applied - const [minXDomain, maxXDomain] = $xScale.domain(); - const [minYDomain, maxYDomain] = $yScale.domain(); - - const _x = accessor(options?.x ?? $xAccessor); - const _y = accessor(options?.y ?? $yAccessor); - const _x1 = accessor(options?.x1 ?? $x1Accessor); - const _y1 = accessor(options?.y1 ?? $y1Accessor); - - // @ts-expect-error - return function getter(item) { - if (isScaleBand($yScale)) { - // Horizontal band - const y = - firstValue($yScale(_y(item)) ?? 0) + ($y1Scale ? $y1Scale(_y1(item)) : 0) + insets.top; - const height = Math.max( - 0, - $yScale.bandwidth - ? ($y1Scale ? ($y1Scale.bandwidth?.() ?? 0) : $yScale.bandwidth()) - - insets.bottom - - insets.top - : 0 - ); - - const xValue = _x(item); - - let left = 0; - let right = 0; - if (Array.isArray(xValue)) { - // Array contains both top and bottom values (stack, etc); - left = min(xValue); - right = max(xValue); - } else if (xValue == null) { - // null/undefined value - left = 0; - right = 0; - } else if (xValue > 0) { - // Positive value - left = max([0, minXDomain]); - right = xValue; - } else { - // Negative value - left = xValue; - right = min([0, maxXDomain]); - } - - const x = $xScale(left) + insets.left; - const width = Math.max(0, $xScale(right) - $xScale(left) - insets.left - insets.right); - - return { x, y, width, height }; - } else { - // Vertical band or linear - const x = - firstValue($xScale(_x(item))) + ($x1Scale ? $x1Scale(_x1(item)) : 0) + insets.left; - const width = Math.max( - 0, - $xScale.bandwidth - ? ($x1Scale ? ($x1Scale.bandwidth?.() ?? 0) : $xScale.bandwidth()) - - insets.left - - insets.right - : 0 - ); - - const yValue = _y(item); - - let top = 0; - let bottom = 0; - if (Array.isArray(yValue)) { - // Array contains both top and bottom values (stack, etc); - top = max(yValue); - bottom = min(yValue); - } else if (yValue == null) { - // null/undefined value - top = 0; - bottom = 0; - } else if (yValue > 0) { - // Positive value - top = yValue; - bottom = max([0, minYDomain]); - } else { - // Negative value - top = min([0, maxYDomain]); - bottom = yValue; - } - - const y = $yScale(top) + insets.top; - const height = $yScale(bottom) - $yScale(top) - insets.bottom - insets.top; - - return { x, y, width, height }; - } - }; - } - ); -} - -/** - * If value is an array, returns first item, else returns original value - * Useful when x/y getters for band scale are an array (such as for histograms) - */ -export function firstValue(value: number | number[]) { - return Array.isArray(value) ? value[0] : value; -} - -function resolveInsets(insets?: Insets): ResolvedInsets { - const all = insets?.all ?? 0; - - const x = insets?.x ?? all; - const y = insets?.y ?? all; - - const left = insets?.left ?? x; - const right = insets?.right ?? x; - const top = insets?.top ?? y; - const bottom = insets?.bottom ?? y; - - return { left, right, bottom, top }; -} diff --git a/packages/layerchart/src/lib/utils/scales.svelte.ts b/packages/layerchart/src/lib/utils/scales.svelte.ts new file mode 100644 index 000000000..598e7eef9 --- /dev/null +++ b/packages/layerchart/src/lib/utils/scales.svelte.ts @@ -0,0 +1,334 @@ +import { unique } from '@layerstack/utils'; +import { scaleBand, scaleLinear, scaleTime, type ScaleBand, type ScaleTime } from 'd3-scale'; +import { + createControlledMotion, + type MotionProp, + type MotionOptions, + type SpringOptions, + type TweenOptions, +} from '$lib/utils/motion.svelte.js'; +import { Spring, Tween } from 'svelte/motion'; +import { accessor, type Accessor } from './common.js'; +import type { OnlyObjects } from './types.js'; +import type { TimeInterval } from 'd3-time'; + +export type AnyScale< + TInput extends SingleDomainType = any, + TOutput extends SingleDomainType = any, + TScaleArgs extends any[] | readonly any[] = any[], +> = { + (value: TInput): TOutput; + domain(domain: TInput[] | readonly TInput[]): AnyScale; + domain(): TInput[]; + range(range: TOutput[] | readonly TOutput[]): AnyScale; + range(): TOutput[]; + rangeRound?: (range: TOutput[] | readonly TOutput[]) => AnyScale; + copy: () => AnyScale; + invert?: (value: TOutput) => TInput; + invertExtent?: (value: TOutput) => [TInput, TInput]; + bandwidth?: () => number; + ticks?: (count?: number) => TInput[]; + tickFormat?: (count?: number) => (value: TInput) => string; + clamp?: (clamp: boolean) => AnyScale; + interpolate?: ( + interpolate: (a: TOutput, b: TOutput) => (t: number) => TOutput + ) => AnyScale; + nice?: (count?: number) => AnyScale; + interpolator?(interpolator: (t: number) => TOutput): AnyScale; + interpolator?(): (t: number) => TOutput; // Getter + thresholds?: () => TInput[]; + quantiles?: () => TInput[]; +}; + +function isAnyScale(scale: any): scale is AnyScale { + return typeof scale === 'function' && typeof scale.range === 'function'; +} + +export function isScaleBand(scale: AnyScale): scale is ScaleBand { + return typeof scale.bandwidth === 'function'; +} + +export function isScaleTime(scale: AnyScale): scale is ScaleTime { + const domain = scale.domain(); + return domain[0] instanceof Date || domain[1] instanceof Date; +} + +export function isScaleNumeric(scale: AnyScale): scale is ScaleTime { + const domain = scale.domain(); + return typeof domain[0] === 'number' || typeof domain[1] === 'number'; +} + +export function getRange(scale: any) { + if (isAnyScale(scale)) { + return scale.range(); + } + console.error("[LayerChart] Your scale doesn't have a `.range` method?"); + return []; +} + +export type SingleDomainType = number | string | Date | null | undefined; + +export type DomainType = + | (number | string | Date | null | undefined)[] + // 'null' and `undefined` useful for Brush component + | null + | undefined; + +// this may need to become a getter for options so we can reactively update after mount +export function createMotionScale( + scale: AnyScale, + motion: MotionProp | undefined, + options: { + defaultDomain?: Domain; + defaultRange?: Range; + } +) { + const domain = createControlledMotion(options.defaultDomain as Domain, motion); + const range = createControlledMotion(options.defaultRange as Range, motion); + + const motionScale = $derived.by(() => { + // @ts-expect-error + const scaleInstance = scale.domain ? scale : scale(); // support `scaleLinear` or `scaleLinear()` (which could have `.interpolate()` and others set) + + if (domain.current) { + scaleInstance.domain(domain.current); + } + if (range.current) { + scaleInstance.range(range.current); + } + + return scaleInstance; + }); + + return { + get current() { + return motionScale; + }, + domain: (values: Domain) => domain.set(values), + range: (values: Range) => range.set(values), + }; +} + +/** + * Implementation for missing `scaleBand().invert()` + * + * See: https://stackoverflow.com/questions/38633082/d3-getting-invert-value-of-band-scales + * https://github.com/d3/d3-scale/pull/64 + * https://github.com/vega/vega-scale/blob/master/src/scaleBand.js#L118 + * https://observablehq.com/@d3/ordinal-brushing + * https://github.com/d3/d3-scale/blob/11777dac7d4b0b3e229d658aee3257ea67bd5ffa/src/band.js#L32 + * https://gist.github.com/LuisSevillano/d53a1dc529eef518780c6df99613e2fd + */ +export function scaleBandInvert(scale: ScaleBand) { + const domain = scale.domain(); + const eachBand = scale.step(); + const paddingOuter = eachBand * (scale.paddingOuter?.() ?? scale.padding()); // `scaleBand` uses paddingOuter(), while `scalePoint` uses padding() for outer paddding - https://github.com/d3/d3-scale#point_padding + + return function (value: number) { + const index = Math.floor((value - paddingOuter / 2) / eachBand); + return domain[Math.max(0, Math.min(index, domain.length - 1))]; + }; +} + +/** + * Generic way to invert a scale value, handling scaleBand and continuous scales (linear, time, etc). + * Useful to map mouse event location (x,y) to domain value + */ +export function scaleInvert(scale: AnyScale, value: number) { + if (isScaleBand(scale)) { + return scaleBandInvert(scale)(value); + } else { + return scale.invert?.(value); + } +} + +/** Create new copy of scale with domain and range */ +export function createScale( + scale: AnyScale, + domain: DomainType, + range: any[] | readonly any[] | Function, + context?: Record +) { + const scaleCopy = scale.copy(); + if (domain) { + scaleCopy.domain(domain); + } + + if (typeof range === 'function') { + scaleCopy.range(range(context)); + } else { + scaleCopy.range(range); + } + return scaleCopy; +} + +/** + * Auto-detect scale type based on domain values or data values + */ +export function autoScale( + domain?: DomainType, + data?: any[], + propAccessor?: Accessor +): AnyScale { + let values = null; + if (domain && domain.length > 0 && domain.some((d) => d != null)) { + // Determine based on non-null domain values + values = domain.filter((d) => d != null); + } else if (data && data.length > 0 && propAccessor) { + // Determine based on data values + const value = accessor(propAccessor)(data[0]); + + // If accessor defined with an array (ex. `x={['start', 'end']}`) use both values + if (Array.isArray(value)) { + values = value; + } else { + values = [value]; + } + } + + if (values) { + if (values.some((v) => v instanceof Date)) { + return scaleTime(); + } else if (values.some((v) => typeof v === 'number')) { + return scaleLinear(); + } else if (values.some((v) => typeof v === 'string')) { + return scaleBand(); + } + } + + // fallback to linear scale + return scaleLinear(); +} + +/** + * Create a `scaleBand()` within another scaleBand()'s bandwidth + * (typically a x1 of an x0 scale, used for grouping) + */ +export function groupScaleBand( + scale: ScaleBand, + flatData: any[], + groupBy: string, + padding?: { inner?: number; outer?: number } +) { + // + const groupKeys = unique(flatData.map((d) => d[groupBy])) as string[]; + + let newScale = scaleBand().domain(groupKeys).range([0, scale.bandwidth()]); + + if (padding) { + if (padding.inner) { + newScale = newScale.paddingInner(padding.inner); + } + if (padding.outer) { + newScale = newScale.paddingOuter(padding.outer); + } + } + + return newScale; +} + +/** + * Animate d3-scale as domain and/or range are updated using tweened store + */ +export function tweenedScale(scale: any, tweenedOptions: TweenOptions = {}) { + const tweenedDomain = new Tween(undefined as Domain, tweenedOptions); + const tweenedRange = new Tween(undefined as Range, tweenedOptions); + + const tweenedScale = $derived.by(() => { + const scaledInstance = scale.domain ? scale : scale(); + if (tweenedDomain.current) { + scaledInstance.domain(tweenedDomain.current); + } + if (tweenedRange.current) { + scaledInstance.range(tweenedRange.current); + } + return scaledInstance; + }); + + return { + get current() { + return tweenedScale; + }, + domain: (values: Domain) => tweenedDomain.set(values), + range: (values: Range) => tweenedRange.set(values), + }; +} + +/** + * Animate d3-scale as domain and/or range are updated using spring store + */ +export function springScale(scale: AnyScale, springOptions: SpringOptions = {}) { + const domainState = new Spring(undefined as Domain, springOptions); + const rangeState = new Spring(undefined as Range, springOptions); + + const sprungScale = $derived.by(() => { + // @ts-expect-error - TODO: investigate/fix + const scaledInstance = scale.domain ? scale : scale(); + + if (domainState.current) { + scaledInstance.domain(domainState.current); + } + if (rangeState.current) { + scaledInstance.range(rangeState.current); + } + + return scaledInstance; + }); + + return { + get current() { + return sprungScale; + }, + domain: (values: Domain) => domainState.set(values), + range: (values: Range) => rangeState.set(values), + }; +} + +/** + * Create a store wrapper around a d3-scale which interpolates the domain and/or range using `tweened()` or `spring()` stores. Fallbacks to `writable()` store if not interpolating + */ +export function motionScale(scale: AnyScale, options: OnlyObjects) { + const domainState = createControlledMotion(undefined as Domain, options); + const rangeState = createControlledMotion(undefined as Range, options); + + const tweenedScale = $derived.by(() => { + // @ts-expect-error + const scaleInstance = scale.domain ? scale : scale(); // support `scaleLinear` or `scaleLinear()` (which could have `.interpolate()` and others set) + if (domainState.current) { + scaleInstance.domain(domainState.current); + } + + if (rangeState.current) { + scaleInstance.range(rangeState.current); + } + return scaleInstance; + }); + + return { + get current() { + return tweenedScale; + }, + domain: (values: Domain) => domainState.set(values), + range: (values: Range) => rangeState.set(values), + }; +} + +function canBeZero(val: unknown) { + if (val === 0) return true; + return val; +} + +export function makeAccessor(acc: Accessor): (d: TData) => any { + if (!canBeZero(acc)) return null as unknown as (d: TData) => any; + if (Array.isArray(acc)) { + return (d: TData) => + acc.map((k) => { + // @ts-expect-error - TODO: Fix these types + return typeof k !== 'function' ? d[k] : k(d); + }); + } else if (typeof acc !== 'function') { + // @ts-expect-error - TODO: Fix these types + return (d: TData) => d[acc]; + } + return acc; +} diff --git a/packages/layerchart/src/lib/utils/scales.ts b/packages/layerchart/src/lib/utils/scales.ts deleted file mode 100644 index 800ab75ca..000000000 --- a/packages/layerchart/src/lib/utils/scales.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { derived } from 'svelte/store'; -import { tweened, spring } from 'svelte/motion'; - -import { type MotionOptions, motionStore } from '$lib/stores/motionStore.js'; -import { scaleBand, type ScaleBand } from 'd3-scale'; -import { unique } from '@layerstack/utils'; - -export interface AnyScale { - (value: Input): Output; - invert?: (value: Output) => Input; - domain(): Domain[]; - domain(domain: Iterable): this; - range(): Range[]; - range(range: Iterable): this; - bandwidth?: Function; - ticks?: Function; - tickFormat?: Function; - copy(): Function; -} - -export type DomainType = - | (number | string | Date | null | undefined)[] - // 'null' useful for Brush component - | null; - -/** - * Implemenation for missing `scaleBand().invert()` - * - * See: https://stackoverflow.com/questions/38633082/d3-getting-invert-value-of-band-scales - * https://github.com/d3/d3-scale/pull/64 - * https://github.com/vega/vega-scale/blob/master/src/scaleBand.js#L118 - * https://observablehq.com/@d3/ordinal-brushing - * https://github.com/d3/d3-scale/blob/11777dac7d4b0b3e229d658aee3257ea67bd5ffa/src/band.js#L32 - * https://gist.github.com/LuisSevillano/d53a1dc529eef518780c6df99613e2fd - */ -export function scaleBandInvert(scale: ScaleBand) { - const domain = scale.domain(); - const eachBand = scale.step(); - const paddingOuter = eachBand * (scale.paddingOuter?.() ?? scale.padding()); // `scaleBand` uses paddingOuter(), while `scalePoint` uses padding() for outer paddding - https://github.com/d3/d3-scale#point_padding - - return function (value: number) { - const index = Math.floor((value - paddingOuter / 2) / eachBand); - return domain[Math.max(0, Math.min(index, domain.length - 1))]; - }; -} - -export function isScaleBand(scale: AnyScale): scale is ScaleBand { - return typeof scale.bandwidth === 'function'; -} - -/** - * Generic way to invert a scale value, handling scaleBand and continuous scales (linear, time, etc). - * Useful to map mouse event location (x,y) to domain value - */ -export function scaleInvert(scale: AnyScale, value: number) { - if (isScaleBand(scale)) { - return scaleBandInvert(scale)(value); - } else { - return scale.invert?.(value); - } -} - -/** Create new copy of scale with domain and range */ -export function createScale( - scale: AnyScale, - domain: DomainType, - range: any[] | readonly any[] | Function, - context?: Record -) { - const scaleCopy = scale.copy() as AnyScale; - if (domain) { - scaleCopy.domain(domain); - } - - if (typeof range === 'function') { - scaleCopy.range(range(context)); - } else { - scaleCopy.range(range); - } - return scaleCopy; -} - -/** Create a `scaleBand()` within another scaleBand()'s bandwidth (typically a x1 of an x0 scale, used for grouping) */ -export function groupScaleBand( - scale: ScaleBand, - flatData: any[], - groupBy: string, - padding?: { inner?: number; outer?: number } -) { - // - const groupKeys = unique(flatData.map((d) => d[groupBy])) as string[]; - - let newScale = scaleBand().domain(groupKeys).range([0, scale.bandwidth()]); - - if (padding) { - if (padding.inner) { - newScale = newScale.paddingInner(padding.inner); - } - if (padding.outer) { - newScale = newScale.paddingOuter(padding.outer); - } - } - - return newScale; -} - -/** - * Animate d3-scale as domain and/or range are updated using tweened store - */ -export function tweenedScale( - scale: any, - tweenedOptions: Parameters>[1] = {} -) { - const tweenedDomain = tweened(undefined as Domain, tweenedOptions); - const tweenedRange = tweened(undefined as Range, tweenedOptions); - - const tweenedScale = derived([tweenedDomain, tweenedRange], ([domain, range]) => { - const scaleInstance = scale.domain ? scale : scale(); // support `scaleLinear` or `scaleLinear()` (which could have `.interpolate()` and others set) - - if (domain) { - scaleInstance.domain(domain); - } - if (range) { - scaleInstance.range(range); - } - - return scaleInstance; - }); - - return { - subscribe: tweenedScale.subscribe, - domain: (values: Domain) => tweenedDomain.set(values), - range: (values: Range) => tweenedRange.set(values), - }; -} - -/** - * Animate d3-scale as domain and/or range are updated using spring store - */ -export function springScale( - scale: AnyScale, - springOptions: Parameters[1] = {} -) { - const domainStore = spring(undefined, springOptions); - const rangeStore = spring(undefined, springOptions); - - const tweenedScale = derived([domainStore, rangeStore], ([domain, range]) => { - // @ts-expect-error - const scaleInstance = scale.domain ? scale : scale(); // support `scaleLinear` or `scaleLinear()` (which could have `.interpolate()` and others set) - - if (domain) { - scaleInstance.domain(domain); - } - if (range) { - scaleInstance.range(range); - } - - return scaleInstance; - }); - - return { - subscribe: tweenedScale.subscribe, - domain: (values: Domain) => domainStore.set(values), - range: (values: Range) => rangeStore.set(values), - }; -} - -/** - * Create a store wrapper around a d3-scale which interpolates the domain and/or range using `tweened()` or `spring()` stores. Fallbacks to `writable()` store if not interpolating - */ -export function motionScale(scale: AnyScale, options: MotionOptions) { - const domainStore = motionStore(undefined as Domain, options); - const rangeStore = motionStore(undefined as Range, options); - - const tweenedScale = derived([domainStore, rangeStore], ([domain, range]) => { - // @ts-expect-error - const scaleInstance = scale.domain ? scale : scale(); // support `scaleLinear` or `scaleLinear()` (which could have `.interpolate()` and others set) - - if (domain) { - scaleInstance.domain(domain); - } - if (range) { - scaleInstance.range(range); - } - - return scaleInstance; - }); - - return { - subscribe: tweenedScale.subscribe, - domain: (values: Domain) => domainStore.set(values), - range: (values: Range) => rangeStore.set(values), - }; -} diff --git a/packages/layerchart/src/lib/utils/shape.ts b/packages/layerchart/src/lib/utils/shape.ts new file mode 100644 index 000000000..c22eb3611 --- /dev/null +++ b/packages/layerchart/src/lib/utils/shape.ts @@ -0,0 +1,91 @@ +import { range } from 'd3-array'; +import { degreesToRadians } from './math.js'; + +/** Get points to create a polygon with given number of points and radius + * + * @param count - Number of points + * @param radius - Radius of the polygon + * @returns Array of points (angle, radius) + */ +export function polygonPoints(count: number, radius: number, rotate: number = 0) { + const angle = 360 / count; + + return range(count).map((index) => { + return { + angle: degreesToRadians(angle * index) + degreesToRadians(rotate), + radius, + }; + }); +} + +/** Create polygon + * + * @param cx - Center x coordinate + * @param cy - Center y coordinate + * @param count - Number of points + * @param radius - Radius of the polygon + * @param rotate - Rotation of the polygon (degrees) + * @param inset - Percent to inset odd points (<1 inset, >1 outset) + * @param scaleX - Horizontal stretch factor + * @param scaleY - Vertical stretch factor + * @param skewX - Skew angle in degrees along the X axis + * @param skewY - Skew angle in degrees along the Y axis + * @param tiltX - Tilt factor for x-coordinates (0 = no tilt, positive moves points top => down, negative moves points bottom => up) + * @param tiltY - Tilt factor for y-coordinates (0 = no tilt, positive moves points left => right, negative moves points right => left) + * @returns Array of points (x, y) + */ +export function polygon(options: { + cx: number; + cy: number; + count: number; + radius: number; + rotate?: number; + inset?: number; + scaleX?: number; + scaleY?: number; + skewX?: number; + skewY?: number; + tiltX?: number; + tiltY?: number; +}) { + const { + cx, + cy, + count, + radius, + rotate = 0, + inset = 1, + scaleX = 1, + scaleY = 1, + skewX = 0, + skewY = 0, + tiltX = 0, + tiltY = 0, + } = options; + const skewXRad = degreesToRadians(skewX); + const skewYRad = degreesToRadians(skewY); + return polygonPoints(count, radius, rotate).map(({ angle, radius }, i) => { + // inset + const insetScale = i % 2 == 0 ? 1 : 1 - inset; + + // scale + let x = radius * insetScale * Math.cos(angle) * scaleX; + let y = radius * insetScale * Math.sin(angle) * scaleY; + + // tilt + const normalizedY = (y + radius) / (2 * radius); + const normalizedX = (x + radius) / (2 * radius); + const tiltScaleX = tiltX > 0 ? 1 + tiltX * (1 - normalizedY) : 1 - tiltX * normalizedY; + const tiltScaleY = tiltY > 0 ? 1 + tiltY * (1 - normalizedX) : 1 - tiltY * normalizedX; + x *= tiltScaleX; + y *= tiltScaleY; + + // skew + const xSkewed = x + Math.tan(skewXRad) * y; + const ySkewed = y + Math.tan(skewYRad) * x; + return { + x: cx + xSkewed, + y: cy + ySkewed, + }; + }); +} diff --git a/packages/layerchart/src/lib/utils/stack.ts b/packages/layerchart/src/lib/utils/stack.ts index 49341447c..795ed0766 100644 --- a/packages/layerchart/src/lib/utils/stack.ts +++ b/packages/layerchart/src/lib/utils/stack.ts @@ -131,7 +131,7 @@ export function stackOffsetSeparated(series, order) { // Standard series for (var i = 1, s0, s1 = series[order[0]], n, m = s1.length; i < n; ++i) { - (s0 = s1), (s1 = series[order[i]]); + ((s0 = s1), (s1 = series[order[i]])); // @ts-expect-error let base = max(s0, (d) => d[1]) + gap; // here is where you calculate the maximum of the previous layer for (var j = 0; j < m; ++j) { diff --git a/packages/layerchart/src/lib/utils/string.ts b/packages/layerchart/src/lib/utils/string.ts index 3624843f5..d93639ea2 100644 --- a/packages/layerchart/src/lib/utils/string.ts +++ b/packages/layerchart/src/lib/utils/string.ts @@ -1,4 +1,4 @@ -import { memoize } from 'lodash-es'; +import memoize from 'memoize'; const MEASUREMENT_ELEMENT_ID = '__text_measurement_id'; @@ -29,10 +29,9 @@ function _getStringWidth(str: string, style?: CSSStyleDeclaration) { } } -export const getStringWidth = memoize( - _getStringWidth, - (str, style) => `${str}_${JSON.stringify(style)}` -); +export const getStringWidth = memoize(_getStringWidth, { + cacheKey: ([str, style]) => `${str}_${JSON.stringify(style)}`, +}); export type RasterizeTextOptions = { fontSize?: string; @@ -95,3 +94,126 @@ function getPixel(imageData: ImageData, x: number, y: number) { var d = imageData.data; return [d[i], d[i + 1], d[i + 2], d[i + 3]]; } + +export function toTitleCase(str: string) { + return str.replace(/^\w/, (d) => d.toUpperCase()); +} + +const DEFAULT_ELLIPSIS = '…'; + +export type TruncateTextOptions = { + /** + * The maximum pixel width (optional if maxChars is provided). + */ + maxWidth?: number; + + /** + * CSS style for width calculation + */ + style?: CSSStyleDeclaration; + + /** + * The maximum character count + */ + maxChars?: number; + + /** + * Where to place the ellipsis: 'start', 'middle', or 'end' + * + * @default 'end' + */ + position?: 'start' | 'middle' | 'end'; + + /** + * The character(s) to use as the ellipsis + * + * @default '…' + */ + ellipsis?: string; +}; + +/** + * Truncates a string to fit within a specified pixel width or character count. + * If the string's width exceeds the maxWidth, it will be truncated. If the character + * count exceeds maxChars, it will also be truncated. + * + * The ellipsis can be placed at the start, middle, or end of the string. + */ +export function truncateText( + text: string, + { position = 'end', ellipsis = DEFAULT_ELLIPSIS, maxWidth, style, maxChars }: TruncateTextOptions +): string { + if (!text) return ''; + + // no constraints, return original text + if (maxWidth === undefined && maxChars === undefined) return text; + + // apply maxChars constraint first (if provided) + let workingText = text; + if (maxChars !== undefined && text.length > maxChars) { + if (position === 'start') { + workingText = ellipsis + text.slice(-maxChars); + } else if (position === 'middle') { + const half = Math.floor(maxChars / 2); + workingText = text.slice(0, half) + ellipsis + text.slice(-half); + } else { + workingText = text.slice(0, maxChars) + ellipsis; + } + } + + // apply maxWidth constraint (if provided) + if (maxWidth !== undefined) { + const fullWidth = getStringWidth(workingText, style); + // if width measurement fails or text fits, return current text + if (fullWidth === null || fullWidth <= maxWidth) return workingText; + + const ellipsisWidth = getStringWidth(ellipsis, style) ?? 0; + let availableWidth = maxWidth - ellipsisWidth; + + if (position === 'start') { + let truncated = workingText.slice(ellipsis.length); // remove initial ellipsis if present + let truncatedWidth = getStringWidth(truncated, style); + while (truncatedWidth !== null && truncatedWidth > availableWidth && truncated.length > 0) { + truncated = truncated.slice(1); + truncatedWidth = getStringWidth(truncated, style); + } + return ellipsis + truncated; + } else if (position === 'middle') { + const halfWidth = availableWidth / 2; + let left = ''; + let right = ''; + let bestLeft = ''; + let bestRight = ''; + + for (let i = 0, j = workingText.length - 1; i < workingText.length && j >= 0; i++, j--) { + const leftTest = workingText.slice(0, i + 1); + const rightTest = workingText.slice(j); + const leftWidth = getStringWidth(leftTest, style); + const rightWidth = getStringWidth(rightTest, style); + + if (leftWidth !== null && leftWidth <= halfWidth) left = leftTest; + if (rightWidth !== null && rightWidth <= halfWidth) right = rightTest; + + const combinedWidth = getStringWidth(left + ellipsis + right, style); + if (combinedWidth !== null && combinedWidth <= maxWidth) { + bestLeft = left; // longest valid left + bestRight = right; // longest valid right + } else { + // we've exceed maxWidth, so break out + break; + } + } + return bestLeft + ellipsis + bestRight; + } else { + let truncated = workingText.slice(0, -ellipsis.length); + let truncatedWidth = getStringWidth(truncated + ellipsis, style); + while (truncatedWidth !== null && truncatedWidth > maxWidth && truncated.length > 0) { + truncated = truncated.slice(0, -1); + truncatedWidth = getStringWidth(truncated + ellipsis, style); + } + return truncated + ellipsis; + } + } + + return workingText; +} diff --git a/packages/layerchart/src/lib/utils/ticks.test.ts b/packages/layerchart/src/lib/utils/ticks.test.ts new file mode 100644 index 000000000..d0b33fcc4 --- /dev/null +++ b/packages/layerchart/src/lib/utils/ticks.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect, vi } from 'vitest'; +import { autoTickVals } from './ticks.js'; +import type { TimeInterval } from 'd3-time'; + +// Mock helpers +const mockTicksFn = vi.fn(); +const mockDomain = vi.fn(() => ['a', 'b', 'c', 'd', 'e']); + +describe('autoTickVals', () => { + it('returns array ticks directly', () => { + const ticks = [1, 2, 3]; + const scale = { ticks: mockTicksFn } as any; + expect(autoTickVals(scale, ticks)).toEqual([1, 2, 3]); + }); + + it('calls function ticks with scale', () => { + const fnTicks = vi.fn(() => [4, 5, 6]); + const scale = { ticks: mockTicksFn } as any; + expect(autoTickVals(scale, fnTicks)).toEqual([4, 5, 6]); + expect(fnTicks).toHaveBeenCalledWith(scale); + }); + + it('uses interval when provided', () => { + const interval = { every: vi.fn() } as unknown as TimeInterval; + const ticksConfig = { interval }; + const scale = { ticks: vi.fn(() => [7, 8, 9]) } as any; + expect(autoTickVals(scale, ticksConfig)).toEqual([7, 8, 9]); + expect(scale.ticks).toHaveBeenCalledWith(interval); + }); + + it('returns empty array if interval is null', () => { + const ticksConfig = { interval: null }; + const scale = { ticks: mockTicksFn } as any; + expect(autoTickVals(scale, ticksConfig)).toEqual([]); + }); + + it('filters band scale domain with number ticks', () => { + const scale = { domain: mockDomain, bandwidth: vi.fn() } as any; + expect(autoTickVals(scale, 2)).toEqual(['a', 'c', 'e']); + }); + + it('returns full domain for band scale without ticks', () => { + const scale = { domain: mockDomain, bandwidth: vi.fn() } as any; + expect(autoTickVals(scale)).toEqual(['a', 'b', 'c', 'd', 'e']); + }); + + it('uses undefined for non-left/right placement', () => { + const scale = { domain: mockDomain, ticks: vi.fn(() => [1, 2]) } as any; + expect(autoTickVals(scale, undefined, undefined)).toEqual([1, 2]); + expect(scale.ticks).toHaveBeenCalledWith(undefined); + }); + + it('passes number ticks to scale.ticks', () => { + const scale = { domain: mockDomain, ticks: vi.fn(() => [10, 20]) } as any; + expect(autoTickVals(scale, 5)).toEqual([10, 20]); + expect(scale.ticks).toHaveBeenCalledWith(5); + }); + + it('returns empty array for scale without ticks', () => { + const scale = { domain: mockDomain } as any; + expect(autoTickVals(scale, 5)).toEqual([]); + }); + + it('handles null ticks with placement', () => { + const scale = { domain: mockDomain, ticks: vi.fn(() => [1, 2, 3]) } as any; + expect(autoTickVals(scale, null, undefined)).toEqual([1, 2, 3]); + expect(scale.ticks).toHaveBeenCalledWith(undefined); + }); +}); diff --git a/packages/layerchart/src/lib/utils/ticks.ts b/packages/layerchart/src/lib/utils/ticks.ts index b23faf18c..fb76e717d 100644 --- a/packages/layerchart/src/lib/utils/ticks.ts +++ b/packages/layerchart/src/lib/utils/ticks.ts @@ -1,179 +1,190 @@ +import { timeYear, timeDay, type TimeInterval, timeTicks } from 'd3-time'; + import { - timeYear, - timeMonth, - timeWeek, - timeDay, - timeHour, - timeMinute, - timeSecond, - timeMillisecond, -} from 'd3-time'; -import { format } from 'date-fns'; - -import { formatDate, PeriodType, getDuration, fail } from '@layerstack/utils'; - -type Duration = ReturnType; - -// TODO: Use PeriodType along with Duration to format (and possibly select intervals) - -const majorTicks = [ - { - predicate: (duration: Duration) => duration == null, // Unknown - interval: timeYear.every(1), // Better than rendering a lot of items - format: (date: Date) => date.toString(), - }, - { - predicate: (duration: Duration) => duration!.years > 1, - interval: timeYear.every(1), - format: (date: Date) => formatDate(date, PeriodType.CalendarYear, { variant: 'short' }), - }, - { - predicate: (duration: Duration) => duration!.years, - interval: timeMonth.every(1), - format: (date: Date) => formatDate(date, PeriodType.Month, { variant: 'short' }), - }, - { - predicate: (duration: Duration) => duration!.days > 30, - interval: timeMonth.every(1), - format: (date: Date) => formatDate(date, PeriodType.Month, { variant: 'short' }), - }, - { - predicate: (duration: Duration) => duration!.days, - interval: timeDay.every(1), - format: (date: Date) => formatDate(date, PeriodType.Day, { variant: 'short' }), - }, - { - predicate: (duration: Duration) => duration!.hours, - interval: timeHour.every(1), - format: (date: Date) => format(date, 'h:mm a'), - }, - { - predicate: (duration: Duration) => duration!.minutes > 10, - interval: timeMinute.every(10), - format: (date: Date) => format(date, 'h:mm a'), - }, - { - predicate: (duration: Duration) => duration!.minutes, - interval: timeMinute.every(1), - format: (date: Date) => format(date, 'h:mm a'), - }, - { - predicate: (duration: Duration) => duration!.seconds > 10, - interval: timeSecond.every(10), - format: (date: Date) => format(date, 'h:mm:ss'), - }, - { - predicate: (duration: Duration) => duration!.seconds, - interval: timeSecond.every(1), - format: (date: Date) => format(date, 'h:mm:ss'), - }, - { - predicate: (duration: Duration) => true, // 0 or more milliseconds - interval: timeMillisecond.every(100), - format: (date: Date) => format(date, 'h:mm:ss.SSS'), - }, -]; - -const minorTicks = [ - { - predicate: (duration: Duration) => duration == null, // Unknown - interval: timeYear.every(1), // Better than rendering a lot of items - format: (date: Date) => date.toString(), - }, - { - predicate: (duration: Duration) => duration!.years, - interval: timeMonth.every(1), - format: (date: Date) => formatDate(date, PeriodType.Month, { variant: 'short' }), - }, - { - predicate: (duration: Duration) => duration!.days > 90, - interval: timeMonth.every(1), - format: (date: Date) => formatDate(date, PeriodType.Month, { variant: 'short' }), - }, - { - predicate: (duration: Duration) => duration!.days > 30, - interval: timeWeek.every(1), - format: (date: Date) => formatDate(date, PeriodType.WeekSun, { variant: 'short' }), - }, - { - predicate: (duration: Duration) => duration!.days > 7, - interval: timeDay.every(1), - format: (date: Date) => formatDate(date, PeriodType.Day, { variant: 'short' }), - }, - { - predicate: (duration: Duration) => duration!.days > 3, - interval: timeHour.every(8), - format: (date: Date) => format(date, 'h:mm a'), - }, - { - predicate: (duration: Duration) => duration!.days, - interval: timeHour.every(1), - format: (date: Date) => format(date, 'h:mm a'), - }, - { - predicate: (duration: Duration) => duration!.hours, - interval: timeMinute.every(15), - format: (date: Date) => format(date, 'h:mm a'), - }, - { - predicate: (duration: Duration) => duration!.minutes > 10, - interval: timeMinute.every(10), - format: (date: Date) => format(date, 'h:mm a'), - }, - { - predicate: (duration: Duration) => duration!.minutes > 2, - interval: timeMinute.every(1), - format: (date: Date) => format(date, 'h:mm a'), - }, - { - predicate: (duration: Duration) => duration!.minutes, - interval: timeSecond.every(10), - format: (date: Date) => format(date, 'h:mm:ss'), - }, - { - predicate: (duration: Duration) => duration!.seconds, - interval: timeSecond.every(1), - format: (date: Date) => format(date, 'h:mm:ss'), - }, - { - predicate: (duration: Duration) => true, // 0 or more milliseconds - interval: timeMillisecond.every(10), - format: (date: Date) => format(date, 'h:mm:ss.SSS'), - }, -]; - -export function getMajorTicks(start: Date, end: Date) { - const duration = getDuration(start, end); - - for (var t of majorTicks) { - if (t.predicate(duration)) { - return t.interval; - } + format, + Duration, + isLiteralObject, + type FormatType, + type FormatConfig, + DateToken, +} from '@layerstack/utils'; +import { isScaleBand, isScaleTime, type AnyScale } from './scales.svelte.js'; +import type { AxisProps } from '$lib/components/Axis.svelte'; + +export function getDurationFormat( + duration: Duration, + options: { multiline?: boolean; placement?: AxisProps['placement'] } = { + multiline: false, } +) { + const { multiline = false, placement = 'bottom' } = options; - fail(`Unable to locate major ticks for duration: ${duration}`); + return function (date: Date, i: number) { + let result: string | Array = ''; + + if (+duration >= +new Duration({ duration: { years: 1 } })) { + // Year + result = format(date, 'year'); + } else if (+duration >= +new Duration({ duration: { days: 28 } })) { + // Month + const isFirst = i === 0 || +timeYear.floor(date) === +date; + if (multiline) { + result = [format(date, 'month', { variant: 'short' }), isFirst && format(date, 'year')]; + } else { + result = + format(date, 'month', { variant: 'short' }) + + (isFirst ? ` '${format(date, 'year', { variant: 'short' })}` : ''); + } + } else if (+duration >= +new Duration({ duration: { days: 1 } })) { + // Day + const isFirst = i === 0 || date.getDate() <= duration.days; + if (multiline) { + result = [ + format(date, 'custom', { custom: DateToken.DayOfMonth_numeric }), + isFirst && format(date, 'month', { variant: 'short' }), + ]; + } else { + result = format(date, 'day', { variant: 'short' }); + } + } else if (+duration >= +new Duration({ duration: { hours: 1 } })) { + // Hours + const isFirst = i === 0 || +timeDay.floor(date) === +date; + if (multiline) { + result = [ + format(date, 'custom', { custom: DateToken.Hour_numeric }), + isFirst && format(date, 'day', { variant: 'short' }), + ]; + } else { + result = isFirst + ? format(date, 'day', { variant: 'short' }) + : format(date, 'custom', { custom: DateToken.Hour_numeric }); + } + } else if (+duration >= +new Duration({ duration: { minutes: 1 } })) { + // Minutes + const isFirst = i === 0 || +timeDay.floor(date) === +date; + if (multiline) { + result = [ + format(date, 'time', { variant: 'short' }), + isFirst && format(date, 'day', { variant: 'short' }), + ]; + } else { + result = format(date, 'time', { variant: 'short' }); + } + } else if (+duration >= +new Duration({ duration: { seconds: 1 } })) { + // Seconds + const isFirst = i === 0 || +timeDay.floor(date) === +date; + result = [ + format(date, 'time'), + multiline && isFirst && format(date, 'day', { variant: 'short' }), + ]; + } else if (+duration >= +new Duration({ duration: { milliseconds: 1 } })) { + // Milliseconds + const isFirst = i === 0 || +timeDay.floor(date) === +date; + result = [ + format(date, 'custom', { + custom: [ + DateToken.Hour_2Digit, + DateToken.Minute_2Digit, + DateToken.Second_2Digit, + DateToken.MiliSecond_3, + DateToken.Hour_woAMPM, + ], + }), + multiline && isFirst && format(date, 'day', { variant: 'short' }), + ]; + } else { + result = date.toString(); + } + + if (Array.isArray(result)) { + switch (placement) { + case 'top': + return result.filter(Boolean).reverse().join('\n'); + case 'bottom': + return result.filter(Boolean).join('\n'); + case 'left': + return result.filter(Boolean).reverse().join(' '); + case 'right': + return result.filter(Boolean).join(' '); + default: + return result.filter(Boolean).join('\n'); + } + } else { + return result; + } + }; } -export function formatMajorTick(date: Date, rangeStart: Date, rangeEnd: Date) { - const duration = getDuration(rangeStart, rangeEnd); +export type TicksConfig = + | number + | any[] + | ((scale: AnyScale) => any[] | undefined) + | { interval: TimeInterval | null } + | null; + +export function autoTickVals(scale: AnyScale, ticks?: TicksConfig, count?: number): any[] { + // Explicit ticks + if (Array.isArray(ticks)) return ticks; + + // Function + if (typeof ticks === 'function') return ticks(scale) ?? []; - for (var t of majorTicks) { - if (t.predicate(duration)) { - return t.format(date); + // Interval + if (isLiteralObject(ticks) && 'interval' in ticks) { + if (ticks.interval === null || !('ticks' in scale) || typeof scale.ticks !== 'function') { + return []; // Explicitly return empty array for null interval or invalid scale } + return scale.ticks(ticks.interval as any); } - fail(`Unable to format major ticks for duration: ${duration}`); + // Band (use domain) + if (isScaleBand(scale)) { + return ticks && typeof ticks === 'number' + ? scale.domain().filter((_, i) => i % ticks === 0) + : scale.domain(); + } + + // Ticks from scale + if (scale.ticks && typeof scale.ticks === 'function') { + return scale.ticks(count ?? (typeof ticks === 'number' ? ticks : undefined)); + } + + return []; } -export function getMinorTicks(start: Date, end: Date) { - const duration = getDuration(start, end); +export function autoTickFormat(options: { + scale: AnyScale; + ticks?: TicksConfig; + count?: number; + formatType?: FormatType | FormatConfig; + multiline?: boolean; + placement?: AxisProps['placement']; +}) { + const { scale, ticks, count, formatType, multiline, placement } = options; - for (var t of minorTicks) { - if (t.predicate(duration)) { - return t.interval; + // Explicit format + if (formatType) { + // @ts-expect-error - improve types + return (tick: any) => format(tick, formatType); + } + + // Time scale + if (isScaleTime(scale) && count) { + if (isLiteralObject(ticks) && 'interval' in ticks && ticks.interval != null) { + const start = ticks.interval.floor(new Date()); + const end = ticks.interval.ceil(new Date()); + return getDurationFormat(new Duration({ start, end }), { multiline, placement }); + } else { + // Compare first 2 ticks to determine duration between ticks for formatting + const [start, end] = timeTicks(scale.domain()[0], scale.domain()[1], count); + return getDurationFormat(new Duration({ start, end }), { multiline, placement }); } } - fail(`Unable to locate minor ticks for duration: ${duration}`); + // Format from scale + if (scale.tickFormat) { + return scale.tickFormat(count); + } + + return (tick: any) => `${tick}`; } diff --git a/packages/layerchart/src/lib/utils/treemap.ts b/packages/layerchart/src/lib/utils/treemap.ts index 5d91de23a..5bace5889 100644 --- a/packages/layerchart/src/lib/utils/treemap.ts +++ b/packages/layerchart/src/lib/utils/treemap.ts @@ -28,7 +28,7 @@ export function aspectTile(tile: TileFunc, width: number, height: number): TileF /** * Show if the node (a) is a child of the selected (b), or any parent above selected */ -export function isNodeVisible(a: HierarchyNode, b: HierarchyNode | null) { +export function isNodeVisible(a: HierarchyNode, b: HierarchyNode | null | undefined) { while (b) { if (a.parent === b) return true; b = b.parent; diff --git a/packages/layerchart/src/lib/utils/types.ts b/packages/layerchart/src/lib/utils/types.ts index e84ca6612..dfb41385d 100644 --- a/packages/layerchart/src/lib/utils/types.ts +++ b/packages/layerchart/src/lib/utils/types.ts @@ -1,3 +1,10 @@ +import type { MouseEventHandler, PointerEventHandler } from 'svelte/elements'; +import type { TransitionConfig } from 'svelte/transition'; +import type { HierarchyNode } from 'd3-hierarchy'; +import type { SankeyGraph } from 'd3-sankey'; + +import type { AnyScale } from './scales.svelte.js'; + /** * Useful to workaround Svelte 3/4 markup type issues * TODO: Remove usage after migrating to Svelte 5 @@ -5,3 +12,111 @@ export function asAny(x: any): any { return x; } + +/** + * Constructs a new type by omitting properties from type + * 'T' that exist in type 'U'. + * + * @template T - The base object type from which properties will be omitted. + * @template U - The object type whose properties will be omitted from 'T'. + * @example + * type Result = Without<{ a: number; b: string; }, { b: string; }>; + * // Result type will be { a: number; } + */ +export type Without = Omit; + +export type AxisKey = 'x' | 'y' | 'z' | 'r'; + +export type Extents = { + [K in AxisKey]?: Array; +}; + +export type Padding = { + top: number; + right: number; + bottom: number; + left: number; +}; + +export type Nice = boolean | number; + +export type BaseRange = + | number[] + | string[] + | ((args: { width: number; height: number }) => number[] | string[]); + +export type YRangeWithScale = + | number[] + | string[] + | ((args: { yScale: Scale; width: number; height: number }) => number[] | string[]); + +export type XRangeWithScale = + | number[] + | string[] + | ((args: { xScale: Scale; width: number; height: number }) => number[] | string[]); + +export type FieldAccessors = { + x?: (d: T) => number | string | (number | string)[]; + y?: (d: T) => number | string | (number | string)[]; + z?: (d: T) => number | string | (number | string)[]; + r?: (d: T) => number | string | (number | string)[]; +}; + +export type PaddingArray = [number, number] | number[] | undefined; + +export type DataType = T[] | HierarchyNode | SankeyGraph | readonly T[]; + +export type Transition = (node: Element, params?: any) => TransitionConfig; +export type TransitionParams = Parameters[1]; + +/** + * Common style properties that apply to many components. + * Includes `fill`, `fillOpacity`, `stroke`, `strokeWidth`, and `opacity`. + */ +export type CommonStyleProps = { + /** + * The fill color of the element. + */ + fill?: string; + + /** + * The fill opacity of the element. + */ + fillOpacity?: number; + + /** + * The stroke color of the element. + */ + stroke?: string; + + /** + * The stroke width of the element. + */ + strokeWidth?: number; + + /** + * The opacity of the element. (0 to 1) + */ + opacity?: number; +}; + +/** + * Events for primatives which support `SVGRectElement` and `HTMLDivElement` elements based on render context + */ +export type CommonEvents = { + onclick?: MouseEventHandler | null; + ondblclick?: MouseEventHandler | null; + onpointerenter?: PointerEventHandler | null; + onpointermove?: PointerEventHandler | null; + onpointerleave?: PointerEventHandler | null; + onpointerover?: PointerEventHandler | null; + onpointerout?: PointerEventHandler | null; +}; + +export type OnlyObjects = T extends object ? T : never; + +export type Getter = () => T; + +export type GetterValues = { + [K in keyof T]: Getter; +}; diff --git a/packages/layerchart/src/routes/+layout.server.ts b/packages/layerchart/src/routes/+layout.server.ts index 8fc6e96e9..af7a19da4 100644 --- a/packages/layerchart/src/routes/+layout.server.ts +++ b/packages/layerchart/src/routes/+layout.server.ts @@ -1,11 +1,10 @@ import { getThemeNames } from '@layerstack/tailwind'; +import themeCss from '@layerstack/tailwind/themes/all.css?raw'; // import { env } from '$env/dynamic/private'; -import themes from '../../themes.json' with { type: 'json' }; - export async function load() { return { - themes: getThemeNames(themes), + themes: getThemeNames(themeCss), // pr_id: env.VERCEL_GIT_PULL_REQUEST_ID, // TODO: Re-add once SvelteKit updated to `2.3.2+` - https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%402.3.2 }; } diff --git a/packages/layerchart/src/routes/+layout.svelte b/packages/layerchart/src/routes/+layout.svelte index 0f6e682ee..00f302d38 100644 --- a/packages/layerchart/src/routes/+layout.svelte +++ b/packages/layerchart/src/routes/+layout.svelte @@ -1,9 +1,8 @@ - {#if $page.url.origin.includes('https')} + {#if page.url.origin.includes('https')}
- -
- LayerChart for Svelte 5 released! - - - Preview - -
- +
+ + + {#if status} {#if !hideTableOfContents}
- {#if !$xlScreen} - {#key $page.route.id} + {#if !xlScreen.current} + {#key page.route.id}
On this page
+ {/each} +
+ + + + + +
+ + +
+ + {#snippet aboveMarks({ context })} + + {/snippet} + +
+
+ +

Vertical placement

+ +
+ + + + {placement} + + + + +
+ {#each placementOptions as option} + + {/each} +
+
+
+ + + +
+ + +
+ + {#snippet aboveMarks({ context })} + + {/snippet} + +
+
+ +

Vertical to point

+ + +
+ + {#snippet aboveMarks({ context })} + {#each annotations as annotation} + + + + {/each} + {/snippet} + +
+
+ +

Horizontal with range

+ + +
+ + {#snippet aboveMarks({ context })} + + + + {/snippet} + +
+
+ +

Bar chart

+ + +
+ + {#snippet aboveMarks({ context })} + + {/snippet} + +
+
diff --git a/packages/layerchart/src/routes/docs/components/AnnotationLine/+page.ts b/packages/layerchart/src/routes/docs/components/AnnotationLine/+page.ts new file mode 100644 index 000000000..2f58fc972 --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/AnnotationLine/+page.ts @@ -0,0 +1,22 @@ +import { parse } from '@layerstack/utils'; + +import api from '$lib/components/AnnotationLine.svelte?raw&sveld'; +import source from '$lib/components/AnnotationLine.svelte?raw'; +import pageSource from './+page.svelte?raw'; + +import type { AppleStockData } from '$static/data/examples/date/apple-stock.js'; + +export async function load({ fetch }) { + return { + appleStock: await fetch('/data/examples/date/apple-stock.json').then(async (r) => + parse(await r.text()) + ), + meta: { + api, + source, + pageSource, + supportedContexts: ['svg', 'canvas', 'html'], + related: ['components/AnnotationPoint', 'components/AnnotationRange'], + }, + }; +} diff --git a/packages/layerchart/src/routes/docs/components/AnnotationPoint/+page.svelte b/packages/layerchart/src/routes/docs/components/AnnotationPoint/+page.svelte new file mode 100644 index 000000000..ede4d92a5 --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/AnnotationPoint/+page.svelte @@ -0,0 +1,339 @@ + + +

Examples

+ +

On axis with tooltip

+ + +
+ + + {#snippet aboveContext({ context })} + + {#each annotations as annotation} + + {/each} + + {/snippet} + + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {#if data.annotation} + +
+ {data.annotation.details} +
+ {:else} + + {format(context.x(data), 'daytime')} + + + + {/if} + {/snippet} +
+ {/snippet} +
+
+
+ +

On series with tooltip

+ + +
+ + {#snippet aboveContext({ context })} + + {#each annotations as annotation} + + {/each} + + {/snippet} + + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {#if data.annotation} + +
+ {data.annotation.details} +
+ {:else} + + {format(context.x(data), 'daytime')} + + + + {/if} + {/snippet} +
+ {/snippet} +
+
+
+ +

On series with line and tooltip

+ + +
+ + {#snippet aboveContext({ context })} + + {#each annotations as annotation} + + + + {/each} + + {/snippet} + + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {#if data.annotation} + +
+ {data.annotation.details} +
+ {:else} + + {format(context.x(data), 'daytime')} + + + + {/if} + {/snippet} +
+ {/snippet} +
+
+
+ +

Series annotation

+ + +
+ + {#snippet aboveMarks({ context })} + {@const lastPoint = data.appleStock[data.appleStock.length - 1]} + + {/snippet} + +
+
+ +

Label placement

+ +
+ + + + {placement} + + + + +
+ {#each placementOptions as option} + + {/each} +
+
+
+ + + + +
+ + +
+ + {#snippet aboveMarks({ context })} + {@const maxPoint = data.appleStock[maxIndex(data.appleStock, (d) => d.value)]} + + {/snippet} + +
+
+ +

Band scale on axis

+ + +
+ + {#snippet aboveContext({ context })} + + + + {/snippet} + +
+
+ +

Band scale on value

+ + +
+ + {#snippet aboveContext({ context })} + + + + {/snippet} + +
+
diff --git a/packages/layerchart/src/routes/docs/components/AnnotationPoint/+page.ts b/packages/layerchart/src/routes/docs/components/AnnotationPoint/+page.ts new file mode 100644 index 000000000..40e902423 --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/AnnotationPoint/+page.ts @@ -0,0 +1,22 @@ +import { parse } from '@layerstack/utils'; + +import api from '$lib/components/AnnotationPoint.svelte?raw&sveld'; +import source from '$lib/components/AnnotationPoint.svelte?raw'; +import pageSource from './+page.svelte?raw'; + +import type { AppleStockData } from '$static/data/examples/date/apple-stock.js'; + +export async function load({ fetch }) { + return { + appleStock: await fetch('/data/examples/date/apple-stock.json').then(async (r) => + parse(await r.text()) + ), + meta: { + api, + source, + pageSource, + supportedContexts: ['svg', 'canvas', 'html'], + related: ['components/AnnotationLine', 'components/AnnotationRange'], + }, + }; +} diff --git a/packages/layerchart/src/routes/docs/components/AnnotationRange/+page.svelte b/packages/layerchart/src/routes/docs/components/AnnotationRange/+page.svelte new file mode 100644 index 000000000..5609afa3a --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/AnnotationRange/+page.svelte @@ -0,0 +1,344 @@ + + +

Examples

+ +

Horizontal with pattern, lower bound

+ + +
+ + {#snippet belowMarks({ context })} + + {/snippet} + +
+
+ +

Horizontal with pattern, upper bound

+ + +
+ + {#snippet belowMarks({ context })} + + {/snippet} + +
+
+ +

Horizontal with pattern, range

+ + +
+ + {#snippet belowMarks({ context })} + + {/snippet} + +
+
+ +

Horizontal with fill, multiple

+ + +
+ + {#snippet belowMarks({ context })} + + + + {/snippet} + +
+
+ +

Vertical with pattern, lower bound

+ + +
+ + {#snippet belowMarks({ context })} + + {/snippet} + +
+
+ +

Vertical with pattern, upper bound

+ + +
+ + {#snippet belowMarks({ context })} + + {/snippet} + +
+
+ +

Vertical with pattern, range

+ + +
+ + {#snippet belowMarks({ context })} + + {/snippet} + +
+
+ +

Vertical with gradient, range

+ + +
+ + {#snippet belowMarks({ context })} + + {/snippet} + +
+
+ +

Label placement

+ +
+ + + + {placement} + + + + +
+ {#each placementOptions as option} + + {/each} +
+
+
+ + + +
+ + +
+ + {#snippet aboveMarks({ context })} + + {/snippet} + +
+
+ +

Bar chart (single)

+ + +
+ + {#snippet belowMarks({ context })} + + {/snippet} + +
+
+ +

Bar chart (multiple)

+ + +
+ + {#snippet belowMarks({ context })} + + {/snippet} + +
+
+ +

Bar chart (value)

+ + +
+ + {#snippet belowMarks({ context })} + + {/snippet} + +
+
+ +

Hide tooltip

+ + +
+ + {#snippet aboveMarks({ context })} + { + e.stopPropagation(); + context.tooltip.hide(); + }, + }, + }} + /> + {/snippet} + +
+
diff --git a/packages/layerchart/src/routes/docs/components/AnnotationRange/+page.ts b/packages/layerchart/src/routes/docs/components/AnnotationRange/+page.ts new file mode 100644 index 000000000..3ba45b059 --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/AnnotationRange/+page.ts @@ -0,0 +1,22 @@ +import { parse } from '@layerstack/utils'; + +import api from '$lib/components/AnnotationRange.svelte?raw&sveld'; +import source from '$lib/components/AnnotationRange.svelte?raw'; +import pageSource from './+page.svelte?raw'; + +import type { AppleStockData } from '$static/data/examples/date/apple-stock.js'; + +export async function load({ fetch }) { + return { + appleStock: await fetch('/data/examples/date/apple-stock.json').then(async (r) => + parse(await r.text()) + ), + meta: { + api, + source, + pageSource, + supportedContexts: ['svg', 'canvas', 'html'], + related: ['components/AnnotationLine', 'components/AnnotationPoint'], + }, + }; +} diff --git a/packages/layerchart/src/routes/docs/components/Arc/+page.svelte b/packages/layerchart/src/routes/docs/components/Arc/+page.svelte index 8bf532f80..dd6daa05d 100644 --- a/packages/layerchart/src/routes/docs/components/Arc/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Arc/+page.svelte @@ -1,21 +1,63 @@ + +

Examples

+ +

Single value

+ + +
+ +
+
+ +

Single value gradient with text

+ + +
+ + {#snippet marks()} + + {#snippet children({ gradient })} + + + {#snippet children({ value })} + + {/snippet} + + + {/snippet} + + {/snippet} + +
+
+ +

Single value with custom color

+ + +
+ +
+
+ +

Single value (track size)

+ + +
+ +
+
+ +

Radius (offset)

+ + +
+ +
+
+ +

Radius (percentage)

+ + +
+ +
+
+ +

Radius (fixed)

+ + +
+ +
+
+ +

Series data

+ + +
+ +
+
+ +

Series data (arc)

+ + +
+ ({ key: d.fruit, data: [d] }))} + range={[-90, 90]} + outerRadius={-25} + innerRadius={-20} + cornerRadius={10} + props={{ group: { y: 70 } }} + {renderContext} + {debug} + /> +
+
+ +

Series data (90° starting angle)

+ + +
+ +
+
+ +

Series data (180° starting angle)

+ + +
+ +
+
+ +

Series data (track color)

+ + +
+ ({ key: d.fruit, data: [d] }))} + props={{ + arc: { + track: { fill: 'var(--color-surface-content)', fillOpacity: 0.1 }, + }, + }} + outerRadius={-25} + innerRadius={-20} + cornerRadius={10} + {renderContext} + {debug} + /> +
+
+ +

Series data (individual tracks, max value, and color)

+ + +
+ { + return { + key: d.key, + data: [d], + maxValue: d.maxValue, + color: d.color, + }; + })} + outerRadius={-25} + innerRadius={-20} + cornerRadius={10} + {renderContext} + {debug} + /> +
+
+ +

Series data (labels)

+ + +
+ { + return { + key: d.key, + data: [d], + maxValue: d.maxValue, + color: d.color, + }; + })} + outerRadius={-25} + innerRadius={-20} + cornerRadius={10} + {renderContext} + {debug} + > + {#snippet arc({ props, seriesIndex, visibleSeries })} + + {#snippet children({ getArcTextProps })} + + {/snippet} + + {/snippet} + +
+
+ + +
+

Motion (tween)

+ + + +
+ +
+ {#if show} + { + return { + key: d.key, + data: [d], + maxValue: d.maxValue, + color: d.color, + }; + })} + props={{ arc: { motion: 'tween' } }} + outerRadius={-25} + innerRadius={-20} + cornerRadius={10} + {renderContext} + {debug} + /> + {/if} +
+
+
+ + +
+

Motion (spring)

+ + + +
+ +
+ {#if show} + { + return { + key: d.key, + data: [d], + maxValue: d.maxValue, + color: d.color, + }; + })} + props={{ arc: { motion: 'spring' } }} + outerRadius={-25} + innerRadius={-20} + cornerRadius={10} + {renderContext} + {debug} + /> + {/if} +
+
+
+ + diff --git a/packages/layerchart/src/routes/docs/components/ArcChart/+page.ts b/packages/layerchart/src/routes/docs/components/ArcChart/+page.ts new file mode 100644 index 000000000..ea872ac2a --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/ArcChart/+page.ts @@ -0,0 +1,16 @@ +import api from '$lib/components/charts/PieChart.svelte?raw&sveld'; +import source from '$lib/components/charts/PieChart.svelte?raw'; +import pageSource from './+page.svelte?raw'; + +export async function load() { + return { + meta: { + api, + source, + pageSource, + description: 'Streamlined Chart configuration for Pie charts', + supportedContexts: ['svg', 'canvas'], + related: ['components/Chart', 'components/Pie', 'examples/Arc'], + }, + }; +} diff --git a/packages/layerchart/src/routes/docs/components/Area/+page.svelte b/packages/layerchart/src/routes/docs/components/Area/+page.svelte index fb874c551..e365f5045 100644 --- a/packages/layerchart/src/routes/docs/components/Area/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Area/+page.svelte @@ -1,29 +1,32 @@

Playground

@@ -41,18 +44,11 @@
-
+
- - - Svg - Canvas - - - @@ -60,78 +56,24 @@
-
- - +
+ + - - - {#if show} {#if showPoints} - - {/if} - {/if} - - -
- - -

Canvas

- -
-
- - - - - - - - - -
- -
- - - - - - - -
-
- - -
- - - - - - - - {#if show} - - - {#if showLine} - - {/if} - - {#if showPoints} - + {/if} {/if} - +
diff --git a/packages/layerchart/src/routes/docs/components/Area/+page.ts b/packages/layerchart/src/routes/docs/components/Area/+page.ts index 5a882c212..74d6b03be 100644 --- a/packages/layerchart/src/routes/docs/components/Area/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Area/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/Area'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte b/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte index 163c49906..b726efe71 100644 --- a/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte @@ -1,49 +1,66 @@ { + onkeydown={(e) => { if (e.metaKey) { lockedTooltip = true; } }} - on:keyup={(e) => { + onkeyup={(e) => { if (!e.metaKey) { lockedTooltip = false; } @@ -123,36 +166,33 @@

Examples

-
- - - Svg - Canvas - - - - - - -
-

Basic

-
+
+

Default series label

+ + +
+ +
+
+

Gradient

-
+
- - - + {#snippet marks()} + + {#snippet children({ gradient })} + + {/snippet} - + {/snippet}
@@ -160,15 +200,16 @@

Threshold Gradient

{@const colors = { - positive: 'hsl(var(--color-success))', - negative: 'hsl(var(--color-danger))', + positive: 'var(--color-success)', + negative: 'var(--color-danger)', }} -
+
- + {#snippet marks({ context })} {@const thresholdValue = 0} - {@const thresholdOffset = yScale(thresholdValue) / (height + padding.bottom)} + {@const thresholdOffset = + context.yScale(thresholdValue) / (context.height + context.padding.bottom)} - thresholdValue} - line={{ stroke: gradient }} - fill={gradient} - fillOpacity={0.2} - /> + {#snippet children({ gradient })} + thresholdValue} + line={{ stroke: gradient }} + fill={gradient} + fillOpacity={0.2} + /> + {/snippet} - + {/snippet} - - {@const value = tooltip.data && y(tooltip.data)} + {#snippet highlight({ context })} + {@const value = context.tooltip?.data && context.y(context.tooltip?.data)} - - - - - {@const value = y(data)} - {format(x(data), PeriodType.Day)} - - - + {/snippet} + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {@const value = context.y(data)} + {format(context.x(data), 'day')} + + + + {/snippet} - + {/snippet}
@@ -212,7 +255,7 @@

Curve

-
+
Series -
+
Series (separate data) -
+
Series (point click) -
+
{ + onPointClick={(e, detail) => { console.log(e, detail); alert(JSON.stringify(detail)); }} @@ -300,10 +343,10 @@
-

Series (voronoi tooltip with highlight)

+

Series (individual tooltip with highlight)

-
+
- + {#snippet marks({ series, context })} {#each series as s} - {@const color = - tooltip.data == null || tooltip.data.fruit === s.key - ? s.color - : 'hsl(var(--color-surface-content) / 20%)'} + {@const activeSeries = + context.tooltip?.data == null || context.tooltip?.data?.fruit === s.key} - + + + {/each} - + {/snippet} - - - {@const activeSeries = [...series].find((s) => s.key === tooltip.data?.fruit)} + {#snippet highlight({ series, context })} + {@const activeSeries = series.find((s) => s.key === context.tooltip?.data?.fruit)} - - - - - {@const activeSeries = [...series].find((s) => s.key === tooltip.data?.fruit)} - - {format(x(data))} - - - + {/snippet} + + {#snippet tooltip({ series, context })} + {@const activeSeries = series.find((s) => s.key === context.tooltip?.data?.fruit)} + + {#snippet children({ data })} + {format(context.x(data))} + + + + {/snippet} - + {/snippet}
@@ -362,19 +405,19 @@

Stack series

-
+
Stack series (expand) -
+
Stack series (diverging) -
+
-d.apples, color: 'hsl(var(--color-danger))' }, + { key: 'apples', value: (d) => -d.apples, color: 'var(--color-danger)' }, { key: 'bananas', - color: 'hsl(var(--color-success))', + color: 'var(--color-success)', }, { key: 'oranges', - color: 'hsl(var(--color-warning))', + color: 'var(--color-warning)', }, ]} seriesLayout="stackDiverging" @@ -437,37 +480,124 @@

Stack series with gradient

-
+
- + {#snippet marks({ series, getAreaProps })} {#each series as s, i (s.key)} - + {#snippet children({ gradient })} + + {/snippet} {/each} - + {/snippet} + +
+ + +

Stack series (separate data)

+ + +
+ +
+
+ +
+

Threshold

+ +
+ + +
+ + {#snippet marks()} + + {#snippet above({ curve })} + + {/snippet} + + {#snippet below({ curve })} + + {/snippet} + + {#snippet children({ curve })} + + + {/snippet} + + {/snippet} + + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {format(data.date)} + + + + + + + {/snippet} + + {/snippet}
@@ -475,7 +605,7 @@

Labels

-
+
@@ -483,7 +613,7 @@

Points

-
+
@@ -491,7 +621,7 @@

Radial

-
+
v + '° F' }, highlight: { points: false }, + tooltip: { + context: { mode: 'bisect-x' }, + }, }} series={[ { key: 'min_max', label: 'min/max', value: ['min', 'max'], - color: 'hsl(var(--color-primary) / 20%)', + color: 'var(--color-primary)', + props: { opacity: 0.2, line: { opacity: 0.2 } }, }, { key: 'minmin_maxmax', label: 'minmin/maxmax', value: ['minmin', 'maxmax'], - color: 'hsl(var(--color-primary) / 20%)', + color: 'var(--color-primary)', + props: { opacity: 0.2, line: { opacity: 0.2 } }, }, ]} {renderContext} {debug} > - + {#snippet belowMarks()} - + {/snippet}
@@ -532,7 +667,7 @@

Funnel

-
+
- - {@const segmentWidth = width / (funnelSegments.length - 1)} + {#snippet marks({ context })} + {@const segmentWidth = context.width / (funnelSegments.length - 1)} {@const areas = [ { padding: 0, opacity: 1 }, { padding: 10, opacity: 0.2 }, { padding: 20, opacity: 0.1 }, ]} - - {#each areas as a} - d.value + a.padding} - y1={(d) => -(d.value + a.padding)} - fill={gradient} - curve={curveBasis} - /> - {/each} + + {#snippet children({ gradient })} + {#each areas as a} + d.value + a.padding} + y1={(d) => -(d.value + a.padding)} + fill={gradient} + curve={curveBasis} + /> + {/each} + {/snippet} - {#each funnelSegments as s} + {#each funnelSegments.slice(0, -1) as s} {/each} - + {/snippet}
@@ -588,7 +725,7 @@

Null gaps

-
+
@@ -615,7 +752,7 @@

Single axis (x)

-
+
@@ -623,7 +760,7 @@

Single axis (y)

-
+
@@ -631,19 +768,19 @@

Legend

-
+
Legend (placement) -
+
Legend (custom labels) -
+
Tooltip click -
+
{ + onTooltipClick={(e, detail) => { console.log(e, detail); alert(JSON.stringify(detail)); }} @@ -729,12 +866,12 @@

Markers

-
+
{ + onTooltipClick={(e, detail) => { if (markerPoints.includes(detail.data)) { markerPoints = markerPoints.filter((d) => d !== detail.data); } else { @@ -744,30 +881,30 @@ {renderContext} {debug} > - + {#snippet aboveMarks({ context })} {#each markerPoints as p} {/each} - + {/snippet} - + {#snippet aboveContext({ context })} {#each markerPoints as p} {/each} - + {/snippet}
@@ -788,31 +925,33 @@

Custom tooltip

-
+
- + {#snippet tooltip({ context })} - {y(data)} + {#snippet children({ data })} + {context.y(data)} + {/snippet} - {format(x(data), PeriodType.Day)} + {#snippet children({ data })} + {format(context.x(data), 'day')} + {/snippet} - + {/snippet}
@@ -820,64 +959,66 @@

Locking and clickable tooltip

-
+
- - - - {format(x(data), PeriodType.Day)} - - - - {#each series as s} - {@const valueAccessor = accessor(s.value ?? s.key)} - {@const value = Math.abs(valueAccessor(data))} - setHighlightSeriesKey(s.key)} - onpointerleave={() => setHighlightSeriesKey(null)} - > - {format(value)} - - - - {/each} - - - - -
Lock position with and view console
+ {format(value)} + + + + {/each} + + + + +
Lock position with and view console
+ {/snippet}
-
+ {/snippet}
@@ -885,41 +1026,43 @@

Fixed tooltip below chart with hide delay

-
+
- - - - {format(x(data), PeriodType.Day)} - - - - {#each series as s} - {@const valueAccessor = accessor(s.value ?? s.key)} - {@const value = valueAccessor(data)} - setHighlightSeriesKey(s.key)} - onpointerleave={() => setHighlightSeriesKey(null)} - > - {format(value)} - - {/each} - + {#snippet tooltip({ context, setHighlightKey, series })} + + {#snippet children({ data })} + + {format(context.x(data), 'day')} + + + + {#each series as s} + {@const valueAccessor = accessor(s.value ?? s.key)} + {@const value = valueAccessor(data)} + setHighlightKey(s.key)} + onpointerleave={() => setHighlightKey(null)} + > + {format(value)} + + {/each} + + {/snippet} - + {/snippet}
@@ -928,83 +1071,204 @@
- {#if $tooltipContext?.data} - date: {format($tooltipContext?.data?.date, PeriodType.Day, { variant: 'short' })} - value: {$tooltipContext?.data?.value} + {#if context && context.tooltip.data} + date: {format(context.tooltip.data.date, 'day', { variant: 'short' })} + value: {context.tooltip.data.value} {:else} [hover chart] {/if}
-
+
- +
+ {data.annotation.details} +
+ {:else} + + {format(context.x(data), 'day')} + + + + {/if} + {/snippet} + + {/snippet} + +
+
+ +
+ See also: AnnotationPoint for more examples +
+ +

Line annotation

+ + +
+ +
+
+ +
+ See also: AnnotationLine for more examples +
+ +

Range annotation

+ + +
+ +
+
+ +
+ See also: AnnotationRange for more examples +
+ +

Series point annotations

+ + + {@const series = [ + { + key: 'apples', + data: multiSeriesDataByFruit.get('apples'), + color: 'var(--color-danger)', + }, + { + key: 'bananas', + data: multiSeriesDataByFruit.get('bananas'), + color: 'var(--color-success)', + }, + { + key: 'oranges', + data: multiSeriesDataByFruit.get('oranges'), + color: 'var(--color-warning)', + }, + ]} +
+ { + const lastDataPoint = s.data?.[s.data.length - 1] ?? null; + return { + type: 'point', + seriesKey: s.key, + label: s.key, + labelPlacement: 'right', + labelXOffset: 4, + x: lastDataPoint.date, + y: lastDataPoint.value, + props: { + circle: { fill: s.color }, + label: { fill: s.color }, + }, + }; + })} + padding={{ ...defaultChartPadding(), right: 60 }} + {renderContext} + {debug} + />
-
--> + + +
+ See also: AnnotationPoint for more examples +

Brushing

-
+
-
+
(xDomain = e.xDomain) }} + brush={{ onBrushEnd: (e) => (xDomain = e.xDomain) }} props={{ - area: { tweened: { duration: 200 } }, - xAxis: { format: undefined, tweened: { duration: 200 } }, + area: { motion: { type: 'tween', duration: 200 } }, + xAxis: { motion: { type: 'tween', duration: 200 }, tickMultiline: true }, }} {renderContext} {debug} />
-
+
(xDomain = e.xDomain) }} + brush={{ onBrushEnd: (e) => (xDomain = e.xDomain) }} props={{ - area: { tweened: { duration: 200 } }, - xAxis: { format: undefined, tweened: { duration: 200 } }, + area: { motion: { type: 'tween', duration: 200 } }, + xAxis: { motion: { type: 'tween', duration: 200 }, tickMultiline: true }, }} {renderContext} {debug} @@ -1056,28 +1320,62 @@
+

Point scale

+ + +
+ d.year === 2019)} + xScale={scalePoint()} + x="fruit" + y="value" + {renderContext} + {debug} + /> +
+
+ +

Band scale

+ + +
+ d.year === 2019)} + xScale={scaleBand()} + x="fruit" + y="value" + tooltip={{ + mode: 'band', + debug, + }} + {renderContext} + {debug} + /> +
+
+

Custom chart

-
- - - - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - - - - - {format(x(data), PeriodType.DayTime)} - - - - +
+ + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {format(context.x(data), 'daytime')} + + + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/components/AreaChart/+page.ts b/packages/layerchart/src/routes/docs/components/AreaChart/+page.ts index e152576a2..e3fffd5fa 100644 --- a/packages/layerchart/src/routes/docs/components/AreaChart/+page.ts +++ b/packages/layerchart/src/routes/docs/components/AreaChart/+page.ts @@ -9,7 +9,7 @@ import type { AppleStockData } from '$static/data/examples/date/apple-stock.js'; import { ascending, flatGroup, max, mean, min } from 'd3-array'; import { celsiusToFahrenheit } from '$lib/utils/math.js'; -export async function load() { +export async function load({ fetch }) { return { appleStock: await fetch('/data/examples/date/apple-stock.json').then(async (r) => parse(await r.text()) @@ -54,6 +54,7 @@ export async function load() { source, pageSource, description: 'Streamlined Chart configuration for Area charts', + supportedContexts: ['svg', 'canvas'], related: ['components/Chart', 'components/Area', 'examples/Area'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Axis/+page.svelte b/packages/layerchart/src/routes/docs/components/Axis/+page.svelte index 61315ede4..405bbeebf 100644 --- a/packages/layerchart/src/routes/docs/components/Axis/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Axis/+page.svelte @@ -1,171 +1,249 @@

Examples

left / bottom placement

- -
+ +
- + - +

top / right placement

- -
+ +
- + - +

rule (left/bottom placement)

- -
+ +
- + - +

rule (top/right placement)

- -
+ +
- + - +

grid

- -
+ +
- + - +

dashed grid lines

- -
+ +
- + - +

multiple axis grids with single rule

- -
+ +
- + - +
@@ -174,22 +252,19 @@

multiple axis grids and rules

- -
+ +
- + - - + +
@@ -198,61 +273,47 @@

multiple axis grids and rules (separate grid)

- -
+ +
- + - +

Arrow markers

- -
+ +
- + - +

tick label styling

- -
- - + +
+ + - - +

rotated tick labels and styling

- -
- - + +
+ + - - +

Remove ticks hashes

- -
- - - - - + +
+ + + +

show only first/last ticks with alignment

- -
+ +
- + scale.domain()} - format={(d) => format(d, PeriodType.Day, { variant: 'long' })} + format={{ type: 'day', options: { variant: 'long' } }} > - - - + {#snippet tickLabel({ props, index })} + + {/snippet} scale.domain()} /> - - -
-
- -

integer-only ticks

- - -
- - - - scale.ticks?.().filter(Number.isInteger)} - format="integer" - /> - +
-

explicit ticks

+

integer-only ticks via filter

- -
- - - - - + +
+ + + scale.ticks?.().filter(Number.isInteger)} /> +
-

Inject tick value

+

integer-only ticks via format

- -
- - - - [45, ...scale.ticks?.()]} /> - + +
+ + + +
-

tick count

+

explicit ticks

- -
- - - - - + +
+ + + +
-

tick count (responsive)

+

Inject tick value

- -
- - - - - + +
+ + + [45, ...(scale.ticks?.() ?? [])]} /> +
-

tick time interval

+

tick count

- -
- - - - - + +
+ + + +
-

remove default tick count

+

tick spacing

- -
- - - - - + +
+ + + +

label next to hash

- -
- - + +
+ + scale.ticks?.().slice(0, -1)} tickLength={22} /> - - +

Hide `0` tick via ticks

- -
- - - - scale.ticks?.().filter(isNotZero)} /> - + +
+ + + scale.ticks?.().filter((d) => d !== 0)} /> +

Hide `0` tick via format

- -
- - - - v || ''} /> - + +
+ + + v || ''} /> +
@@ -572,51 +502,28 @@

Override axis ticks with custom scale

-
- - +
+ + scaleTime(scale.domain(), scale.range()).ticks()} - format={PeriodType.Day} + ticks={(scale) => scaleTime(scale.domain(), scale.range()).ticks(scale.range()[1] / 80)} /> - - +
-
-

Axis label placements (top/bottom)

+

Axis label placements (top/bottom)

-
- - - -
-
- - -
+ +
- + {#if debug} @@ -629,33 +536,17 @@ - +
-
-

Axis label placements (left/right)

- -
- - - -
-
+

Axis label placements (left/right)

- -
- - + +
+ + {#if debug} @@ -668,104 +559,375 @@ - +
-
-

Multiple axis with same placement

- -
- - - -
-
+

Multiple time axis with same placement (bottom)

- -
- - + +
+ + {#if debug} {/if} 'Q' + (d.getMonth() / 3 + 1)} rule - labelProps={{ dx: -60 }} /> x * (9 / 5) + 32)} - placement="right" + placement="bottom" + ticks={{ interval: timeYear.every(1) }} + tickLength={0} + tickLabelProps={{ dy: 20, class: 'text-sm' }} + /> + + +
+ + +

Multiline labels with format (\n)

+ + +
+ + + {#if debug} + + + {/if} + + + 'Q' + (d.getMonth() / 3 + 1) + (d.getMonth() === 0 ? '\n' + d.getFullYear() : '')} rule - class="translate-x-[50px]" - labelProps={{ dx: -50 }} /> - + + +
+
+ +

Multiple different axis with same placement (right)

+ + +
+ + {#snippet children({ context })} + + {#if debug} + + + {/if} + + + x * (9 / 5) + 32)} + placement="right" + rule + class="translate-x-[50px]" + labelProps={{ dx: -50 }} + /> + + {/snippet}

radial rule

- -
- - + +
+ + - scale.ticks?.().splice(1)} /> - + +

radial grid

- -
- - + +
+ + - scale.ticks?.().splice(1)} /> - + +

Log scale

- -
- - + +
+ + - - +
+ +
+

Time scale (auto)

+ +
+ + +
+ {#each timeScaleExamples as example} +
+
{example.label}
+
+ + + + + +
+
+ {/each} +
+
+ +
+

Time scale (auto) with multiline ticks

+ +
+ + +
+ {#each timeScaleExamples as example} +
+
{example.label}
+
+ + + + + + +
+
+ {/each} +
+
+ +
+

Time scale (auto) with format (filtering)

+ +
+ + +
+ {#each timeScaleExamples as example} +
+
{example.label}
+
+ + + + + +
+
+ {/each} +
+
+ +

Time scale (explicit intervals)

+ + +
+ {#each timeScaleExamples as example} +
+
{example.label}
+
+ + + + + +
+
+ {/each} +
+
+ +

Time scale (explicit intervals) with multiline ticks

+ + +
+ {#each timeScaleExamples as example} +
+
{example.label}
+
+ + + + + +
+
+ {/each} +
+
+ +
+

Time scale (brush)

+ +
+ + +
+
+ { + xDomain = asAny(e.xDomain); + }, + }} + > + + + + + +
+ +
+ { + xDomain = asAny(e.xDomain); + }, + }} + > + + + + +
+
+
+ +
+

Time scale (brush) with multiline ticks

+ +
+ + +
+
+ { + xDomain = asAny(e.xDomain); + }, + }} + > + + + + + +
+ +
+ { + xDomain = asAny(e.xDomain); + }, + }} + > + + + + +
+
+
diff --git a/packages/layerchart/src/routes/docs/components/Axis/+page.ts b/packages/layerchart/src/routes/docs/components/Axis/+page.ts index 17597df7b..fd09df389 100644 --- a/packages/layerchart/src/routes/docs/components/Axis/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Axis/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas', 'html'], related: ['components/Grid', 'components/Rule'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Bar/+page.svelte b/packages/layerchart/src/routes/docs/components/Bar/+page.svelte index 87857a3c4..06a474ed1 100644 --- a/packages/layerchart/src/routes/docs/components/Bar/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Bar/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/Bar/+page.ts b/packages/layerchart/src/routes/docs/components/Bar/+page.ts index 6f60ad2a9..77605f7fa 100644 --- a/packages/layerchart/src/routes/docs/components/Bar/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Bar/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['components/Bars', 'examples/Bars', 'examples/Columns'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/BarChart/+page.svelte b/packages/layerchart/src/routes/docs/components/BarChart/+page.svelte index a14aa194e..657ac9e92 100644 --- a/packages/layerchart/src/routes/docs/components/BarChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/BarChart/+page.svelte @@ -1,26 +1,31 @@

Examples

-
- - - Svg - Canvas - - - - - - -
-

Vertical (default)

-
+
@@ -80,7 +120,7 @@

Horizontal

-
+
+

Time scale / interval

+ + +
+ +
+
+ +

Time scale / interval (horizontal)

+ + +
+ +
+
+ +

Time scale / interval with inset

+ + +
+ +
+
+

Color (Bars class)

-
+
Color using scale -
+
@@ -125,17 +212,17 @@

Color per value

-
+
d.year === 2019)} x="fruit" y="value" c="fruit" cRange={[ - 'hsl(var(--color-danger))', - 'hsl(var(--color-warning))', - 'hsl(var(--color-success))', - 'hsl(var(--color-info))', + 'var(--color-danger)', + 'var(--color-warning)', + 'var(--color-success)', + 'var(--color-info)', ]} props={{ yAxis: { format: 'metric' }, @@ -149,7 +236,7 @@

Color threshold

-
+
@@ -167,20 +254,17 @@

Gradient

-
+
- + {#snippet marks({ series, getBarsProps })} {#each series as s, i (s.key)} - - + + {#snippet children({ gradient })} + + {/snippet} {/each} - + {/snippet}
@@ -188,7 +272,7 @@

Remove rounding

-
+
+

Duration

+ + +
+ + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {format(context.y(data))} + + + + + {/snippet} + + {/snippet} + +
+
+

Highlight below marks

-
+
- + {#snippet belowMarks()} - + {/snippet}
@@ -222,15 +353,15 @@

Series

-
+
Series (horizontal) -
+
Series data -
+
Series (diverging) -
+
-d.baseline, - color: 'hsl(var(--color-secondary))', + color: 'var(--color-secondary)', }, ]} {renderContext} @@ -312,7 +444,7 @@ {@const totalPopulation = sum(data.worldPopulationDemographics, (d) => d.male + d.female)} -
+
-d.male, - color: 'hsl(var(--color-primary))', + color: 'var(--color-primary)', }, { key: 'female', - color: 'hsl(var(--color-secondary))', + color: 'var(--color-secondary)', }, ]} {renderContext} {debug} > - - - Age: {format(y(data))} - - {#each series as s} - {@const valueAccessor = accessor(s.value ?? s.key)} - {@const value = Math.abs(valueAccessor(data))} - - {format(value)} - ({format(value / totalPopulation, 'percent')}) - - {/each} - + {#snippet tooltip({ context, series })} + + {#snippet children({ data })} + Age: {format(context.y(data))} + + {#each series as s} + {@const valueAccessor = accessor(s.value ?? s.key)} + {@const value = Math.abs(valueAccessor(data))} + + {format(value)} + ({format(value / totalPopulation, 'percent')}) + + {/each} + + {/snippet} - + {/snippet}
@@ -367,7 +501,7 @@ {@const totalPopulation = sum(data.worldPopulationDemographics, (d) => d.male + d.female)} -
+
-d.male / totalPopulation, - color: 'hsl(var(--color-primary))', + color: 'var(--color-primary)', }, { key: 'female', value: (d) => d.female / totalPopulation, - color: 'hsl(var(--color-secondary))', + color: 'var(--color-secondary)', }, ]} {renderContext} {debug} > - - - Age: {format(y(data))} - - {#each series as s} - {@const valueAccessor = accessor(s.value ?? s.key)} - {@const value = Math.abs(valueAccessor(data))} - - {format(value * totalPopulation)} - ({format(value, 'percent')}) - - {/each} - + {#snippet tooltip({ series, context })} + + {#snippet children({ data })} + Age: {format(context.y(data))} + + {#each series as s} + {@const valueAccessor = accessor(s.value ?? s.key)} + {@const value = Math.abs(valueAccessor(data))} + + {format(value * totalPopulation)} + ({format(value, 'percent')}) + + {/each} + + {/snippet} - + {/snippet}
@@ -420,23 +556,23 @@

Group series

-
+
+

Group series (labels)

+ + +
+ + + {#snippet aboveMarks({ context, visibleSeries })} + {#each visibleSeries as s} + {#each wideData as d} + {@const valueAccessor = accessor(s.key)} + {@const value = valueAccessor(d)} + + {/each} + {/each} + {/snippet} + +
+
+

Group series (horizontal)

-
+
Group series (bar click) -
+
{ + onBarClick={(e, detail) => { console.log(e, detail); alert(JSON.stringify(detail)); }} @@ -536,26 +727,26 @@

Group series (series / long data)

-
+
Stack series -
+
Stack series (horizontal) -
+
Stack series (padded) -
+
Stack series (expand) -
+
Stack series (diverging) -
+
-d.apples, - color: 'hsl(var(--color-danger))', + color: 'var(--color-danger)', props: { rounded: 'bottom' }, }, { key: 'bananas', - color: 'hsl(var(--color-warning))', + color: 'var(--color-warning)', }, { key: 'cherries', - color: 'hsl(var(--color-success))', + color: 'var(--color-success)', }, { key: 'grapes', - color: 'hsl(var(--color-info))', + color: 'var(--color-info)', }, ]} seriesLayout="stackDiverging" @@ -764,26 +955,26 @@

Stack series (series data / long data)

-
+
Legend (group series) -
+
Legend (stack series) -
+
Legend (placement) -
+
Legend (custom labels) -
+
Labels -
+
@@ -961,7 +1152,7 @@

Labels (inside placement)

-
+
Axis labels inside bars -
+
Axis labels inside bars (using Labels) -
+
- - d.date} class="text-sm fill-surface-300 stroke-none" /> - + {#snippet aboveMarks()} + 0} value="date" class="text-sm fill-surface-300 stroke-none" /> + {/snippet}
@@ -1052,11 +1243,7 @@ c="value" cScale={scaleThreshold()} cDomain={[10, 50]} - cRange={[ - 'hsl(var(--color-danger))', - 'hsl(var(--color-warning))', - 'hsl(var(--color-success))', - ]} + cRange={['var(--color-danger)', 'var(--color-warning)', 'var(--color-success)']} axis="x" bandPadding={0.1} props={{ @@ -1068,14 +1255,114 @@ {renderContext} {debug} > - - - {format(x(data))} - - - + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {format(context.x(data))} + + + + {/snippet} + + {/snippet} + +
+
+ +

Single stack with indicator

+ + +
+ 1} + xBaseline={undefined} + xNice={false} + c="label" + cRange={[ + 'var(--color-blue-500)', + 'var(--color-blue-400)', + 'var(--color-teal-500)', + 'var(--color-yellow-500)', + 'var(--color-orange-500)', + 'var(--color-red-500)', + ]} + bandPadding={0} + padding={{ top: 12, bottom: 12 }} + orientation="horizontal" + props={{ + tooltip: { + context: { mode: 'bounds' }, + }, + }} + {renderContext} + {debug} + > + {#snippet axis({ context })} + + {#snippet tickLabel({ props })} + + {/snippet} + + {/snippet} + + {#snippet aboveMarks({ context })} + + {/snippet} + + {#snippet tooltip({ context })} + + {#snippet children({ data })} + + + + + {/snippet} - + {/snippet}
@@ -1083,7 +1370,7 @@

Single axis (x)

-
+
@@ -1091,7 +1378,7 @@

Single axis (y)

-
+
@@ -1099,7 +1386,7 @@

Override axis ticks with custom scale

-
+
Both axis grid -
+
@@ -1122,7 +1409,7 @@

Both axis grid (align between)

-
+
Scale override -
+
+

Brushing

+ + +
+ +
+
+ +
+ Brushing is a work in progress and only supports time/interval scales and does not support band + scales yet. +
+ +

Radial (vertical)

+ + +
+ +
+
+ +

Radial (vertical) - yRange

+ + +
+ [height / 5, height / 2]} + radial + {renderContext} + {debug} + /> +
+
+ +

Radial (vertical) - arcPadding

+ + +
+ [height / 5, height / 2]} + radial + props={{ bars: { padAngle: 0.1 } }} + {renderContext} + {debug} + /> +
+
+ +

Radial (horizontal)

+ + +
+ [height / 5, height / 2]} + radial + orientation="horizontal" + {renderContext} + {debug} + /> +
+
+ +

Radial (horizontal) - color per value

+ + +
+ [height / 5, height / 2]} + c="browser" + cRange={[ + 'var(--color-success)', + 'var(--color-danger)', + 'var(--color-warning)', + 'var(--color-info)', + 'var(--color-secondary)', + ]} + radial + orientation="horizontal" + {renderContext} + {debug} + /> +
+
+ +

Radial (horizontal) - grid between

+ + +
+ [height / 5, height / 2]} + c="browser" + cRange={[ + 'var(--color-success)', + 'var(--color-danger)', + 'var(--color-warning)', + 'var(--color-info)', + 'var(--color-secondary)', + ]} + radial + orientation="horizontal" + grid={{ bandAlign: 'between' }} + {renderContext} + {debug} + /> +
+
+ +

Radial (horizontal) - duration

+ + +
+ [height / 5, height / 2]} + c="category" + cRange={[ + 'var(--color-success)', + 'var(--color-danger)', + 'var(--color-warning)', + 'var(--color-info)', + 'var(--color-secondary)', + ]} + radial + orientation="horizontal" + props={{ + xAxis: { + format: 'month', + }, + grid: { bandAlign: 'between' }, + tooltip: { + context: { mode: 'bounds' }, + }, + }} + padding={{ top: 10, bottom: 10 }} + {renderContext} + {debug} + > + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {format(context.y(data))} + + + + + {/snippet} + + {/snippet} + +
+
+ +

Radial weather

+ + + {@const avgExtents = extent(data.sfoTemperatures, (d) => d.avg)} +
+ [height / 5, height / 2]} + c="avg" + cScale={scaleLinear()} + cDomain={quantize(interpolate(avgExtents[0], asAny(avgExtents[1])), 7)} + cRange={quantize(interpolateSpectral, 7).reverse()} + radial + props={{ + xAxis: { ticks: { interval: timeMonth.every(3) } }, + yAxis: { ticks: 4, format: (v) => v + '° F' }, + grid: { xTicks: 12 }, + }} + {renderContext} + {debug} + /> +
+
+

Tooltip click

-
+
{ + onTooltipClick={(e, detail) => { console.log(e, detail); alert(JSON.stringify(detail)); }} @@ -1173,47 +1679,202 @@

Custom tooltip

-
+
- - - {format(x(data), PeriodType.DayTime)} - - - + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {format(context.x(data), 'daytime')} + + + + {/snippet} - + {/snippet}
+

Point annotation

+ + +
+ +
+
+ +
+ See also: AnnotationPoint for more examples +
+ +

Line annotation

+ + +
+ d.value), + label: 'Avg', + props: { + line: { class: '[stroke-dasharray:2,2] stroke-danger' }, + label: { class: 'fill-danger' }, + }, + }, + ]} + {renderContext} + {debug} + /> +
+
+ +
+ See also: AnnotationLine for more examples +
+ +

Range annotation (single)

+ + +
+ +
+
+ +
+ See also: AnnotationRange for more examples +
+ +

Range annotation (multiple)

+ + +
+ +
+
+ +

Range annotation (value)

+ + +
+ +
+
+ +
+ See also: AnnotationRange for more examples +
+

Custom chart

-
- - - format(value, undefined, { variant: 'short' })} - /> - format(value, undefined, { variant: 'short' })} - /> - - - - - - {format(x(data))} - - - - +
+ + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {format(context.x(data))} + + + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/components/BarChart/+page.ts b/packages/layerchart/src/routes/docs/components/BarChart/+page.ts index 4819e4651..94e0b68a4 100644 --- a/packages/layerchart/src/routes/docs/components/BarChart/+page.ts +++ b/packages/layerchart/src/routes/docs/components/BarChart/+page.ts @@ -5,17 +5,38 @@ import source from '$lib/components/charts/BarChart.svelte?raw'; import pageSource from './+page.svelte?raw'; import type { WorldPopulationDemographicsData } from '$static/data/examples/world-population-demographics.js'; +import { ascending, flatGroup, max, mean, min } from 'd3-array'; -export async function load() { +export async function load({ fetch }) { return { worldPopulationDemographics: (await fetch( '/data/examples/world-population-demographics.csv' ).then(async (r) => csvParse(await r.text(), autoType))) as WorldPopulationDemographicsData, + sfoTemperatures: await fetch('/data/examples/sfoTemperatures.csv').then(async (r) => { + return flatGroup( + csvParse<{ date: Date; tavg: number; tmax: number; tmin: number }>( + await r.text(), + // @ts-expect-error + autoType + ), + (d) => new Date(Date.UTC(2000, d.date.getUTCMonth(), d.date.getUTCDate())) // group by day of year + ) + .sort(([a], [b]) => ascending(a, b)) // sort chronologically + .map(([date, v]) => ({ + date, + avg: mean(v, (d) => d.tavg || NaN), + min: mean(v, (d) => d.tmin || NaN), + max: mean(v, (d) => d.tmax || NaN), + minmin: min(v, (d) => d.tmin || NaN), + maxmax: max(v, (d) => d.tmax || NaN), + })); + }), meta: { api, source, pageSource, description: 'Streamlined Chart configuration for Bar charts', + supportedContexts: ['svg', 'canvas'], related: [ 'components/Chart', 'components/Bars', diff --git a/packages/layerchart/src/routes/docs/components/Bars/+page.svelte b/packages/layerchart/src/routes/docs/components/Bars/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/Bars/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Bars/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/Bars/+page.ts b/packages/layerchart/src/routes/docs/components/Bars/+page.ts index f979343fe..1c00d58cd 100644 --- a/packages/layerchart/src/routes/docs/components/Bars/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Bars/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['components/Bar', 'examples/Bars', 'examples/Columns', 'examples/Histogram'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Blur/+page.svelte b/packages/layerchart/src/routes/docs/components/Blur/+page.svelte index a04d1abd4..2665c0dd7 100644 --- a/packages/layerchart/src/routes/docs/components/Blur/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Blur/+page.svelte @@ -1,5 +1,5 @@ diff --git a/packages/layerchart/src/routes/docs/components/Bounds/+page.ts b/packages/layerchart/src/routes/docs/components/Bounds/+page.ts index 74d3ba84f..d554203ae 100644 --- a/packages/layerchart/src/routes/docs/components/Bounds/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Bounds/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/Partition', 'examples/Sunburst', 'examples/Treemap'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte b/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte index f103fb3fd..98cce1790 100644 --- a/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte @@ -1,47 +1,73 @@

Examples

@@ -50,10 +76,10 @@
- - + + - +
@@ -65,13 +91,12 @@ - + - +
@@ -83,13 +108,12 @@ - + - +
@@ -101,50 +125,40 @@ - - + {#snippet children({ context })} + + - {#if brush.isActive} - - - - - - - - - - {/if} - + {#if context.brush.isActive} + + + + + + {/if} + + {/snippet}
@@ -153,68 +167,62 @@
- - - - {#if brush.isActive} - - - {/if} - - -
-
- -

Constant labels

- - - -
- - + + {#snippet children({ context })} + - - {#if brush.isActive} + {#if context.brush.isActive} {/if} - + + {/snippet} + +
+
+ +

Constant labels

+ + + +
+ + {#snippet children({ context })} + + + + {#if context.brush.isActive} + + + {/if} + + {/snippet}
@@ -223,34 +231,35 @@

Integrated brush (x-axis)

-
+
{ + onBrushEnd: (e) => { // @ts-expect-error set(e.xDomain); }, }} > - + - - + + {#snippet children({ gradient })} + + {/snippet} - +
@@ -260,34 +269,35 @@

Integrated brush (y-axis)

-
+
{ + onBrushEnd: (e) => { // @ts-expect-error set(e.yDomain); }, }} > - + - - + + {#snippet children({ gradient })} + + {/snippet} - +
@@ -297,13 +307,12 @@

Integrated brush (both axis / area)

-
+
{ + onBrushEnd: (e) => { set({ // @ts-expect-error xDomain: e.xDomain, @@ -321,15 +330,17 @@ }, }} > - + - - + + {#snippet children({ gradient })} + + {/snippet} - +
@@ -339,27 +350,28 @@

Separate chart (clip data)

-
+
- + - - + + {#snippet children({ gradient })} + + {/snippet} - +
@@ -367,19 +379,18 @@ { + onChange: (e) => { // @ts-expect-error set(e.xDomain); }, }} > - + - +
@@ -389,26 +400,25 @@

Separate chart (clip data: y-axis)

-
+
{ + onChange: (e) => { // @ts-expect-error set(e.yDomain); }, }} > - + - +
@@ -416,20 +426,21 @@ - + - - + + {#snippet children({ gradient })} + + {/snippet} - +
@@ -439,7 +450,7 @@

Separate chart (filter data)

-
+
- - + + - - + + {#snippet children({ gradient })} + + {/snippet} - +
@@ -474,19 +489,18 @@ { + onChange: (e) => { // @ts-expect-error set(e.xDomain); }, }} > - + - +
@@ -505,37 +519,31 @@
{#each seriesData as data, i} -
+
- + - format(v, PeriodType.Day, { variant: 'short' })} - /> + - + {#snippet children({ gradient })} + + {/snippet} - +
@@ -543,23 +551,22 @@ (xDomain = e.xDomain), - onreset: (e) => (xDomain = null), + onChange: (e) => (xDomain = e.xDomain), + onReset: (e) => (xDomain = null), }} > - + - +
@@ -570,61 +577,64 @@

Tooltip interop

-
+
{ + onBrushEnd: (e) => { // @ts-expect-error set(e.xDomain); }, }} - let:height - let:padding > - - - - - - - - - - - - - {format(data.value, 'currency')} - - - - {format(data.date, PeriodType.Day)} - + {#snippet children({ context })} + + + + + + {#snippet children({ gradient })} + + {/snippet} + + + + + + + {#snippet children({ data })} + {format(data.value, 'currency')} + {/snippet} + + + + {#snippet children({ data })} + {format(data.date, 'day')} + {/snippet} + + {/snippet}
@@ -635,7 +645,7 @@ -
+
{ + onChange: (e) => { set({ // @ts-expect-error xDomain: e.xDomain, @@ -655,31 +665,33 @@ }, }} > - + - - {#each points as point} - {@const isSelected = - value && - (value.xDomain?.[0] == null || value.xDomain?.[0] <= point.data.x) && - (value.xDomain?.[1] == null || point.data.x <= value.xDomain?.[1]) && - (value.yDomain?.[0] == null || value.yDomain?.[0] <= point.data.y) && - (value.yDomain?.[1] == null || point.data.y <= value.yDomain?.[1])} - - - {/each} + + {#snippet children({ points })} + {#each points as point} + {@const isSelected = + value && + (value.xDomain?.[0] == null || value.xDomain?.[0] <= point.data.x) && + (value.xDomain?.[1] == null || point.data.x <= value.xDomain?.[1]) && + (value.yDomain?.[0] == null || value.yDomain?.[0] <= point.data.y) && + (value.yDomain?.[1] == null || point.data.y <= value.yDomain?.[1])} + + + {/each} + {/snippet} - +
@@ -690,7 +702,7 @@
-
+
{ + onBrushEnd: (e) => { set({ // @ts-expect-error xDomain: e.xDomain, @@ -712,18 +724,18 @@ }, }} > - + - +
-
+
{ + onChange: (e) => { set({ // @ts-expect-error xDomain: e.xDomain, @@ -744,43 +756,57 @@ }, }} > - - - {#each points as point} - {@const isSelected = - value && - (value.xDomain?.[0] == null || value.xDomain?.[0] <= point.data.x) && - (value.xDomain?.[1] == null || point.data.x <= value.xDomain?.[1]) && - (value.yDomain?.[0] == null || value.yDomain?.[0] <= point.data.y) && - (value.yDomain?.[1] == null || point.data.y <= value.yDomain?.[1])} - - - {/each} + + + {#snippet children({ points })} + {#each points as point} + {@const isSelected = + value && + (value.xDomain?.[0] == null || value.xDomain?.[0] <= point.data.x) && + (value.xDomain?.[1] == null || point.data.x <= value.xDomain?.[1]) && + (value.yDomain?.[0] == null || value.yDomain?.[0] <= point.data.y) && + (value.yDomain?.[1] == null || point.data.y <= value.yDomain?.[1])} + + + {/each} + {/snippet} - +
+ + diff --git a/packages/layerchart/src/routes/docs/components/BrushContext/+page.ts b/packages/layerchart/src/routes/docs/components/BrushContext/+page.ts index ddbb398c0..71f326315 100644 --- a/packages/layerchart/src/routes/docs/components/BrushContext/+page.ts +++ b/packages/layerchart/src/routes/docs/components/BrushContext/+page.ts @@ -5,7 +5,7 @@ import source from '$lib/components/BrushContext.svelte?raw'; import pageSource from './+page.svelte?raw'; import type { AppleStockData } from '$static/data/examples/date/apple-stock.js'; -export async function load() { +export async function load({ fetch }) { return { appleStock: await fetch('/data/examples/date/apple-stock.json').then(async (r) => parse(await r.text()) @@ -14,6 +14,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/components/Calendar/+page.svelte b/packages/layerchart/src/routes/docs/components/Calendar/+page.svelte index 94078a249..03f44f29d 100644 --- a/packages/layerchart/src/routes/docs/components/Calendar/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Calendar/+page.svelte @@ -1,17 +1,24 @@ diff --git a/packages/layerchart/src/routes/docs/components/ChartClipPath/+page.svelte b/packages/layerchart/src/routes/docs/components/ChartClipPath/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/ChartClipPath/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/ChartClipPath/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/ChartClipPath/+page.ts b/packages/layerchart/src/routes/docs/components/ChartClipPath/+page.ts index e1fc36992..ce652dd3e 100644 --- a/packages/layerchart/src/routes/docs/components/ChartClipPath/+page.ts +++ b/packages/layerchart/src/routes/docs/components/ChartClipPath/+page.ts @@ -10,6 +10,7 @@ export async function load() { pageSource, description: 'Convenient way to clip specific components (axis labels, etc) within chart while still allowing some to overflow (tooltips, etc)', + supportedContexts: ['svg'], related: [ 'components/RectClipPath', 'components/Rect', diff --git a/packages/layerchart/src/routes/docs/components/Circle/+page.svelte b/packages/layerchart/src/routes/docs/components/Circle/+page.svelte index 3727dc172..35e4ecd31 100644 --- a/packages/layerchart/src/routes/docs/components/Circle/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Circle/+page.svelte @@ -1,20 +1,90 @@

Examples

+

Styling using classes

+ + +
+ + + + + + + + + + +
+
+ +

Styling using attributes

+ + +
+ + + + + + + + + + +
+
+ +

Styling using CSS variables

+ -
- - +
+ + - - - + + + +
diff --git a/packages/layerchart/src/routes/docs/components/Circle/+page.ts b/packages/layerchart/src/routes/docs/components/Circle/+page.ts index 4ad351ce6..fb388cf28 100644 --- a/packages/layerchart/src/routes/docs/components/Circle/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Circle/+page.ts @@ -9,6 +9,7 @@ export async function load() { source, pageSource, description: '`` element with tweened properties using `motionStore`', + supportedContexts: ['svg', 'canvas', 'html'], related: ['components/Points', 'examples/Pack', 'examples/PunchCard'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/CircleClipPath/+page.svelte b/packages/layerchart/src/routes/docs/components/CircleClipPath/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/CircleClipPath/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/CircleClipPath/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/CircleClipPath/+page.ts b/packages/layerchart/src/routes/docs/components/CircleClipPath/+page.ts index b6808545a..d4d966cf2 100644 --- a/packages/layerchart/src/routes/docs/components/CircleClipPath/+page.ts +++ b/packages/layerchart/src/routes/docs/components/CircleClipPath/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg'], }, }; } diff --git a/packages/layerchart/src/routes/docs/components/ClipPath/+page.svelte b/packages/layerchart/src/routes/docs/components/ClipPath/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/ClipPath/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/ClipPath/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/ClipPath/+page.ts b/packages/layerchart/src/routes/docs/components/ClipPath/+page.ts index ec076e604..31ec6f397 100644 --- a/packages/layerchart/src/routes/docs/components/ClipPath/+page.ts +++ b/packages/layerchart/src/routes/docs/components/ClipPath/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg'], related: [ 'components/ChartClipPath', 'components/CircleClipPath', diff --git a/packages/layerchart/src/routes/docs/components/ColorRamp/+page.svelte b/packages/layerchart/src/routes/docs/components/ColorRamp/+page.svelte index 4492c2be3..db25d87a3 100644 --- a/packages/layerchart/src/routes/docs/components/ColorRamp/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/ColorRamp/+page.svelte @@ -9,13 +9,13 @@ import Preview from '$lib/docs/Preview.svelte'; - let width = '100%'; - let height = 20; - let steps = 5; + let width = $state('100%'); + let height = $state(20); + let steps = $state(5); const interpolators: [string, (value: number) => string][] = entries( d3chromatic - ).filter(([key, value]) => key.startsWith('interpolate')); + ).filter(([key]) => key.startsWith('interpolate')); interpolators.push([`interpolateRgb('red', 'blue')`, interpolateRgb('red', 'blue')]); interpolators.push([`interpolateLab('red', 'blue')`, interpolateLab('red', 'blue')]); interpolators.push([`interpolateHclLong('red', 'blue')`, interpolateHclLong('red', 'blue')]); diff --git a/packages/layerchart/src/routes/docs/components/ColorRamp/+page.ts b/packages/layerchart/src/routes/docs/components/ColorRamp/+page.ts index ca22345b9..3011f2c52 100644 --- a/packages/layerchart/src/routes/docs/components/ColorRamp/+page.ts +++ b/packages/layerchart/src/routes/docs/components/ColorRamp/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg'], }, }; } diff --git a/packages/layerchart/src/routes/docs/components/Connector/+page.svelte b/packages/layerchart/src/routes/docs/components/Connector/+page.svelte new file mode 100644 index 000000000..618ad3768 --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/Connector/+page.svelte @@ -0,0 +1,78 @@ + + +

Playground

+ +
+ + {#if type === 'd3'} + + {/if} + + {#if type === 'beveled' || type === 'rounded'} + + {/if} +
+ + +
+ + + + { + source.x += e.detail.dx; + source.y += e.detail.dy; + }, + }} + /> + + { + target.x += e.detail.dx; + target.y += e.detail.dy; + }, + }} + /> + + +
+
diff --git a/packages/layerchart/src/routes/docs/components/Connector/+page.ts b/packages/layerchart/src/routes/docs/components/Connector/+page.ts new file mode 100644 index 000000000..ba99b59f5 --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/Connector/+page.ts @@ -0,0 +1,15 @@ +import api from '$lib/components/Connector.svelte?raw&sveld'; +import source from '$lib/components/Connector.svelte?raw'; +import pageSource from './+page.svelte?raw'; + +export async function load() { + return { + meta: { + api, + source, + pageSource, + supportedContexts: ['svg', 'canvas'], + related: ['components/Link', 'examples/Tree'], + }, + }; +} diff --git a/packages/layerchart/src/routes/docs/components/Dagre/+page.svelte b/packages/layerchart/src/routes/docs/components/Dagre/+page.svelte new file mode 100644 index 000000000..1f3a40471 --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/Dagre/+page.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/layerchart/src/routes/docs/components/Dagre/+page.ts b/packages/layerchart/src/routes/docs/components/Dagre/+page.ts new file mode 100644 index 000000000..16d7e77e8 --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/Dagre/+page.ts @@ -0,0 +1,15 @@ +import api from '$lib/components/Dagre.svelte?raw&sveld'; +import source from '$lib/components/Dagre.svelte?raw'; +import pageSource from './+page.svelte?raw'; + +export async function load() { + return { + meta: { + api, + source, + pageSource, + supportedContexts: ['svg', 'canvas'], + related: ['examples/Dagre'], + }, + }; +} diff --git a/packages/layerchart/src/routes/docs/components/Ellipse/+page.svelte b/packages/layerchart/src/routes/docs/components/Ellipse/+page.svelte new file mode 100644 index 000000000..845a912e3 --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/Ellipse/+page.svelte @@ -0,0 +1,105 @@ + + +

Examples

+ +

Styling using classes

+ + +
+ + + + + + + + + + +
+
+ +

Styling using attributes

+ + +
+ + + + + + + + + + +
+
+ +

Styling using CSS variables

+ + +
+ + + + + + + + + + +
+
diff --git a/packages/layerchart/src/routes/docs/components/Ellipse/+page.ts b/packages/layerchart/src/routes/docs/components/Ellipse/+page.ts new file mode 100644 index 000000000..bc49fffce --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/Ellipse/+page.ts @@ -0,0 +1,15 @@ +import api from '$lib/components/Ellipse.svelte?raw&sveld'; +import source from '$lib/components/Ellipse.svelte?raw'; +import pageSource from './+page.svelte?raw'; + +export async function load() { + return { + meta: { + api, + source, + pageSource, + description: '`` element with tweened properties using `motionStore`', + supportedContexts: ['svg', 'canvas', 'html'], + }, + }; +} diff --git a/packages/layerchart/src/routes/docs/components/ForceSimulation/+page.svelte b/packages/layerchart/src/routes/docs/components/ForceSimulation/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/ForceSimulation/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/ForceSimulation/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/ForceSimulation/+page.ts b/packages/layerchart/src/routes/docs/components/ForceSimulation/+page.ts index 22992338f..a45bc5565 100644 --- a/packages/layerchart/src/routes/docs/components/ForceSimulation/+page.ts +++ b/packages/layerchart/src/routes/docs/components/ForceSimulation/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: [ 'examples/Beeswarm', 'examples/CollisionDetection', diff --git a/packages/layerchart/src/routes/docs/components/Frame/+page.svelte b/packages/layerchart/src/routes/docs/components/Frame/+page.svelte index c27bf05b3..e892bafbd 100644 --- a/packages/layerchart/src/routes/docs/components/Frame/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Frame/+page.svelte @@ -1,6 +1,7 @@

Examples

@@ -8,7 +9,7 @@

Basic

-
+
- + - +
@@ -30,7 +31,7 @@

full

-
+
- + - +
@@ -52,7 +53,7 @@

border

-
+
- + - +
@@ -74,7 +75,7 @@

gradient background

-
+
- - - + + + {#snippet children({ gradient })} + + {/snippet} - - -
- - -

Canvas

- - -
- - - - - - - -
-
- -

Canvas gradient background

- - -
- - - - - - - - +
diff --git a/packages/layerchart/src/routes/docs/components/Frame/+page.ts b/packages/layerchart/src/routes/docs/components/Frame/+page.ts index b32b06f3f..bdbe0e0ce 100644 --- a/packages/layerchart/src/routes/docs/components/Frame/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Frame/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/components/GeoCircle/+page.svelte b/packages/layerchart/src/routes/docs/components/GeoCircle/+page.svelte index 7fc3e7b3d..201d47679 100644 --- a/packages/layerchart/src/routes/docs/components/GeoCircle/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/GeoCircle/+page.svelte @@ -11,20 +11,21 @@ import { range } from 'd3-array'; import { feature } from 'topojson-client'; - import { Chart, GeoCircle, GeoPath, Graticule, Svg } from 'layerchart'; + import { Chart, GeoCircle, GeoPath, Graticule, Layer } from 'layerchart'; import { Field, RangeField, SelectField, ToggleGroup, ToggleOption } from 'svelte-ux'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); - let example: 'single' | 'multi' = 'single'; - let latitude = 0; - let longitude = 0; - let radius = 600; - let precision = 6; + let example: 'single' | 'multi' = $state('single'); + let latitude = $state(0); + let longitude = $state(0); + let radius = $state(600); + let precision = $state(6); - let projection = geoNaturalEarth1; + let projection = $state(geoNaturalEarth1); const projections = [ { label: 'Albers', value: geoAlbers }, { label: 'Albers USA', value: geoAlbersUsa }, @@ -35,11 +36,12 @@ { label: 'Orthographic', value: geoOrthographic }, ]; - $: geojson = feature(data.geojson, data.geojson.objects.countries); - $: features = + const geojson = $derived(feature(data.geojson, data.geojson.objects.countries)); + const features = $derived( projection === geoAlbersUsa ? geojson.features.filter((f) => f.properties.name === 'United States of America') - : geojson.features; + : geojson.features + ); const step = 10; const coordinates = range(-80, 80 + step, step).flatMap((y) => { @@ -97,7 +99,7 @@ }} padding={{ left: 100, right: 100 }} > - + @@ -109,15 +111,12 @@ {/each} {#if example === 'single'} - - {#key [longitude, latitude]} - - {/key} + {:else if example === 'multi'} {#each coordinates as coords} {/each} {/if} - +
diff --git a/packages/layerchart/src/routes/docs/components/GeoCircle/+page.ts b/packages/layerchart/src/routes/docs/components/GeoCircle/+page.ts index 0a70d9b50..98eef6292 100644 --- a/packages/layerchart/src/routes/docs/components/GeoCircle/+page.ts +++ b/packages/layerchart/src/routes/docs/components/GeoCircle/+page.ts @@ -3,7 +3,7 @@ import source from '$lib/components/GeoCircle.svelte?raw'; import pageSource from './+page.svelte?raw'; import type { GeometryCollection, Topology } from 'topojson-specification'; -export async function load() { +export async function load({ fetch }) { return { geojson: (await fetch('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json').then( (r) => r.json() @@ -15,6 +15,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/Timezones'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/GeoContext/+page.svelte b/packages/layerchart/src/routes/docs/components/GeoContext/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/GeoContext/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/GeoContext/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/GeoContext/+page.ts b/packages/layerchart/src/routes/docs/components/GeoContext/+page.ts index 655ce88dd..145e2f51b 100644 --- a/packages/layerchart/src/routes/docs/components/GeoContext/+page.ts +++ b/packages/layerchart/src/routes/docs/components/GeoContext/+page.ts @@ -10,6 +10,7 @@ export async function load() { pageSource, description: 'Setup geo context, particularly the projection used by other geo components. Typically used indirectly via the `geo` prop on Chart', + supportedContexts: ['svg', 'canvas'], related: ['components/Chart'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/GeoEdgeFade/+page.svelte b/packages/layerchart/src/routes/docs/components/GeoEdgeFade/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/GeoEdgeFade/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/GeoEdgeFade/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/GeoEdgeFade/+page.ts b/packages/layerchart/src/routes/docs/components/GeoEdgeFade/+page.ts index 0a361d816..61289eba6 100644 --- a/packages/layerchart/src/routes/docs/components/GeoEdgeFade/+page.ts +++ b/packages/layerchart/src/routes/docs/components/GeoEdgeFade/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/LoftedArcs'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/GeoPath/+page.svelte b/packages/layerchart/src/routes/docs/components/GeoPath/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/GeoPath/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/GeoPath/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/GeoPath/+page.ts b/packages/layerchart/src/routes/docs/components/GeoPath/+page.ts index fe05cce00..c1126d10a 100644 --- a/packages/layerchart/src/routes/docs/components/GeoPath/+page.ts +++ b/packages/layerchart/src/routes/docs/components/GeoPath/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: [ 'components/Graticule', 'examples/AnimatedGlobe', diff --git a/packages/layerchart/src/routes/docs/components/GeoPoint/+page.svelte b/packages/layerchart/src/routes/docs/components/GeoPoint/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/GeoPoint/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/GeoPoint/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/GeoPoint/+page.ts b/packages/layerchart/src/routes/docs/components/GeoPoint/+page.ts index 53c22a784..1e511f446 100644 --- a/packages/layerchart/src/routes/docs/components/GeoPoint/+page.ts +++ b/packages/layerchart/src/routes/docs/components/GeoPoint/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/GeoPoint'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/GeoSpline/+page.svelte b/packages/layerchart/src/routes/docs/components/GeoSpline/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/GeoSpline/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/GeoSpline/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/GeoSpline/+page.ts b/packages/layerchart/src/routes/docs/components/GeoSpline/+page.ts index 4eec2cfa9..e0fd2f591 100644 --- a/packages/layerchart/src/routes/docs/components/GeoSpline/+page.ts +++ b/packages/layerchart/src/routes/docs/components/GeoSpline/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/LoftedArcs'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/GeoTile/+page.svelte b/packages/layerchart/src/routes/docs/components/GeoTile/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/GeoTile/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/GeoTile/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/GeoTile/+page.ts b/packages/layerchart/src/routes/docs/components/GeoTile/+page.ts index 9015d86e1..a39673fe1 100644 --- a/packages/layerchart/src/routes/docs/components/GeoTile/+page.ts +++ b/packages/layerchart/src/routes/docs/components/GeoTile/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/GeoTile', 'examples/ZoomableTileMap'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/GeoVisible/+page.svelte b/packages/layerchart/src/routes/docs/components/GeoVisible/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/GeoVisible/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/GeoVisible/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/GeoVisible/+page.ts b/packages/layerchart/src/routes/docs/components/GeoVisible/+page.ts index e6b3bebdb..366c3afc8 100644 --- a/packages/layerchart/src/routes/docs/components/GeoVisible/+page.ts +++ b/packages/layerchart/src/routes/docs/components/GeoVisible/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/SubmarineCablesGlobe'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Graticule/+page.svelte b/packages/layerchart/src/routes/docs/components/Graticule/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/Graticule/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Graticule/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/Graticule/+page.ts b/packages/layerchart/src/routes/docs/components/Graticule/+page.ts index 9c35abb86..9b28ebd29 100644 --- a/packages/layerchart/src/routes/docs/components/Graticule/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Graticule/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/AnimatedGlobe', 'examples/GeoProjection', 'examples/LoftedArcsGlobe'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Grid/+page.svelte b/packages/layerchart/src/routes/docs/components/Grid/+page.svelte index d0a9b3e30..5380cb447 100644 --- a/packages/layerchart/src/routes/docs/components/Grid/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Grid/+page.svelte @@ -1,14 +1,14 @@

Examples

@@ -16,18 +16,17 @@

Both axis

-
+
- + - +
@@ -35,18 +34,17 @@

Single axis (x)

-
+
- + - +
@@ -54,18 +52,17 @@

Single axis (y)

-
+
- + - +
@@ -73,18 +70,17 @@

Dashed lines

-
+
- + - +
@@ -92,23 +88,18 @@

Band scale (align center / default)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - /> - + +
@@ -116,23 +107,18 @@

Band scale (align between)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - /> - + +
@@ -140,11 +126,11 @@

Radial

-
- - +
+ + scale.ticks?.().splice(1)} y /> - +
@@ -152,11 +138,11 @@

Radial (linear)

-
- - +
+ + scale.ticks?.().splice(1)} y radialY="linear" /> - +
@@ -164,16 +150,15 @@

Integer-only ticks

-
+
- + scale.ticks?.().filter(Number.isInteger)} /> scale.ticks?.().filter(Number.isInteger)} format="integer" /> - +
@@ -189,19 +174,18 @@

Explicit ticks

-
+
- + - +
@@ -209,19 +193,18 @@

Inject tick value

-
+
- - [45, ...scale.ticks?.()]} /> - [45, ...scale.ticks?.()]} /> - + + [45, ...(scale.ticks?.() ?? [])]} /> + [45, ...(scale.ticks?.() ?? [])]} /> +
@@ -229,19 +212,18 @@

Tick count

-
+
- + - +
@@ -249,19 +231,18 @@

Tick count (responsive)

-
+
- - - - + + + +
@@ -269,19 +250,18 @@

Remove default tick count

-
+
- + - +
diff --git a/packages/layerchart/src/routes/docs/components/Grid/+page.ts b/packages/layerchart/src/routes/docs/components/Grid/+page.ts index 6dc2f03e7..77c540900 100644 --- a/packages/layerchart/src/routes/docs/components/Grid/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Grid/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas', 'html'], related: ['components/Axis', 'components/Rule'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Group/+page.svelte b/packages/layerchart/src/routes/docs/components/Group/+page.svelte index 0cf1119cf..6d6c2d45f 100644 --- a/packages/layerchart/src/routes/docs/components/Group/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Group/+page.svelte @@ -1,24 +1,25 @@

Examples

-
+
- + - + - + - +
diff --git a/packages/layerchart/src/routes/docs/components/Group/+page.ts b/packages/layerchart/src/routes/docs/components/Group/+page.ts index 09c93207a..8d92f4904 100644 --- a/packages/layerchart/src/routes/docs/components/Group/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Group/+page.ts @@ -10,6 +10,11 @@ export async function load() { pageSource, description: '`` element with convenient x/y and center placement along with tweened properties using `motionStore`', + supportedContexts: [ + 'svg', + // 'canvas' // TODO: Supported, but limited use cases + 'html', + ], related: [ 'examples/Pack', 'examples/Partition', diff --git a/packages/layerchart/src/routes/docs/components/Highlight/+page.svelte b/packages/layerchart/src/routes/docs/components/Highlight/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/Highlight/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Highlight/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/Highlight/+page.ts b/packages/layerchart/src/routes/docs/components/Highlight/+page.ts index e7bdbb8ea..4c361b911 100644 --- a/packages/layerchart/src/routes/docs/components/Highlight/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Highlight/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['components/Tooltip', 'components/TooltipContext'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Hull/+page.svelte b/packages/layerchart/src/routes/docs/components/Hull/+page.svelte index a198835d1..a7d712ab6 100644 --- a/packages/layerchart/src/routes/docs/components/Hull/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Hull/+page.svelte @@ -5,21 +5,22 @@ import { curveLinearClosed } from 'd3-shape'; import { feature } from 'topojson-client'; - import { Axis, Chart, GeoPath, GeoPoint, Hull, Points, Svg, Text } from 'layerchart'; + import { Axis, Chart, Circle, GeoPath, GeoPoint, Hull, Layer, Points, Text } from 'layerchart'; import Preview from '$lib/docs/Preview.svelte'; import CurveMenuField from '$lib/docs/CurveMenuField.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); - export let curve = curveLinearClosed; + let curve = $state(curveLinearClosed); const states = feature(data.us.geojson, data.us.geojson.objects.states); const groupColor = scaleOrdinal([ - 'hsl(var(--color-info))', - 'hsl(var(--color-warning))', - 'hsl(var(--color-danger))', + 'var(--color-info)', + 'var(--color-warning)', + 'var(--color-danger)', ]); @@ -32,7 +33,7 @@

Scatter

-
+
{@const dataByGroup = group(data.groupData, (d) => d.group)} - + {#each dataByGroup as [group, data]} - + {@const color = groupColor(group)} + + {/each} - +
@@ -75,15 +78,11 @@ fitGeojson: states, }} > - - - {#each states.features as feature} - - {/each} - + + { @@ -96,8 +95,9 @@ /> {#each data.us.stateCaptitals as capital} + - + {/each} - +
diff --git a/packages/layerchart/src/routes/docs/components/Hull/+page.ts b/packages/layerchart/src/routes/docs/components/Hull/+page.ts index 60b456b78..1cdb3cf32 100644 --- a/packages/layerchart/src/routes/docs/components/Hull/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Hull/+page.ts @@ -6,7 +6,7 @@ import source from '$lib/components/Hull.svelte?raw'; import type { GeometryCollection, Topology } from 'topojson-specification'; import type { USStateCapitalsData } from '$static/data/examples/geo/us-state-capitals.js'; -export async function load() { +export async function load({ fetch }) { return { groupData: (await fetch('/data/examples/group-data.json').then((r) => r.json())) as { x: number; @@ -27,6 +27,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/components/Labels/+page.svelte b/packages/layerchart/src/routes/docs/components/Labels/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/Labels/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Labels/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/Labels/+page.ts b/packages/layerchart/src/routes/docs/components/Labels/+page.ts index 38317cc57..4a7223c96 100644 --- a/packages/layerchart/src/routes/docs/components/Labels/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Labels/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: [ 'examples/Area', 'examples/Bars', diff --git a/packages/layerchart/src/routes/docs/components/Legend/+page.svelte b/packages/layerchart/src/routes/docs/components/Legend/+page.svelte index 33d6e0ef0..bc077baff 100644 --- a/packages/layerchart/src/routes/docs/components/Legend/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Legend/+page.svelte @@ -232,11 +232,7 @@ data={[{ name: 'One' }, { name: 'Two' }, { name: 'Three' }]} c="name" cScale={scaleOrdinal()} - cRange={[ - 'hsl(var(--color-success))', - 'hsl(var(--color-warning))', - 'hsl(var(--color-danger))', - ]} + cRange={['var(--color-success)', 'var(--color-warning)', 'var(--color-danger)']} > @@ -251,11 +247,7 @@ data={[{ name: 'One' }, { name: 'Two' }, { name: 'Three' }]} c="name" cScale={scaleOrdinal()} - cRange={[ - 'hsl(var(--color-success))', - 'hsl(var(--color-warning))', - 'hsl(var(--color-danger))', - ]} + cRange={['var(--color-success)', 'var(--color-warning)', 'var(--color-danger)']} > @@ -281,7 +273,7 @@ )} title="Age (years)" variant="swatches" - classes={{ swatch: 'rounded' }} + classes={{ swatch: 'rounded-sm' }} />
@@ -312,7 +304,7 @@ tickFontSize={12} tickFormat={(value) => value + '°'} classes={{ - root: 'border px-3 py-2 bg-surface-200 rounded', + root: 'border px-3 py-2 bg-surface-200 rounded-sm', title: 'text-lg text-center', label: 'fill-surface-content/50', tick: 'stroke-surface-100', @@ -320,10 +312,10 @@ /> -

Click handler

+

Responsive swatches

-
+
console.log(d)} + classes={{ + root: 'w-full', + swatch: 'size-2', + item: 'text-xs', + }} />
-

slot override

+

Click handler

+ + + console.log(d)} + /> + + +

children override

-
- {#each values as value} -
-
-
{value}
-
- {/each} -
+ {#snippet children({ scale, values })} +
+ {#each values as value} +
+
+
{value}
+
+ {/each} +
+ {/snippet}
diff --git a/packages/layerchart/src/routes/docs/components/Legend/+page.ts b/packages/layerchart/src/routes/docs/components/Legend/+page.ts index d518ee838..8bb8abdc0 100644 --- a/packages/layerchart/src/routes/docs/components/Legend/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Legend/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['html'], }, }; } diff --git a/packages/layerchart/src/routes/docs/components/Line/+page.svelte b/packages/layerchart/src/routes/docs/components/Line/+page.svelte index b15bc1373..b2dc952a3 100644 --- a/packages/layerchart/src/routes/docs/components/Line/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Line/+page.svelte @@ -1,29 +1,115 @@

Examples

+

Styling using classes

+ -
- - +
+ + - - + + + + + +
+ + +

Styling using attributes

+ + +
+ + + + + + + + + +
+
+ +

Styling using CSS variables

+ + +
+ + + + + + - +
diff --git a/packages/layerchart/src/routes/docs/components/Line/+page.ts b/packages/layerchart/src/routes/docs/components/Line/+page.ts index d22dc001d..61c7b05a6 100644 --- a/packages/layerchart/src/routes/docs/components/Line/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Line/+page.ts @@ -9,6 +9,7 @@ export async function load() { source, pageSource, description: '`` element with tweened properties using `motionStore`', + supportedContexts: ['svg', 'canvas', 'html'], related: ['components/Rule', 'components/Spline'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte b/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte index 7bc2c429d..0921218f8 100644 --- a/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte @@ -1,37 +1,41 @@ + const annotations = $derived( + [...data.appleStock] + .sort(() => Math.random() - 0.5) + .slice(0, 5) + .sort(sortFunc('date')) + .map((d, i) => ({ + date: d.date, + label: String.fromCharCode(65 + i), + details: `This is an annotation for ${format(d.date)}`, + })) + ); -

Examples

+ let show = $state(true); -
- - - Svg - Canvas - - + let renderContext = $derived(shared.renderContext as 'svg' | 'canvas'); + let debug = $derived(shared.debug); - - - -
+ const monitorSeries = [ + { + key: 'RespondActivityTaskCompleted', + data: [ + { + date: new Date('2025-09-14T00:00:00.000Z'), + value: 0.05875000000000004, + }, + { + date: new Date('2025-09-14T00:05:00.000Z'), + value: 0.0195, + }, + { + date: new Date('2025-09-14T12:00:00.000Z'), + value: 0.0195, + }, + { + date: new Date('2025-09-15T00:00:00.000Z'), + value: 0.08083333333333337, + }, + { + date: new Date('2025-09-15T00:05:00.000Z'), + value: 0.04592857142857144, + }, + ], + color: 'var(--color-blue-500)', + }, + { + key: 'RespondWorkflowTaskCompleted', + data: [ + { + date: new Date('2025-09-14T00:00:00.000Z'), + value: 0.08999999999999998, + }, + { + date: new Date('2025-09-14T00:05:00.000Z'), + value: 0.03275000000000002, + }, + { + date: new Date('2025-09-14T12:00:00.000Z'), + value: 0.047, + }, + { + date: new Date('2025-09-15T00:00:00.000Z'), + value: 0.08666666666666673, + }, + { + date: new Date('2025-09-15T00:05:00.000Z'), + value: 0.04625, + }, + { + date: new Date('2025-09-15T12:00:00.000Z'), + value: 0.0485, + }, + ], + color: 'var(--color-purple-500)', + }, + { + key: 'StartWorkflowExecution', + data: [ + { + date: new Date('2025-09-14T00:00:00.000Z'), + value: 0.16666666666666669, + }, + { + date: new Date('2025-09-15T00:00:00.000Z'), + value: 0.1300000000000001, + }, + ], + color: 'var(--color-green-500)', + }, + ]; + + +

Examples

Basic

-
+
@@ -109,11 +187,11 @@

Override color

-
+
@@ -123,7 +201,7 @@

Curve

-
+
+

Vertical

+ + +
+ +
+
+

Series

-
+
+

Series (with nulls)

+ + +
+ + {#snippet belowMarks({ visibleSeries, highlightKey })} + {#each visibleSeries as s} + d[s.key] !== null)} + y={s.key} + stroke={s.color} + class={cls( + '[stroke-dasharray:3,3] transition-opacity', + highlightKey && highlightKey !== s.key && 'opacity-10' + )} + /> + {/each} + {/snippet} + +
+
+

Series (separate data)

-
+
-

Series (voronoi tooltip with highlight)

+

Series (separate data with different length)

+ + +
+ Math.random() > 0.3), + color: 'var(--color-danger)', + }, + { + key: 'bananas', + data: multiSeriesDataByFruit.get('bananas')?.filter((d, i) => Math.random() > 0.3), + color: 'var(--color-success)', + }, + { + key: 'oranges', + data: multiSeriesDataByFruit.get('oranges')?.filter((d, i) => Math.random() > 0.3), + color: 'var(--color-warning)', + }, + ]} + {renderContext} + {debug} + /> +
+
+ + + +

Series (vertical)

+ + +
+ +
+
+ +

Series (individual tooltip with highlight)

-
+
- - {#each series as s} - {@const color = - tooltip.data == null || tooltip.data.fruit === s.key - ? s.color - : 'hsl(var(--color-surface-content) / 20%)'} - + {#snippet marks({ context, visibleSeries, highlightKey })} + {#each visibleSeries as s} + {@const active = + (context.tooltip.data == null || s.key === context.tooltip.data?.fruit) && + (highlightKey === null || s.key === highlightKey)} + {/each} - + {/snippet} - - - {@const activeSeriesColor = [...series].find((s) => s.key === tooltip.data?.fruit)?.color} + {#snippet highlight({ series, context })} + {@const activeSeriesColor = series.find( + (s) => s.key === context.tooltip.data?.fruit + )?.color} - - - - - {@const activeSeriesColor = [...series].find((s) => s.key === tooltip.data?.fruit)?.color} - - {format(x(data))} - - - + {/snippet} + + {#snippet tooltip({ context, series })} + {@const activeSeriesColor = series.find( + (s) => s.key === context.tooltip.data?.fruit + )?.color} + + {#snippet children({ data })} + {format(context.x(data))} + + + + {/snippet} - + {/snippet}
@@ -234,16 +430,16 @@

Series (point click)

-
+
{ + onPointClick={(e, detail) => { console.log(e, detail); alert(JSON.stringify(detail)); }} @@ -256,14 +452,14 @@

Series (custom highlight point)

-
+
Series (labels on point hover) -
+
- - {#if highlightSeriesKey} - - {@const activeSeriesIndex = [...series].findIndex((s) => s.key === highlightSeriesKey)} + {#snippet aboveMarks({ getLabelsProps, series, highlightKey })} + {#if highlightKey} + {@const activeSeriesIndex = series.findIndex((s) => s.key === highlightKey)} {/if} - + {/snippet}
@@ -301,7 +496,7 @@

Labels

-
+
Points -
+
@@ -324,7 +519,7 @@

Labels with Points

-
+
Labels within points -
+
Radar (linear grid) -
+
@@ -401,11 +599,10 @@

Radar (rounded grid)

-
+
@@ -439,22 +640,21 @@

Radar with series data

-
+
@@ -486,7 +690,7 @@

Gradient encoding

-
+
- - - + {#snippet marks()} + + {#snippet children({ gradient })} + + {/snippet} - + {/snippet} - - {#if tooltip.data} - + {#snippet highlight({ context })} + {#if context.tooltip.data} + {/if} - - - - - {@const value = y(data)} - {format(x(data))} - - - + {/snippet} + + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {@const value = context.y(data)} + {format(context.x(data))} + + + + {/snippet} - + {/snippet}
@@ -527,7 +731,7 @@

Gradient threshold

-
+
- - {@const thresholdOffset = yScale(50) / (height + padding.bottom)} + {#snippet marks({ context })} + {@const thresholdOffset = context.yScale(50) / (context.height + context.padding.bottom)} - + {#snippet children({ gradient })} + + {/snippet} - + {/snippet} + + {#snippet highlight({ context })} + {#if context.tooltip.data} + 50 ? 'var(--color-danger)' : 'var(--color-info)', + }} + /> + {/if} + {/snippet} + + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {@const value = context.y(data)} + {format(context.x(data))} + + 50 ? 'var(--color-danger)' : 'var(--color-info)'} + /> + + {/snippet} + + {/snippet}
@@ -557,31 +790,30 @@

Large series

-
+
v + '° F' }, highlight: { points: false }, + tooltip: { + context: { + mode: 'manual', + }, + }, }} series={flatGroup(data.dailyTemperatures, (d) => d.year).map(([year, data]) => { return { - key: year, + key: year.toString(), data, - color: - year === 2024 - ? 'hsl(var(--color-primary))' - : year === 2023 - ? 'hsl(var(--color-primary) / 50%)' - : 'hsl(var(--color-surface-content))', - props: { opacity: [2023, 2024].includes(year) ? 1 : 0.1 }, + color: year >= 2023 ? 'var(--color-primary)' : 'var(--color-surface-content)', + props: { opacity: year === 2024 ? 1 : year === 2023 ? 0.5 : 0.1 }, }; })} - tooltip={{ mode: 'manual' }} {renderContext} {debug} /> @@ -591,7 +823,7 @@

Large radial series

-
+
v + '° F' }, highlight: { points: false }, + tooltip: { + context: { + mode: 'manual', + }, + }, }} series={flatGroup(data.dailyTemperatures, (d) => d.year).map(([year, data]) => { return { - key: year, + key: year.toString(), data, - color: - year === 2024 - ? 'hsl(var(--color-primary))' - : year === 2023 - ? 'hsl(var(--color-primary) / 50%)' - : 'hsl(var(--color-surface-content))', - props: { opacity: [2023, 2024].includes(year) ? 1 : 0.1 }, + color: year >= 2023 ? 'var(--color-primary)' : 'var(--color-surface-content)', + props: { opacity: year === 2024 ? 1 : year === 2023 ? 0.5 : 0.1 }, }; })} - tooltip={{ mode: 'manual' }} {renderContext} {debug} /> @@ -629,11 +860,11 @@

Dynamic data (move over chart)

- +
{ + class="h-[300px] p-4 border rounded-sm" + onmousemove={(e) => { const x = e.clientX; const y = e.clientY; dynamicData = dynamicData.slice(-200).concat(Math.atan2(x, y)); @@ -646,8 +877,8 @@ yBaseline={undefined} tooltip={false} props={{ - yAxis: { tweened: true }, - grid: { tweened: true }, + yAxis: { motion: 'tween' }, + grid: { motion: 'tween' }, // spline: { // draw: { // // easing function to only draw the last data point @@ -670,7 +901,7 @@

Null gaps

-
+
@@ -678,9 +909,9 @@

Null with dashed lines

-
+
- + {#snippet belowMarks({ series })} {#each series as s} d.value !== null)} @@ -689,7 +920,7 @@ stroke={s.color} /> {/each} - + {/snippet}
@@ -714,7 +945,7 @@

Single axis (x)

-
+
@@ -722,22 +953,50 @@

Single axis (y)

-
+
+

Axis labels inside

+ + +
+ +
+
+

Legend

-
+
Tooltip click -
+
{ + onTooltipClick={(e, detail) => { console.log(e, detail); alert(JSON.stringify(detail)); }} @@ -767,101 +1026,217 @@

Custom tooltip

-
+
- + {#snippet tooltip({ context })} - {y(data)} + {#snippet children({ data })} + {context.y(data)} + {/snippet} - {format(x(data), PeriodType.Day)} + {#snippet children({ data })} + {format(context.x(data), 'day')} + {/snippet} - + {/snippet}
-

Simple annotations

+

Point annotations

- -
- - - - {#each annotations as annotation} - { - e.stopPropagation(); - tooltip.show(e, { annotation }); - }} - onpointerleave={() => { - tooltip.hide(); - }} - /> - - {/each} - - - - - - {#if data.annotation} - -
- {data.annotation.description} -
- {:else} - - {format(x(data), PeriodType.DayTime)} - - - - {/if} + +
+ { + return { + type: 'point', + label: a.label, + details: a.details, + x: a.date, + r: 6, + props: { + circle: { class: 'fill-secondary' }, + label: { class: 'text-[10px] fill-secondary-content font-bold' }, + }, + }; + })} + {renderContext} + {debug} + > + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {#if data.annotation} + +
+ {data.annotation.details} +
+ {:else} + + {format(context.x(data), 'daytime')} + + + + {/if} + {/snippet}
- + {/snippet}
+
+ See also: AnnotationPoint for more examples +
+ +

Line annotation

+ + +
+ +
+
+ +
+ See also: AnnotationLine for more examples +
+ +

Range annotation

+ + +
+ +
+
+ +
+ See also: AnnotationRange for more examples +
+ +

Series point annotations

+ + + {@const series = [ + { + key: 'apples', + data: multiSeriesDataByFruit.get('apples'), + color: 'var(--color-danger)', + }, + { + key: 'bananas', + data: multiSeriesDataByFruit.get('bananas'), + color: 'var(--color-success)', + }, + { + key: 'oranges', + data: multiSeriesDataByFruit.get('oranges'), + color: 'var(--color-warning)', + }, + ]} +
+ { + const lastDataPoint = s.data?.[s.data.length - 1] ?? null; + return { + type: 'point', + seriesKey: s.key, + label: s.key, + labelPlacement: 'right', + labelXOffset: 4, + x: lastDataPoint.date, + y: lastDataPoint.value, + props: { + circle: { fill: s.color }, + label: { fill: s.color }, + }, + }; + })} + padding={{ ...defaultChartPadding(), right: 60 }} + {renderContext} + {debug} + /> +
+
+ +
+ See also: AnnotationPoint for more examples +
+

Brushing

- -
+ +
Brush with series point events -
- { - console.log(e, detail); - alert(JSON.stringify(detail)); - }} - brush - {renderContext} - {debug} - /> + +
+ { + console.log(e, detail); + alert(JSON.stringify(detail)); + }} + brush + {renderContext} + {debug} + /> +
+
+ +
+

Draw

+ + + +
+ + {#if show} +
+ +
+ {/if} +
+

Custom chart

-
- - - - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - - - - - {format(x(data), PeriodType.DayTime)} - - - - +
+ + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {format(context.x(data), 'daytime')} + + + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/components/LineChart/+page.ts b/packages/layerchart/src/routes/docs/components/LineChart/+page.ts index 204bda6c9..a6cd96a57 100644 --- a/packages/layerchart/src/routes/docs/components/LineChart/+page.ts +++ b/packages/layerchart/src/routes/docs/components/LineChart/+page.ts @@ -8,7 +8,7 @@ import pageSource from './+page.svelte?raw'; import type { AppleStockData } from '$static/data/examples/date/apple-stock.js'; -export async function load() { +export async function load({ fetch }) { return { appleStock: await fetch('/data/examples/date/apple-stock.json').then(async (r) => parse(await r.text()) @@ -37,6 +37,7 @@ export async function load() { source, pageSource, description: 'Streamlined Chart configuration for Line charts', + supportedContexts: ['svg', 'canvas'], related: ['components/Chart', 'components/Spline', 'examples/Line'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/LinearGradient/+page.svelte b/packages/layerchart/src/routes/docs/components/LinearGradient/+page.svelte index 8db5482ca..31ec5a742 100644 --- a/packages/layerchart/src/routes/docs/components/LinearGradient/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/LinearGradient/+page.svelte @@ -1,6 +1,7 @@

Examples

@@ -8,31 +9,72 @@

Direction with custom colors

-
+
- - + + + {#snippet children({ gradient })} + + {/snippet} + + + + {#snippet children({ gradient })} + + {/snippet} + + + + {#snippet children({ gradient })} + + {/snippet} + + + +
+ + +

Explicit offsets

+ + +
+ + + {#snippet children({ gradient })} + + {/snippet} + + + + > + {#snippet children({ gradient })} + + {/snippet} + + - {#each { length: 3 } as _, i} - - {/each} - + > + {#snippet children({ gradient })} + + {/snippet} + +
@@ -40,22 +82,63 @@

Tailwind colors

-
+
- - - - - - - - - - - {#each { length: 9 } as _, i} - - {/each} - + + + {#snippet children({ gradient })} + + {/snippet} + + + + {#snippet children({ gradient })} + + {/snippet} + + + + {#snippet children({ gradient })} + + {/snippet} + + + + {#snippet children({ gradient })} + + {/snippet} + + + + {#snippet children({ gradient })} + + {/snippet} + + + + {#snippet children({ gradient })} + + {/snippet} + + + + {#snippet children({ gradient })} + + {/snippet} + + + + {#snippet children({ gradient })} + + {/snippet} + + + + {#snippet children({ gradient })} + + {/snippet} + +
@@ -63,21 +146,25 @@

units `objectBoundingBox` (default) vs `userSpaceOnUse`

-
+
- - - {#each { length: 6 } as _, i} - - {/each} + + + {#snippet children({ gradient })} + {#each { length: 6 } as _, i} + + {/each} + {/snippet} - - {#each { length: 6 } as _, i} - - {/each} + + {#snippet children({ gradient })} + {#each { length: 6 } as _, i} + + {/each} + {/snippet} - +
diff --git a/packages/layerchart/src/routes/docs/components/LinearGradient/+page.ts b/packages/layerchart/src/routes/docs/components/LinearGradient/+page.ts index 3e9278869..509d4e126 100644 --- a/packages/layerchart/src/routes/docs/components/LinearGradient/+page.ts +++ b/packages/layerchart/src/routes/docs/components/LinearGradient/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas', 'html'], related: ['components/RadialGradient', 'components/Pattern'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Link/+page.svelte b/packages/layerchart/src/routes/docs/components/Link/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/Link/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Link/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/Link/+page.ts b/packages/layerchart/src/routes/docs/components/Link/+page.ts index 0cd6f4224..328b7e72a 100644 --- a/packages/layerchart/src/routes/docs/components/Link/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Link/+page.ts @@ -8,7 +8,8 @@ export async function load() { api, source, pageSource, - related: ['components/Points', 'examples/Sankey', 'examples/Tree'], + supportedContexts: ['svg', 'canvas'], + related: ['components/Connector', 'components/Points', 'examples/Sankey', 'examples/Tree'], }, }; } diff --git a/packages/layerchart/src/routes/docs/components/Marker/+page.svelte b/packages/layerchart/src/routes/docs/components/Marker/+page.svelte index 0e24e2741..2c14ba2b7 100644 --- a/packages/layerchart/src/routes/docs/components/Marker/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Marker/+page.svelte @@ -1,34 +1,37 @@ @@ -59,18 +62,18 @@
{#each markerTypes as marker}
{marker}
-
+
- + - +
{/each} @@ -101,18 +104,18 @@
{#each markerTypes as marker}
{marker}
-
+
- + - +
{/each} @@ -121,7 +124,7 @@

Line

-
+
@@ -134,20 +137,22 @@
{#each markerTypes as marker}
{marker}
-
- - - - +
+ + {#snippet children({ context })} + + + + {/snippet}
{/each} @@ -158,9 +163,9 @@
-
+
- + - +
@@ -183,41 +188,41 @@
default (auto)
-
+
- + - +
0
-
+
- + - +
90
-
+
- + - +
diff --git a/packages/layerchart/src/routes/docs/components/Marker/+page.ts b/packages/layerchart/src/routes/docs/components/Marker/+page.ts index abc493315..e9b94ac02 100644 --- a/packages/layerchart/src/routes/docs/components/Marker/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Marker/+page.ts @@ -9,6 +9,7 @@ export async function load() { source, pageSource, description: 'Graphic used for drawing arrowheads or polymarkers on Line, Spline, etc', + supportedContexts: ['svg'], related: ['components/Spline', 'components/Line', 'components/Rule', 'components/GeoSpline'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/MotionPath/+page.svelte b/packages/layerchart/src/routes/docs/components/MotionPath/+page.svelte index ef178f4a4..9b2753323 100644 --- a/packages/layerchart/src/routes/docs/components/MotionPath/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/MotionPath/+page.svelte @@ -2,29 +2,32 @@ import type { ComponentProps } from 'svelte'; import { linear } from 'svelte/easing'; - import { Axis, Chart, MotionPath, Spline, Svg } from 'layerchart'; + import { Axis, Chart, Circle, Layer, MotionPath, Spline } from 'layerchart'; import { Field, RangeField, Switch, Toggle } from 'svelte-ux'; import Preview from '$lib/docs/Preview.svelte'; import CurveMenuField from '$lib/docs/CurveMenuField.svelte'; import PathDataMenuField from '$lib/docs/PathDataMenuField.svelte'; import Blockquote from '$lib/docs/Blockquote.svelte'; + import { shared } from '../../shared.svelte.js'; - let pointCount = 100; + let pointCount = $state(100); - let pathGenerator = (x: number) => x; - let curve: ComponentProps['value'] = undefined; + let pathGenerator = $state((x: number) => x); + let curve: ComponentProps['value'] = $state(undefined); - let amplitude = 1; - let frequency = 10; - let phase = 0; + let amplitude = $state(1); + let frequency = $state(10); + let phase = $state(0); - $: data = Array.from({ length: pointCount }).map((_, i) => { - return { - x: i + 1, - y: pathGenerator(i / pointCount) ?? i, - }; - }); + const data = $derived( + Array.from({ length: pointCount }).map((_, i) => { + return { + x: i + 1, + y: pathGenerator(i / pointCount) ?? i, + }; + }) + );

Examples

@@ -42,18 +45,20 @@
-
+
- + {#if show} - - - + + {#snippet children({ pathId, objectId })} + + + {/snippet} {/if} - +
@@ -72,31 +77,27 @@
-
+
- + {#if show} - - - + + {#snippet children({ pathId, objectId })} + + + {/snippet} {/if} - +
@@ -115,20 +116,22 @@
-
+
- + {#if show} {#key data} - - - + + {#snippet children({ pathId, objectId })} + + + {/snippet} {/key} {/if} - +
diff --git a/packages/layerchart/src/routes/docs/components/MotionPath/+page.ts b/packages/layerchart/src/routes/docs/components/MotionPath/+page.ts index 90f65e3bc..2f42686e5 100644 --- a/packages/layerchart/src/routes/docs/components/MotionPath/+page.ts +++ b/packages/layerchart/src/routes/docs/components/MotionPath/+page.ts @@ -9,6 +9,7 @@ export async function load() { source, pageSource, description: 'Animate an object along a path', + supportedContexts: ['svg'], related: ['components/Spline'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Pack/+page.svelte b/packages/layerchart/src/routes/docs/components/Pack/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/Pack/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Pack/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/Pack/+page.ts b/packages/layerchart/src/routes/docs/components/Pack/+page.ts index d833c7fda..dbb5c15b6 100644 --- a/packages/layerchart/src/routes/docs/components/Pack/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Pack/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/Pack'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Partition/+page.svelte b/packages/layerchart/src/routes/docs/components/Partition/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/Partition/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Partition/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/Partition/+page.ts b/packages/layerchart/src/routes/docs/components/Partition/+page.ts index 9cd7ecd09..6d85be411 100644 --- a/packages/layerchart/src/routes/docs/components/Partition/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Partition/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/Partition', 'examples/Sunburst'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Pattern/+page.svelte b/packages/layerchart/src/routes/docs/components/Pattern/+page.svelte index 8aad5d60f..b536fc6cd 100644 --- a/packages/layerchart/src/routes/docs/components/Pattern/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Pattern/+page.svelte @@ -1,6 +1,7 @@

Examples

@@ -8,41 +9,93 @@

Lines

-
+
- - - + + + {#snippet children({ pattern })} + + {/snippet} - - + + + {#snippet children({ pattern })} + + {/snippet} - - - + + + {#snippet children({ pattern })} + + {/snippet} - - + + + {#snippet children({ pattern })} + + {/snippet} - - + + + {#snippet children({ pattern })} + + {/snippet} - - - + + + {#snippet children({ pattern })} + + {/snippet} - {#each { length: 6 } as _, i} - - {/each} - +
@@ -50,47 +103,93 @@

Circles

-
+
- - - + + + {#snippet children({ pattern })} + + {/snippet} - - + + + {#snippet children({ pattern })} + + {/snippet} - - + + + {#snippet children({ pattern })} + + {/snippet} - - - - - - + + + {#snippet children({ pattern })} + + {/snippet} - - - - - - + + + {#snippet children({ pattern })} + + {/snippet} - - + + + {#snippet children({ pattern })} + + {/snippet} - {#each { length: 6 } as _, i} - - {/each} - +
@@ -98,53 +197,60 @@

With Fill color

-
+
- - - - + + + {#snippet children({ pattern })} + + {/snippet} - - - + + + {#snippet children({ pattern })} + + {/snippet} - - - - - - - + + + {#snippet children({ pattern })} + + {/snippet} - - - - - - - + + + {#snippet children({ pattern })} + + {/snippet} - - - + + + {#snippet children({ pattern })} + + {/snippet} - - - - + + + {#snippet children({ pattern })} + + {/snippet} - {#each { length: 6 } as _, i} - - {/each} - +
@@ -152,124 +258,333 @@

With LinearGradient

-
+
- - - - - - - - - + + + {#snippet children({ gradient })} + + + + {#snippet children({ pattern })} + + {/snippet} + + {/snippet} + + + + {#snippet children({ gradient })} + + + + {#snippet children({ pattern })} + + {/snippet} + + {/snippet} + + + + {#snippet children({ gradient })} + + + + {#snippet children({ pattern })} + + {/snippet} + + {/snippet} + + + + {#snippet children({ gradient })} + + + + {#snippet children({ pattern })} + + {/snippet} + + {/snippet} + + + + {#snippet children({ gradient })} + + + + {#snippet children({ pattern })} + + {/snippet} + + {/snippet} + + + + {#snippet children({ gradient })} + + + + {#snippet children({ pattern })} + + {/snippet} + + {/snippet} + + + +
+ + +

LinearGradient as Pattern

+ + +
+ + + + {#snippet children({ gradient })} + + {#snippet children({ pattern })} + + {/snippet} + + {/snippet} + + + + {#snippet children({ gradient })} + + {#snippet children({ pattern })} + + {/snippet} + + {/snippet} + + + + {#snippet children({ gradient })} + + {#snippet children({ pattern })} + + {/snippet} + + {/snippet} + + + + {#snippet children({ gradient })} + + {#snippet children({ pattern })} + + {/snippet} + + {/snippet} + + + + {#snippet children({ gradient })} + + {#snippet children({ pattern })} + + {/snippet} + + {/snippet} + + + +
+
+ +

Lines (custom pattern - svg only)

+ + +
+ + + + {#snippet patternContent()} + + {/snippet} + + {#snippet children({ pattern })} + + {/snippet} - - + + + {#snippet patternContent()} + + {/snippet} + + {#snippet children({ pattern })} + + {/snippet} - - - - - - + + + {#snippet patternContent()} + + + {/snippet} + + {#snippet children({ pattern })} + + {/snippet} - - - - - - + + + {#snippet patternContent()} + + {/snippet} + + {#snippet children({ pattern })} + + {/snippet} - - + + + {#snippet patternContent()} + + {/snippet} + + {#snippet children({ pattern })} + + {/snippet} - - - + + + {#snippet patternContent()} + + + {/snippet} + + {#snippet children({ pattern })} + + {/snippet} - {#each { length: 6 } as _, i} - - - {/each} - +
-

LinearGradient as Pattern

+

Circles (custom pattern - svg only)

-
+
- - - - - - + + + {#snippet patternContent()} + + {/snippet} - - + + + {#snippet patternContent()} + + {/snippet} - - + + + {#snippet patternContent()} + + {/snippet} - - + + + {#snippet patternContent()} + + + + + + {/snippet} - - + + + {#snippet patternContent()} + + + + + + {/snippet} - {#each { length: 5 } as _, i} + + + {#snippet patternContent()} + + {/snippet} + + + {#each { length: 6 } as _, i} {/each} - +
diff --git a/packages/layerchart/src/routes/docs/components/Pattern/+page.ts b/packages/layerchart/src/routes/docs/components/Pattern/+page.ts index cb64c34b6..dc6104793 100644 --- a/packages/layerchart/src/routes/docs/components/Pattern/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Pattern/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['components/LinearGradient', 'components/RadialGradient'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Pie/+page.svelte b/packages/layerchart/src/routes/docs/components/Pie/+page.svelte index bf1ae67a8..94424c661 100644 --- a/packages/layerchart/src/routes/docs/components/Pie/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Pie/+page.svelte @@ -1,27 +1,25 @@

Examples

-
- - - Svg - Canvas - - - - - - -
-

Basic

-
+
-

ontooltipclick

+

onTooltipClick

-
+
{ + onTooltipClick={(e, detail) => { console.log(e, detail); alert(JSON.stringify(detail)); }} @@ -78,7 +80,7 @@

Outer radius (fixed)

-
+
@@ -86,7 +88,7 @@

Outer radius (offset)

-
+
@@ -94,7 +96,7 @@

Donut (innerRadius)

-
+
Donut with inner text -
+
- + {#snippet aboveMarks()} d.value))} textAnchor="middle" @@ -137,7 +139,7 @@ class="text-sm fill-surface-content/50" dy={26} /> - + {/snippet}
@@ -145,7 +147,7 @@

Arc (range)

-
+
-

Single value

+
+

Segments

+ + +
- -
+ +
-
-
- -

Single value gradient with text

- - -
- - - - - - - - - - + > + {#snippet aboveMarks()} + + {/snippet}
-

Single value (arc) with custom color

- - -
- -
-
-

Series data

-
+
({ key, data }))} + series={Array.from(dataByYear, ([key, data]) => ({ key: key.toString(), data }))} outerRadius={-25} innerRadius={-20} cornerRadius={5} @@ -255,98 +215,16 @@
-

Series data (individual tracks)

- - -
- ({ key: d.fruit, data: [d] }))} - outerRadius={-25} - innerRadius={-20} - cornerRadius={10} - {renderContext} - {debug} - /> -
-
- -

Series data (arc)

- - -
- ({ key: d.fruit, data: [d] }))} - range={[-90, 90]} - outerRadius={-25} - innerRadius={-20} - cornerRadius={10} - props={{ group: { y: 70 } }} - {renderContext} - {debug} - /> -
-
- -

Series data (track color)

- - -
- ({ key: d.fruit, data: [d] }))} - props={{ - arc: { - track: { fill: 'hsl(var(--color-surface-content) / 10%)' }, - }, - }} - outerRadius={-25} - innerRadius={-20} - cornerRadius={10} - {renderContext} - {debug} - /> -
-
- -

Series data (individual tracks, max value, and color)

- - -
- { - return { - key: d.key, - data: [d], - maxValue: d.maxValue, - color: d.color, - }; - })} - outerRadius={-25} - innerRadius={-20} - cornerRadius={10} - {renderContext} - {debug} - /> -
-
-

Series props

-
+
Series data (arc click) -
+
({ key, data }))} + series={Array.from(dataByYear, ([key, data]) => ({ key: key.toString(), data }))} outerRadius={-25} innerRadius={-20} cornerRadius={5} padAngle={0.01} - onarcclick={(e, detail) => { + onArcClick={(e, detail) => { console.log(e, detail); alert(JSON.stringify(detail)); }} @@ -379,7 +257,7 @@

Inner component props (Arc class)

-
+
Legend -
+
@@ -402,7 +280,7 @@

Legend (placement with orientation)

-
+
+

Legend (small / responsive)

+ + +
+ +
+
+

Legend (custom label)

-
+
Customize colors (CSS variables) -
+
Customize colors (scheme) -
+
@@ -472,7 +373,7 @@

Customize colors (interpolator)

-
+
Customize colors (data prop) -
+
@@ -495,7 +396,7 @@

Legend with padding

-
+
Placement (left) -
+
Placement (right) -
+
Custom placement -
+
+

Offset Slice

+ + +
+ + {#snippet arc({ index, props })} + + {/snippet} + +
+
+ + +
+

Motion (tween)

+ + + +
+ +
+ {#if show} + + {/if} +
+
+
+ + +
+

Motion (spring)

+ + + +
+ +
+ {#if show} + + {/if} +
+
+
+ diff --git a/packages/layerchart/src/routes/docs/components/PieChart/+page.ts b/packages/layerchart/src/routes/docs/components/PieChart/+page.ts index d1eaceae4..ea872ac2a 100644 --- a/packages/layerchart/src/routes/docs/components/PieChart/+page.ts +++ b/packages/layerchart/src/routes/docs/components/PieChart/+page.ts @@ -9,6 +9,7 @@ export async function load() { source, pageSource, description: 'Streamlined Chart configuration for Pie charts', + supportedContexts: ['svg', 'canvas'], related: ['components/Chart', 'components/Pie', 'examples/Arc'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Point/+page.svelte b/packages/layerchart/src/routes/docs/components/Point/+page.svelte index 2a8781308..f9bafc5e1 100644 --- a/packages/layerchart/src/routes/docs/components/Point/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Point/+page.svelte @@ -1,6 +1,7 @@ @@ -8,25 +9,31 @@

Examples

-
+
d.x} y={(d) => d.y} xDomain={[0, 100]} yDomain={[0, 100]} - padding={{ bottom: 20, left: 20 }} + padding={{ top: 10, bottom: 20, left: 24, right: 10 }} > - + - - + + + {#snippet children({ x, y })} + + {/snippet} - - + + + {#snippet children({ x, y })} + + {/snippet} - +
diff --git a/packages/layerchart/src/routes/docs/components/Point/+page.ts b/packages/layerchart/src/routes/docs/components/Point/+page.ts index 5940b7f57..c99343062 100644 --- a/packages/layerchart/src/routes/docs/components/Point/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Point/+page.ts @@ -9,6 +9,7 @@ export async function load() { source, pageSource, description: 'Convenient way to translate a data item to SVG x/y coordinates', + supportedContexts: ['svg', 'canvas', 'html'], related: ['examples/Area'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Points/+page.svelte b/packages/layerchart/src/routes/docs/components/Points/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/Points/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Points/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/Points/+page.ts b/packages/layerchart/src/routes/docs/components/Points/+page.ts index 0e378e54b..4998d6d5a 100644 --- a/packages/layerchart/src/routes/docs/components/Points/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Points/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: [ 'components/Area', 'components/Spline', diff --git a/packages/layerchart/src/routes/docs/components/Polygon/+page.svelte b/packages/layerchart/src/routes/docs/components/Polygon/+page.svelte new file mode 100644 index 000000000..138e1d044 --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/Polygon/+page.svelte @@ -0,0 +1,408 @@ + + +

Playground

+ +
+ + + + + + + + + + +
+ +
+ + {#snippet children({ context })} + + + + {/snippet} + +
+ +

Examples

+ +
+

Simple

+ + +
+ +
+
+

Triangle

+ +
+ + {#snippet children({ context })} + + + + {/snippet} + +
+
+
+ +
+

Square

+ +
+ + {#snippet children({ context })} + + + + {/snippet} + +
+
+
+ +
+

Rectangle

+ +
+ + {#snippet children({ context })} + + + + {/snippet} + +
+
+
+ +
+

Diamond

+ +
+ + {#snippet children({ context })} + + + + {/snippet} + +
+
+
+ +
+

Rhombus

+ +
+ + {#snippet children({ context })} + + + + {/snippet} + +
+
+
+ +
+

Parallelogram

+ +
+ + {#snippet children({ context })} + + + + {/snippet} + +
+
+
+ +
+

Trapezoid

+ +
+ + {#snippet children({ context })} + + + + {/snippet} + +
+
+
+ +
+

Pentagon

+ +
+ + {#snippet children({ context })} + + + + {/snippet} + +
+
+
+ +
+

Hexagon

+ +
+ + {#snippet children({ context })} + + + + {/snippet} + +
+
+
+ +
+

Octagon

+ +
+ + {#snippet children({ context })} + + + + {/snippet} + +
+
+
+
+ +
+

Stars

+ + + +
+ +
+ {#each [6, 8, 10, 12, 14, 16, 18, 20] as points} +
+

{points} point

+ +
+ + {#snippet children({ context })} + + + + {/snippet} + +
+
+
+ {/each} +
+ +
+

Custom points

+ +
+ +
+
+

Arrow

+ +
+ + {#snippet children({ context })} + + + {@const size = 60} + + + + {/snippet} + +
+
+
+ +
+

Cross

+ +
+ + {#snippet children({ context })} + + + {@const size = 50} + + + + {/snippet} + +
+
+
+
diff --git a/packages/layerchart/src/routes/docs/components/Polygon/+page.ts b/packages/layerchart/src/routes/docs/components/Polygon/+page.ts new file mode 100644 index 000000000..0ecfdc6cc --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/Polygon/+page.ts @@ -0,0 +1,14 @@ +import api from '$lib/components/Polygon.svelte?raw&sveld'; +import source from '$lib/components/Polygon.svelte?raw'; +import pageSource from './+page.svelte?raw'; + +export async function load() { + return { + meta: { + api, + source, + pageSource, + supportedContexts: ['svg', 'canvas'], + }, + }; +} diff --git a/packages/layerchart/src/routes/docs/components/RadialGradient/+page.svelte b/packages/layerchart/src/routes/docs/components/RadialGradient/+page.svelte index 23ab5919d..f883254ec 100644 --- a/packages/layerchart/src/routes/docs/components/RadialGradient/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/RadialGradient/+page.svelte @@ -1,6 +1,7 @@ @@ -10,26 +11,27 @@

Focal location and radius with custom colors

-
+
- - - + + + {#snippet children({ gradient })} + + {/snippet} - - + + {#snippet children({ gradient })} + + {/snippet} - - + + {#snippet children({ gradient })} + + {/snippet} - +
@@ -37,9 +39,9 @@

Tailwind colors

-
+
- + @@ -50,9 +52,9 @@ {#each { length: 9 } as _, i} - + {/each} - +
@@ -60,31 +62,27 @@

spreadMethod

-
+
- - - + + + {#snippet children({ gradient })} + + {/snippet} - - + + {#snippet children({ gradient })} + + {/snippet} - - + + {#snippet children({ gradient })} + + {/snippet} - +
@@ -92,21 +90,25 @@

units `objectBoundingBox` (default) vs `userSpaceOnUse`

-
+
- - - {#each { length: 6 } as _, i} - - {/each} + + + {#snippet children({ gradient })} + {#each { length: 6 } as _, i} + + {/each} + {/snippet} - - {#each { length: 6 } as _, i} - - {/each} + + {#snippet children({ gradient })} + {#each { length: 6 } as _, i} + + {/each} + {/snippet} - +
diff --git a/packages/layerchart/src/routes/docs/components/RadialGradient/+page.ts b/packages/layerchart/src/routes/docs/components/RadialGradient/+page.ts index 3b7a2a354..769763703 100644 --- a/packages/layerchart/src/routes/docs/components/RadialGradient/+page.ts +++ b/packages/layerchart/src/routes/docs/components/RadialGradient/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg'], // TODO: `canvas` coming soon related: ['components/LinearGradient', 'components/Pattern'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Rect/+page.svelte b/packages/layerchart/src/routes/docs/components/Rect/+page.svelte index dbc62960a..2ed03e59d 100644 --- a/packages/layerchart/src/routes/docs/components/Rect/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Rect/+page.svelte @@ -1,20 +1,88 @@

Examples

+

Styling using classes

+ + +
+ + + + + + + + + +
+
+ +

Styling using attributes

+ + +
+ + + + + + + + + +
+
+ +

Styling using CSS variables

+ -
- - +
+ + - - - + + +
diff --git a/packages/layerchart/src/routes/docs/components/Rect/+page.ts b/packages/layerchart/src/routes/docs/components/Rect/+page.ts index 37b779489..643927949 100644 --- a/packages/layerchart/src/routes/docs/components/Rect/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Rect/+page.ts @@ -9,6 +9,7 @@ export async function load() { source, pageSource, description: '`` element with tweened properties using `motionStore`', + supportedContexts: ['svg', 'canvas', 'html'], related: [ 'components/Bars', 'components/Highlight', diff --git a/packages/layerchart/src/routes/docs/components/RectClipPath/+page.svelte b/packages/layerchart/src/routes/docs/components/RectClipPath/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/RectClipPath/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/RectClipPath/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/RectClipPath/+page.ts b/packages/layerchart/src/routes/docs/components/RectClipPath/+page.ts index 30c0b1c15..b841ed4ed 100644 --- a/packages/layerchart/src/routes/docs/components/RectClipPath/+page.ts +++ b/packages/layerchart/src/routes/docs/components/RectClipPath/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg'], related: ['components/ChartClipPath', 'examples/Partition', 'examples/Treemap'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Rule/+page.svelte b/packages/layerchart/src/routes/docs/components/Rule/+page.svelte index b6cffb912..85f6bf2a1 100644 --- a/packages/layerchart/src/routes/docs/components/Rule/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Rule/+page.svelte @@ -1,9 +1,16 @@

Examples

@@ -11,17 +18,17 @@

x and y baselines

-
+
- + - +
@@ -29,17 +36,17 @@

top right x and y baselines

-
+
- + - - + +
@@ -47,17 +54,17 @@

x baseline with negative values

-
+
- + - +
@@ -65,17 +72,17 @@

x annotation

-
+
- + - +
@@ -83,18 +90,17 @@

y baseline with negative values

-
+
- + - +
@@ -102,17 +108,96 @@

y annotation

-
+
- + - + + +
+ + +

data driven (x time / y value)

+ + +
+ + + + + + + +
+
+ +

data driven (x band / y value)

+ + +
+ + + + + + + +
+
+ +

data driven (x range)

+ + +
+ + + + + + + +
+
+ +

data driven (y range)

+ + +
+ + + + + +
diff --git a/packages/layerchart/src/routes/docs/components/Rule/+page.ts b/packages/layerchart/src/routes/docs/components/Rule/+page.ts index 9e98af688..e58e8af90 100644 --- a/packages/layerchart/src/routes/docs/components/Rule/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Rule/+page.ts @@ -1,14 +1,21 @@ +import { autoType, csvParse } from 'd3-dsv'; +import type { AlphabetData } from '$static/data/examples/alphabet.js'; + import api from '$lib/components/Rule.svelte?raw&sveld'; import source from '$lib/components/Rule.svelte?raw'; import pageSource from './+page.svelte?raw'; export async function load() { return { + alphabet: (await fetch('/data/examples/alphabet.csv').then(async (r) => + csvParse(await r.text(), autoType) + )) as AlphabetData[], meta: { api, source, pageSource, - related: ['components/Axis', 'components/Line'], + supportedContexts: ['svg', 'canvas'], + related: ['components/Axis', 'components/Line', 'examples/Candlestick', 'examples/Duration'], }, }; } diff --git a/packages/layerchart/src/routes/docs/components/Sankey/+page.svelte b/packages/layerchart/src/routes/docs/components/Sankey/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/Sankey/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Sankey/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/Sankey/+page.ts b/packages/layerchart/src/routes/docs/components/Sankey/+page.ts index 69fb6b3ab..69d49fbc5 100644 --- a/packages/layerchart/src/routes/docs/components/Sankey/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Sankey/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/Sankey'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte b/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte index 645756b0e..1b517322b 100644 --- a/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte @@ -1,61 +1,51 @@ + const pengiunSeries = $derived( + penguinDataBySpecies.map(([species, data], i) => { + return { + key: species, + data, + color: ['var(--color-primary)', 'var(--color-secondary)', 'var(--color-success)'][i], + }; + }) + ); -

Examples

+ const dateSeriesData = createDateSeries({ count: 30, min: 20, max: 100, value: 'integer' }); -
- - - Svg - Canvas - - + let renderContext = $derived(shared.renderContext as 'svg' | 'canvas'); + let debug = $derived(shared.debug); + - - - -
+

Examples

Basic

-
+
@@ -63,7 +53,7 @@

Domain padding

-
+
Radius via rScale -
+
@@ -97,7 +93,7 @@

0 baseline/domain

-
+
Series -
+
{ + const color = ['var(--color-primary)', 'var(--color-secondary)', 'var(--color-success)'][i]; return { key: species, data, - color: [ - 'hsl(var(--color-primary))', - 'hsl(var(--color-secondary))', - 'hsl(var(--color-success))', - ][i], + color, + props: { + stroke: color, + fillOpacity: 0.3, + }, }; })} {renderContext} @@ -137,21 +134,22 @@

Series with radius

-
+
{ + const color = ['var(--color-primary)', 'var(--color-secondary)', 'var(--color-success)'][i]; return { key: species, data, - color: [ - 'hsl(var(--color-primary))', - 'hsl(var(--color-secondary))', - 'hsl(var(--color-success))', - ][i], + color, + props: { + stroke: color, + fillOpacity: 0.3, + }, }; })} {renderContext} @@ -163,7 +161,7 @@

Labels

-
+
@@ -171,7 +169,7 @@

Legend

-
+
Legend (show/hide series with tweening) -
+
Legend (custom labels) -
+
Single axis (x) -
+
@@ -252,7 +242,7 @@

Single axis (y)

-
+
@@ -267,28 +257,52 @@ y={(d) => 0} axis={false} grid={false} - props={{ highlight: { lines: false } }} + props={{ + points: { opacity: 0.3 }, + highlight: { lines: false }, + }} {renderContext} {debug} > - - - {format(x(data))} + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {format(context.x(data))} + {/snippet} - + {/snippet}
+

Date series with threshold color scale

+ + +
+ +
+
+

Tooltip click

-
+
{ + onTooltipClick={(e, detail) => { console.log(e, detail); alert(JSON.stringify(detail)); }} @@ -301,47 +315,230 @@

Custom tooltip

-
+
- + {#snippet tooltip({ context })} - {format(y(data), 'integer')} + {#snippet children({ data })} + {format(context.y(data), 'integer')} + {/snippet} - {format(x(data), 'integer')} + {#snippet children({ data })} + {format(context.x(data), 'integer')} + {/snippet} - + {/snippet}
+

Point annotations

+ + +
+ +
+
+ +
+ See also: AnnotationPoint for more examples +
+ +

Line annotations

+ + +
+ +
+
+ +
+ See also: AnnotationLine for more examples +
+ +

Range annotations (vertical)

+ + +
+ +
+
+ +
+ See also: AnnotationRange for more examples +
+ +

Range annotations (horizontal)

+ + +
+ +
+
+ +
+ See also: AnnotationRange for more examples +
+ +

Range annotations (both)

+ + +
+ +
+
+

Brushing

-
+
Custom chart -
- - - - - - - - - - {format(x(data), 'integer')} - - - - +
+ + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {format(context.x(data), 'integer')} + + + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/components/ScatterChart/+page.ts b/packages/layerchart/src/routes/docs/components/ScatterChart/+page.ts index caeed1dcc..cf7ea3f9e 100644 --- a/packages/layerchart/src/routes/docs/components/ScatterChart/+page.ts +++ b/packages/layerchart/src/routes/docs/components/ScatterChart/+page.ts @@ -6,7 +6,7 @@ import pageSource from './+page.svelte?raw'; import type { PenguinsData } from '$static/data/examples/penguins.js'; -export async function load() { +export async function load({ fetch }) { return { penguins: (await fetch('/data/examples/penguins.csv').then(async (r) => csvParse(await r.text(), autoType) @@ -16,6 +16,7 @@ export async function load() { source, pageSource, description: 'Streamlined Chart configuration for Scatter charts', + supportedContexts: ['svg', 'canvas'], related: ['components/Chart', 'components/Points', 'examples/Scatter'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Spline/+page.svelte b/packages/layerchart/src/routes/docs/components/Spline/+page.svelte index 90f48faff..20c62ce47 100644 --- a/packages/layerchart/src/routes/docs/components/Spline/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Spline/+page.svelte @@ -1,33 +1,36 @@

Playground

@@ -42,21 +45,14 @@
-
+
- - - Svg - Canvas - - - - tweened + tween draw none @@ -65,25 +61,29 @@
-
+
- + {#if show} {#if showPoints} - + {/if} {/if} - +
@@ -103,15 +103,15 @@
-
+
- + {#if show} {/if} - +
@@ -130,15 +130,15 @@
-
+
- + {#if show} - + {/if} - +
@@ -157,9 +157,9 @@
-
+
- + {#if show} @@ -170,13 +170,13 @@ markerEnd={{ type: 'arrow', class: 'stroke-2' }} /> {/if} - +
-

basic start and end slots

+

basic start and end snippets

@@ -189,24 +189,28 @@
-
+
- + {#if show} - - + {#snippet startContent()} + + {/snippet} + {#snippet endContent()} + + {/snippet} {/if} - +
-

label using start/end slots

+

label using start/end snippets

@@ -219,31 +223,31 @@
-
+
- + {#if show} - - + {#snippet startContent()} + - + {/snippet} - - + {#snippet endContent()} + - + {/snippet} {/if} - +
-

end slot with draw

+

end snippet with draw

@@ -256,23 +260,25 @@
-
+
- + {#if show} - + {#snippet endContent()} + + {/snippet} {/if} - +
-

Canvas

+

end slot with draw with value

@@ -281,21 +287,29 @@ - +
-
- - +
+ + - - {#if show} - + + {#snippet endContent({ value })} + + + {/snippet} + {/if} - +
diff --git a/packages/layerchart/src/routes/docs/components/Spline/+page.ts b/packages/layerchart/src/routes/docs/components/Spline/+page.ts index 2674058e7..3c7292017 100644 --- a/packages/layerchart/src/routes/docs/components/Spline/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Spline/+page.ts @@ -10,6 +10,7 @@ export async function load() { pageSource, description: '`` using `d3-shape` line generator to support `curve` and `defined`. Works as data-driven via context or `data` prop, or pre-made `pathData`. Adding tweening via `d3-interpolate-path`', + supportedContexts: ['svg', 'canvas'], related: ['components/MotionPath', 'examples/Sparkline'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Text/+page.svelte b/packages/layerchart/src/routes/docs/components/Text/+page.svelte index 5fc744c1f..98152daef 100644 --- a/packages/layerchart/src/routes/docs/components/Text/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Text/+page.svelte @@ -1,34 +1,47 @@ + const config = $state({ + x: 0, + y: 0, + value: 'This is really long text', + width: 300, + textAnchor: 'start' as ComponentProps['textAnchor'], + verticalAnchor: 'start' as ComponentProps['verticalAnchor'], + lineHeight: '1em', + rotate: 0, + scaleToFit: false, + showAnchor: true, + resizeSvg: true, + }); -

Examples

+ let truncate = $state(false); + + const truncateOptions: TruncateTextOptions = $state({ + maxChars: 22, + minChars: 0, + ellipsis: '…', + position: 'end', + }); + -

Playground

+

Playground

- +
- - - + + + - + start middle end @@ -36,7 +49,13 @@ - + start middle end @@ -44,43 +63,82 @@ - + - + - + - + - + + + + + {#if truncate} + + + + + + start + middle + end + + + {/if}
-
-
+
+ {#each ['svg', 'canvas', 'html'] as const as type} +
+

{toTitleCase(type)}

+
+
+ + + + {#if config.showAnchor} + + {/if} + + +
+
+
+ {/each} +
+ +

Examples

+ +

Word wrap with explicit `\n`

+ + +
- - - - {#if showAnchor} - - {/if} - + + +
-
+ diff --git a/packages/layerchart/src/routes/docs/components/Text/+page.ts b/packages/layerchart/src/routes/docs/components/Text/+page.ts index 8cf48d4b7..875f03bb0 100644 --- a/packages/layerchart/src/routes/docs/components/Text/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Text/+page.ts @@ -5,6 +5,10 @@ import pageSource from './+page.svelte?raw'; export async function load() { return { meta: { + api, + source, + pageSource, + supportedContexts: ['svg', 'canvas', 'html'], features: [ 'Adjustable anchor/origin point (center horizontally and vertically)', 'Rotate (based on origin)', @@ -12,9 +16,6 @@ export async function load() { 'Scale to fit', 'Easy offset with `dx` and `dy`', ], - api, - source, - pageSource, }, }; } diff --git a/packages/layerchart/src/routes/docs/components/Threshold/+page.svelte b/packages/layerchart/src/routes/docs/components/Threshold/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/Threshold/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Threshold/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/Threshold/+page.ts b/packages/layerchart/src/routes/docs/components/Threshold/+page.ts index 4f0ac202f..ed36c5aad 100644 --- a/packages/layerchart/src/routes/docs/components/Threshold/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Threshold/+page.ts @@ -10,6 +10,7 @@ export async function load() { pageSource, description: 'Areas between two values (`y={["value", "baseline"]}`) depending on which is greater (ex. green/red)', + supportedContexts: ['svg'], // dependency on ClipPath getting canvas support related: ['examples/Threshold'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/TileImage/+page.svelte b/packages/layerchart/src/routes/docs/components/TileImage/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/TileImage/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/TileImage/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/TileImage/+page.ts b/packages/layerchart/src/routes/docs/components/TileImage/+page.ts index d475db034..fc0ad2547 100644 --- a/packages/layerchart/src/routes/docs/components/TileImage/+page.ts +++ b/packages/layerchart/src/routes/docs/components/TileImage/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['components/GeoTile'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Tooltip/+page.svelte b/packages/layerchart/src/routes/docs/components/Tooltip/+page.svelte index 6ce0efd25..71672c40d 100644 --- a/packages/layerchart/src/routes/docs/components/Tooltip/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Tooltip/+page.svelte @@ -1,16 +1,28 @@

Examples

@@ -134,32 +146,29 @@

Basic

-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - {format(data.date, 'eee, MMMM do')} - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -168,28 +177,23 @@

Custom content

-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - Anything can go here test + + Anything can go here test
@@ -197,32 +201,29 @@

color swatch

-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - {format(data.date, 'eee, MMMM do')} - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -231,32 +232,29 @@

color swatch using theme

-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - {format(data.date, 'eee, MMMM do')} - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -265,34 +263,29 @@

invert variant

-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - - {format(data.date, 'eee, MMMM do')} - - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -302,32 +295,29 @@

Default (mouse position with offset)

-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - {format(data.date, 'eee, MMMM do')} - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -336,32 +326,29 @@

Data snapping

-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - {format(data.date, 'eee, MMMM do')} - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -370,50 +357,49 @@

Multiple tooltips with fixed single axis

-
+
- - - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - - - - - {data.value} - - - - {formatDate(data.date, PeriodType.Day)} - + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {data.value} + {/snippet} + + + + {#snippet children({ data })} + {format(data.date, 'day')} + {/snippet} + + {/snippet}
@@ -421,7 +407,7 @@

Multiple tooltips with fixed single axis (scaleBand)

-
+
- - - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - - + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {data.value} + {/snippet} + + + + {#snippet children({ data })} + {format(data.date, 'day')} + {/snippet} + + {/snippet} + +
+ - - {data.value} - +

Disable motion

- - {formatDate(data.date, PeriodType.Day)} + +
+ + + + + + + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -517,27 +534,22 @@
-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - + - {format(data.date, 'eee, MMMM do')} - - - + {#snippet children({ data })} + + + + + {/snippet}
@@ -560,41 +573,40 @@
- {#if $tooltipContext?.data} - date: {formatDate($tooltipContext?.data?.date, PeriodType.Day, { variant: 'short' })} - value: {$tooltipContext?.data?.value} - {:else} - [hover chart] + {#if context} + {#if context.tooltip.data} + date: {format(context.tooltip.data.date, 'day', { variant: 'short' })} + value: {context.tooltip.data.value} + {:else} + [hover chart] + {/if} {/if}
-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - {format(data.date, 'eee, MMMM do')} - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -604,16 +616,15 @@

Area x: scaleTime, y: scaleLinear

- bisect-x recommended. voronoi and quadtree supported. bounds and band to be improved + quadtree-x recommended. bisect-x, voronoi, and quadtree supported. bounds and band to be improved -
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - + - {format(data.date, 'eee, MMMM do')} - - - + {#snippet children({ data })} + + + + + {/snippet}
@@ -659,62 +667,58 @@ -
+
d.data.date} - xScale={scaleTime()} + x={(d) => asAny(d).data.date} y={[0, 1]} yNice c="key" - cScale={scaleOrdinal()} cDomain={keys} - cRange={['hsl(var(--color-info))', 'hsl(var(--color-success))', 'hsl(var(--color-warning))']} + cRange={['var(--color-info)', 'var(--color-success)', 'var(--color-warning)']} padding={{ left: 16, bottom: 24 }} tooltip={{ mode: charts.areaStack.mode, debug: charts.areaStack.debug, }} - let:cGet > - - - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + {#snippet children({ context })} + + + + + {#each stackData as seriesData} + {@const color = context.cGet(seriesData)} + + {/each} - {#each stackData as seriesData} - {@const color = cGet(seriesData)} - - {/each} - - - - - {format(data.data.date, 'eee, MMMM do')} - - {#each keys as key} - - {/each} - - + + + {#snippet children({ data })} + + + {#each keys as key} + + {/each} + + {/snippet} + + {/snippet}
@@ -726,39 +730,42 @@ -
+
- + - format(d, 'h:mm aa')} /> - + + - + - {data.name} - - - + {#snippet children({ data })} + {data.name} + + + + {/snippet}
@@ -773,44 +780,52 @@ -
+
- + - format(d, 'h:mm aa')} /> - + + + - + - {data.name} - - - - - - - - + {#snippet children({ data })} + {data.name} + + + + + + + + + {/snippet}
@@ -824,44 +839,52 @@ -
+
- + - format(d, 'h:mm aa')} /> - + + + - + - {data.name} - - - - - - - - + {#snippet children({ data })} + {data.name} + + + + + + + + + {/snippet}
@@ -876,7 +899,7 @@ -
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - + - {format(data.date, 'eee, MMMM do')} - - - + {#snippet children({ data })} + + + + + {/snippet}
@@ -930,7 +950,7 @@ -
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - + - {format(data.date, 'eee, MMMM do')} - - - - + {#snippet children({ data })} + + + + + + {/snippet}
@@ -990,7 +1007,7 @@ -
+
- + @@ -1011,16 +1028,17 @@ area={charts.scatter.highlight.includes('area')} axis={charts.scatter.axis} /> - + - - - - + {#snippet children({ data })} + + + + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/components/Tooltip/+page.ts b/packages/layerchart/src/routes/docs/components/Tooltip/+page.ts index 4f53cbfce..c67bef5af 100644 --- a/packages/layerchart/src/routes/docs/components/Tooltip/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Tooltip/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], features: [ 'Various modes', [ @@ -15,7 +16,7 @@ export async function load() { 'band', 'bounds', 'voronoi', - 'quadtree', + 'quadtree, quadtree-x, quadtree-y', 'manual (ex. path, etc)', ], 'Flexible positioning', diff --git a/packages/layerchart/src/routes/docs/components/Tooltip/TooltipControls.svelte b/packages/layerchart/src/routes/docs/components/Tooltip/TooltipControls.svelte index d0691f6dc..b67999bbe 100644 --- a/packages/layerchart/src/routes/docs/components/Tooltip/TooltipControls.svelte +++ b/packages/layerchart/src/routes/docs/components/Tooltip/TooltipControls.svelte @@ -5,17 +5,21 @@ import type TooltipContext from '$lib/components/tooltip/TooltipContext.svelte'; import type Highlight from '$lib/components/Highlight.svelte'; - type TooltipContextProps = ComponentProps; - type HighlightProps = ComponentProps; + type TooltipContextProps = ComponentProps; + type HighlightProps = ComponentProps; - export let settings: { - mode: TooltipContextProps['mode']; - highlight: Array<'none' | 'points' | 'lines' | 'area' | 'bar'>; - axis: HighlightProps['axis']; - snapToDataX: boolean; - snapToDataY: boolean; - debug: TooltipContextProps['debug']; - }; + let { + settings = $bindable(), + }: { + settings: { + mode: TooltipContextProps['mode']; + highlight: Array<'none' | 'points' | 'lines' | 'area' | 'bar'>; + axis: HighlightProps['axis']; + snapToDataX: boolean; + snapToDataY: boolean; + debug: TooltipContextProps['debug']; + }; + } = $props();
@@ -30,6 +34,8 @@ { label: 'bounds', value: 'bounds' }, { label: 'voronoi', value: 'voronoi' }, { label: 'quadtree', value: 'quadtree' }, + { label: 'quadtree-x', value: 'quadtree-x' }, + { label: 'quadtree-y', value: 'quadtree-y' }, ]} /> diff --git a/packages/layerchart/src/routes/docs/components/TooltipContext/+page.svelte b/packages/layerchart/src/routes/docs/components/TooltipContext/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/TooltipContext/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/TooltipContext/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/TooltipContext/+page.ts b/packages/layerchart/src/routes/docs/components/TooltipContext/+page.ts index c0b1b388f..4453243d1 100644 --- a/packages/layerchart/src/routes/docs/components/TooltipContext/+page.ts +++ b/packages/layerchart/src/routes/docs/components/TooltipContext/+page.ts @@ -10,6 +10,7 @@ export async function load() { pageSource, description: 'Setup tooltip context, include mode to identify related data based on pointer position. Typically used indirectly via the `tooltip` prop Chart', + supportedContexts: ['svg', 'canvas'], related: ['components/Chart', 'components/Tooltip', 'components/Highlight'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/TransformContext/+page.svelte b/packages/layerchart/src/routes/docs/components/TransformContext/+page.svelte index b197c84e5..2ce3eb79a 100644 --- a/packages/layerchart/src/routes/docs/components/TransformContext/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/TransformContext/+page.svelte @@ -2,23 +2,26 @@ import type { ComponentProps } from 'svelte'; import { cubicOut } from 'svelte/easing'; - import { Chart, Circle, Html, Points, Spline, Svg } from 'layerchart'; + import { Chart, Circle, Layer, Points, Spline } from 'layerchart'; import TransformControls from '$lib/components/TransformControls.svelte'; import { Field, RangeField, Switch } from 'svelte-ux'; import Preview from '$lib/docs/Preview.svelte'; import CurveMenuField from '$lib/docs/CurveMenuField.svelte'; import { getSpiral } from '$lib/utils/genData.js'; + import { shared } from '../../shared.svelte.js'; - let pointCount = 500; - let angle = 137.5; // - let showPoints = true; - let showPath = false; - let tweened = true; + let pointCount = $state(500); + let angle = $state(137.5); // + let showPoints = $state(true); + let showPath = $state(false); + let tweened = $state(true); - $: data = getSpiral({ angle, radius: 10, count: pointCount, width: 500, height: 500 }); + const data = $derived( + getSpiral({ angle, radius: 10, count: pointCount, width: 500, height: 500 }) + ); - let curve: ComponentProps['value'] = undefined; + let curve: ComponentProps['value'] = $state(undefined);

Examples

@@ -42,36 +45,38 @@
-
+
- + {#if showPath} - + {/if} {#if showPoints} - - {#each points as point, index} - - {/each} + + {#snippet children({ points })} + {#each points as point, index} + + {/each} + {/snippet} {/if} - +
@@ -79,22 +84,22 @@

Pan/Zoom SVG image

-
+
- + - +
@@ -102,16 +107,16 @@

Pan/Zoom HTML image

-
+
- +
- +
diff --git a/packages/layerchart/src/routes/docs/components/TransformContext/+page.ts b/packages/layerchart/src/routes/docs/components/TransformContext/+page.ts index e0d008421..59863711d 100644 --- a/packages/layerchart/src/routes/docs/components/TransformContext/+page.ts +++ b/packages/layerchart/src/routes/docs/components/TransformContext/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas', 'html'], related: [ 'components/Chart', 'examples/Pack', diff --git a/packages/layerchart/src/routes/docs/components/Tree/+page.svelte b/packages/layerchart/src/routes/docs/components/Tree/+page.svelte index f5e63b239..1f3a40471 100644 --- a/packages/layerchart/src/routes/docs/components/Tree/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Tree/+page.svelte @@ -1,4 +1,4 @@ diff --git a/packages/layerchart/src/routes/docs/components/Tree/+page.ts b/packages/layerchart/src/routes/docs/components/Tree/+page.ts index 6c3c40d2b..78fdef251 100644 --- a/packages/layerchart/src/routes/docs/components/Tree/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Tree/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/Tree'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Treemap/+page.svelte b/packages/layerchart/src/routes/docs/components/Treemap/+page.svelte index f5e63b239..0db90f743 100644 --- a/packages/layerchart/src/routes/docs/components/Treemap/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Treemap/+page.svelte @@ -1,4 +1,397 @@ + +

Example

+ +

Playground

+ +
+
+ + + Squarify + Resquarify + Binary + Slice + Dice + Slice / Dice + + + + + No + Yes + + + + + Children + Depth + Parent + + +
+
+ + +
+
+ + + + +
+
+ + +
+ + {#snippet children({ context })} + + + {#snippet children({ nodes })} + {#each nodes as node} + context.tooltip.show(e, node)} + onpointerleave={context.tooltip.hide} + > + {@const nodeWidth = node.x1 - node.x0} + {@const nodeHeight = node.y1 - node.y0} + {@const nodeColor = getNodeColor(node, colorBy)} + + + + {node.data.name} + {#if node.children} + + {format(node.value ?? 0, 'integer')} + + {/if} + + + {#if !node.children} + + {/if} + + + {/each} + {/snippet} + + + + + {#snippet children({ data })} + {data.data.name} + + + + {/snippet} + + {/snippet} + +
+
+ +

Complex

+ +
+
+ + + Squarify + Resquarify + Binary + Slice + Dice + Slice / Dice + + + + + No + Yes + + + + + Children + Depth + Parent + + +
+
+ + +
+
+ + + + +
+
+ + +
+ + {#snippet children({ context })} + + + {#snippet children({ nodes })} + {#each nodes as node} + context.tooltip.show(e, node)} + onpointerleave={context.tooltip.hide} + > + {@const nodeWidth = node.x1 - node.x0} + {@const nodeHeight = node.y1 - node.y0} + {@const nodeColor = getNodeColor(node, colorBy)} + + + + {node.data.name} + {#if node.children} + + {format(node.value ?? 0, 'integer')} + + {/if} + + + {#if !node.children} + + {/if} + + + {/each} + {/snippet} + + + + + {#snippet children({ data })} + {data.data.name} + + + + {/snippet} + + {/snippet} + +
+
+ +

Simple / flat

+ + +
+ + + + {#snippet children({ nodes })} + {#each nodes.filter((n) => n.depth > 0) as node} + + {@const nodeWidth = node.x1 - node.x0} + {@const nodeHeight = node.y1 - node.y0} + + + + {/each} + {/snippet} + + + +
+
diff --git a/packages/layerchart/src/routes/docs/components/Treemap/+page.ts b/packages/layerchart/src/routes/docs/components/Treemap/+page.ts index 974f58f2d..252822ce6 100644 --- a/packages/layerchart/src/routes/docs/components/Treemap/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Treemap/+page.ts @@ -4,10 +4,62 @@ import pageSource from './+page.svelte?raw'; export async function load() { return { + flare: await fetch('/data/examples/hierarchy/flare.json').then((r) => r.json()), + population: { + name: 'World', + children: [ + { + name: 'Europe', + children: [ + { name: 'Western Europe', value: 200 }, // ~200M based on UN data + { name: 'Southern Europe', value: 151 }, // ~151M based on UN data + { name: 'Eastern Europe', value: 284 }, // ~284M based on UN data + { name: 'Northern Europe', value: 109 }, // ~109M based on UN data + ], + }, + { + name: 'Asia', + children: [ + { name: 'East Asia', value: 1652 }, // 1,652M based on UN data + { name: 'South Asia', value: 2085 }, // 2,085M based on UN data + { name: 'Southeast Asia', value: 700 }, // 700M based on UN data + { name: 'Western Asia', value: 314 }, // 314M based on UN data + { name: 'Central Asia', value: 84 }, // 84M based on UN data + ], + }, + { + name: 'North America', + children: [ + { name: 'Northern America', value: 388 }, // ~388M based on UN data + { name: 'Central America', value: 184 }, // ~184M (estimated from total minus Northern America) + ], + }, + { + name: 'South America', + children: [{ name: 'South America', value: 434 }], // ~434M based on UN data + }, + { + name: 'Africa', + children: [ + { name: 'Western Africa', value: 467 }, // 467M based on UN data + { name: 'Southern Africa', value: 74 }, // 74M based on UN data + { name: 'Northern Africa', value: 276 }, // 276M based on UN data + { name: 'Eastern Africa', value: 513 }, // 513M based on UN data + { name: 'Middle Africa', value: 220 }, // 220M based on UN data + ], + }, + { + name: 'Oceania', + children: [{ name: 'Oceania', value: 47 }], // 47M based on UN data + }, + ], + }, + meta: { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/Treemap'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Voronoi/+page.svelte b/packages/layerchart/src/routes/docs/components/Voronoi/+page.svelte index e6fe2e89e..2af77d713 100644 --- a/packages/layerchart/src/routes/docs/components/Voronoi/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Voronoi/+page.svelte @@ -1,58 +1,49 @@ -

Svg

+ let radius = $state(0); + - -
- - - - - - - - - -
-
+

Example

-

Canvas

+ -
- - - - - - - - +
+ + {#snippet children({ context })} + + + + + + + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/components/Voronoi/+page.ts b/packages/layerchart/src/routes/docs/components/Voronoi/+page.ts index 036a98d98..8339061e8 100644 --- a/packages/layerchart/src/routes/docs/components/Voronoi/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Voronoi/+page.ts @@ -8,6 +8,7 @@ export async function load() { api, source, pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte index b25339803..7e6635542 100644 --- a/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte @@ -3,38 +3,41 @@ import { feature } from 'topojson-client'; import { index } from 'd3-array'; - import { mdiPlay, mdiStop } from '@mdi/js'; + import LucidePlay from '~icons/lucide/play'; + import LucideSquare from '~icons/lucide/square'; - import { Canvas, Chart, GeoPath, Graticule, Tooltip, TransformContext, Svg } from 'layerchart'; - import { Button, ButtonGroup, Field, Switch, ToggleGroup, ToggleOption } from 'svelte-ux'; + import { Chart, GeoPath, Graticule, Layer, Tooltip, type ChartContextValue } from 'layerchart'; + import { Button, ButtonGroup } from 'svelte-ux'; import { sortFunc } from '@layerstack/utils'; import { scrollIntoView } from '@layerstack/svelte-actions'; import { cls } from '@layerstack/tailwind'; - import { timerStore } from '@layerstack/svelte-stores'; + import { TimerState } from '@layerstack/svelte-state'; import Preview from '$lib/docs/Preview.svelte'; import GeoDebug from '$lib/docs/GeoDebug.svelte'; import TransformDebug from '$lib/docs/TransformDebug.svelte'; import { timings } from './timings.js'; - import type { Component } from 'svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); const countries = feature(data.geojson, data.geojson.objects.countries); - let Context: Component = Svg; - let transformContext: TransformContext; + let context = $state(null!); - let selectedFeature: (typeof countries.features)[0] | null; - $: if (selectedFeature) { - const centroid = geoCentroid(selectedFeature); + let selectedFeature: (typeof countries.features)[0] | null = $state(null); - transformContext.setTranslate({ - x: -centroid[0], - y: -centroid[1], - }); - } + $effect.pre(() => { + if (selectedFeature && context?.transform) { + const centroid = geoCentroid(selectedFeature); + + context.transform.setTranslate({ + x: -centroid[0], + y: -centroid[1], + }); + } + }); // Animate to Yakko's song // https://animaniacs.fandom.com/wiki/Yakko%27s_World_(song)#New_Updated_Verse @@ -53,22 +56,27 @@ }); // Set to jump to a country - let currentIndex = -1; - let isPlaying = false; - - $: if (isPlaying && ($audioCurrentTime ?? 0) >= countryTimings[currentIndex + 1]?.audioTime) { - const countryName = countryTimings[currentIndex + 1].country; - selectedFeature = countryFeaturesByName.get(countryName) ?? null; - currentIndex += 1; - } + let currentIndex = $state(-1); + let isPlaying = $state(false); + + $effect(() => { + if ( + isPlaying && + (audioCurrentTime.current ?? 0) >= countryTimings[currentIndex + 1]?.audioTime + ) { + const countryName = countryTimings[currentIndex + 1].country; + selectedFeature = countryFeaturesByName.get(countryName) ?? null; + currentIndex += 1; + } + }); const audioFile = new Audio('/audio/yakko_world.mp3'); audioFile.addEventListener('ended', () => stop()); - const audioCurrentTime = timerStore({ + const audioCurrentTime = new TimerState({ initial: 0, delay: 100, - onTick: () => audioFile.currentTime, + tick: () => audioFile.currentTime, }); async function play() { @@ -85,24 +93,11 @@ selectedFeature = null; } - let debug = false; + let debug = $derived(shared.debug); -
- - - Svg - Canvas - - - - - - -
- -
+
{#if isPlaying && selectedFeature} @@ -110,14 +105,24 @@ {/if} -
- {#each countries.features.sort(sortFunc('properties.name')) as country} - {@const isSelected = selectedFeature === country} + {#each countries.features.sort(sortFunc('properties.name')) as country (country)} + {@const isSelected = selectedFeature?.properties.name === country.properties.name}
diff --git a/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.ts b/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.ts index d026f0e02..f1b3f6a65 100644 --- a/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.ts @@ -13,6 +13,7 @@ export async function load({ fetch }) { }>, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], hideTableOfContents: true, }, }; diff --git a/packages/layerchart/src/routes/docs/examples/Arc/+page.svelte b/packages/layerchart/src/routes/docs/examples/Arc/+page.svelte index 32f22bce0..c961c222b 100644 --- a/packages/layerchart/src/routes/docs/examples/Arc/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Arc/+page.svelte @@ -4,15 +4,13 @@ import { Arc, Chart, - Circle, ClipPath, Group, + Layer, LinearGradient, - Svg, Text, Tooltip, cartesianToPolar, - degreesToRadians, radiansToDegrees, } from 'layerchart'; import { Field, RangeField, SpringValue, Switch, Toggle } from 'svelte-ux'; @@ -21,9 +19,10 @@ import Preview from '$lib/docs/Preview.svelte'; import Blockquote from '$lib/docs/Blockquote.svelte'; + import { shared } from '../../shared.svelte.js'; - let value = 75; - let segments = 60; + let value = $state(75); + let segments = $state(60); // color wheel const layerCount = 6; @@ -49,7 +48,8 @@

Examples

- See also: PieChart for simplified examples + See also: ArcChart and + PieChart for simplified examples

Partial Arc

@@ -60,30 +60,33 @@
- + - - - - + + {#snippet children({ gradient })} + + {#snippet children({ value })} + + {/snippet} + + {/snippet} - +
@@ -93,7 +96,7 @@
- + - +
@@ -134,7 +137,7 @@
- + {#each { length: segments } as _, segmentIndex} {@const segmentAngle = (2 * Math.PI) / segments} @@ -160,7 +163,7 @@ class="text-6xl tabular-nums" /> - +
@@ -174,10 +177,10 @@
- + - + {#snippet clip()} {#each { length: segments } as _, segmentIndex} {@const segmentAngle = (2 * Math.PI) / segments} {/each} - + {/snippet} @@ -206,7 +209,7 @@ class="text-6xl tabular-nums" /> - +
@@ -215,33 +218,37 @@
- - - {#each { length: layerCount } as _, layerIndex} - {@const layer = layerIndex + 1} - {#each { length: divisions } as _, segmentIndex} - {@const segmentAngle = (2 * Math.PI) / divisions} - {@const startAngle = segmentIndex * segmentAngle} - {@const endAngle = (segmentIndex + 1) * segmentAngle} - {@const color = wheelSegmentColor(startAngle, layer)} - tooltip?.show(e, color)} - onpointerleave={(e) => tooltip?.hide()} - /> + + {#snippet children({ context })} + + {#each { length: layerCount } as _, layerIndex} + {@const layer = layerIndex + 1} + {#each { length: divisions } as _, segmentIndex} + {@const segmentAngle = (2 * Math.PI) / divisions} + {@const startAngle = segmentIndex * segmentAngle} + {@const endAngle = (segmentIndex + 1) * segmentAngle} + {@const color = wheelSegmentColor(startAngle, layer)} + context.tooltip.show(e, color)} + onpointerleave={() => context.tooltip.hide()} + /> + {/each} {/each} - {/each} - - - {data} - + + + {#snippet children({ data })} + {data} + {/snippet} + + {/snippet}
@@ -258,7 +265,7 @@
- + {#if show} {/if} - +
@@ -300,73 +307,73 @@
- - - {@const arcWidth = 20} - {@const maxValue = 100} - - { - // pointer releative to center of chart and arc center - const { x, y } = localPoint(e); - const centerX = x - width / 2; - const centerY = y - height / 2; - - const pointerAngle = radiansToDegrees(cartesianToPolar(centerX, centerY).radians); - value = Math.round((pointerAngle / 360) * maxValue); - }, - onpointermove: (e) => { - if (e.buttons !== 1) { - // button not pressed, ignoring - return; - } - - // @ts-expect-error - e.currentTarget?.setPointerCapture(e.pointerId); - - // pointer releative to center of chart and arc center - const { x, y } = localPoint(e); - const centerX = x - width / 2; - const centerY = y - height / 2; - - const pointerAngle = radiansToDegrees(cartesianToPolar(centerX, centerY).radians); - - const newValue = Math.round((pointerAngle / 360) * maxValue); - - // 1.) No clamping - // value = newValue; - - // 2.) Clamp to prevent wrapping around below 0 / above max - if (value > maxValue * 0.75 && newValue < maxValue * 0.25) { - // Do not allow wrapping around above max - value = maxValue; - } else if (value < maxValue * 0.25 && newValue > maxValue * 0.75) { - // Do not allow wrapping around below 0 - value = 0; - } else { - value = newValue; - } - }, - }} - /> + + {#snippet children({ context })} + + {@const arcWidth = 20} + {@const maxValue = 100} + + { + // pointer releative to center of chart and arc center + const { x, y } = localPoint(e); + const centerX = x - context.width / 2; + const centerY = y - context.height / 2; + + const pointerAngle = radiansToDegrees(cartesianToPolar(centerX, centerY).radians); + value = Math.round((pointerAngle / 360) * maxValue); + }, + onpointermove: (e) => { + if (e.buttons !== 1) { + // button not pressed, ignoring + return; + } + + e.currentTarget?.setPointerCapture(e.pointerId); + + // pointer relative to center of chart and arc center + const { x, y } = localPoint(e); + const centerX = x - context.width / 2; + const centerY = y - context.height / 2; + + const pointerAngle = radiansToDegrees(cartesianToPolar(centerX, centerY).radians); + + const newValue = Math.round((pointerAngle / 360) * maxValue); + + // 1.) No clamping + // value = newValue; + + // 2.) Clamp to prevent wrapping around below 0 / above max + if (value > maxValue * 0.75 && newValue < maxValue * 0.25) { + // Do not allow wrapping around above max + value = maxValue; + } else if (value < maxValue * 0.25 && newValue > maxValue * 0.75) { + // Do not allow wrapping around below 0 + value = 0; + } else { + value = newValue; + } + }, + }} + /> - - + + - - + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/Arc/+page.ts b/packages/layerchart/src/routes/docs/examples/Arc/+page.ts index 5cc4e3910..1a21167b0 100644 --- a/packages/layerchart/src/routes/docs/examples/Arc/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Arc/+page.ts @@ -4,6 +4,7 @@ export async function load() { return { meta: { pageSource, + supportedContexts: ['svg', 'canvas'], related: ['components/Arc', 'components/Pie'], }, }; diff --git a/packages/layerchart/src/routes/docs/examples/Area/+page.svelte b/packages/layerchart/src/routes/docs/examples/Area/+page.svelte index fa26ca30a..8b910d30f 100644 --- a/packages/layerchart/src/routes/docs/examples/Area/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Area/+page.svelte @@ -1,37 +1,40 @@ @@ -67,27 +86,21 @@

Basic

- -
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -95,32 +108,29 @@

With Tooltip and Highlight

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - {formatDate(data.date, 'eee, MMMM do')} - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -129,26 +139,21 @@

With Labels

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -156,30 +161,24 @@

Explicit axis ticks (min/max)

-
+
- + - format(d, PeriodType.Day)} - rule - ticks={(scale) => scale.domain()} - > - - - + scale.domain()}> + {#snippet tickLabel({ props, index })} + + {/snippet} - +
@@ -187,27 +186,24 @@

Gradient

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - + + + {#snippet children({ gradient })} + + {/snippet} - +
@@ -215,29 +211,28 @@

Gradient (separate stroke)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - - - + + + {#snippet children({ gradient: strokeGradient })} + + {#snippet children({ gradient: fillGradient })} + + {/snippet} + + {/snippet} - +
@@ -245,54 +240,59 @@

Multiple series

-
+
- - - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - {#each dataByFruit as [fruit, data]} - {@const color = cScale?.(fruit)} - - - - + + + {#each dataByFruit as [fruit, data]} + {@const color = context.cScale?.(fruit)} + - - {/each} - - - - {formatDate(data.date, 'eee, MMMM do')} - - - - + + {#snippet children({ x, y })} + + + {/snippet} + + {/each} + + + + + {#snippet children({ data })} + + + + + {/snippet} + + {/snippet}
@@ -300,7 +300,7 @@

Multiple series (using overrides)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + d.apples} @@ -346,14 +342,17 @@ d.bananas} points={{ fill: fruitColors.bananas }} /> d.oranges} points={{ fill: fruitColors.oranges }} /> - - - {formatDate(data.date, 'eee, MMMM do')} - - - - - + + + + {#snippet children({ data })} + + + + + + + {/snippet}
@@ -362,58 +361,62 @@

Multiple series (highlight on hover)

-
+
- - - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - {#each dataByFruit as [fruit, data]} - {@const color = - tooltip.data == null || tooltip.data.fruit === fruit - ? cScale?.(fruit) - : 'hsl(var(--color-surface-content) / 20%)'} - - - - - - {/each} - - - - {formatDate(data.date, 'eee, MMMM do')} - - - - + {#snippet children({ context })} + + + + {#each dataByFruit as [fruit, data]} + {@const active = context.tooltip.data == null || context.tooltip.data.fruit === fruit} + {@const color = context.cScale?.(fruit)} + + + + {#snippet children({ x, y })} + + + {/snippet} + + + {/each} + + + + + {#snippet children({ data })} + + + + + {/snippet} + + {/snippet}
@@ -421,42 +424,45 @@

Multiple series with labels

-
+
- - - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - {#each dataByFruit as [fruit, data]} - {@const color = cScale?.(fruit)} - - {/each} - - - - - {formatDate(data.date, 'eee, MMMM do')} - - - - + {#snippet children({ context })} + + + + {#each dataByFruit as [fruit, data]} + {@const color = context.cScale?.(fruit)} + + {/each} + + + + + + {#snippet children({ data })} + + + + + {/snippet} + + {/snippet}
@@ -464,53 +470,48 @@

Stack

-
+
d.data.date} - xScale={scaleTime()} + x={(d) => asAny(d).data.date} y={[0, 1]} yNice c="key" - cScale={scaleOrdinal()} cDomain={Object.keys(fruitColors)} cRange={Object.values(fruitColors)} padding={{ left: 16, bottom: 24 }} - tooltip={{ mode: 'bisect-x' }} - let:data - let:cGet - let:cScale + tooltip={{ mode: 'quadtree-x' }} > - - - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - {#each stackData as seriesData} - {@const color = cGet(seriesData)} - - {/each} - - - + {#snippet children({ context })} + + + - - {formatDate(data.data.date, 'eee, MMMM do')} - - {#each keys as key} - + {#each stackData as seriesData} + {@const color = context.cGet(seriesData)} + {/each} - - + + + + + + {#snippet children({ data })} + + + {#each keys as key} + + {/each} + + {/snippet} + + {/snippet}
@@ -518,48 +519,45 @@

Stack with gradient

-
+
d.data.date} - xScale={scaleTime()} + x={(d) => asAny(d).data.date} y={[0, 1]} yNice padding={{ left: 16, bottom: 24 }} > - + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + {@const primaryColors = [ - 'hsl(var(--color-danger-500))', - 'hsl(var(--color-success-500))', - 'hsl(var(--color-info-500))', + 'var(--color-danger)', + 'var(--color-success)', + 'var(--color-info)', ]} {@const secondaryColors = [ - 'hsl(var(--color-danger-500) / 10%)', - 'hsl(var(--color-success-500) / 10%)', - 'hsl(var(--color-info-500) / 10%)', + 'color-mix(in lch, var(--color-danger) 10%, transparent)', + 'color-mix(in lch, var(--color-success) 10%, transparent)', + 'color-mix(in lch, var(--color-info) 10%, transparent)', ]} {#each chartDataArray(stackData) as seriesData, index} {@const primaryColor = primaryColors[index]} {@const secondaryColor = secondaryColors[index]} - - + + {#snippet children({ gradient })} + + {/snippet} {/each} - +
@@ -574,32 +572,27 @@
-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + {#if show} {/if} - +
@@ -613,33 +606,28 @@
-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + {#if show} {/if} - +
@@ -653,69 +641,66 @@
-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + {#if show} {/if} - +
-

Threshold with RectClipPath

-
+
- - - format(d, PeriodType.Day, { variant: 'short' })} /> - - - - - - - - + {#snippet children({ context })} + + + + + + + + + + + + {/snippet}
@@ -723,30 +708,33 @@

Threshold with RectClipPath (over/under)

-
+
- - - format(d, PeriodType.Day, { variant: 'short' })} /> - - - - - - 0} line={{ class: 'stroke-2 stroke-danger' }} class="fill-danger/20" /> - - + {#snippet children({ context })} + + + + + + + + + 0} line={{ class: 'stroke-2 stroke-danger' }} class="fill-danger/20" /> + + + {/snippet}
@@ -754,42 +742,50 @@

Highlight color based on value using color scale

-
+
(d.value < 0 ? 'under' : 'over')} - cScale={scaleOrdinal()} cDomain={['over', 'under']} - cRange={['hsl(var(--color-success))', 'hsl(var(--color-danger))']} - let:width - let:height - let:yScale + cRange={['var(--color-success)', 'var(--color-danger)']} > - - - format(d, PeriodType.Day, { variant: 'short' })} /> - - - 0} line={{ class: 'stroke-2 stroke-success' }} class="fill-success/20" /> - - - 0} line={{ class: 'stroke-2 stroke-danger' }} class="fill-danger/20" /> - - - - - - {formatDate(data.date, 'eee, MMMM do')} - - - - + {#snippet children({ context })} + + + + + + 0} + line={{ class: 'stroke-2 stroke-success' }} + class="fill-success/20" + /> + + + 0} line={{ class: 'stroke-2 stroke-danger' }} class="fill-danger/20" /> + + + + + + {#snippet children({ data })} + + + + + {/snippet} + + {/snippet}
@@ -797,46 +793,62 @@

Highlight color based on value using tooltip slot prop

-
+
- - - format(d, PeriodType.Day, { variant: 'short' })} /> - - - 0} line={{ class: 'stroke-2 stroke-success' }} class="fill-success/20" /> - - - 0} line={{ class: 'stroke-2 stroke-danger' }} class="fill-danger/20" /> - - - - - - {formatDate(data.date, 'eee, MMMM do')} - - - - + {#snippet children({ context })} + + + + + + 0} + line={{ class: 'stroke-2 stroke-success' }} + class="fill-success/20" + /> + + + 0} line={{ class: 'stroke-2 stroke-danger' }} class="fill-danger/20" /> + + + + + + {#snippet children({ data })} + + + + + {/snippet} + + {/snippet}
@@ -844,37 +856,41 @@

Threshold with LinearGradient

-
+
- {@const thresholdValue = 0} - {@const thresholdOffset = yScale(thresholdValue) / (height + padding.bottom)} - - - format(d, PeriodType.Day, { variant: 'short' })} /> - - - - - + {#snippet children({ context })} + {@const thresholdValue = 0} + {@const thresholdOffset = + context.yScale(thresholdValue) / (context.height + context.padding.bottom)} + + + + + + {#snippet children({ gradient })} + + {/snippet} + + + {/snippet}
@@ -882,42 +898,42 @@

Threshold with LinearGradient (over/under)

-
+
- {@const thresholdValue = 0} - {@const thresholdOffset = yScale(thresholdValue) / (height + padding.bottom)} - - - format(d, PeriodType.Day, { variant: 'short' })} /> - - - 0} - line={{ stroke: gradient, class: 'stroke-2' }} - fill={gradient} - fillOpacity={0.2} - /> - - + {#snippet children({ context })} + {@const thresholdValue = 0} + {@const thresholdOffset = + context.yScale(thresholdValue) / (context.height + context.padding.bottom)} + + + + + + {#snippet children({ gradient })} + 0} + line={{ stroke: gradient, class: 'stroke-2' }} + fill={gradient} + fillOpacity={0.2} + /> + {/snippet} + + + {/snippet}
@@ -925,56 +941,65 @@

Clipped area on Tooltip

-
+
- - - - - - - - - - - - - {format(data.value, 'currency')} - - - - {format(data.date, PeriodType.Day)} - - - - {format(data.date, PeriodType.Day)} - + {#snippet children({ context })} + + + {#snippet children({ gradient })} + + + + + {/snippet} + + + + + + + {#snippet children({ data })} + {format(data.value, 'currency')} + {/snippet} + + + + {#snippet children({ data })} + {format(data.date, 'day')} + {/snippet} + + + + {#snippet children({ data })} + {format(data.date, 'day')} + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/Area/+page.ts b/packages/layerchart/src/routes/docs/examples/Area/+page.ts index 92d05db4d..22e389db0 100644 --- a/packages/layerchart/src/routes/docs/examples/Area/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Area/+page.ts @@ -3,13 +3,14 @@ import { parse } from '@layerstack/utils'; import pageSource from './+page.svelte?raw'; import type { AppleStockData } from '$static/data/examples/date/apple-stock.js'; -export async function load() { +export async function load({ fetch }) { return { appleStock: await fetch('/data/examples/date/apple-stock.json').then(async (r) => parse(await r.text()) ), meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/Bars/+page.svelte b/packages/layerchart/src/routes/docs/examples/Bars/+page.svelte index 627925925..cd551d31d 100644 --- a/packages/layerchart/src/routes/docs/examples/Bars/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Bars/+page.svelte @@ -1,11 +1,11 @@

Examples

@@ -109,7 +113,7 @@

Basic

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -135,7 +135,7 @@

Rounded (right-only)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -161,7 +157,7 @@

Tooltip and Highlight

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - {format(data.date, PeriodType.Custom, { custom: 'eee, MMMM do' })} - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -197,7 +189,7 @@

Tooltip and Bar Highlight

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - {format(data.date, PeriodType.Custom, { custom: 'eee, MMMM do' })} - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -233,7 +221,7 @@

Tooltip and Clipped Highlight

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - + + {#snippet area({ area })} + - + {/snippet} - - - {format(data.date, PeriodType.Custom, { custom: 'eee, MMMM do' })} - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -275,7 +265,7 @@

Calculated value domain (positive)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -300,7 +286,7 @@

Calculated value domain (negative)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -325,7 +307,7 @@

Outside Labels (default)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -353,7 +331,7 @@

Inside Labels

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -380,7 +354,7 @@

Limit ticks (count)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - ticks={4} - rule - /> + - +
@@ -407,7 +376,7 @@

Limit ticks (second scale)

-
+
- + format(d, PeriodType.Day, { variant: 'short' })} ticks={(scale) => scaleTime(scale.domain(), scale.range()).ticks(4)} rule /> - +
@@ -434,7 +402,7 @@

Gradient

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - + + + {#snippet children({ gradient })} + + {/snippet} - +
@@ -462,7 +428,7 @@

Customize individual styles

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - {#each data as bar, i} + {#each data as d, i} {/each} - +
@@ -496,7 +458,7 @@

Highlight individual bar

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + @@ -522,7 +480,7 @@ data={data[3]} area={{ fill: 'url(#highlight-pattern)', class: 'stroke-secondary/50' }} /> - +
@@ -530,7 +488,7 @@

Highlight individual bar (line)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -561,7 +515,7 @@

Average annotation Rule

-
+
- {@const avg = mean(data, (d) => d.value)} - - - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - - - + {#snippet children({ context })} + {@const avg = mean(data, (d) => d.value)} + + + + + + + + {/snippet}
@@ -599,7 +553,7 @@

with grid on top

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - + +
@@ -625,7 +575,7 @@

with grid on top (mix-blend)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - + +
@@ -651,7 +597,7 @@

Multiple (overlapping)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - {format(data.date, PeriodType.Custom, { custom: 'eee, MMMM do' })} - - - - + + + {#snippet children({ data })} + + + + + + {/snippet}
@@ -689,7 +631,7 @@

Multiple (diverging)

-
+
-d.baseline]} @@ -699,28 +641,94 @@ padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'band' }} > - + - format(d, PeriodType.Day, { variant: 'short' })} /> - + + -d.baseline} rounded="left" strokeWidth={1} class="fill-secondary" /> - - - {format(data.date, PeriodType.Custom, { custom: 'eee, MMMM do' })} - - - - + + + {#snippet children({ data })} + + + + + + {/snippet}
+

Time scale (with interval)

+ + +
+ + + + + + + +
+
+ +

Time scale with missing data

+ + +
+ (Math.random() > 0.5 ? true : false))} + x="value" + xDomain={[0, null]} + xNice={4} + y="date" + yInterval={timeDay} + padding={{ left: 20, bottom: 20 }} + > + + + + + + +
+
+ +

Time scale with inset

+ + +
+ + + + + + + +
+
+

Tween on mount

@@ -731,7 +739,7 @@
-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + {#if show} {/if} - +
@@ -776,7 +780,7 @@
-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + {#if show} - {#each data as bar, i} + {#each data as d, i} {/each} {/if} - +
@@ -817,56 +817,57 @@

Grouped

-
+
[0, yScale.bandwidth?.()]} padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'band' }} - let:cScale > - - - - - - - - - {data.year} - - {#each data.data as d} - - {/each} - - - - - d.value)} - format="integer" - valueAlign="right" - /> - - + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {data.year} + + {#each data.data as d} + + {/each} + + + + + d.value)} + format="integer" + valueAlign="right" + /> + + {/snippet} + + {/snippet}
@@ -874,52 +875,54 @@

Stacked

-
+
- - - - - - - - - {data.year} - - {#each data.data as d} - - {/each} - - - - - d.value)} - format="integer" - valueAlign="right" - /> - - + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {data.year} + + {#each data.data as d} + + {/each} + + + + + d.value)} + format="integer" + valueAlign="right" + /> + + {/snippet} + + {/snippet}
@@ -927,52 +930,54 @@

Stacked (Percent)

-
+
- - - - - - - - - {data.year} - - {#each data.data as d} - - {/each} - - - - - d.value)} - format="integer" - valueAlign="right" - /> - - + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {data.year} + + {#each data.data as d} + + {/each} + + + + + d.value)} + format="integer" + valueAlign="right" + /> + + {/snippet} + + {/snippet}
@@ -980,15 +985,14 @@

Grouped and Stacked

-
+
[0, yScale.bandwidth?.()]} padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'band' }} - let:cScale > - - - - - - - - - {data.year} - - {#each data.data as d} - - {/each} - - - - - d.value)} - format="integer" - valueAlign="right" - /> - - + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {data.year} + + {#each data.data as d} + + {/each} + + + + + d.value)} + format="integer" + valueAlign="right" + /> + + {/snippet} + + {/snippet}
@@ -1047,7 +1054,7 @@
-
+
d[transitionChart.groupBy])) : undefined} - y1Range={({ yScale }) => [0, yScale.bandwidth?.()]} + y1Range={({ yScale }) => [0, yScale.bandwidth()]} padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'band' }} - let:data - let:cScale > - - - - - - {#each transitionData as bar (bar.year + '-' + bar.fruit)} - - {/each} - - - - - - {data.year} - - {#each data.data as d} - - {/each} - - - - - d.value)} - format="integer" - valueAlign="right" - /> - - + {#snippet children({ context })} + + + + + + {#each transitionData as d (d.year + '-' + d.fruit)} + + {/each} + + + + + + {#snippet children({ data })} + {data.year} + + {#each data.data as d} + + {/each} + + + + + d.value)} + format="integer" + valueAlign="right" + /> + + {/snippet} + + {/snippet}
@@ -1133,7 +1157,7 @@
-
+
d[transitionChart.groupBy])) : undefined} - y1Range={({ yScale }) => [0, yScale.bandwidth?.()]} + y1Range={({ yScale }) => [0, yScale.bandwidth()]} padding={{ left: 16, bottom: 24 }} - let:tooltip - let:data - let:cScale > - - - - - - {#each transitionData as bar (bar.year + '-' + bar.fruit)} - { - alert('You clicked on:\n' + JSON.stringify(bar, null, 2)); - }} - onpointerenter={(e) => tooltip?.show(e, bar)} - onpointermove={(e) => tooltip?.show(e, bar)} - onpointerleave={(e) => tooltip?.hide()} - /> - {/each} - - - - - {data.year} - - - - + {#snippet children({ context })} + + + + + + {#each transitionData as d (d.year + '-' + d.fruit)} + { + alert('You clicked on:\n' + JSON.stringify(d, null, 2)); + }} + onpointerenter={(e) => context.tooltip.show(e, d)} + onpointermove={(e) => context.tooltip.show(e, d)} + onpointerleave={(e) => context.tooltip.hide()} + /> + {/each} + + + + + {#snippet children({ data })} + {data.year} + + + + {/snippet} + + {/snippet}
@@ -1203,7 +1243,7 @@

Click handler

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
diff --git a/packages/layerchart/src/routes/docs/examples/Bars/+page.ts b/packages/layerchart/src/routes/docs/examples/Bars/+page.ts index b40169ef0..8d3a50c7b 100644 --- a/packages/layerchart/src/routes/docs/examples/Bars/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Bars/+page.ts @@ -5,6 +5,7 @@ export async function load() { meta: { title: 'Bar Chart (Horizontal)', pageSource, + supportedContexts: ['svg', 'canvas'], related: ['components/Bars', 'examples/Columns', 'examples/Histogram', 'charts/BarChart'], }, }; diff --git a/packages/layerchart/src/routes/docs/examples/Beeswarm/+page.svelte b/packages/layerchart/src/routes/docs/examples/Beeswarm/+page.svelte index d23fb3a1a..5faeec629 100644 --- a/packages/layerchart/src/routes/docs/examples/Beeswarm/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Beeswarm/+page.svelte @@ -1,70 +1,80 @@

Examples

-
+
d.date_of_birth.getFullYear()} xNice - padding={{ bottom: 12, left: 8, right: 8 }} - let:xGet - let:height - let:tooltip + padding={{ bottom: 20, left: 12, right: 12 }} > - {@const r = 6} - - - xGet(d)), - y: yForce.y(height / 2), - collide: collideForce.radius(r), - }} - static - cloneData - let:nodes - > - {#each nodes as node} - tooltip.show(e, node)} - onpointerleave={tooltip.hide} - /> - {/each} - - + {#snippet children({ context })} + {@const r = 6} + + + context.xGet(asAny(d))), + y: yForce.y(context.height / 2), + collide: collideForce.radius(r), + }} + data={{ nodes }} + static + > + {#snippet children({ nodes })} + {#each nodes as node} + context.tooltip.show(e, node)} + onpointerleave={context.tooltip.hide} + /> + {/each} + {/snippet} + + - - {data.name} - - - - - - - + + {#snippet children({ data })} + {data.name} + + + + + + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/Beeswarm/+page.ts b/packages/layerchart/src/routes/docs/examples/Beeswarm/+page.ts index f965cdb65..c09878684 100644 --- a/packages/layerchart/src/routes/docs/examples/Beeswarm/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Beeswarm/+page.ts @@ -3,13 +3,14 @@ import type { USSenatorsData } from '$static/data/examples/us-senators.js'; import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { usSenators: (await fetch('/data/examples/us-senators.csv').then(async (r) => csvParse(await r.text(), autoType) )) as USSenatorsData, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/BubbleMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/BubbleMap/+page.svelte index ac40e2f45..db89adfa5 100644 --- a/packages/layerchart/src/routes/docs/examples/BubbleMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/BubbleMap/+page.svelte @@ -7,12 +7,14 @@ import { feature } from 'topojson-client'; import { sortFunc } from '@layerstack/utils'; - import { Chart, Canvas, GeoPath, Legend, Svg, Tooltip, Circle } from 'layerchart'; + import { Chart, GeoPath, Legend, Layer, Tooltip, Circle } from 'layerchart'; import TransformControls from '$lib/components/TransformControls.svelte'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; + + let { data } = $props(); - export let data; const states = feature(data.geojson, data.geojson.objects.states); const counties = feature(data.geojson, data.geojson.objects.counties); @@ -32,34 +34,40 @@ const populationByFips = index(population, (d) => d.fips); const maxRadius = 40; - $: rScale = scaleSqrt() - .domain([0, max(population, (d) => d.population) ?? 0]) - .range([0, maxRadius]); + const rScale = $derived( + scaleSqrt() + .domain([0, max(population, (d) => d.population) ?? 0]) + .range([0, maxRadius]) + ); - $: colors = quantize(interpolateViridis, 5); + const colors = $derived(quantize(interpolateViridis, 5)); // $: colorScale = scaleQuantize() // .domain([0, max(population, d => d.percentUnder18)]) // .range(colors) - $: colorScale = scaleThreshold() - .domain([16, 20, 24, 28, Math.ceil(max(population, (d) => d.percentUnder18) ?? 0)]) - .range(colors); - - $: enrichedCountiesFeatures = counties.features - .map((feature) => { - return { - ...feature, - properties: { - ...feature.properties, - data: populationByFips.get(String(feature.id)), - }, - }; - }) - .sort(sortFunc('properties.data.population', 'desc')); + const colorScale = $derived( + scaleThreshold() + .domain([16, 20, 24, 28, Math.ceil(max(population, (d) => d.percentUnder18) ?? 0)]) + .range(colors) + ); + + const enrichedCountiesFeatures = $derived( + counties.features + .map((feature) => { + return { + ...feature, + properties: { + ...feature.properties, + data: populationByFips.get(String(feature.id)), + }, + }; + }) + .sort(sortFunc('properties.data.population', 'desc')) + );

Examples

-

SVG

+

Basic

@@ -73,170 +81,94 @@ initialScrollMode: 'scale', }} padding={{ top: 60 }} - let:tooltip - let:transform > - {@const strokeWidth = 1 / transform.scale} - - - - + {#snippet children({ context })} + {@const strokeWidth = 1 / context.transform.scale} + - {#each enrichedCountiesFeatures as feature} - - {@const [cx, cy] = geoPath.centroid(feature)} - {@const d = feature.properties.data} - - - {/each} - - {#each enrichedCountiesFeatures as feature} + - {/each} - - - - - - {@const d = data.properties.data} - - {data.properties.name + ' - ' + data.properties.data?.state} - - - - - - - - -
-
- -

Canvas

- -
- - {@const strokeWidth = 1 / transform.scale} - - - - - - {#each enrichedCountiesFeatures as feature} - - {@const [cx, cy] = geoPath.centroid(feature)} - {@const d = feature.properties.data} - + {#snippet children({ geoPath })} + {@const [cx, cy] = geoPath?.centroid(feature) ?? []} + {@const d = feature.properties.data} + + {/snippet} + + {/each} + + {#each enrichedCountiesFeatures as feature} + - - {/each} - + {/each} + + + + + {#if context.tooltip.data && shared.renderContext === 'canvas'} + + {/if} + + + - - {#if tooltip.data} - - {/if} - - - - - - {@const d = data.properties.data} - - {data.properties.name + ' - ' + data.properties.data?.state} - - - - - - - + + {#snippet children({ data })} + {@const d = data.properties.data} + + {data.properties.name + ' - ' + data.properties.data?.state} + + + + + + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/BubbleMap/+page.ts b/packages/layerchart/src/routes/docs/examples/BubbleMap/+page.ts index beeb54e67..53f8b7218 100644 --- a/packages/layerchart/src/routes/docs/examples/BubbleMap/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/BubbleMap/+page.ts @@ -17,6 +17,7 @@ export async function load({ fetch }) { )) as USCountyPopulationData, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/Candlestick/+page.svelte b/packages/layerchart/src/routes/docs/examples/Candlestick/+page.svelte index 53ddeb669..eb1623753 100644 --- a/packages/layerchart/src/routes/docs/examples/Candlestick/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Candlestick/+page.svelte @@ -1,49 +1,199 @@

Examples

Basic

- -
+
(d.close < d.open ? 'desc' : 'asc')} - cScale={scaleOrdinal()} cDomain={['desc', 'asc']} - cRange={['#e41a1c', '#4daf4a']} - padding={{ left: 16, bottom: 24 }} - tooltip={{ mode: 'bisect-x' }} + cRange={['var(--color-danger)', 'var(--color-success)']} + padding={{ left: 20, bottom: 32 }} + tooltip={{ mode: 'quadtree-x' }} > - - - ''} /> - - [d.open, d.close]} radius={2} /> + + + + + + + + + + {#snippet children({ data })} + + + + + + + + {/snippet} + + +
+ + +

with brushing

+ + +
+
+ + (xDomain?.[0] == null || d.date >= xDomain?.[0]) && + (xDomain?.[1] == null || d.date <= xDomain?.[1]) + )} + x="date" + xScale={scaleUtc()} + {xDomain} + y={['high', 'low']} + yNice + c={(d) => (d.close < d.open ? 'desc' : 'asc')} + cDomain={['desc', 'asc']} + cRange={['var(--color-danger)', 'var(--color-success)']} + padding={{ left: 20, bottom: 32 }} + tooltip={{ mode: 'quadtree-x' }} + > + + + + + + + + + + {#snippet children({ data })} + + + + + + + + {/snippet} + + +
+ +
+ { + xDomain = e.xDomain; + }, + }} + > + + + + +
+
+
+ +

Open/close line color

+ + +
+ (d.close < d.open ? 'desc' : 'asc')} + cDomain={['desc', 'asc']} + cRange={['var(--color-danger)', 'var(--color-success)']} + padding={{ left: 20, bottom: 32 }} + tooltip={{ mode: 'quadtree-x' }} + > + + + + + + + + + + {#snippet children({ data })} + + + + + + + + {/snippet} + + +
+
+ +

Bars

+ + +
+ (d.close < d.open ? 'desc' : 'asc')} + cDomain={['desc', 'asc']} + cRange={['var(--color-danger)', 'var(--color-success)']} + padding={{ left: 20, bottom: 32 }} + tooltip={{ mode: 'quadtree-x' }} + > + + + + + - - - {formatDate(data.date, PeriodType.Day)} - - - - - - + + + + {#snippet children({ data })} + + + + + + + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/Candlestick/+page.ts b/packages/layerchart/src/routes/docs/examples/Candlestick/+page.ts index cbc4301e5..4dde51d0b 100644 --- a/packages/layerchart/src/routes/docs/examples/Candlestick/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Candlestick/+page.ts @@ -3,20 +3,15 @@ import type { AppleTickerData } from '$static/data/examples/date/apple-ticker.js import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { appleTicker: (await fetch('/data/examples/date/apple-ticker.json').then(async (r) => parse(await r.text()) )) as AppleTickerData, meta: { pageSource, - related: [ - 'components/Bars', - 'components/Points', - 'examples/Bars', - 'examples/Histogram', - 'examples/Sparkbar', - ], + supportedContexts: ['svg', 'canvas'], + related: ['components/Rule', 'components/Bars'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/Choropleth/+page.svelte b/packages/layerchart/src/routes/docs/examples/Choropleth/+page.svelte index ee8d2a8bc..df9812e14 100644 --- a/packages/layerchart/src/routes/docs/examples/Choropleth/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Choropleth/+page.svelte @@ -4,14 +4,14 @@ import { schemeBlues } from 'd3-scale-chromatic'; import { geoIdentity, type GeoProjection } from 'd3-geo'; import { feature } from 'topojson-client'; - import { format } from '@layerstack/utils'; - import { Canvas, Chart, GeoPath, Legend, Svg, Tooltip } from 'layerchart'; + import { Chart, GeoPath, Legend, Layer, Tooltip } from 'layerchart'; import TransformControls from '$lib/components/TransformControls.svelte'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); const states = feature(data.geojson, data.geojson.objects.states); const counties = feature(data.geojson, data.geojson.objects.counties); @@ -28,26 +28,31 @@ percentUnder18: +d.DP05_0019PE, }; }); - const populationByFips = index(population, (d) => d.id); - $: enrichedCountiesFeatures = counties.features.map((feature) => { - return { - ...feature, - properties: { - ...feature.properties, - data: populationByFips.get(feature.id as string), - }, - }; - }); + const populationByFips = index(population, (d) => d.id); - $: colorScale = scaleQuantile() - .domain(population.map((d) => d.population)) - .range(schemeBlues[9]); + const enrichedCountiesFeatures = $derived( + counties.features.map((feature) => { + return { + ...feature, + properties: { + ...feature.properties, + data: populationByFips.get(feature.id as string), + }, + }; + }) + ); + + const colorScale = $derived( + scaleQuantile() + .domain(population.map((d) => d.population)) + .range(schemeBlues[9]) + );

Examples

-

SVG

+

Basic

@@ -61,145 +66,72 @@ initialScrollMode: 'scale', }} padding={{ top: 60 }} - tooltip={{ raiseTarget: true }} - let:tooltip - let:transform + tooltip={{ raiseTarget: shared.renderContext === 'svg' }} > - {@const strokeWidth = 1 / transform.scale} - + {#snippet children({ context })} + {@const strokeWidth = 1 / context.transform.scale} + - - + {#each enrichedCountiesFeatures as feature} {/each} - - - - - format(d, 'metric', { maximumSignificantDigits: 2 })} - class="absolute bg-surface-100/80 px-2 py-1 backdrop-blur-sm rounded m-1" - /> - - - {@const d = populationByFips.get(data.id)} - - {data.properties.name + ' - ' + data.properties.data?.state} - - - - - - - - -
-
-

Canvas

- - -
- - {@const strokeWidth = 1 / transform.scale} - - - - {#each enrichedCountiesFeatures as feature} - {/each} - - - - - - - - {#if tooltip.data} - - {/if} - - - format(d, 'metric', { maximumSignificantDigits: 2 })} - placement="top-left" - class="absolute bg-surface-100/80 px-2 py-1 backdrop-blur-sm rounded m-1" - /> - - - {@const d = populationByFips.get(data.id)} - - {data.properties.name + ' - ' + data.properties.data?.state} - - - - - - - + + + + + {#if context.tooltip.data && shared.renderContext === 'canvas'} + + {/if} + + + + + + {#snippet children({ data })} + {@const d = populationByFips.get(data.id)} + + {data.properties.name + ' - ' + data.properties.data?.state} + + + + + + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/Choropleth/+page.ts b/packages/layerchart/src/routes/docs/examples/Choropleth/+page.ts index beeb54e67..53f8b7218 100644 --- a/packages/layerchart/src/routes/docs/examples/Choropleth/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Choropleth/+page.ts @@ -17,6 +17,7 @@ export async function load({ fetch }) { )) as USCountyPopulationData, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/CollisionDetection/+page.svelte b/packages/layerchart/src/routes/docs/examples/CollisionDetection/+page.svelte index 43a77dd16..6114817c1 100644 --- a/packages/layerchart/src/routes/docs/examples/CollisionDetection/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/CollisionDetection/+page.svelte @@ -3,65 +3,78 @@ import { randomUniform } from 'd3-random'; import { forceX, forceY, forceManyBody, forceCollide, type SimulationNodeDatum } from 'd3-force'; - import { Chart, Circle, Group, ForceSimulation, Svg } from 'layerchart'; + import { Chart, Circle, Group, ForceSimulation, Layer } from 'layerchart'; import Preview from '$lib/docs/Preview.svelte'; + import type { Prettify } from '@layerstack/utils'; + import { shared } from '../../shared.svelte.js'; + + type NodeDatum = { r: number; group: number }; + type MySimulationNodeDatum = Prettify; const k = 600 / 200; const r = randomUniform(k, k * 4); const n = 4; - const randomData = Array.from({ length: 200 }, (_, i) => ({ r: r(), group: i && (i % n) + 1 })); + const randomData: MySimulationNodeDatum[] = Array.from({ length: 200 }, (_, i) => ({ + r: r(), + group: i && (i % n) + 1, + })); const groupColor = scaleOrdinal([ - 'hsl(var(--color-info))', - 'hsl(var(--color-warning))', - 'hsl(var(--color-danger))', + 'var(--color-info)', + 'var(--color-warning)', + 'var(--color-danger)', ]); - const xForce = forceX().strength(0.01); - const yForce = forceY().strength(0.01); - const collideForce = forceCollide() + const xForce = forceX().strength(0.01); + const yForce = forceY().strength(0.01); + const collideForce = forceCollide() .radius((d) => d.r + 1) .iterations(3); - const manyBodyForce = forceManyBody(); + const manyBodyForce = forceManyBody();

Examples

-
- - +
+ + {#snippet children({ context })} (i ? 0 : (-width * 2) / 3)), + charge: manyBodyForce.strength((d, i) => (i ? 0 : (-context.width * 2) / 3)), }} + data={{ nodes: randomData }} alphaTarget={0.3} velocityDecay={0.1} - let:nodes > - - {#each nodes as node, i} - {#if i > 0} - - {/if} - {/each} - - - { - nodes[0].fx = e.offsetX - width / 2; - nodes[0].fy = e.offsetY - height / 2; - }} - class="fill-transparent" - /> + {#snippet children({ nodes, simulation })} + { + simulation.nodes()[0].fx = e.offsetX - context.width / 2; + simulation.nodes()[0].fy = e.offsetY - context.height / 2; + }} + > + + {#each nodes as node, i} + {#if i > 0} + + {/if} + {/each} + + + {/snippet} - + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/CollisionDetection/+page.ts b/packages/layerchart/src/routes/docs/examples/CollisionDetection/+page.ts index 0a45eaea6..a97d410be 100644 --- a/packages/layerchart/src/routes/docs/examples/CollisionDetection/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/CollisionDetection/+page.ts @@ -4,6 +4,7 @@ export async function load() { return { meta: { pageSource, + supportedContexts: ['svg', 'canvas'], related: [ 'https://d3js.org/d3-force/collide', 'https://observablehq.com/@d3/collision-detection', diff --git a/packages/layerchart/src/routes/docs/examples/Columns/+page.svelte b/packages/layerchart/src/routes/docs/examples/Columns/+page.svelte index d2adba4fe..02a97d461 100644 --- a/packages/layerchart/src/routes/docs/examples/Columns/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Columns/+page.svelte @@ -3,6 +3,7 @@ import { scaleBand, scaleOrdinal, scaleTime } from 'd3-scale'; import { mean, sum } from 'd3-array'; import { stackOffsetExpand } from 'd3-shape'; + import { timeDay } from 'd3-time'; import { Axis, @@ -15,7 +16,7 @@ Pattern, RectClipPath, Rule, - Svg, + Layer, Text, Tooltip, groupStackData, @@ -23,11 +24,12 @@ } from 'layerchart'; import { Field, ToggleGroup, ToggleOption, Toggle, Switch } from 'svelte-ux'; - import { format, PeriodType, unique } from '@layerstack/utils'; + import { format, unique } from '@layerstack/utils'; import Preview from '$lib/docs/Preview.svelte'; import Blockquote from '$lib/docs/Blockquote.svelte'; import { createDateSeries, longData } from '$lib/utils/genData.js'; + import { shared } from '../../shared.svelte.js'; const data = createDateSeries({ count: 30, @@ -58,14 +60,14 @@ const colorKeys = [...new Set(longData.map((x) => x.fruit))]; const keyColors = [ - 'hsl(var(--color-info))', - 'hsl(var(--color-success))', - 'hsl(var(--color-warning))', - 'hsl(var(--color-danger))', + 'var(--color-info)', + 'var(--color-success)', + 'var(--color-warning)', + 'var(--color-danger)', ]; - let transitionChartMode = 'group'; - $: transitionChart = + let transitionChartMode = $state('group'); + const transitionChart = $derived( transitionChartMode === 'group' ? ({ groupBy: 'fruit', @@ -84,19 +86,22 @@ : ({ groupBy: undefined, stackBy: undefined, - } as const); - $: transitionData = groupStackData(longData, { - xKey: 'year', - groupBy: transitionChart.groupBy, - stackBy: transitionChart.stackBy, - }) as { - year: string; - fruit: string; - basket: number; - keys: string[]; - value: number; - values: number[]; - }[]; + } as const) + ); + const transitionData = $derived( + groupStackData(longData, { + xKey: 'year', + groupBy: transitionChart.groupBy, + stackBy: transitionChart.stackBy, + }) as { + year: string; + fruit: string; + basket: number; + keys: string[]; + value: number; + values: number[]; + }[] + );

Examples

@@ -108,25 +113,21 @@

Basic

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -134,25 +135,21 @@

Rounded (top-only)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -160,34 +157,30 @@

Tooltip and Highlight

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - {format(data.date, PeriodType.Custom, { custom: 'eee, MMMM do' })} - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -196,34 +189,30 @@

Tooltip and Bar Highlight

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - {format(data.date, PeriodType.Custom, { custom: 'eee, MMMM do' })} - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -232,40 +221,42 @@

Tooltip and Clipped Highlight

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - + + {#snippet area({ area })} + - + {/snippet} - - - {format(data.date, PeriodType.Custom, { custom: 'eee, MMMM do' })} - - - + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -274,24 +265,20 @@

Calculated value domain (positive)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -299,24 +286,20 @@

Calculated value domain (negative)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -324,28 +307,24 @@

Outside Labels (default)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -353,27 +332,23 @@

Inside Labels

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -381,26 +356,21 @@

Limit ticks (count)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - ticks={4} - rule - /> + - +
@@ -408,26 +378,25 @@

Limit ticks (second scale)

-
+
- + format(d, PeriodType.Day, { variant: 'short' })} ticks={(scale) => scaleTime(scale.domain(), scale.range()).ticks(4)} rule /> - +
@@ -435,32 +404,25 @@

Gradient

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - + + + {#snippet children({ gradient })} + + {/snippet} - +
@@ -468,33 +430,29 @@

Customize individual styles

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - {#each data as bar, i} + {#each data as d, i} {/each} - +
@@ -502,23 +460,19 @@

Highlight individual bar

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + @@ -528,7 +482,7 @@ data={data[3]} area={{ fill: 'url(#highlight-pattern)', class: 'stroke-secondary/50' }} /> - +
@@ -536,30 +490,26 @@

Highlight individual bar (line)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -567,38 +517,37 @@

Average annotation Rule

-
+
- {@const avg = mean(data, (d) => d.value)} - - - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - - - + {#snippet children({ context })} + {@const avg = mean(data, (d) => d.value)} + + + + + + + + {/snippet}
@@ -606,25 +555,21 @@

with grid on top

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - + +
@@ -632,25 +577,21 @@

with grid on top (mix-blend)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> - + +
@@ -658,36 +599,32 @@

Multiple (overlapping)

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - {format(data.date, PeriodType.Custom, { custom: 'eee, MMMM do' })} - - - - + + + {#snippet children({ data })} + + + + + + {/snippet}
@@ -696,37 +633,106 @@

Multiple (diverging)

-
+
-d.baseline]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'bisect-x' }} > - + format(Math.abs(d), 'integer')} /> - format(d, PeriodType.Day, { variant: 'short' })} /> - + + -d.baseline} rounded="bottom" strokeWidth={1} class="fill-secondary" /> - - - {format(data.date, PeriodType.Custom, { custom: 'eee, MMMM do' })} - - - - + + + {#snippet children({ data })} + + + + + + {/snippet}
+

Time scale (with interval)

+ + +
+ + + + + + + +
+
+ +

Time scale with missing data

+ + +
+ (Math.random() > 0.5 ? true : false))} + x="date" + xScale={scaleTime()} + xInterval={timeDay} + y="value" + yDomain={[0, null]} + yNice + padding={{ left: 16, bottom: 24 }} + > + + + + + + +
+
+ +

Time scale with inset

+ + +
+ + + + + + + +
+
+

Tween on mount

@@ -737,36 +743,32 @@
-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + {#if show} {/if} - +
@@ -782,26 +784,23 @@
-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + {#if show} {/if} - +
@@ -827,39 +826,35 @@
-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + {#if show} - {#each data as bar, i} + {#each data as d, i} {/each} {/if} - +
@@ -875,28 +870,25 @@
-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + {#if show} - {#each data as bar, i} + {#each data as d, i} {/each} {/if} - +
@@ -917,56 +909,56 @@

Grouped

-
+
[0, xScale.bandwidth?.()]} + x1Range={({ xScale }) => [0, xScale.bandwidth()]} padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'band' }} - let:cScale > - - - - - - - - - {data.year} - - {#each data.data as d} - - {/each} - - - - - d.value)} - format="integer" - valueAlign="right" - /> - - + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {data.year} + + {#each data.data as d} + + {/each} + + + + d.value)} + format="integer" + valueAlign="right" + /> + + {/snippet} + + {/snippet}
@@ -974,52 +966,53 @@

Stacked

-
+
- - - - - - - - - {data.year} - - {#each data.data as d} - - {/each} - - - - - d.value)} - format="integer" - valueAlign="right" - /> - - + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {data.year} + + {#each data.data as d} + + {/each} + + + + d.value)} + format="integer" + valueAlign="right" + /> + + {/snippet} + + {/snippet}
@@ -1027,52 +1020,53 @@

Stacked (Percent)

-
+
- - - - - - - - - {data.year} - - {#each data.data as d} - - {/each} - - - - - d.value)} - format="integer" - valueAlign="right" - /> - - + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {data.year} + + {#each data.data as d} + + {/each} + + + + d.value)} + format="integer" + valueAlign="right" + /> + + {/snippet} + + {/snippet}
@@ -1080,56 +1074,57 @@

Grouped and Stacked

-
+
[0, xScale.bandwidth?.()]} + x1Range={({ xScale }) => [0, xScale.bandwidth()]} padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'band' }} - let:cScale > - - - - - - - - - {data.year} - - {#each data.data as d} - - {/each} - - - - - d.value)} - format="integer" - valueAlign="right" - /> - - + {#snippet children({ context })} + + + + + + + + + {#snippet children({ data })} + {data.year} + + {#each data.data as d} + + {/each} + + + + d.value)} + format="integer" + valueAlign="right" + /> + + {/snippet} + + {/snippet}
@@ -1147,15 +1142,14 @@
-
+
d[transitionChart.groupBy])) : undefined} - x1Range={({ xScale }) => [0, xScale.bandwidth?.()]} + x1Range={({ xScale }) => [0, xScale.bandwidth()]} padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'band' }} - let:data - let:cScale > - - - - - - {#each transitionData as bar (bar.year + '-' + bar.fruit)} - - {/each} - - - - - - {data.year} - - {#each data.data as d} - - {/each} - - - - - d.value)} - format="integer" - valueAlign="right" - /> - - + {#snippet children({ context })} + + + + + + {#each transitionData as d (d.year + '-' + d.fruit)} + + {/each} + + + + + + {#snippet children({ data })} + {data.year} + + {#each data.data as d} + + {/each} + + + + + d.value)} + format="integer" + valueAlign="right" + /> + + {/snippet} + + {/snippet}
@@ -1232,15 +1244,14 @@
-
+
d[transitionChart.groupBy])) : undefined} - x1Range={({ xScale }) => [0, xScale.bandwidth?.()]} + x1Range={({ xScale }) => [0, xScale.bandwidth()]} padding={{ left: 16, bottom: 24 }} - let:data - let:cScale - let:tooltip > - - - - - - {#each transitionData as bar (bar.year + '-' + bar.fruit)} - { - alert('You clicked on:\n' + JSON.stringify(bar, null, 2)); - }} - onpointerenter={(e) => tooltip?.show(e, bar)} - onpointermove={(e) => tooltip?.show(e, bar)} - onpointerleave={(e) => tooltip?.hide()} - /> - {/each} - - - - - {data.year} - - - - + {#snippet children({ context })} + + + + + {#each transitionData as d (d.year + '-' + d.fruit)} + { + alert('You clicked on:\n' + JSON.stringify(d, null, 2)); + }} + onpointerenter={(e) => context.tooltip.show(e, d)} + onpointermove={(e) => context.tooltip.show(e, d)} + onpointerleave={(e) => context.tooltip.hide()} + /> + {/each} + + + + + {#snippet children({ data })} + {data.year} + + + + {/snippet} + + {/snippet}
@@ -1301,14 +1328,14 @@

Click handler

-
+
- + - format(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
diff --git a/packages/layerchart/src/routes/docs/examples/Columns/+page.ts b/packages/layerchart/src/routes/docs/examples/Columns/+page.ts index 2f23eab19..af07044bf 100644 --- a/packages/layerchart/src/routes/docs/examples/Columns/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Columns/+page.ts @@ -5,6 +5,7 @@ export async function load() { meta: { title: 'Bar Chart (Vertical)', pageSource, + supportedContexts: ['svg', 'canvas'], related: ['components/Bars', 'examples/Bars', 'examples/Histogram', 'examples/Sparkbar'], }, }; diff --git a/packages/layerchart/src/routes/docs/examples/Compound/+page.svelte b/packages/layerchart/src/routes/docs/examples/Compound/+page.svelte index aef309d23..2a31b3b7c 100644 --- a/packages/layerchart/src/routes/docs/examples/Compound/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Compound/+page.svelte @@ -1,11 +1,13 @@

Examples

-

Common scale

+

Common scale with extra marks

- -
+ +
- + {#snippet aboveMarks()} - + {/snippet} - - - - {formatDate(data.date, PeriodType.Day)} - - - - - + {#snippet tooltip()} + + {#snippet children({ data })} + + + + + + {/snippet} - + {/snippet}
-

Stacked Charts

+

Separate scales with stacked charts and overridden marks

- -
- + +
+ @@ -76,33 +78,251 @@ data={data.appleTicker} x="date" y={['open', 'close']} - yNice={4} + yNice yDomain={null} padding={{ left: 16, bottom: 16 }} - tooltip={{ mode: 'band' }} props={{ xAxis: { ticks: 10, rule: true }, + tooltip: { context: { mode: 'band' } }, }} + {renderContext} > - + {#snippet marks()} - + {/snippet} + + {#snippet tooltip({ context })} + + {#snippet children({ data })} + + + + + + + + + {/snippet} + + {/snippet} + +
+
+ +

Dual axis with single chart using remapped scale

+ + +
+ + yScale.domain()} + padding={{ top: 24, bottom: 24, left: 24, right: 24 }} + tooltip={{ mode: 'quadtree-x' }} + > + {#snippet children({ context })} + + + + + + context.y1Scale?.(d.efficiency)} class="stroke-2 stroke-secondary" /> + + context.y1Scale?.(d.efficiency)} + /> + + + + {#snippet children({ data })} + {data.year} + + + + + {/snippet} + + {/snippet} + +
+
+ +

Dual axis with stacked charts

+ + +
+ + + + + + + + + + + + + + + + + + - - - - {formatDate(data.date, PeriodType.Day)} - + + {#snippet children({ data })} + {data.year} - - - - - + + + {/snippet} + + +
+
+ +

Separate scales with stacked charts with inverted range (top down)

+ + +
+ + [0, height]} + padding={{ left: 32, right: 32, bottom: 20 }} + props={{ + bars: { + rounded: 'none', + class: '_stroke-none fill-blue-500', + }, + }} + {renderContext} + /> + + Math.abs(d.infiltration), + value: (d) => (d.infiltration > 0 ? d.infiltration : 0), + color: 'hsl(25, 95%, 53%)', + props: { + rounded: 'none', + }, + }, + { + key: 'dirtyh2o', + color: 'hsl(0, 84%, 60%)', + props: { + rounded: 'none', + }, + }, + { + key: 'rain_induced', + color: 'hsl(142, 71%, 45%)', + props: { + rounded: 'none', + }, + }, + ]} + seriesLayout="stack" + {renderContext} + > + {#snippet axis({ context })} + + + d.date), + [0, context.width] + )} + tickMultiline + rule + /> + {/snippet} + + {#snippet tooltip({ context })} + + {#snippet children({ data })} + + + + + + + + {/snippet} - + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/Compound/+page.ts b/packages/layerchart/src/routes/docs/examples/Compound/+page.ts index 3976beaae..156960bff 100644 --- a/packages/layerchart/src/routes/docs/examples/Compound/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Compound/+page.ts @@ -1,15 +1,27 @@ +import { autoType, csvParse } from 'd3-dsv'; import { parse } from '@layerstack/utils'; + import type { AppleTickerData } from '$static/data/examples/date/apple-ticker.js'; +import type { NewPassengerCars } from '$static/data/examples/new-passenger-cars.js'; +import type { HydroData } from '$static/data/examples/date/hydro.js'; import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { appleTicker: (await fetch('/data/examples/date/apple-ticker.json').then(async (r) => parse(await r.text()) )) as AppleTickerData, + newPassengerCars: await fetch('/data/examples/new-passenger-cars.csv').then(async (r) => + // @ts-expect-error + csvParse(await r.text(), autoType) + ), + hydro: (await fetch('/data/examples/date/hydro.json').then(async (r) => + parse(await r.text()) + )) as HydroData, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], related: [ 'components/Bars', 'examples/Bars', diff --git a/packages/layerchart/src/routes/docs/examples/CountryMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/CountryMap/+page.svelte index aa13dd37c..2a9124785 100644 --- a/packages/layerchart/src/routes/docs/examples/CountryMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/CountryMap/+page.svelte @@ -2,16 +2,17 @@ import { geoAlbersUsa } from 'd3-geo'; import { feature } from 'topojson-client'; - import { Canvas, Chart, GeoPath, renderText, Svg, Text } from 'layerchart'; + import { Canvas, Chart, GeoPath, Layer, Text } from 'layerchart'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); const states = feature(data.geojson, data.geojson.objects.states);

Examples

-

SVG

+

Basic

@@ -21,7 +22,7 @@ fitGeojson: states, }} > - + {#each states.features as feature} {#each states.features as feature} - - {@const [x, y] = geoPath.centroid(feature)} - + + {#snippet children({ geoPath })} + {@const [x, y] = geoPath?.centroid(feature) ?? []} + + {/snippet} {/each} - - -
-
- -

Canvas

- - -
- - - - - {#each states.features as feature} - - {@const [x, y] = geoPath.centroid(feature)} - - - {/each} - +
diff --git a/packages/layerchart/src/routes/docs/examples/CountryMap/+page.ts b/packages/layerchart/src/routes/docs/examples/CountryMap/+page.ts index 8630877d6..406d5bdbe 100644 --- a/packages/layerchart/src/routes/docs/examples/CountryMap/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/CountryMap/+page.ts @@ -1,7 +1,7 @@ import type { GeometryCollection, Topology } from 'topojson-specification'; import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { geojson: (await fetch('https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json').then((r) => r.json() @@ -10,6 +10,7 @@ export async function load() { }>, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/Dagre/+page.svelte b/packages/layerchart/src/routes/docs/examples/Dagre/+page.svelte index cfbe91d4e..051f06c21 100644 --- a/packages/layerchart/src/routes/docs/examples/Dagre/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Dagre/+page.svelte @@ -5,19 +5,20 @@ import { slide } from 'svelte/transition'; import { cls } from '@layerstack/tailwind'; - import { Chart, Dagre, Group, Rect, Spline, Svg, Text, Tooltip } from 'layerchart'; + import { Chart, Dagre, Group, Layer, Rect, Spline, Text, Tooltip } from 'layerchart'; import { Field, MenuField, Switch, Toggle } from 'svelte-ux'; import Preview from '$lib/docs/Preview.svelte'; import DagreControls from './DagreControls.svelte'; import TransformControls from '$lib/components/TransformControls.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); - let selectedGraphValue: keyof typeof data = 'simple'; - $: selectedGraph = data[selectedGraphValue]; + let selectedGraphValue: keyof typeof data = $state('simple'); + const selectedGraph = $derived(data[selectedGraphValue]); - let settings = { + let settings = $state({ playground: { ranker: 'network-simplex', direction: 'left-right', @@ -78,7 +79,7 @@ curve: curveLinear, arrow: 'arrow', }, - } satisfies Record['settings']>; + }) satisfies Record['settings']>;

Examples

@@ -110,63 +111,59 @@
-
+
- - d.links} - {...settings.playground} - let:nodes - let:edges - > - - {#each edges as edge, i (edge.v + '-' + edge.w)} - - {/each} - - - - {#each nodes as node (node.label)} - - + d.links} {...settings.playground}> + {#snippet children({ nodes, edges })} + + {#each edges as edge, i (edge.v + '-' + edge.w)} + - - - - {/each} - + {/each} + + + + {#each nodes as node (node.label)} + + + + + + {/each} + + {/snippet} - +
@@ -190,63 +187,58 @@
-
+
- - d.links} - {...settings.simple} - let:nodes - let:edges - > - - {#each edges as edge, i (edge.v + '-' + edge.w)} - - {/each} - - - - {#each nodes as node (node.label)} - - + d.links} {...settings.simple}> + {#snippet children({ nodes, edges })} + + {#each edges as edge, i (edge.v + '-' + edge.w)} + - - - - {/each} - + {/each} + + + + {#each nodes as node (node.label)} + + + + + + {/each} + + {/snippet} - +
@@ -272,88 +264,83 @@
-
+
- - d.links} - {...settings.tcpState} - let:nodes - let:edges - > - - {#each edges as edge, i (edge.v + '-' + edge.w)} - + + d.links} {...settings.tcpState}> + {#snippet children({ nodes, edges })} + + {#each edges as edge, i (edge.v + '-' + edge.w)} + - - + - - {/each} - - - - {#each nodes as node (node.label)} - - - - - {/each} - + {/each} + + + + {#each nodes as node (node.label)} + + + + + + {/each} + + {/snippet} - +
@@ -379,88 +366,87 @@
-
+
- + d.links} {...settings.softwareUserFlow} - let:nodes - let:edges > - - {#each edges as edge, i (edge.v + '-' + edge.w)} - + {#snippet children({ nodes, edges })} + + {#each edges as edge, i (edge.v + '-' + edge.w)} + - - + - - {/each} - - - - {#each nodes as node (node.label)} - - - - - {/each} - + {/each} + + + + {#each nodes as node (node.label)} + + + + + + {/each} + + {/snippet} - +
@@ -485,7 +471,7 @@
-
+
- + d.links} @@ -546,7 +532,7 @@ {/each} - +
diff --git a/packages/layerchart/src/routes/docs/examples/Dagre/+page.ts b/packages/layerchart/src/routes/docs/examples/Dagre/+page.ts index 27ba7c206..fca93d091 100644 --- a/packages/layerchart/src/routes/docs/examples/Dagre/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Dagre/+page.ts @@ -4,7 +4,7 @@ import { unique } from '@layerstack/utils/array'; import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { const alpha = [...Array(26)].map((val, i) => String.fromCharCode(i + 65)); function getRandomDownstreamIds(index: number) { @@ -58,6 +58,7 @@ export async function load() { meta: { pageSource, + supportedContexts: ['svg'], // TODO: `canvas` coming soon related: ['components/Dagre', 'components/Link'], }, }; diff --git a/packages/layerchart/src/routes/docs/examples/Dagre/DagreControls.svelte b/packages/layerchart/src/routes/docs/examples/Dagre/DagreControls.svelte index b8d17c661..7cd900bac 100644 --- a/packages/layerchart/src/routes/docs/examples/Dagre/DagreControls.svelte +++ b/packages/layerchart/src/routes/docs/examples/Dagre/DagreControls.svelte @@ -8,23 +8,40 @@ import type Spline from '$lib/components/Spline.svelte'; import CurveMenuField from '$lib/docs/CurveMenuField.svelte'; - type DagreProps = ComponentProps; + type DagreProps = ComponentProps; - export let settings = { - ranker: 'network-simplex' as DagreProps['ranker'], - direction: 'left-right' as DagreProps['direction'], - align: 'up-left' as DagreProps['align'], - rankSeparation: 50 as DagreProps['rankSeparation'], - nodeSeparation: 50 as DagreProps['nodeSeparation'], - edgeSeparation: 10 as DagreProps['rankSeparation'], - edgeLabelPosition: 'center' as DagreProps['edgeLabelPosition'], - edgeLabelOffset: 10 as DagreProps['edgeLabelOffset'], - curve: curveLinear as ComponentProps['value'], - arrow: 'arrow' as ComponentProps['marker'], - }; + let { + settings = $bindable({ + ranker: 'network-simplex', + direction: 'left-right', + align: 'up-left', + rankSeparation: 50, + nodeSeparation: 50, + edgeSeparation: 10, + edgeLabelPosition: 'center', + edgeLabelOffset: 10, + curve: curveLinear, + arrow: 'arrow', + }), + class: className, + }: { + settings: { + ranker: DagreProps['ranker']; + direction: DagreProps['direction']; + align: DagreProps['align']; + rankSeparation: DagreProps['rankSeparation']; + nodeSeparation: DagreProps['nodeSeparation']; + edgeSeparation: DagreProps['rankSeparation']; + edgeLabelPosition: DagreProps['edgeLabelPosition']; + edgeLabelOffset: DagreProps['edgeLabelOffset']; + curve: ComponentProps['value']; + arrow: ComponentProps['marker']; + }; + class?: string; + } = $props(); -
+
- import { scaleBand, scaleTime } from 'd3-scale'; - import { addMinutes, startOfDay } from 'date-fns'; - import { Duration } from 'svelte-ux'; - import { PeriodType, format } from '@layerstack/utils'; - - import { Axis, Chart, Highlight, Points, Svg, Tooltip } from 'layerchart'; - - import Preview from '$lib/docs/Preview.svelte'; - import { getRandomInteger } from '$lib/utils/genData.js'; - - const count = 10; - const now = startOfDay(new Date()); - let lastStartDate = now; - - const data = Array.from({ length: count }).map((_, i) => { - const startDate = addMinutes(lastStartDate, getRandomInteger(0, 60)); - const endDate = addMinutes(startDate, getRandomInteger(0, 60)); - lastStartDate = startDate; - return { - name: `Item ${i + 1}`, - startDate, - endDate, - }; - }); - - // TODO: Update to use better data example: https://observablehq.com/@d3/dot-plot - - -

Examples

- -

Basic

- - -
- - - - format(d, PeriodType.TimeOnly, { custom: 'h:mm aa' })} - /> - - - - - - {data.name} - - - - - - - - - - -
-
- -

Colors

- - -
- - - - format(d, PeriodType.TimeOnly, { custom: 'h:mm aa' })} - /> - - - - - - {data.name} - - - - - - - - - - -
-
diff --git a/packages/layerchart/src/routes/docs/examples/DualAxis/+page.svelte b/packages/layerchart/src/routes/docs/examples/DualAxis/+page.svelte deleted file mode 100644 index a6663a7cf..000000000 --- a/packages/layerchart/src/routes/docs/examples/DualAxis/+page.svelte +++ /dev/null @@ -1,128 +0,0 @@ - - -

Examples

- -

Single chart with remapping scale

- - -
- - yScale.domain()} - padding={{ top: 24, bottom: 24, left: 24, right: 24 }} - tooltip={{ mode: 'bisect-x' }} - let:height - let:y1Scale - > - - - - - - y1Scale?.(d.efficiency)} class="stroke-2 stroke-secondary" /> - - y1Scale?.(d.efficiency)} /> - - - - {data.year} - - - - - - -
-
- -

Stacked Charts

- - -
- - - - - - - - - - - - - - - - - - - - - {data.year} - - - - - - -
-
diff --git a/packages/layerchart/src/routes/docs/examples/DualAxis/+page.ts b/packages/layerchart/src/routes/docs/examples/DualAxis/+page.ts deleted file mode 100644 index 39f712aa8..000000000 --- a/packages/layerchart/src/routes/docs/examples/DualAxis/+page.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { autoType, csvParse } from 'd3-dsv'; - -import pageSource from './+page.svelte?raw'; -import type { NewPassengerCars } from '$static/data/examples/new-passenger-cars.js'; - -export async function load() { - return { - newPassengerCars: await fetch('/data/examples/new-passenger-cars.csv').then(async (r) => - // @ts-expect-error - csvParse(await r.text(), autoType) - ), - meta: { - pageSource, - related: ['examples/Compound'], - }, - }; -} diff --git a/packages/layerchart/src/routes/docs/examples/Duration/+page.svelte b/packages/layerchart/src/routes/docs/examples/Duration/+page.svelte new file mode 100644 index 000000000..24da8d308 --- /dev/null +++ b/packages/layerchart/src/routes/docs/examples/Duration/+page.svelte @@ -0,0 +1,458 @@ + + +

Examples

+ +

Bars

+ + +
+ + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {data.name} + + + + + + + + + {/snippet} + + {/snippet} + +
+
+ +

Bars (color)

+ + +
+ + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {data.name} + + + + + + + + + {/snippet} + + {/snippet} + +
+
+ +

Bars (lanes)

+ + +
+ + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {data.name} + + + + + + + + + {/snippet} + + {/snippet} + +
+
+ +

Bars (dense)

+ + +
+ + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {data.event} + + + + + + + + + {/snippet} + + {/snippet} + +
+
+ +

Bars (dense lanes)

+ + +
+ + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {data.event} + + + + + + + + + {/snippet} + + {/snippet} + +
+
+ +

Points

+ + +
+ + {#snippet marks()} + + + {/snippet} + + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {data.name} + + + + + + + + + {/snippet} + + {/snippet} + +
+
+ +

Points (color)

+ + +
+ + {#snippet marks()} + + + {/snippet} + + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {data.name} + + + + + + + + + {/snippet} + + {/snippet} + +
+
+ +

Civilization timeline

+ + +
+ + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {data.civilization} + + + + + + + + {/snippet} + + {/snippet} + +
+
+ +

Civilization timeline (dense)

+ + +
+ + {#snippet tooltip({ context })} + + {#snippet children({ data })} + {data.civilization} + + + + + + + + {/snippet} + + {/snippet} + +
+
diff --git a/packages/layerchart/src/routes/docs/examples/Duration/+page.ts b/packages/layerchart/src/routes/docs/examples/Duration/+page.ts new file mode 100644 index 000000000..b630c13dd --- /dev/null +++ b/packages/layerchart/src/routes/docs/examples/Duration/+page.ts @@ -0,0 +1,37 @@ +import { csvParse, autoType } from 'd3-dsv'; +import pageSource from './+page.svelte?raw'; +import type { USEvents } from '$static/data/examples/date/us-events.js'; +import type { CivilizationTimeline } from '$static/data/examples/date/civilization-timeline.js'; +import { sortFunc } from '@layerstack/utils'; + +export async function load() { + return { + usEvents: await fetch('/data/examples/date/us-events.csv').then(async (r) => { + return csvParse( + await r.text(), + // @ts-expect-error + autoType + ).map((d) => { + return { + startDate: new Date(d.startYear, 0, 1), + endDate: new Date(d.endYear, 11, 31), + event: d.event, + }; + }); + }), + civilizationEvents: await fetch('/data/examples/date/civilization-timeline.csv').then( + async (r) => { + return csvParse( + await r.text(), + // @ts-expect-error + autoType + ).sort(sortFunc('start')); + } + ), + meta: { + pageSource, + supportedContexts: ['svg', 'canvas'], + related: ['components/BarChart', 'components/Points'], + }, + }; +} diff --git a/packages/layerchart/src/routes/docs/examples/EarthquakeGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/EarthquakeGlobe/+page.svelte index b33547047..a48cbe7cc 100644 --- a/packages/layerchart/src/routes/docs/examples/EarthquakeGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/EarthquakeGlobe/+page.svelte @@ -4,75 +4,72 @@ import { feature } from 'topojson-client'; import { Button, ButtonGroup, Field, RangeField } from 'svelte-ux'; - import { timerStore } from '@layerstack/svelte-stores'; - - import { Chart, GeoCircle, GeoPath, Graticule, Svg, Tooltip, TransformContext } from 'layerchart'; + import { TimerState } from '@layerstack/svelte-state'; + + import { + Chart, + GeoCircle, + GeoPath, + Graticule, + Layer, + Tooltip, + type ChartContextValue, + } from 'layerchart'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); // https://observablehq.com/@tansi/earthquake // https://observablehq.com/@observablehq/plot-earthquake-globe const countries = feature(data.geojson, data.geojson.objects.countries); - let transformContext: TransformContext; + let context = $state>(); + + let velocity = $state(3); - let velocity = 3; - let isSpinning = false; - const timer = timerStore({ + const timer = new TimerState({ delay: 1, - onTick() { - transformContext.translate.update((value) => { - return { - x: (value.x += velocity), - y: value.y, - }; - }); + tick: () => { + if (!context) return; + const curr = context.transform.translate; + + context.transform.translate = { + x: (curr.x += velocity), + y: curr.y, + }; }, - disabled: !isSpinning, + disabled: true, }); - $: isSpinning ? timer.start() : timer.stop(); - $timer; + + let debug = $derived(shared.debug);

Examples

-
-

SVG

- -
- - - - - - - - -
+
+ + + + + + + +
timer.stop()} - ondragend={() => { - if (isSpinning) { - // Restart - timer.start(); - } - }} - bind:transformContext - let:tooltip - let:rScale + ondragstart={timer.stop} > - - - - - - - - - {#each data.earthquakes as eq} - tooltip?.show(e, eq)} - onpointerleave={() => tooltip?.hide()} - /> - {/each} - - - - {data.place} - - - - - - + {#snippet children({ context })} + + + + + + + + + {#each data.earthquakes as eq} + context.tooltip.show(e, eq)} + onpointerleave={() => context.tooltip.hide()} + /> + {/each} + + + + {#snippet children({ data })} + {data.place} + + + + + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/EarthquakeGlobe/+page.ts b/packages/layerchart/src/routes/docs/examples/EarthquakeGlobe/+page.ts index 3d69ba38c..ae1ac4161 100644 --- a/packages/layerchart/src/routes/docs/examples/EarthquakeGlobe/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/EarthquakeGlobe/+page.ts @@ -30,6 +30,7 @@ export async function load({ fetch }) { ), meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/EclipsesGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/EclipsesGlobe/+page.svelte index 9982127c5..4e5e97740 100644 --- a/packages/layerchart/src/routes/docs/examples/EclipsesGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/EclipsesGlobe/+page.svelte @@ -5,66 +5,61 @@ import { interpolateGreens, interpolatePurples } from 'd3-scale-chromatic'; import { feature } from 'topojson-client'; - import { Chart, GeoPath, Graticule, Legend, Svg, Tooltip, TransformContext } from 'layerchart'; + import { + Chart, + GeoPath, + Graticule, + Legend, + Layer, + Tooltip, + type ChartContextValue, + } from 'layerchart'; import { Button, ButtonGroup, Field, RangeField } from 'svelte-ux'; - import { format, PeriodType } from '@layerstack/utils'; + import { format } from '@layerstack/utils'; import { cls } from '@layerstack/tailwind'; - import { timerStore } from '@layerstack/svelte-stores'; + import { TimerState } from '@layerstack/svelte-state'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); const countries = feature(data.geojson, data.geojson.objects.countries); const eclipses = feature(data.eclipses, data.eclipses.objects.eclipses); - let transformContext: TransformContext; + let context = $state(null!); - let velocity = 3; - let isSpinning = false; - const timer = timerStore({ + let velocity = $state(3); + const timer = new TimerState({ delay: 1, - onTick() { - transformContext.translate.update((value) => { - return { - x: (value.x += velocity), - y: value.y, - }; - }); + tick: () => { + const value = context.transform.translate; + + context.transform.translate = { + x: (value.x += velocity), + y: value.y, + }; }, - disabled: !isSpinning, + disabled: true, }); - $: isSpinning ? timer.start() : timer.stop(); - $timer; - $: dateExtents = extent(eclipses.features.map((f) => f.properties.Date)); - $: colorScale = scaleDiverging( - [dateExtents[0] ?? 0, new Date(), dateExtents[1] ?? 0], - (t) => (t < 0.5 ? interpolatePurples(1 - t) : interpolateGreens(t)) + const dateExtents = $derived(extent(eclipses.features.map((f) => f.properties.Date))); + const colorScale = $derived( + scaleDiverging([dateExtents[0] ?? 0, new Date(), dateExtents[1] ?? 0], (t) => + t < 0.5 ? interpolatePurples(1 - t) : interpolateGreens(t) + ) );

Examples

-
-

SVG

- +
- - + + @@ -73,7 +68,7 @@ bind:value={velocity} min={-10} max={10} - disabled={!isSpinning} + disabled={!timer.running} labelPlacement="left" />
@@ -87,44 +82,41 @@ fitGeojson: countries, applyTransform: ['rotate'], }} - ondragstart={() => timer.stop()} - ondragend={() => { - if (isSpinning) { - // Restart - timer.start(); - } - }} - bind:transformContext + ondragstart={timer.stop} + bind:context padding={{ top: 60 }} - let:tooltip > - new Date(d).getFullYear().toString()} - /> - - - - - - - {#each eclipses.features as feature} - {@const hasColor = tooltip.data == null || tooltip.data.ID === feature.properties.ID} + {#snippet children({ context })} + + tooltip?.show(e, feature.properties)} - onpointerleave={(e) => tooltip?.hide()} + geojson={{ type: 'Sphere' }} + class="fill-surface-200 stroke-surface-content/20" /> - {/each} - - - - {format(data.Date, PeriodType.Day, { variant: 'long' })} - + + + + {#each eclipses.features as feature} + {@const hasColor = + context.tooltip.data == null || context.tooltip.data.ID === feature.properties.ID} + + context.tooltip.show(e, feature.properties)} + onpointerleave={(e) => context.tooltip.hide()} + /> + {/each} + + + + {#snippet children({ data })} + {format(data.Date, 'day', { variant: 'long' })} + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/EclipsesGlobe/+page.ts b/packages/layerchart/src/routes/docs/examples/EclipsesGlobe/+page.ts index b7f81d1f3..a40ad273a 100644 --- a/packages/layerchart/src/routes/docs/examples/EclipsesGlobe/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/EclipsesGlobe/+page.ts @@ -17,6 +17,7 @@ export async function load({ fetch }) { }>, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], related: [ 'https://www.visionscarto.net/empreintes-d-eclipses', 'http://xjubier.free.fr/en/site_pages/Solar_Eclipses.html', diff --git a/packages/layerchart/src/routes/docs/examples/ForceDisjointGraph/+page.svelte b/packages/layerchart/src/routes/docs/examples/ForceDisjointGraph/+page.svelte index b488e5ad9..62efd5a58 100644 --- a/packages/layerchart/src/routes/docs/examples/ForceDisjointGraph/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/ForceDisjointGraph/+page.svelte @@ -1,32 +1,63 @@

Examples

-
- - +
+ + - {#key nodes} - {#each links as link} - + {#snippet children({ nodes, links, linkPositions })} + {#each links as link, i (keyForLink(link))} + {/each} - {/key} - {#each nodes as node} - - {/each} + {#each nodes as node} + + {/each} + {/snippet} - +
diff --git a/packages/layerchart/src/routes/docs/examples/ForceDisjointGraph/+page.ts b/packages/layerchart/src/routes/docs/examples/ForceDisjointGraph/+page.ts index 0a6f555c1..855f1b38d 100644 --- a/packages/layerchart/src/routes/docs/examples/ForceDisjointGraph/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/ForceDisjointGraph/+page.ts @@ -1,10 +1,11 @@ import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { miserables: await fetch('/data/examples/graph/disjoint-graph.json').then((r) => r.json()), meta: { pageSource, + supportedContexts: ['svg', 'canvas'], related: ['https://observablehq.com/@d3/disjoint-force-directed-graph'], }, }; diff --git a/packages/layerchart/src/routes/docs/examples/ForceDrag/+page.svelte b/packages/layerchart/src/routes/docs/examples/ForceDrag/+page.svelte index 9ce2faaec..57ea87983 100644 --- a/packages/layerchart/src/routes/docs/examples/ForceDrag/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/ForceDrag/+page.svelte @@ -1,17 +1,38 @@

Examples

@@ -50,67 +71,86 @@ -
- - - - {#key nodes} - {#each links as link} - - {/each} - {/key} +
+ + {#snippet children({ context })} + + + {#snippet children({ nodes, simulation, linkPositions })} + {#each links as link, i} + + {/each} - {#each nodes as node} - - - { - tooltip.hide(); - dragging = true; - }} - on:move={(e) => { - node.fx = clamp((node.fx ?? node.x) + e.detail.dx, 0, width); - node.fy = clamp((node.fy ?? node.y) + e.detail.dy, 0, height); - simulation.alpha(1).restart(); - }} - on:moveend={(e) => { - dragging = false; - if (!sticky) { - delete node.fx; - delete node.fy; - simulation.alpha(1).restart(); - } - }} - on:click={(e) => { - if (node.fx) { - delete node.fx; - delete node.fy; - simulation.alpha(1).restart(); - } - }} - on:pointermove={(e) => !dragging && tooltip.show(e, node)} - on:pointerleave={tooltip.hide} - class={cls('cursor-all-scroll', node.fx ? 'fill-primary' : 'fill-surface-content')} - /> - {/each} - - + {#each nodes as node, i} + {@const thisNode = simulation.nodes()[i]} + + { + context.tooltip.hide(); + dragging = true; + }, + onMove: (e) => { + thisNode.fx = clamp( + (thisNode.fx ?? thisNode.x ?? 0) + e.detail.dx, + 0, + context.width + ); + thisNode.fy = clamp( + (thisNode.fy ?? thisNode.y ?? 0) + e.detail.dy, + 0, + context.height + ); + simulation.alpha(1).restart(); + }, + onMoveEnd: (e) => { + dragging = false; + if (!sticky) { + const thisNode = simulation.nodes()[i]; + delete thisNode.fx; + delete thisNode.fy; + simulation.alpha(1).restart(); + } + }, + }} + onclick={() => { + if (thisNode.fx) { + delete thisNode.fx; + delete thisNode.fy; + simulation.alpha(1).restart(); + } + }} + onpointermove={(e) => !dragging && context.tooltip.show(e, node)} + onpointerleave={context.tooltip.hide} + class={cls( + 'cursor-all-scroll', + node.fx ? 'fill-primary' : 'fill-surface-content' + )} + /> + {/each} + {/snippet} + + - - {data.id} - + + {context.tooltip.data?.id} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/ForceDrag/+page.ts b/packages/layerchart/src/routes/docs/examples/ForceDrag/+page.ts index d2c80ef9c..871b56056 100644 --- a/packages/layerchart/src/routes/docs/examples/ForceDrag/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/ForceDrag/+page.ts @@ -4,6 +4,7 @@ export async function load() { return { meta: { pageSource, + supportedContexts: ['svg', 'canvas'], related: ['https://observablehq.com/@d3/sticky-force-layout'], }, }; diff --git a/packages/layerchart/src/routes/docs/examples/ForceGraph/+page.svelte b/packages/layerchart/src/routes/docs/examples/ForceGraph/+page.svelte index 19438c883..c27467467 100644 --- a/packages/layerchart/src/routes/docs/examples/ForceGraph/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/ForceGraph/+page.svelte @@ -1,81 +1,112 @@

Examples

-
- - +
+ + {#snippet children({ context })} - {#key nodes} - {#each links as link} - - {/each} - {/key} + {#snippet children({ nodes, linkPositions })} + + {#each links as link, i} + + {/each} - {#each nodes as node} - tooltip.show(e, node)} - onpointerleave={tooltip.hide} - /> - {/each} + {#each nodes as node ([node.data.name, node.parent?.data?.name].join('-'))} + context.tooltip.show(e, node)} + onpointerleave={context.tooltip.hide} + /> + {/each} + + {/snippet} - - - {data.data.name} - - {#if data.data.children} - - {/if} - {#if data.data.value} - - {/if} - - + + {#snippet children({ data })} + {data.name} + + {#if data.children} + + {/if} + {#if data.value} + + {/if} + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/ForceTree/+page.ts b/packages/layerchart/src/routes/docs/examples/ForceTree/+page.ts index 0bce83660..d85b3dd4f 100644 --- a/packages/layerchart/src/routes/docs/examples/ForceTree/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/ForceTree/+page.ts @@ -1,10 +1,11 @@ import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { flare: await fetch('/data/examples/hierarchy/flare.json').then((r) => r.json()), meta: { pageSource, + supportedContexts: ['svg', 'canvas'], related: ['https://observablehq.com/@d3/force-directed-tree'], }, }; diff --git a/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte b/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte index cfd7ab9de..5fc81b995 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte @@ -2,17 +2,19 @@ import { geoAlbersUsa } from 'd3-geo'; import { feature } from 'topojson-client'; - import { Canvas, Chart, GeoPath, Svg, Tooltip } from 'layerchart'; + import { Chart, GeoPath, Layer, Tooltip } from 'layerchart'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; + + let { data } = $props(); - export let data; const states = feature(data.geojson, data.geojson.objects.states); const counties = feature(data.geojson, data.geojson.objects.counties);

Examples

-

SVG

+

Basic

@@ -21,73 +23,48 @@ projection: geoAlbersUsa, fitGeojson: states, }} - let:projection - let:tooltip > - - {#each states.features as feature} + {#snippet children({ context })} + + {#each states.features as feature} + + {/each} + - {/each} - - - - - - {@const [longitude, latitude] = projection.invert?.([tooltip.x, tooltip.y]) ?? []} - {data.properties.name} - - - - - - -
-
- -

Canvas

- - -
- - - {#each states.features as feature} - - {/each} - - - - - + - - - {#if tooltip.data} - + + + {#if shared.renderContext === 'canvas'} + + {#if context.tooltip.data} + + {/if} + {/if} - - - {@const [longitude, latitude] = projection.invert?.([tooltip.x, tooltip.y]) ?? []} - {data.properties.name} - - - - - + + {#snippet children({ data })} + {@const [longitude, latitude] = + context.geo.projection?.invert?.([context.tooltip.x, context.tooltip.y]) ?? []} + {data.properties.name} + + + + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/GeoPath/+page.ts b/packages/layerchart/src/routes/docs/examples/GeoPath/+page.ts index af04f1af5..5a147d814 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoPath/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/GeoPath/+page.ts @@ -13,6 +13,7 @@ export async function load({ fetch }) { }>, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte b/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte index 8e7a2427b..1b64810a0 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte @@ -1,24 +1,40 @@

Examples

+
+ + + quadtree + voronoi + + + + +
+

US State Capitals

- +
- - - {#each states.features as feature} - - {/each} - + + {#each states.features as feature} + + {/each} + - {#each data.us.captitals as capital} - - - - + {#each data.us.capitals as capital} + + {#if shared.renderContext === 'svg'} + + + + + {:else if shared.renderContext === 'canvas'} + + {#snippet children({ x, y })} + + + {/snippet} + + {/if} {/each} - +
-
-

World Capitals

-
- - - -
-
+

World Capitals

- +
- - + {#snippet children({ context })} + {#each countries.features as feature} {/each} - - - {#each data.world.captitals as capital} + + {#each data.world.capitals as capital} + {/each} + - {#if tooltip.data} - - + + + + {#if context.tooltip.data} + {#if shared.renderContext === 'svg'} + + + {:else if shared.renderContext === 'canvas'} + + {#snippet children({ x, y })} + + + {/snippet} + {/if} - {/each} - - - - + {/if} + + {/snippet}
-
-

US Airports

-
- - - -
-
+

US Airports

- +
- - + {#snippet children({ context })} + {#each states.features as feature} {/each} - - {#each data.us.airports as airport} - + {/each} + - {#if tooltip.data} + + + {#if context.tooltip.data} {/if} - - + - - {data.name} - - - - - + + {#snippet children({ data })} + {data.name} + + + + + {/snippet} + + {/snippet}
-
-

World Airports

-
- - - -
-
+

World Airports

@@ -208,46 +232,54 @@ projection: geoNaturalEarth1, fitGeojson: countries, }} - tooltip={{ mode: 'voronoi', debug: debugTooltip }} - let:tooltip + tooltip={{ mode: tooltipMode, debug, radius: tooltipRadius }} > - - + {#snippet children({ context })} + {#each countries.features as feature} {/each} - - + {#each data.world.airports as airport} - + {/each} + - {#if tooltip.data} + + + {#if context.tooltip.data} {/if} - - + - - {data.name} - - - - - + + {#snippet children({ data })} + {data.name} + + + + + {/snippet} + + {/snippet}
-

Canvas

+

Icons

- +
- - {#each states.features as feature} - - {/each} - {#each data.us.captitals as capital} - - - + {#each states.features as feature} + - - {/each} - + {/each} + + {#each data.us.capitals as capital} + + + + + context.tooltip.show(e, capital)} + onpointermove={(e) => context.tooltip.show(e, capital)} + onpointerleave={(e) => context.tooltip.hide(e)} + /> + + {/each} + + + + {#snippet children({ data })} + {data.description} + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.ts b/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.ts index bc1c0e7b5..ebe7b3db1 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.ts @@ -6,7 +6,7 @@ import type { USStateCapitalsData } from '$static/data/examples/geo/us-state-cap import type { WorldAirportsData } from '$static/data/examples/geo/world-airports.js'; import type { WorldCapitalsData } from '$static/data/examples/geo/world-capitals.js'; -export async function load() { +export async function load({ fetch }) { return { us: { geojson: (await fetch('https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json').then((r) => @@ -17,7 +17,7 @@ export async function load() { airports: (await fetch('/data/examples/geo/us-airports.csv').then(async (r) => csvParse(await r.text(), autoType) )) as USAirportsData, - captitals: (await fetch('/data/examples/geo/us-state-capitals.csv').then(async (r) => + capitals: (await fetch('/data/examples/geo/us-state-capitals.csv').then(async (r) => csvParse(await r.text(), autoType) )) as USStateCapitalsData, }, @@ -31,12 +31,13 @@ export async function load() { airports: (await fetch('/data/examples/geo/world-airports.csv').then(async (r) => csvParse(await r.text(), autoType) )) as WorldAirportsData, - captitals: (await fetch('/data/examples/geo/world-capitals.json').then(async (r) => + capitals: (await fetch('/data/examples/geo/world-capitals.json').then(async (r) => r.json() )) as WorldCapitalsData, }, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/GeoProjection/+page.svelte b/packages/layerchart/src/routes/docs/examples/GeoProjection/+page.svelte index 8869d8de6..a6f07fb2f 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoProjection/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/GeoProjection/+page.svelte @@ -7,17 +7,30 @@ geoMercator, geoNaturalEarth1, geoOrthographic, + geoStereographic, + geoGnomonic, } from 'd3-geo'; import { feature } from 'topojson-client'; - import { Canvas, Chart, GeoPath, Graticule, Svg, Tooltip } from 'layerchart'; + import { Chart, GeoPath, Graticule, Layer, Tooltip } from 'layerchart'; import { Field, RangeField, SelectField, Switch } from 'svelte-ux'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); + + let config = $state({ + projection: geoOrthographic, + detailed: false, + rotate: { + yaw: 0, + pitch: 0, + roll: 0, + }, + scale: 0, + }); - let projection = geoOrthographic; const projections = [ { label: 'Albers', value: geoAlbers }, { label: 'Albers USA', value: geoAlbersUsa }, @@ -26,109 +39,71 @@ { label: 'Mercator', value: geoMercator }, { label: 'Natural Earth', value: geoNaturalEarth1 }, { label: 'Orthographic', value: geoOrthographic }, + { label: 'Stereographic', value: geoStereographic }, + { label: 'Gnomonic', value: geoGnomonic }, ]; - let detailed = false; + const dataGeoJson = $derived(config.detailed ? data.geojsonDetail : data.geojson); - $: dataGeoJson = detailed ? data.geojsonDetail : data.geojson; - $: geojson = feature(dataGeoJson, dataGeoJson.objects.countries); - $: features = - projection === geoAlbersUsa + const geojson = $derived(feature(dataGeoJson, dataGeoJson.objects.countries)); + const features = $derived( + config.projection === geoAlbersUsa ? geojson.features.filter((f) => f.properties.name === 'United States of America') - : geojson.features; - - let scale = 0; - let yaw = 0; - let pitch = 0; - let roll = 0; + : geojson.features + );
- + - +
- - - + + +

Examples

-

SVG

-
- - - - {#each features as feature} - - {/each} - - - - {data.properties.name} - - -
-
- -

Canvas

+ {#snippet children({ context })} + + + + {#each features as feature} + + {/each} + - -
- - - - - - - - - - + + {context.tooltip.data?.properties.name} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/GeoProjection/+page.ts b/packages/layerchart/src/routes/docs/examples/GeoProjection/+page.ts index 6126ea720..6ae8e1b4f 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoProjection/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/GeoProjection/+page.ts @@ -20,6 +20,7 @@ export async function load({ fetch }) { meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/GeoTile/+page.svelte b/packages/layerchart/src/routes/docs/examples/GeoTile/+page.svelte index cea3db6e8..308b89fa0 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoTile/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/GeoTile/+page.svelte @@ -3,41 +3,40 @@ import { geoMercator } from 'd3-geo'; import { feature } from 'topojson-client'; - import { Canvas, ClipPath, Chart, GeoPath, GeoTile, Svg, Tooltip } from 'layerchart'; - import { Field, RangeField, Switch } from 'svelte-ux'; + import { ClipPath, Chart, GeoPath, GeoTile, Layer, Tooltip } from 'layerchart'; + import { RangeField } from 'svelte-ux'; import Preview from '$lib/docs/Preview.svelte'; import TilesetField from '$lib/docs/TilesetField.svelte'; + import { shared } from '../../shared.svelte.js'; + + let { data } = $props(); - export let data; const states = feature(data.geojson, data.geojson.objects.states); - $: filteredStates = { + const filteredStates = { ...states, features: states.features.filter( (d) => Number(d.id) < 60 && d.properties.name !== 'Alaska' && d.properties.name !== 'Hawaii' ), }; // $: filteredStates = { ...states, features: states.features.filter(d => d.properties.name === 'West Virginia')} - let selectedFeature: typeof filteredStates | (typeof filteredStates.features)[0]; - $: selectedFeature = filteredStates; + let selectedFeature: typeof filteredStates | (typeof filteredStates.features)[0] = + $state(filteredStates); - let serviceUrl: ComponentProps['url']; - let zoomDelta = 0; - let debug = false; + let serviceUrl = $state['url']>(null!); + let zoomDelta = $state(0); + let debug = $derived(shared.debug); -
+
- - -

Examples

-

SVG

+

Basic

@@ -46,35 +45,39 @@ projection: geoMercator, fitGeojson: selectedFeature, }} - let:tooltip - let:projection > - - - {#each filteredStates.features as feature} - - (selectedFeature = selectedFeature === feature ? filteredStates : feature)} - /> - {/each} - + {#snippet children({ context })} + + + {#each filteredStates.features as feature} + + + (selectedFeature = selectedFeature === feature ? filteredStates : feature)} + /> + {/each} + - - {@const [longitude, latitude] = projection.invert?.([tooltip.x, tooltip.y]) ?? []} - {data.properties.name} - - - - - + + {#snippet children({ data })} + {@const [longitude, latitude] = + context.geo.projection?.invert?.([context.tooltip.x, context.tooltip.y]) ?? []} + {data.properties.name} + + + + + {/snippet} + + {/snippet}
-

SVG (clipped)

+

Clippped (currently svg-only)

@@ -83,53 +86,36 @@ projection: geoMercator, fitGeojson: selectedFeature, }} - let:tooltip - let:projection > - - - - - - {#each filteredStates.features as feature} - - (selectedFeature = selectedFeature === feature ? filteredStates : feature)} - /> - {/each} - + {#snippet children({ context })} + + + + + + {#each filteredStates.features as feature} + + (selectedFeature = selectedFeature === feature ? filteredStates : feature)} + /> + {/each} + - - {@const [longitude, latitude] = projection.invert?.([tooltip.x, tooltip.y]) ?? []} - {data.properties.name} - - - - - - -
-
- -

Canvas

- - -
- - - - - - - + + {#snippet children({ data })} + {@const [longitude, latitude] = + context.geo.projection?.invert?.([context.tooltip.x, context.tooltip.y]) ?? []} + {data.properties.name} + + + + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/GeoTile/+page.ts b/packages/layerchart/src/routes/docs/examples/GeoTile/+page.ts index 799cd1712..fc885698a 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoTile/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/GeoTile/+page.ts @@ -1,7 +1,7 @@ import type { GeometryCollection, Topology } from 'topojson-specification'; import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { geojson: (await fetch('https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json').then((r) => r.json() @@ -10,6 +10,7 @@ export async function load() { }>, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], related: ['examples/ZoomableTileMap'], }, }; diff --git a/packages/layerchart/src/routes/docs/examples/Histogram/+page.svelte b/packages/layerchart/src/routes/docs/examples/Histogram/+page.svelte index e9e7752eb..b2385d720 100644 --- a/packages/layerchart/src/routes/docs/examples/Histogram/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Histogram/+page.svelte @@ -9,42 +9,48 @@ randomNormal, randomUniform, } from 'd3-random'; - import { timeDays, timeMonths, timeWeeks } from 'd3-time'; - import { subDays } from 'date-fns'; + import { timeDay, timeMonth, timeWeek } from 'd3-time'; import { BarChart, Tooltip, thresholdTime } from 'layerchart'; import { MenuField, RangeField, NumberStepper, State } from 'svelte-ux'; - import { format, PeriodType } from '@layerstack/utils'; + import { format } from '@layerstack/utils'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); - let thresholds = 10; + let thresholds = $state(10); - $: binByWeight = bin<(typeof data.olympians)[0], number>() - .value((d) => d.weight) - .thresholds(thresholds); - $: olympiansBins = binByWeight(data.olympians); + const binByWeight = $derived( + bin<(typeof data.olympians)[0], number>() + .value((d) => d.weight) + .thresholds(thresholds) + ); + const olympiansBins = $derived(binByWeight(data.olympians)); - let selectedGenerator = 'normal'; - let randomCount = 1000; - $: random = randomNormal(); - $: randomData = Array.from({ length: randomCount }, () => random()); - $: binByValues = bin(); //.domain([0, 1]); - $: randomBins = binByValues(randomData); + let selectedGenerator = $state('normal'); + let randomCount = $state(1000); + let random = $state(randomNormal()); + const randomData = $derived(Array.from({ length: randomCount }, () => random())); + const binByValues = $derived(bin()); //.domain([0, 1]); + const randomBins = $derived(binByValues(randomData)); - $: getRandomDate = (from: Date, to: Date) => { + function getRandomDate(from: Date, to: Date) { const fromTime = from.getTime(); const toTime = to.getTime(); return new Date(fromTime + random() * (toTime - fromTime)); - }; + } const now = new Date(); - let dateRange = 10; - $: randomDateData = Array.from({ length: randomCount }, () => - getRandomDate(subDays(now, dateRange), now) - ) as any[]; // TODO: Make typescript happy + let dateRange = $state(10); + const randomDateData = $derived( + Array.from({ length: randomCount }, () => + getRandomDate(timeDay.offset(now, -dateRange), now) + ) as any[] + ); // TODO: Make typescript happy + + let renderContext = $derived(shared.renderContext as 'svg' | 'canvas');

Examples

@@ -54,34 +60,37 @@

Vertical

-
+
- - - {data.x0 + ' - ' + (data.x1 - 1)} - - - - {#each data.slice(0, 5) as d} - - {/each} - {#if data.length > 5} - - ... - {/if} - + {#snippet tooltip()} + + {#snippet children({ data })} + {data.x0 + ' - ' + (data.x1 - 1)} + + + + {#each data.slice(0, 5) as d} + + {/each} + {#if data.length > 5} + + ... + {/if} + + {/snippet} - + {/snippet}
@@ -89,35 +98,38 @@

Horizontal

-
+
- - - {data.x0 + ' - ' + (data.x1 - 1)} - - - - {#each data.slice(0, 5) as d} - - {/each} - {#if data.length > 5} - - ... - {/if} - + {#snippet tooltip()} + + {#snippet children({ data })} + {data.x0 + ' - ' + (data.x1 - 1)} + + + + {#each data.slice(0, 5) as d} + + {/each} + {#if data.length > 5} + + ... + {/if} + + {/snippet} - + {/snippet}
@@ -182,34 +194,38 @@

Random distribution

-
+
- - - {data.x0 + ' - ' + (data.x1 - 0.01)} - - - - {#each data.slice(0, 5) as d} - - {/each} - {#if data.length > 5} - - ... - {/if} - + {#snippet tooltip()} + + {#snippet children({ data })} + {data.x0 + ' - ' + (data.x1 - 0.01)} + + + + {#each data.slice(0, 5) as d} + + {/each} + {#if data.length > 5} + + ... + {/if} + + {/snippet} - + {/snippet}
@@ -231,50 +247,45 @@
-
+
- - - - {format(data.x0, PeriodType.Day) + - ' - ' + - format(data.x1, PeriodType.Day)} - - - - {#each data.slice(0, 5) as d} - format(value, PeriodType.DayTime)} - /> - {/each} - {#if data.length > 5} - - ... - {/if} - + {#snippet tooltip()} + + {#snippet children({ data })} + + {format(data.x0, 'day') + ' - ' + format(data.x1, 'day')} + + + + {#each data.slice(0, 5) as d} + + {/each} + {#if data.length > 5} + + ... + {/if} + + {/snippet} - + {/snippet}
- - - + {@const binByTime = bin().thresholds( (_data, min, max) => value?.intervalFunc(new Date(min), new Date(max)).map((d) => d.valueOf()) ?? [] @@ -288,9 +299,9 @@ { @@ -302,7 +313,7 @@
-
+
scaleTime(scale.domain(), scale.range()).ticks(), tickLabelProps: { rotate: 315, textAnchor: 'end', verticalAnchor: 'middle', dy: 8 }, - tweened: true, + motion: 'tween', }, - yAxis: { format: 'metric', tweened: true }, - bars: { tweened: true }, + yAxis: { format: 'metric', motion: 'tween' }, + bars: { motion: 'tween' }, }} + {renderContext} > - - - - {format(data.x0, PeriodType.Day) + - ' - ' + - format(data.x1, PeriodType.Day)} - - - - {#each data.slice(0, 5) as d} - format(value, PeriodType.DayTime)} - /> - {/each} - {#if data.length > 5} - - ... - {/if} - + {#snippet tooltip()} + + {#snippet children({ data })} + + {format(data.x0, 'day') + ' - ' + format(data.x1, 'day')} + + + + {#each data.slice(0, 5) as d} + + {/each} + {#if data.length > 5} + + ... + {/if} + + {/snippet} - + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/Histogram/+page.ts b/packages/layerchart/src/routes/docs/examples/Histogram/+page.ts index b395a94c5..a2a8acbee 100644 --- a/packages/layerchart/src/routes/docs/examples/Histogram/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Histogram/+page.ts @@ -1,13 +1,14 @@ import pageSource from './+page.svelte?raw'; import type { OlympiansData } from '$static/data/examples/olympians.js'; -export async function load() { +export async function load({ fetch }) { return { olympians: (await fetch('/data/examples/olympians.json').then((r) => r.json() )) as OlympiansData, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], related: ['components/Bars', 'examples/Bars', 'examples/Columns'], }, }; diff --git a/packages/layerchart/src/routes/docs/examples/Line/+page.svelte b/packages/layerchart/src/routes/docs/examples/Line/+page.svelte index d5a3187a6..2621f5265 100644 --- a/packages/layerchart/src/routes/docs/examples/Line/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Line/+page.svelte @@ -1,20 +1,19 @@ @@ -62,53 +61,20 @@

Basic

-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - -
- - -

Canvas

- - -
- - - - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - - - +
@@ -116,33 +82,30 @@

With Tooltip and Highlight

-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - - - - {format(data.date, 'eee, MMMM do')} - - - + + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -151,26 +114,21 @@

With Labels

-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -178,30 +136,17 @@

Gradient encoding

-
- - +
+ + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - + + + {#snippet children({ gradient })} + + {/snippet} - + Gradient threshold -
+
+ + {#snippet children({ context })} + {@const thresholdOffset = + (context.yScale(50) / (context.height + context.padding.bottom)) * 100 + '%'} + + + + + {#snippet children({ gradient })} + + {/snippet} + + + {/snippet} + +
+ + +

Vertical

+ + +
- {@const thresholdOffset = (yScale(50) / (height + padding.bottom)) * 100 + '%'} - + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> - - - - + + + + + + + {#snippet children({ data })} + + + + + {/snippet} +
@@ -255,54 +221,51 @@

Multiple series

-
+
- - - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> - {#each dataByFruit as [fruit, data]} - {@const color = cScale?.(fruit)} - - - - - - - {/each} - - - - - {format(data.date, 'eee, MMMM do')} - - - - + {#snippet children({ context })} + + + + {#each dataByFruit as [fruit, data]} + {@const color = context.cScale?.(fruit)} + + {#snippet endContent()} + + + {/snippet} + + {/each} + + + + + {#snippet children({ data })} + + + + + {/snippet} + + {/snippet}
@@ -310,7 +273,7 @@

Multiple series (using overrides)

-
+
({ x: i, @@ -322,27 +285,25 @@ yDomain={[0, null]} yNice padding={{ left: 16, bottom: 24 }} - tooltip={{ mode: 'bisect-x' }} + tooltip={{ mode: 'quadtree-x' }} > - + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + d.y} class="stroke-2" stroke={fruitColors.bananas} /> d.y1} class="stroke-2" stroke={fruitColors.oranges} /> d.y} points={{ fill: fruitColors.bananas }} /> d.y1} points={{ fill: fruitColors.oranges }} /> - - - - - - - + + + + {#snippet children({ data })} + + + + + {/snippet}
@@ -351,57 +312,51 @@

Multiple series (highlight on hover)

-
+
- - - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> - {#each dataByFruit as [fruit, data]} - {@const color = - tooltip.data == null || tooltip.data.fruit === fruit - ? cScale?.(fruit) - : 'hsl(var(--color-surface-content) / 20%)'} - - - - - - - {/each} - - - - {format(data.date, 'eee, MMMM do')} - - - - + {#snippet children({ context })} + + + + {#each dataByFruit as [fruit, data]} + {@const active = context.tooltip.data == null || context.tooltip.data.fruit === fruit} + {@const color = context.cScale?.(fruit)} + + + {#snippet endContent()} + + + {/snippet} + + + {/each} + + + + + + + + + {/snippet}
@@ -409,42 +364,39 @@

Multiple series with labels

-
+
- - - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> - {#each dataByFruit as [fruit, data]} - {@const color = cScale?.(fruit)} - - {/each} - - - - - {format(data.date, 'eee, MMMM do')} - - - - + {#snippet children({ context })} + + + + {#each dataByFruit as [fruit, data]} + {@const color = context.cScale?.(fruit)} + + {/each} + + + + + {#snippet children({ data })} + + + + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/Line/+page.ts b/packages/layerchart/src/routes/docs/examples/Line/+page.ts index f2cb67afe..4f08aa0c7 100644 --- a/packages/layerchart/src/routes/docs/examples/Line/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Line/+page.ts @@ -2,13 +2,14 @@ import { parse } from '@layerstack/utils'; import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { dailyTemperature: await fetch('/data/examples/date/daily-temperature.json').then(async (r) => parse<{ date: Date; value: number }[]>(await r.text()) ), meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/LoftedArcs/+page.svelte b/packages/layerchart/src/routes/docs/examples/LoftedArcs/+page.svelte index f15143c32..e0989bafe 100644 --- a/packages/layerchart/src/routes/docs/examples/LoftedArcs/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/LoftedArcs/+page.svelte @@ -3,26 +3,29 @@ import { flatRollup } from 'd3-array'; import { feature } from 'topojson-client'; - import { Chart, GeoEdgeFade, GeoPath, GeoPoint, GeoSpline, Graticule, Svg } from 'layerchart'; + import { Chart, GeoEdgeFade, GeoPath, GeoPoint, GeoSpline, Graticule, Layer } from 'layerchart'; import { Field, Switch } from 'svelte-ux'; import GeoDebug from '$lib/docs/GeoDebug.svelte'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); const countries = feature(data.geojson, data.geojson.objects.countries); - let debug = false; - // Use a single link per source - $: singleLinks = flatRollup( - data.worldLinks, - (values) => { - return values[1]; - }, - (d) => d.sourceId - ).map((d) => d[1]); + const singleLinks = $derived( + flatRollup( + data.worldLinks, + (values) => { + return values[1]; + }, + (d) => d.sourceId + ).map((d) => d[1]) + ); + + let debug = $derived(shared.debug);

Examples

@@ -38,7 +41,7 @@ }} padding={{ top: 16, bottom: 16, left: 16, right: 16 }} > - + {#each countries.features as country} @@ -50,23 +53,12 @@ {/each} - +
-
-

Draggable globe with EdgeFade

- -
- - - -
-
+

Draggable globe with EdgeFade

@@ -81,7 +73,7 @@ {#if debug} {/if} - + {#each countries.features as country} @@ -98,7 +90,7 @@ {/each} - +
diff --git a/packages/layerchart/src/routes/docs/examples/LoftedArcs/+page.ts b/packages/layerchart/src/routes/docs/examples/LoftedArcs/+page.ts index 1be6a8043..9b47b9e8a 100644 --- a/packages/layerchart/src/routes/docs/examples/LoftedArcs/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/LoftedArcs/+page.ts @@ -17,6 +17,7 @@ export async function load({ fetch }) { )) as WorldLinksData, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], reference: 'https://observablehq.com/@armollica/globe-with-lofted-arcs', }, }; diff --git a/packages/layerchart/src/routes/docs/examples/Lollipop/+page.svelte b/packages/layerchart/src/routes/docs/examples/Lollipop/+page.svelte new file mode 100644 index 000000000..5ae817f8e --- /dev/null +++ b/packages/layerchart/src/routes/docs/examples/Lollipop/+page.svelte @@ -0,0 +1,45 @@ + + +

Examples

+ +

Basic

+ + +
+ + + + + + + + + + + {#snippet children({ data })} + + + + + {/snippet} + + +
+
diff --git a/packages/layerchart/src/routes/docs/examples/Lollipop/+page.ts b/packages/layerchart/src/routes/docs/examples/Lollipop/+page.ts new file mode 100644 index 000000000..50f9e2f8b --- /dev/null +++ b/packages/layerchart/src/routes/docs/examples/Lollipop/+page.ts @@ -0,0 +1,17 @@ +import { autoType, csvParse } from 'd3-dsv'; +import type { AlphabetData } from '$static/data/examples/alphabet.js'; + +import pageSource from './+page.svelte?raw'; + +export async function load({ fetch }) { + return { + alphabet: (await fetch('/data/examples/alphabet.csv').then(async (r) => + csvParse(await r.text(), autoType) + )) as AlphabetData[], + meta: { + pageSource, + supportedContexts: ['svg', 'canvas'], + related: ['components/Rule'], + }, + }; +} diff --git a/packages/layerchart/src/routes/docs/examples/Oscilloscope/+page.svelte b/packages/layerchart/src/routes/docs/examples/Oscilloscope/+page.svelte index 9945fca1f..cd9bd50ac 100644 --- a/packages/layerchart/src/routes/docs/examples/Oscilloscope/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Oscilloscope/+page.svelte @@ -1,5 +1,5 @@

Examples

@@ -70,7 +76,7 @@

Time

-
+
@@ -87,7 +94,7 @@

Frequency

-
+
decibels(d)?.toFixed(1) }, }} + {renderContext} > - + {#snippet marks()} - + {#snippet children({ gradient })} + + {/snippet} - + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/Oscilloscope/+page.ts b/packages/layerchart/src/routes/docs/examples/Oscilloscope/+page.ts index a20d59c43..70f8e4352 100644 --- a/packages/layerchart/src/routes/docs/examples/Oscilloscope/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Oscilloscope/+page.ts @@ -4,6 +4,7 @@ export async function load() { return { meta: { pageSource, + supportedContexts: ['svg', 'canvas'], related: [ 'components/Bars', 'components/Spline', diff --git a/packages/layerchart/src/routes/docs/examples/Pack/+page.svelte b/packages/layerchart/src/routes/docs/examples/Pack/+page.svelte index 95603369a..8a1384c60 100644 --- a/packages/layerchart/src/routes/docs/examples/Pack/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Pack/+page.svelte @@ -1,5 +1,4 @@
@@ -79,29 +97,35 @@

General

- - - -
+
- (selected = complexHierarchy)}> - - {#each nodes as node} + (selected = complexHierarchy)}> + + {#each nodes as node ([node.data.name, node.parent?.data.name].join('-'))} {/each} - - {#each selected ? (selected.children ?? [selected]) : [] as node (node.data.name + node.depth)} - {@const fontSize = 1 / transform.scale} + + {@const selectedNodes = selected + ? (selected.children ?? [selected]) + : nodes[0] + ? (nodes[0].children ?? [nodes[0]]) + : []} + + {#each selectedNodes as node ([node.data.name, node.parent?.data.name].join('-'))} + {@const trueNode = findSelectedNodeInHierarchy(node, nodes)} + {@const fontSize = 1 / context.transform.scale} - - {node.data.name} - + style="font-size: {fontSize}rem; stroke-width: {fontSize * 2}px" + class="fill-black stroke-white/70 pointer-events-none [text-anchor:middle] [paint-order:stroke]" + /> {/each} - +
diff --git a/packages/layerchart/src/routes/docs/examples/Pack/+page.ts b/packages/layerchart/src/routes/docs/examples/Pack/+page.ts index 6eaed4ca6..2f2bf42d5 100644 --- a/packages/layerchart/src/routes/docs/examples/Pack/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Pack/+page.ts @@ -1,10 +1,11 @@ import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { flare: await fetch('/data/examples/hierarchy/flare.json').then((r) => r.json()), meta: { pageSource, + supportedContexts: ['svg'], // TODO: `canvas` coming soon }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/Partition/+page.svelte b/packages/layerchart/src/routes/docs/examples/Partition/+page.svelte index f7ebc1544..c73136020 100644 --- a/packages/layerchart/src/routes/docs/examples/Partition/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Partition/+page.svelte @@ -15,7 +15,7 @@ Partition, Rect, RectClipPath, - Svg, + Layer, Text, findAncestor, } from 'layerchart'; @@ -33,8 +33,9 @@ import { cls } from '@layerstack/tailwind'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); const complexHierarchy = hierarchy(data.flare) .sum((d) => d.value) @@ -43,39 +44,45 @@ const horizontalHierarchy = complexHierarchy.copy(); const verticalHierarchy = complexHierarchy.copy(); - let isFiltered = false; - $: groupedCars = rollup( - data.cars - // Limit dataset - .filter((d) => - ['BMW', 'Chevrolet', 'Dodge', 'Ford', 'Honda', 'Toyota', 'Volkswagen'].includes(d.make) - ) - // Hide some models in each group to show transitions - .filter((d) => (isFiltered ? d.year > 2010 : true)) - // Apply `make` selection - .filter((d) => { - if (selectedCarNode?.depth === 1) { - return d.make === selectedCarNode.data[0]; - } else { - return true; - } - }), - (items) => items[0], //.slice(0, 3), - (d) => d.make, - (d) => d.model - // d => d.year, - ); - let groupedHierarchy: HierarchyRectangularNode; - $: groupedHierarchy = hierarchy(groupedCars).count() as HierarchyRectangularNode; + let isFiltered = $state(false); + + let selectedCarNode = $state>(); + + function getGrouped(selected?: HierarchyRectangularNode) { + return rollup( + data.cars + // Limit dataset + .filter((d) => + ['BMW', 'Chevrolet', 'Dodge', 'Ford', 'Honda', 'Toyota', 'Volkswagen'].includes(d.make) + ) + // Hide some models in each group to show transitions + .filter((d) => (isFiltered ? d.year > 2010 : true)) + // Apply `make` selection + .filter((d) => { + if (selected && selected?.depth === 1) { + return d.make === selected.data[0]; + } else { + return true; + } + }), + (items) => items[0], //.slice(0, 3), + (d) => d.make, + (d) => d.model + // d => d.year, + ); + } - let colorBy = 'children'; + let colorBy = $state('children'); + let horizontalNodes = $state[]>([]); + let verticalNodes = $state[]>([]); + let carNodes = $state[]>([]); + let groupedHierarchy = $derived(hierarchy(getGrouped()).count()); - let padding = 0; - let round = false; - let fullSizeLeafNodes = false; - let selectedHorizontal = horizontalHierarchy; // select root initially - let selectedVertical = verticalHierarchy; // select root initially - let selectedCarNode = groupedHierarchy; + let padding = $state(0); + let round = $state(false); + let fullSizeLeafNodes = $state(false); + let selectedHorizontal = $state>(); // select root initially + let selectedVertical = $state>(); // select root initially const sequentialColor = scaleSequential([4, -1], chromatic.interpolateGnBu); // filter out hard to see yellow and green @@ -87,7 +94,7 @@ function getNodeColor(node: HierarchyNode, colorBy: string) { switch (colorBy) { case 'children': - return node.children ? 'hsl(var(--color-primary))' : 'hsl(var(--color-primary-600))'; + return node.children ? 'var(--color-primary)' : 'var(--color-primary-600)'; case 'depth': return sequentialColor(node.depth).toString(); case 'parent': @@ -100,6 +107,23 @@ } return ''; } + + const horizontalBreadcrumbItems = $derived( + selectedHorizontal + ? selectedHorizontal?.ancestors().reverse() + : (horizontalNodes[0]?.ancestors().reverse() ?? []) + ); + const verticalBreadcrumbItems = $derived( + selectedVertical + ? selectedVertical?.ancestors().reverse() + : (verticalNodes[0]?.ancestors().reverse() ?? []) + ); + + const carNodeBreadcrumbItems = $derived( + selectedCarNode + ? selectedCarNode?.ancestors().reverse() + : (carNodes[0]?.ancestors().reverse() ?? []) + );
@@ -132,13 +156,13 @@

Horizontal

- + -
- - - - - - {#each nodes as node} - {@const nodeWidth = - node.children || !fullSizeLeafNodes - ? xScale(node.y1) - xScale(node.y0) - : width - xScale(node.y0)} - {@const nodeHeight = yScale(node.x1) - yScale(node.x0)} - (selectedHorizontal = node)} +
+ + {#snippet children({ context })} + + + {#snippet children({ xScale, yScale })} + + - - {@const nodeColor = getNodeColor(node, colorBy)} - - - (selectedHorizontal = node)} > - {node.data.name} - - {format(node.value ?? 0, 'integer')} - - - - - - {/each} - - - - + + {@const nodeColor = getNodeColor(node, colorBy)} + + + + {node.data.name} + + {format(node.value ?? 0, 'integer')} + + + + + + {/each} + {/snippet} + + + {/snippet} + + + {/snippet}
@@ -218,13 +251,13 @@

Vertical

- + -
- - - - - - {#each nodes as node} - {@const nodeWidth = xScale(node.x1) - xScale(node.x0)} - {@const nodeHeight = - node.children || !fullSizeLeafNodes - ? yScale(node.y1) - yScale(node.y0) - : height - yScale(node.y0)} - (selectedVertical = node)} +
+ + {#snippet children({ context })} + + + {#snippet children({ xScale, yScale })} + + - - {@const nodeColor = getNodeColor(node, colorBy)} - - - - - - - - {/each} - - - - + {#snippet children({ nodes })} + {#each nodes as node} + {@const nodeWidth = xScale(node.x1) - xScale(node.x0)} + {@const nodeHeight = + node.children || !fullSizeLeafNodes + ? yScale(node.y1) - yScale(node.y0) + : context.height - yScale(node.y0)} + (selectedVertical = node)} + > + + {@const nodeColor = getNodeColor(node, colorBy)} + + + + + + + + {/each} + {/snippet} + + + {/snippet} + + + {/snippet}
@@ -310,13 +357,13 @@
- + -
- - +
+ + - - - {#each nodes as node (node - .ancestors() - .map((n) => n.data[0]) - .join('_'))} - (selectedCarNode = node)} - tweened={{ delay: 600 }} - > - {@const nodeWidth = xScale(node.y1) - xScale(node.y0)} - {@const nodeHeight = yScale(node.x1) - yScale(node.x0)} - {@const nodeColor = getNodeColor(node, colorBy)} - - - - + + {#snippet children({ nodes })} + {#each nodes as node (node + .ancestors() + .map((n) => n.data[0]) + .join('_'))} + + (selectedCarNode = node)} + motion={{ type: 'tween', delay: 600 }} > - {node.data[0] ?? 'Overall'} - {#if node.children} - + + - {format(node.value ?? 0, 'integer')} - - {/if} - - - - - {/each} - - + {node.data[0] ?? 'Overall'} + {#if node.children} + + {format(node.value ?? 0, 'integer')} + + {/if} + + + + + {/each} + {/snippet} + + + {/snippet} - +
diff --git a/packages/layerchart/src/routes/docs/examples/Partition/+page.ts b/packages/layerchart/src/routes/docs/examples/Partition/+page.ts index 2862d0397..82edd0c32 100644 --- a/packages/layerchart/src/routes/docs/examples/Partition/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Partition/+page.ts @@ -2,7 +2,7 @@ import { csvParse, autoType } from 'd3-dsv'; import pageSource from './+page.svelte?raw'; import type { CarData } from '$static/data/examples/cars.js'; -export async function load() { +export async function load({ fetch }) { return { flare: await fetch('/data/examples/hierarchy/flare.json').then((r) => r.json()), cars: await fetch('/data/examples/cars.csv').then(async (r) => @@ -11,6 +11,7 @@ export async function load() { ), meta: { pageSource, + supportedContexts: ['svg'], // TODO: `canvas` coming soon }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/PunchCard/+page.svelte b/packages/layerchart/src/routes/docs/examples/PunchCard/+page.svelte index 347c6918b..4a1064252 100644 --- a/packages/layerchart/src/routes/docs/examples/PunchCard/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/PunchCard/+page.svelte @@ -1,17 +1,19 @@

Examples

@@ -19,38 +21,42 @@

Basic

-
+
getWeek(d.date)} + x={(d) => timeWeek.count(timeYear(d.date), d.date)} xScale={scaleBand()} - y={(d) => getDay(d.date)} + y={(d) => d.date.getDay()} yScale={scaleBand()} yDomain={range(7)} r="value" rRange={[0, 16]} - padding={{ left: 48, bottom: 16 }} - tooltip={{ mode: 'band' }} + padding={{ left: 32, bottom: 16 }} props={{ xAxis: { format: (d) => 'Week ' + d }, yAxis: { format: (d) => daysOfWeek[d] }, rule: { x: true, y: false }, grid: { x: false, y: true, bandAlign: 'between' }, + tooltip: { context: { mode: 'band' } }, }} + {renderContext} + debug={shared.debug} > - + {#snippet highlight()} - - - - - {formatDate(data.date, PeriodType.Day)} - - - + {/snippet} + + {#snippet tooltip()} + + {#snippet children({ data })} + + + + + {/snippet} - + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/PunchCard/+page.ts b/packages/layerchart/src/routes/docs/examples/PunchCard/+page.ts index 48dcda64e..2f92cb817 100644 --- a/packages/layerchart/src/routes/docs/examples/PunchCard/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/PunchCard/+page.ts @@ -4,6 +4,7 @@ export async function load() { return { meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/RadialLine/+page.svelte b/packages/layerchart/src/routes/docs/examples/RadialLine/+page.svelte index 9e237ef88..4d779cb45 100644 --- a/packages/layerchart/src/routes/docs/examples/RadialLine/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/RadialLine/+page.svelte @@ -1,17 +1,17 @@

Examples

@@ -40,17 +40,16 @@
-
+
- + - +
@@ -68,7 +67,7 @@

Line with Areas

-
+
[height / 5, height / 2]} radial + padding={{ top: 12, bottom: 12 }} > - + d.avg} curve={curveCatmullRom} class="stroke-primary" /> d.min} @@ -91,14 +91,14 @@ curve={curveCatmullRomClosed} class="fill-primary/20" /> - + v + '° F'} /> - +
@@ -106,7 +106,7 @@

Multi-year Lines

-
+
- - {#each flatGroup(data.dailyTemperatures, (d) => d.year) as [year, yearData]} - + {#each flatGroup(data.dailyTemperatures, (d) => d.year) as [year, yearData]} + + {/each} + + v + '° F'} /> - {/each} - - v + '° F'} - /> - + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/RadialLine/+page.ts b/packages/layerchart/src/routes/docs/examples/RadialLine/+page.ts index 66da15f67..3d86e1786 100644 --- a/packages/layerchart/src/routes/docs/examples/RadialLine/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/RadialLine/+page.ts @@ -4,7 +4,7 @@ import { ascending, flatGroup, max, mean, min } from 'd3-array'; import pageSource from './+page.svelte?raw'; import { celsiusToFahrenheit } from '$lib/utils/math.js'; -export async function load() { +export async function load({ fetch }) { return { dailyTemperatures: await fetch('/data/examples/dailyTemperatures.csv').then(async (r) => { return csvParse<{ dayOfYear: number; year: number; value: number | 'NA' }>( @@ -43,6 +43,7 @@ export async function load() { }), meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/Sankey/+page.svelte b/packages/layerchart/src/routes/docs/examples/Sankey/+page.svelte index fc640e299..2ea8e819d 100644 --- a/packages/layerchart/src/routes/docs/examples/Sankey/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Sankey/+page.svelte @@ -8,7 +8,8 @@ import { Icon } from 'svelte-ux'; import { sortFunc } from '@layerstack/utils'; import { cls } from '@layerstack/tailwind'; - import { mdiArrowRightBold } from '@mdi/js'; + + import LucideArrowRight from '~icons/lucide/arrow-right'; import { Chart, @@ -16,30 +17,31 @@ Link, Rect, Sankey, - Svg, + Layer, Text, Tooltip, - graphFromHierarchy, - graphFromNode, + sankeyGraphFromHierarchy, + sankeyGraphFromNode, } from 'layerchart'; import Preview from '$lib/docs/Preview.svelte'; import SankeyControls from './SankeyControls.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); const colorScale = scaleSequential(interpolateCool); - type SankeyControlsProps = ComponentProps; + type SankeyControlsProps = ComponentProps; - let highlightLinkIndexes: Array = []; - let nodeAlign: SankeyControlsProps['nodeAlign'] = 'justify'; - let nodePadding: SankeyControlsProps['nodePadding'] = 4; - let nodeWidth: SankeyControlsProps['nodeWidth'] = 10; - let nodeColorBy: SankeyControlsProps['nodeColorBy'] = 'layer'; - let linkColorBy: SankeyControlsProps['linkColorBy'] = 'static'; + let highlightLinkIndexes: Array = $state([]); + let nodeAlign: SankeyControlsProps['nodeAlign'] = $state('justify'); + let nodePadding: SankeyControlsProps['nodePadding'] = $state(4); + let nodeWidth: SankeyControlsProps['nodeWidth'] = $state(10); + let nodeColorBy: SankeyControlsProps['nodeColorBy'] = $state('layer'); + let linkColorBy: SankeyControlsProps['linkColorBy'] = $state('static'); - $: linkOpacity = + const linkOpacity = $derived( linkColorBy === 'static' ? { default: 0.1, @@ -48,15 +50,18 @@ : { default: 0.2, inactive: 0.01, - }; + } + ); - const complexDataHierarchy = hierarchy(data.flare) - .sum((d) => d.value) - .sort(sortFunc('value', 'desc')); + const complexDataHierarchy = $derived( + hierarchy(data.flare) + .sum((d) => d.value) + .sort(sortFunc('value', 'desc')) + ); - $: hierarchyGraph = graphFromHierarchy(complexDataHierarchy); + const hierarchyGraph = $derived(sankeyGraphFromHierarchy(complexDataHierarchy)); - let selectedNode: SankeyNode<{}, {}> | null = null; + let selectedNode: HierarchySankeyNode | null = $state.raw(null); type HierarchySankeyNodeProperties = { data: { name: string }; @@ -64,10 +69,6 @@ }; // TODO: Fix type type HierarchySankeyNode = SankeyNode; - - function getHierarchyNodeKey(node: HierarchySankeyNode) { - return [node.data.name, node.parent?.data.name].join('_'); - }

Examples

@@ -75,160 +76,178 @@

Simple

-
- - - d.id} let:links let:nodes> - {#each links as link ([link.source.id, link.target.id].join('_'))} - - {/each} - {#each nodes as node (node.id)} - {@const nodeWidth = (node.x1 ?? 0) - (node.x0 ?? 0)} - {@const nodeHeight = (node.y1 ?? 0) - (node.y0 ?? 0)} - - - - - {/each} +
+ + + d.id}> + {#snippet children({ links, nodes })} + {#each links as link ([link.value, link.source.id, link.target.id].join('-'))} + + {/each} + {#each nodes as node (node.id)} + {@const nodeWidth = (node.x1 ?? 0) - (node.x0 ?? 0)} + {@const nodeHeight = (node.y1 ?? 0) - (node.y0 ?? 0)} + + + + + {/each} + {/snippet} - +

Tooltip

- -
- - - d.name} nodeWidth={8} let:links let:nodes> - {#each links as link ([link.source.name, link.target.name].join('_'))} - tooltip.show(e, { link })} - onpointerleave={tooltip.hide} - /> - {/each} - - {#each nodes as node (node.name)} - {@const nodeWidth = (node.x1 ?? 0) - (node.x0 ?? 0)} - {@const nodeHeight = (node.y1 ?? 0) - (node.y0 ?? 0)} - - tooltip.show(e, { node })} - onpointerleave={tooltip.hide} - /> - - - {/each} - - - - - - {#if data.node} - {data.node.name} - {:else if data.link} - {data.link.source.name} - - {data.link.target.name} - {/if} - - - - {#if data.node} - - - {#if data.node.targetLinks.length} - -
Sources
- {#each data.node.targetLinks as link} - + +
+ + {#snippet children({ context })} + + d.name} nodeWidth={8}> + {#snippet children({ links, nodes })} + {#each links as link ([link.value, link.source.name, link.target.name].join('-'))} + context.tooltip.show(e, { link })} + onpointerleave={context.tooltip.hide} + /> {/each} - {/if} - {#if data.node.sourceLinks.length} - -
Targets
- {#each data.node.sourceLinks as link} - + {#each nodes as node (node.name)} + {@const nodeWidth = (node.x1 ?? 0) - (node.x0 ?? 0)} + {@const nodeHeight = (node.y1 ?? 0) - (node.y0 ?? 0)} + + context.tooltip.show(e, { node })} + onpointerleave={context.tooltip.hide} + /> + + {/each} - {/if} - {:else if data.link} - - {/if} - - + {/snippet} +
+
+ + + {#snippet children({ data })} + + {#if data.node} + {data.node.name} + {:else if data.link} + {data.link.source.name} + + {data.link.target.name} + {/if} + + + + {#if data.node} + + + {#if data.node.targetLinks.length} + +
Sources
+ {#each data.node.targetLinks as link} + + {/each} + {/if} + + {#if data.node.sourceLinks.length} + +
Targets
+ {#each data.node.sourceLinks as link} + + {/each} + {/if} + {:else if data.link} + + {/if} +
+ {/snippet} +
+ {/snippet}

Node select

- -
- - - d.name} nodeWidth={8} let:links let:nodes> - {#each links as link ([link.source.name, link.target.name].join('_'))} - - {/each} - - {#each nodes as node (node.name)} - {@const nodeWidth = (node.x1 ?? 0) - (node.x0 ?? 0)} - {@const nodeHeight = (node.y1 ?? 0) - (node.y0 ?? 0)} - { - selectedNode = - node === selectedNode || node.sourceLinks?.length === 0 ? null : node; - }} - > - +
+ + + d.name} nodeWidth={8}> + {#snippet children({ links, nodes })} + {#each links as link ([link.value, link.source.name, link.target.name].join('-'))} + - - - {/each} + {/each} + + {#each nodes as node (node.name)} + {@const nodeWidth = (node.x1 ?? 0) - (node.x0 ?? 0)} + {@const nodeHeight = (node.y1 ?? 0) - (node.y0 ?? 0)} + + { + if (selectedNode) { + selectedNode = + node.name === selectedNode.name || node.sourceLinks?.length === 0 + ? null + : node; + } else { + selectedNode = node; + } + }} + > + + + + {/each} + {/snippet} - +
@@ -238,120 +257,124 @@ -
- - - { - // Calculate domain extents from Sankey data - // TODO: Update as 'nodeColorBy' changes - // @ts-expect-error - const extents = extent(e.detail.nodes, (d) => d[nodeColorBy]); - // @ts-expect-error - colorScale.domain(extents); - }} - > - {#each links as link ([link.source.name, link.target.name].join('_'))} - (highlightLinkIndexes = [link.index])} - onpointermove={(e) => tooltip.show(e, { link })} - onpointerleave={() => { - highlightLinkIndexes = []; - tooltip.hide(); - }} - tweened - /> - {/each} - - {#each nodes as node (node.name)} - {@const nodeWidth = (node.x1 ?? 0) - (node.x0 ?? 0)} - {@const nodeHeight = (node.y1 ?? 0) - (node.y0 ?? 0)} - - { - highlightLinkIndexes = [ - ...(node.sourceLinks?.map((l) => l.index) ?? []), - ...(node.targetLinks?.map((l) => l.index) ?? []), - ]; - }} - onpointermove={(e) => tooltip.show(e, { node })} - onpointerleave={() => { - highlightLinkIndexes = []; - tooltip.hide(); - }} - tweened - /> - - - {/each} - - - - - - {#if data.node} - {data.node.name} - {:else if data.link} - {data.link.source.name} - - {data.link.target.name} - {/if} - - - - {#if data.node} - - - {#if data.node.targetLinks.length} - -
Sources
- {#each data.node.targetLinks as link} - +
+ + {#snippet children({ context })} + + { + // Calculate domain extents from Sankey data + // TODO: Update as 'nodeColorBy' changes + // @ts-expect-error + const extents = extent(e.nodes, (d) => d[nodeColorBy]); + // @ts-expect-error + colorScale.domain(extents); + }} + > + {#snippet children({ links, nodes })} + {#each links as link ([link.source.name, link.target.name, link.value].join('-'))} + (highlightLinkIndexes = [link.index])} + onpointermove={(e) => context.tooltip.show(e, { link })} + onpointerleave={() => { + highlightLinkIndexes = []; + context.tooltip.hide(); + }} + motion="tween" + /> {/each} - {/if} - {#if data.node.sourceLinks.length} - -
Targets
- {#each data.node.sourceLinks as link} - + {#each nodes as node (node.name)} + {@const nodeWidth = (node.x1 ?? 0) - (node.x0 ?? 0)} + {@const nodeHeight = (node.y1 ?? 0) - (node.y0 ?? 0)} + + { + highlightLinkIndexes = [ + ...(node.sourceLinks?.map((l) => l.index) ?? []), + ...(node.targetLinks?.map((l) => l.index) ?? []), + ]; + }} + onpointermove={(e) => context.tooltip.show(e, { node })} + onpointerleave={() => { + highlightLinkIndexes = []; + context.tooltip.hide(); + }} + motion="tween" + /> + + {/each} - {/if} - {:else if data.link} - - {/if} - - + {/snippet} +
+
+ + + {#snippet children({ data })} + + {#if data.node} + {data.node.name} + {:else if data.link} + {data.link.source.name} + + {data.link.target.name} + {/if} + + + + {#if data.node} + + + {#if data.node.targetLinks.length} + +
Sources
+ {#each data.node.targetLinks as link} + + {/each} + {/if} + + {#if data.node.sourceLinks.length} + +
Targets
+ {#each data.node.sourceLinks as link} + + {/each} + {/if} + {:else if data.link} + + {/if} +
+ {/snippet} +
+ {/snippet}
@@ -361,76 +384,76 @@ -
- - +
+ + { + onUpdate={(e) => { // Calculate domain extents from Sankey data // TODO: Update as 'nodeColorBy' changes // @ts-expect-error - const extents = extent(e.detail.nodes, (d) => d[nodeColorBy]); + const extents = extent(e.nodes, (d) => d[nodeColorBy]); // @ts-expect-error colorScale.domain(extents); }} > - {#each links as link ([getHierarchyNodeKey(link.source), getHierarchyNodeKey(link.target)].join('_'))} - (highlightLinkIndexes = [link.index])} - onpointerleave={() => (highlightLinkIndexes = [])} - tweened - /> - {/each} - - {#each nodes as node (getHierarchyNodeKey(node))} - {@const nodeWidth = (node.x1 ?? 0) - (node.x0 ?? 0)} - {@const nodeHeight = (node.y1 ?? 0) - (node.y0 ?? 0)} - - { - highlightLinkIndexes = [ - ...(node.sourceLinks?.map((l) => l.index) ?? []), - ...(node.targetLinks?.map((l) => l.index) ?? []), - ]; - }} + {#snippet children({ links, nodes })} + {#each links as link ([link.source.data.name, link.target.data.name, link.value].join('-'))} + (highlightLinkIndexes = [link.index])} onpointerleave={() => (highlightLinkIndexes = [])} - tweened - /> - - - {/each} + {/each} + + {#each nodes as node ([node.data.name, node.value].join('-'))} + {@const nodeWidth = (node.x1 ?? 0) - (node.x0 ?? 0)} + {@const nodeHeight = (node.y1 ?? 0) - (node.y0 ?? 0)} + + { + highlightLinkIndexes = [ + ...(node.sourceLinks?.map((l) => l.index) ?? []), + ...(node.targetLinks?.map((l) => l.index) ?? []), + ]; + }} + onpointerleave={() => (highlightLinkIndexes = [])} + motion="tween" + /> + + + {/each} + {/snippet} - +
diff --git a/packages/layerchart/src/routes/docs/examples/Sankey/+page.ts b/packages/layerchart/src/routes/docs/examples/Sankey/+page.ts index 406d807ea..5578c6939 100644 --- a/packages/layerchart/src/routes/docs/examples/Sankey/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Sankey/+page.ts @@ -1,6 +1,6 @@ import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { simple: await fetch('/data/examples/graph/simple.json').then((r) => r.json()), complex: await fetch('/data/examples/graph/complex.json').then((r) => r.json()), @@ -9,6 +9,7 @@ export async function load() { meta: { pageSource, + supportedContexts: ['svg'], // TODO: `canvas` coming soon related: ['components/Sankey', 'components/Link'], }, }; diff --git a/packages/layerchart/src/routes/docs/examples/Sankey/SankeyControls.svelte b/packages/layerchart/src/routes/docs/examples/Sankey/SankeyControls.svelte index 1b32deb59..d5fb89906 100644 --- a/packages/layerchart/src/routes/docs/examples/Sankey/SankeyControls.svelte +++ b/packages/layerchart/src/routes/docs/examples/Sankey/SankeyControls.svelte @@ -3,14 +3,21 @@ import { RangeField, MenuField } from 'svelte-ux'; import type Sankey from '$lib/components/Sankey.svelte'; + type SankeyProps = ComponentProps; - type SankeyProps = ComponentProps; - - export let nodeAlign: SankeyProps['nodeAlign']; - export let nodeColorBy: 'layer' | 'depth' | 'height' | 'index'; - export let linkColorBy: 'static' | 'source' | 'target'; - export let nodePadding: number; - export let nodeWidth: number; + let { + nodeAlign = $bindable(), + nodeColorBy = $bindable(), + linkColorBy = $bindable(), + nodePadding = $bindable(), + nodeWidth = $bindable(), + }: { + nodeAlign: SankeyProps['nodeAlign']; + nodeColorBy: 'layer' | 'depth' | 'height' | 'index'; + linkColorBy: 'source' | 'target' | 'static'; + nodePadding: number; + nodeWidth: number; + } = $props();
diff --git a/packages/layerchart/src/routes/docs/examples/Scatter/+page.svelte b/packages/layerchart/src/routes/docs/examples/Scatter/+page.svelte index 70d27248f..aed2477b1 100644 --- a/packages/layerchart/src/routes/docs/examples/Scatter/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Scatter/+page.svelte @@ -1,15 +1,16 @@

Examples

@@ -21,25 +22,13 @@

Basic

-
- - +
+ + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -47,33 +36,30 @@

With Tooltip and Highlight

-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - + - - {format(data.date, 'eee, MMMM do')} - - - + + {#snippet children({ data })} + + + + + {/snippet}
@@ -82,26 +68,14 @@

With Labels

-
- - +
+ + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
@@ -111,33 +85,24 @@

red (0-49), yellow (50-89), green (90+)

-
+
- + - formatDate(d, PeriodType.Day, { variant: 'short' })} - rule - /> + - +
diff --git a/packages/layerchart/src/routes/docs/examples/Scatter/+page.ts b/packages/layerchart/src/routes/docs/examples/Scatter/+page.ts index 48dcda64e..2f92cb817 100644 --- a/packages/layerchart/src/routes/docs/examples/Scatter/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Scatter/+page.ts @@ -4,6 +4,7 @@ export async function load() { return { meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/SketchyGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/SketchyGlobe/+page.svelte index ccd52083f..dca4aef52 100644 --- a/packages/layerchart/src/routes/docs/examples/SketchyGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/SketchyGlobe/+page.svelte @@ -4,39 +4,38 @@ import { feature } from 'topojson-client'; import { presimplify, simplify } from 'topojson-simplify'; - import { Chart, GeoPath, Graticule, Svg, TransformContext } from 'layerchart'; + import { Chart, GeoPath, Graticule, Layer, type ChartContextValue } from 'layerchart'; import { Button, ButtonGroup, Field, RangeField } from 'svelte-ux'; - import { timerStore } from '@layerstack/svelte-stores'; + import { TimerState } from '@layerstack/svelte-state'; import Preview from '$lib/docs/Preview.svelte'; import CurveMenuField from '$lib/docs/CurveMenuField.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); - let curve = curveCatmullRomClosed; - let minArea = 2; + let curve = $state(curveCatmullRomClosed); + let minArea = $state(2); - $: geojson = simplify(presimplify(data.geojson), Math.pow(10, 2 - minArea)); - $: land = feature(geojson, data.geojson.objects.land); + const geojson = $derived(simplify(presimplify(data.geojson), Math.pow(10, 2 - minArea))); + const land = $derived(feature(geojson, data.geojson.objects.land)); - let transformContext: TransformContext; + let context = $state(null!); - let velocity = 3; - let isSpinning = false; - const timer = timerStore({ + let velocity = $state(1); + const timer = new TimerState({ delay: 1, - onTick() { - transformContext.translate.update((value) => { - return { - x: (value.x += velocity), - y: value.y, - }; - }); + tick: () => { + if (!context) return; + const curr = context.transform.translate; + + context.transform.translate = { + x: (curr.x += velocity), + y: curr.y, + }; }, - disabled: !isSpinning, + disabled: true, }); - $: isSpinning ? timer.start() : timer.stop(); - $timer;
@@ -47,23 +46,13 @@

Examples

-

SVG

+

Basic

- - + + @@ -72,7 +61,7 @@ bind:value={velocity} min={-10} max={10} - disabled={!isSpinning} + disabled={!timer.running} labelPlacement="left" />
@@ -86,20 +75,14 @@ fitGeojson: land, applyTransform: ['rotate'], }} - ondragstart={() => timer.stop()} - ondragend={() => { - if (isSpinning) { - // Restart - timer.start(); - } - }} - bind:transformContext + ondragstart={timer.stop} + bind:context > - + - +
diff --git a/packages/layerchart/src/routes/docs/examples/SketchyGlobe/+page.ts b/packages/layerchart/src/routes/docs/examples/SketchyGlobe/+page.ts index e0cee5eac..480d72db5 100644 --- a/packages/layerchart/src/routes/docs/examples/SketchyGlobe/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/SketchyGlobe/+page.ts @@ -11,6 +11,7 @@ export async function load({ fetch }) { }>, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/Sparkbar/+page.svelte b/packages/layerchart/src/routes/docs/examples/Sparkbar/+page.svelte index c122fa1b4..672fc1c28 100644 --- a/packages/layerchart/src/routes/docs/examples/Sparkbar/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Sparkbar/+page.svelte @@ -1,10 +1,10 @@

Examples

@@ -30,6 +32,7 @@ grid={false} bandPadding={0.1} props={{ bars: { radius: 1, strokeWidth: 0 } }} + {renderContext} />
@@ -49,6 +52,7 @@ grid={false} bandPadding={0.1} props={{ bars: { radius: 1, strokeWidth: 0 } }} + {renderContext} /> Sed ipsum justo, facilisis id tempor hendrerit, suscipit eu ipsum. Mauris ut sapien quis nibh volutpat venenatis. Ut viverra justo varius sapien convallis venenatis vel faucibus urna. @@ -68,6 +72,7 @@ grid={false} bandPadding={0.1} props={{ bars: { radius: 1, strokeWidth: 0 } }} + {renderContext} />
@@ -84,24 +89,27 @@ grid={false} bandPadding={0.1} props={{ bars: { radius: 1, strokeWidth: 0 } }} + {renderContext} > - + {#snippet tooltip({ context })} -
- {format(data.date, 'eee, MMM do')} -
-
- {data.value} -
+ {#snippet children({ data })} +
+ {format(data.date, 'day')} +
+
+ {data.value} +
+ {/snippet}
-
+ {/snippet}
@@ -121,15 +129,24 @@ grid={false} bandPadding={0.1} props={{ bars: { radius: 1, strokeWidth: 0 } }} + {renderContext} > - - - {format(data.date, 'eee, MMM do')} - - - + {#snippet tooltip({ context })} + + {#snippet children({ data })} + + + + + {/snippet} - + {/snippet} Sed ipsum justo, facilisis id tempor hendrerit, suscipit eu ipsum. Mauris ut sapien quis nibh volutpat venenatis. Ut viverra justo varius sapien convallis venenatis vel faucibus urna. diff --git a/packages/layerchart/src/routes/docs/examples/Sparkbar/+page.ts b/packages/layerchart/src/routes/docs/examples/Sparkbar/+page.ts index 614c76c2d..6b4c20cc9 100644 --- a/packages/layerchart/src/routes/docs/examples/Sparkbar/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Sparkbar/+page.ts @@ -4,6 +4,7 @@ export async function load() { return { meta: { pageSource, + supportedContexts: ['svg', 'canvas'], related: [ 'components/BarChart', 'components/Bars', diff --git a/packages/layerchart/src/routes/docs/examples/Sparkline/+page.svelte b/packages/layerchart/src/routes/docs/examples/Sparkline/+page.svelte index 54b046ce7..7ac8fd652 100644 --- a/packages/layerchart/src/routes/docs/examples/Sparkline/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Sparkline/+page.svelte @@ -1,12 +1,14 @@

Examples

@@ -15,7 +17,15 @@
- +
@@ -27,7 +37,15 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam pretium, ligula ac sollicitudin ullamcorper, leo justo pretium tellus, at gravida ex quam et orci. - + Sed ipsum justo, facilisis id tempor hendrerit, suscipit eu ipsum. Mauris ut sapien quis nibh volutpat venenatis. Ut viverra justo varius sapien convallis venenatis vel faucibus urna.

@@ -37,7 +55,7 @@

Basic zero axis

- +
@@ -54,24 +72,27 @@ props={{ highlight: { points: { r: 3, class: 'stroke-none' } }, }} + {renderContext} > - + {#snippet tooltip({ context })} -
- {format(data.date, 'eee, MMM do')} -
-
- {data.value} -
+ {#snippet children({ data })} +
+ {format(data.date, 'day')} +
+
+ {data.value} +
+ {/snippet}
-
+ {/snippet}
@@ -93,17 +114,26 @@ props={{ highlight: { points: { r: 3, class: 'stroke-none' } }, }} + {renderContext} > - - -
- {format(data.date, 'eee, MMM do')} -
-
- {data.value} -
+ {#snippet tooltip({ context })} + + {#snippet children({ data })} +
+ {format(data.date, 'day')} +
+
+ {data.value} +
+ {/snippet}
-
+ {/snippet} Sed ipsum justo, facilisis id tempor hendrerit, suscipit eu ipsum. Mauris ut sapien quis nibh volutpat venenatis. Ut viverra justo varius sapien convallis venenatis vel faucibus urna. diff --git a/packages/layerchart/src/routes/docs/examples/Sparkline/+page.ts b/packages/layerchart/src/routes/docs/examples/Sparkline/+page.ts index 52b933f4d..94a9a1755 100644 --- a/packages/layerchart/src/routes/docs/examples/Sparkline/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Sparkline/+page.ts @@ -4,6 +4,7 @@ export async function load() { return { meta: { pageSource, + supportedContexts: ['svg', 'canvas'], related: [ 'components/LineChart', 'components/Spline', diff --git a/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte index ba8d8221b..0e0fe9293 100644 --- a/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte @@ -4,12 +4,13 @@ import { scaleLinear } from 'd3-scale'; import { feature } from 'topojson-client'; - import { Canvas, Chart, GeoPath, spikePath, Spline, Svg, Tooltip } from 'layerchart'; + import { Chart, GeoPath, Layer, spikePath, Spline, Tooltip } from 'layerchart'; import TransformControls from '$lib/components/TransformControls.svelte'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); const projection = geoIdentity as unknown as () => GeoProjection; @@ -31,11 +32,11 @@ const width = 7; const maxHeight = 200; - $: heightScale = scaleLinear() + const heightScale = scaleLinear() .domain([0, max(population, (d) => d.population) ?? 0]) .range([0, maxHeight]); - $: enrichedCountiesFeatures = counties.features + const enrichedCountiesFeatures = counties.features .map((feature) => { return { ...feature, @@ -50,7 +51,7 @@

Examples

-

SVG

+

Basic

@@ -63,147 +64,84 @@ mode: 'canvas', initialScrollMode: 'scale', }} - let:tooltip - let:transform > - {@const strokeWidth = 1 / transform.scale} - - - - - - - {#each enrichedCountiesFeatures as feature} - - {@const [x, y] = geoPath.centroid(feature)} - {@const d = feature.properties.data} - {@const height = heightScale(d?.population ?? 0)} - - - {/each} + {#snippet children({ context })} + {@const strokeWidth = 1 / context.transform.scale} + + - {#each enrichedCountiesFeatures as feature} + - {/each} - - - - {@const d = data.properties.data} - {data.properties.name + ' - ' + data.properties.data?.state} - - - - - - - -
-
-

Canvas

- - -
- - {@const strokeWidth = 1 / transform.scale} - - - - - - {#each enrichedCountiesFeatures as feature} - - {@const [x, y] = geoPath.centroid(feature)} - {@const d = feature.properties.data} - {@const height = heightScale(d?.population ?? 0)} - + {#snippet children({ geoPath })} + {@const [x, y] = geoPath?.centroid(feature) ?? [0, 0]} + {@const d = feature.properties.data} + {@const height = heightScale(d?.population ?? 0)} + + {/snippet} + + {/each} + + {#each enrichedCountiesFeatures as feature} + - - {/each} - - - - {#if tooltip.data} - - {/if} - - - - {@const d = data.properties.data} - {data.properties.name + ' - ' + data.properties.data?.state} - - - - - - + {/each} + + + + + {#if context.tooltip.data && shared.renderContext === 'canvas'} + + {/if} + + + + {#snippet children({ data })} + {@const d = data.properties.data} + {data.properties.name + ' - ' + data.properties.data?.state} + + + + + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.ts b/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.ts index beeb54e67..53f8b7218 100644 --- a/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.ts @@ -17,6 +17,7 @@ export async function load({ fetch }) { )) as USCountyPopulationData, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/StateMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/StateMap/+page.svelte index c9ab65f73..f5d84fe18 100644 --- a/packages/layerchart/src/routes/docs/examples/StateMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/StateMap/+page.svelte @@ -2,13 +2,14 @@ import { geoAlbersUsa, geoAlbers, geoMercator } from 'd3-geo'; import { feature } from 'topojson-client'; - import { Chart, ChartClipPath, GeoPath, Svg, Tooltip } from 'layerchart'; + import { Chart, ChartClipPath, GeoPath, Layer, Tooltip } from 'layerchart'; import { SelectField } from 'svelte-ux'; import { sort } from '@layerstack/utils'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); const counties = feature(data.geojson, data.geojson.objects.counties); const states = feature(data.geojson, data.geojson.objects.states); @@ -19,13 +20,13 @@ .map((x) => ({ label: x.properties.name, value: x.id })), (d) => d.value ); - let selectedStateId = '54'; // 'West Virginia'; - $: selectedStateFeature = states.features.find((f) => f.id === selectedStateId); - $: selectedCountiesFeatures = counties.features.filter( - (f) => String(f.id).slice(0, 2) === selectedStateId + let selectedStateId = $state('54'); // 'West Virginia'; + const selectedStateFeature = $derived(states.features.find((f) => f.id === selectedStateId)); + const selectedCountiesFeatures = $derived( + counties.features.filter((f) => String(f.id).slice(0, 2) === selectedStateId) ); - let projection = geoAlbersUsa; + let projection = $state(geoAlbersUsa); const projections = [ { label: 'Albers', value: geoAlbers }, { label: 'Albers USA', value: geoAlbersUsa }, @@ -64,9 +65,9 @@ fitGeojson: selectedStateFeature, }} > - + - +
@@ -80,25 +81,26 @@ projection, fitGeojson: selectedStateFeature, }} - let:tooltip > - - {#each selectedCountiesFeatures as feature} + {#snippet children({ context })} + + {#each selectedCountiesFeatures as feature} + + {/each} - {/each} - - - - - {data.properties.name} - + + + + {context.tooltip.data?.properties.name} + + {/snippet}
@@ -112,33 +114,34 @@ projection, fitGeojson: selectedStateFeature, }} - let:tooltip > - - - {#each counties.features as feature} + {#snippet children({ context })} + + + {#each counties.features as feature} + + {/each} + {#each states.features as feature} + + {/each} - {/each} - {#each states.features as feature} - - {/each} - - - + + - - {data.properties.name} - + + {context.tooltip.data?.properties.name} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/StateMap/+page.ts b/packages/layerchart/src/routes/docs/examples/StateMap/+page.ts index af04f1af5..5a147d814 100644 --- a/packages/layerchart/src/routes/docs/examples/StateMap/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/StateMap/+page.ts @@ -13,6 +13,7 @@ export async function load({ fetch }) { }>, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.svelte index c63797675..b2ad55196 100644 --- a/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.svelte @@ -4,7 +4,7 @@ import { Button, ButtonGroup, Field, RangeField } from 'svelte-ux'; import { cls } from '@layerstack/tailwind'; - import { timerStore } from '@layerstack/svelte-stores'; + import { TimerState } from '@layerstack/svelte-state'; import { Chart, @@ -12,58 +12,47 @@ GeoPoint, GeoVisible, Graticule, - Svg, + Layer, Tooltip, - TransformContext, + type ChartContextValue, } from 'layerchart'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); // https://vizhub.com/curran/submarine-cables-globe const countries = feature(data.geojson, data.geojson.objects.countries); - let transformContext: TransformContext; + let context = $state(); - let velocity = 3; - let isSpinning = false; - const timer = timerStore({ + let velocity = $state(3); + const timer = new TimerState({ delay: 1, - onTick() { - transformContext.translate.update((value) => { - return { - x: (value.x += velocity), - y: value.y, - }; - }); + tick: () => { + if (!context) return; + const value = context.transform.translate; + + context.transform.translate = { + x: (value.x += velocity), + y: value.y, + }; }, - disabled: !isSpinning, + disabled: true, }); - $: isSpinning ? timer.start() : timer.stop(); - $timer;

Examples

-

SVG

+

Basic

- - + + @@ -72,7 +61,7 @@ bind:value={velocity} min={-10} max={10} - disabled={!isSpinning} + disabled={!timer.running} labelPlacement="left" />
@@ -86,69 +75,54 @@ fitGeojson: countries, applyTransform: ['rotate'], }} - ondragstart={() => timer.stop()} - ondragend={() => { - if (isSpinning) { - // Restart - timer.start(); - } - }} - bind:transformContext - let:tooltip + ondragstart={timer.stop} + bind:context > - - - - - - {#each data.cables.features as feature} - {@const hasColor = tooltip.data == null || tooltip.data.id === feature.properties.id} + {#snippet children({ context })} + tooltip?.show(e, feature.properties)} - onpointerleave={(e) => tooltip?.hide()} + geojson={{ type: 'Sphere' }} + class="fill-surface-200 stroke-surface-content/20" /> - {/each} - - - + {/each} - {#each data.landingPoints.features as feature} - {@const [long, lat] = feature.geometry.coordinates} - - - + tooltip?.show(e, feature.properties)} - on:pointerleave={(e) => tooltip?.hide()} + onpointermove={(e) => context.tooltip.show(e, feature.properties)} + onpointerleave={(e) => context.tooltip.hide()} /> - - - {/each} - - - - {data.name} - - + + {/each} + + + + {#snippet children({ data })} + {data.name} + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.ts b/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.ts index 1b819a36e..1b581f7e2 100644 --- a/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.ts @@ -16,6 +16,7 @@ export async function load({ fetch }) { ), meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/Sunburst/+page.svelte b/packages/layerchart/src/routes/docs/examples/Sunburst/+page.svelte index b6fc5d31c..33009fb10 100644 --- a/packages/layerchart/src/routes/docs/examples/Sunburst/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Sunburst/+page.svelte @@ -5,22 +5,27 @@ import * as chromatic from 'd3-scale-chromatic'; import { hsl } from 'd3-color'; - import { Arc, Bounds, Chart, Partition, Svg, Tooltip, findAncestor } from 'layerchart'; + import { Arc, Bounds, Chart, Layer, Partition, Tooltip, findAncestor } from 'layerchart'; import { Breadcrumb, Button, Field, ToggleGroup, ToggleOption } from 'svelte-ux'; import { format, sortFunc, compoundSortFunc } from '@layerstack/utils'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; + let { data } = $props(); const complexHierarchy = hierarchy(data.flare) .sum((d) => d.value) - .sort(compoundSortFunc(sortFunc('height', 'desc'), sortFunc('value', 'desc'))); + .sort( + compoundSortFunc(sortFunc('height', 'desc'), sortFunc('value', 'desc')) + ) as HierarchyRectangularNode; - let colorBy = 'parent'; + let colorBy = $state('parent'); - let selected: HierarchyRectangularNode = complexHierarchy as HierarchyRectangularNode; // select root initially + let selected: HierarchyRectangularNode = $state( + complexHierarchy + ) as HierarchyRectangularNode; // select root initially const sequentialColor = scaleSequential([4, -1], chromatic.interpolateGnBu); // filter out hard to see yellow and green @@ -64,61 +69,69 @@ - -
- - - ({ - x0: 0, - x1: 2 * Math.PI, - y0: selected?.y0 ? 20 : 0, - y1: height / 2, - })} - tweened={{ duration: 800, easing: cubicOut }} - let:xScale - let:yScale - > - - {#each nodes as node} - {@const nodeColor = getNodeColor(node, colorBy)} - { - selected = node; - }} - onpointermove={(e) => tooltip.show(e, node)} - onpointerleave={tooltip.hide} - > - - - {/each} - - - +
+ + {#snippet children({ context })} + + ({ + x0: 0, + x1: 2 * Math.PI, + y0: selected?.y0 ? 20 : 0, + y1: height / 2, + })} + motion={{ type: 'tween', duration: 800, easing: cubicOut }} + > + {#snippet children({ xScale, yScale })} + + {#snippet children({ nodes })} + {#each nodes as node} + {@const nodeColor = getNodeColor(node, colorBy)} + { + selected = node; + }} + onpointermove={(e) => context.tooltip.show(e, node)} + onpointerleave={context.tooltip.hide} + > + {/each} + {/snippet} + + {/snippet} + + - - {data.data.name} - - - - + + {#snippet children({ data })} + {data.data.name} + + + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/Sunburst/+page.ts b/packages/layerchart/src/routes/docs/examples/Sunburst/+page.ts index 6eaed4ca6..e22ed0828 100644 --- a/packages/layerchart/src/routes/docs/examples/Sunburst/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Sunburst/+page.ts @@ -1,10 +1,11 @@ import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { flare: await fetch('/data/examples/hierarchy/flare.json').then((r) => r.json()), meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/Threshold/+page.svelte b/packages/layerchart/src/routes/docs/examples/Threshold/+page.svelte index 53b59531b..57c2b57a0 100644 --- a/packages/layerchart/src/routes/docs/examples/Threshold/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Threshold/+page.svelte @@ -1,14 +1,17 @@

Examples

-

SVG

+

Basic

- - + + @@ -68,7 +57,7 @@ bind:value={velocity} min={-10} max={10} - disabled={!isSpinning} + disabled={!timer.running} labelPlacement="left" />
@@ -82,52 +71,44 @@ fitGeojson: countries, applyTransform: ['rotate'], }} - ondragstart={() => timer.stop()} - ondragend={() => { - if (isSpinning) { - // Restart - timer.start(); - } - }} - bind:transformContext - let:tooltip - let:projection + ondragstart={timer.stop} + bind:context > - {@const [yaw, pitch, roll] = projection.rotate()} - - + {#snippet children({ context })} + {@const [yaw, pitch, roll] = context.geo.projection?.rotate() ?? [0, 0, 0]} + + - - - + + + + {#each countries.features as country} + + {/each} + + + + {#each countries.features as country} {/each} - - - - - {#each countries.features as country} - - {/each} - + - - {data.properties.name} - + + {#snippet children({ data })} + {data.properties.name} + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/TranslucentGlobe/+page.ts b/packages/layerchart/src/routes/docs/examples/TranslucentGlobe/+page.ts index 4f6aadb26..f42afc5ae 100644 --- a/packages/layerchart/src/routes/docs/examples/TranslucentGlobe/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/TranslucentGlobe/+page.ts @@ -13,6 +13,7 @@ export async function load({ fetch }) { }>, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/Tree/+page.svelte b/packages/layerchart/src/routes/docs/examples/Tree/+page.svelte index b0ebe1890..4f68df8a1 100644 --- a/packages/layerchart/src/routes/docs/examples/Tree/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Tree/+page.svelte @@ -1,31 +1,37 @@

Examples

-
+
Horizontal @@ -52,113 +59,128 @@ - - - BumpX - BumpY - Step - Step Before - Step After - - - Chart Node +
- +
+ + + + {#if type === 'd3'} + + + BumpX + BumpY + Step + Step Before + Step After + + + {/if} + + {#if type === 'beveled' || type === 'rounded'} + + {/if}

Basic

-
+
- - - - - {#each links as link (getNodeKey(link.source) + '_' + getNodeKey(link.target))} - - {/each} - - {#each nodes as node (getNodeKey(node))} - { - if (expandedNodeNames.includes(node.data.name)) { - expandedNodeNames = expandedNodeNames.filter((name) => name !== node.data.name); - } else { - expandedNodeNames = [...expandedNodeNames, node.data.name]; - } - selected = node; - - // transform.zoomTo({ - // x: orientation === 'horizontal' ? selected.y : selected.x, - // y: orientation === 'horizontal' ? selected.x : selected.y, - // }); - }} - class={cls(node.data.children && 'cursor-pointer')} - > - - - - {/each} - - + {#snippet children()} + + + + {#snippet children({ nodes, links })} + + {#each links as link (getNodeKey(link.source) + '_' + getNodeKey(link.target))} + + {/each} + + {#each nodes as node (getNodeKey(node))} + { + if (expandedNodeNames.includes(node.data.name)) { + expandedNodeNames = expandedNodeNames.filter( + (name) => name !== node.data.name + ); + } else { + expandedNodeNames = [...expandedNodeNames, node.data.name]; + } + selected = node; + + // transform.zoomTo({ + // x: orientation === 'horizontal' ? selected.y : selected.x, + // y: orientation === 'horizontal' ? selected.x : selected.y, + // }); + }} + class={cls(node.data.children && 'cursor-pointer')} + > + + + + {/each} + + {/snippet} + + {/snippet}
@@ -166,61 +188,72 @@

Html nodes

-
+
- - - - - {#each links as link (getNodeKey(link.source) + '_' + getNodeKey(link.target))} - - {/each} - - - - {#each nodes as node (getNodeKey(node))} - {@const x = (orientation === 'horizontal' ? node.y : node.x) - nodeWidth / 2} - {@const y = (orientation === 'horizontal' ? node.x : node.y) - nodeHeight / 2} - { - if (expandedNodeNames.includes(node.data.name)) { - expandedNodeNames = expandedNodeNames.filter((name) => name !== node.data.name); - } else { - expandedNodeNames = [...expandedNodeNames, node.data.name]; - } - selected = node; - }} - > - {node.data.name} - - {/each} - - + {#snippet children()} + + + + {#snippet children({ nodes, links })} + + {#each links as link (getNodeKey(link.source) + '_' + getNodeKey(link.target))} + + {/each} + + + + {#each nodes as node} + {@const x = (orientation === 'horizontal' ? node.y : node.x) - nodeWidth / 2} + {@const y = (orientation === 'horizontal' ? node.x : node.y) - nodeHeight / 2} + { + if (expandedNodeNames.includes(node.data.name)) { + expandedNodeNames = expandedNodeNames.filter( + (name) => name !== node.data.name + ); + } else { + expandedNodeNames = [...expandedNodeNames, node.data.name]; + } + selected = node; + }} + > + {node.data.name} + + {/each} + + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/Tree/+page.ts b/packages/layerchart/src/routes/docs/examples/Tree/+page.ts index 6eaed4ca6..2f2bf42d5 100644 --- a/packages/layerchart/src/routes/docs/examples/Tree/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Tree/+page.ts @@ -1,10 +1,11 @@ import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { flare: await fetch('/data/examples/hierarchy/flare.json').then((r) => r.json()), meta: { pageSource, + supportedContexts: ['svg'], // TODO: `canvas` coming soon }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/Treemap/+page.svelte b/packages/layerchart/src/routes/docs/examples/Treemap/+page.svelte index 9faade119..e85462ba1 100644 --- a/packages/layerchart/src/routes/docs/examples/Treemap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Treemap/+page.svelte @@ -1,4 +1,5 @@
@@ -50,7 +53,7 @@

Examples

-

SVG (projection transform)

+

Projection transform

@@ -62,68 +65,84 @@ }} transform={{ initialScrollMode: 'none', - tweened: { duration: 800, easing: cubicOut }, + motion: { type: 'tween', duration: 800, easing: cubicOut }, }} - let:projection - let:transform - let:tooltip - let:width - let:height > - - - - {#each states.features as feature} - { - if (selectedStateId === feature.id) { - selectedStateId = null; - transform.reset(); - } else { - selectedStateId = feature.id; - const featureTransform = geoFitObjectTransform( - projection, - [width, height], - feature - ); - transform.setTranslate(featureTransform.translate); - transform.setScale(featureTransform.scale); - } - }} - /> - {/each} + {#snippet children({ context })} + - {#each selectedCountiesFeatures as feature (feature.id)} - + + {#each states.features as feature} { - selectedStateId = null; - transform.reset(); + context.tooltip.hide(); + if (selectedStateId === feature.id) { + selectedStateId = null; + context.transform.reset(); + } else { + selectedStateId = feature.id; + if (context.geo.projection) { + const featureTransform = geoFitObjectTransform( + context.geo.projection, + [context.width, context.height], + feature + ); + context.transform.setTranslate(featureTransform.translate); + context.transform.setScale(featureTransform.scale); + } + } }} /> - - {/each} - - - - {@const [longitude, latitude] = projection.invert?.([tooltip.x, tooltip.y]) ?? []} - {data.properties.name} - - - - - + {/each} + + {#each selectedCountiesFeatures as feature (feature.id)} + + + { + selectedStateId = null; + context.tooltip.hide(); + context.transform.reset(); + }} + /> + + {/each} + + + + + {#if context.tooltip.data && shared.renderContext === 'canvas'} + + {/if} + + + + {#snippet children({ data })} + {@const [longitude, latitude] = + context.geo.projection?.invert?.([context.tooltip.x, context.tooltip.y]) ?? []} + {data.properties.name} + + + + + {/snippet} + + {/snippet}
-

SVG (canvas transform)

+

Canvas transform

@@ -135,242 +154,82 @@ transform={{ mode: 'canvas', initialScrollMode: 'none', - tweened: { duration: 800, easing: cubicOut }, + motion: { type: 'tween', duration: 800, easing: cubicOut }, }} - let:projection - let:transform - let:tooltip > - + {#snippet children({ context })} + - - {#each states.features as feature} - { - if (selectedStateId === feature.id) { - selectedStateId = null; - transform.reset(); - } else { - selectedStateId = feature.id; - const [[left, top], [right, bottom]] = geoPath.bounds(feature); - const width = right - left; - const height = bottom - top; - const x = (left + right) / 2; - const y = (top + bottom) / 2; - const padding = 20; - transform.zoomTo({ x, y }, { width: width + padding, height: height + padding }); - } - }} - /> - {/each} - - {#each selectedCountiesFeatures as feature (feature.id)} - + + {#each states.features as feature} { - selectedStateId = null; - transform.reset(); + class="stroke-surface-content fill-surface-100 hover:fill-surface-content/10" + strokeWidth={1 / context.transform.scale} + tooltipContext={context.tooltip} + onclick={(e, geoPath) => { + context.tooltip.hide(); + if (selectedStateId === feature.id) { + selectedStateId = null; + context.transform.reset(); + } else { + selectedStateId = feature.id; + if (!geoPath) return; + const [[left, top], [right, bottom]] = geoPath.bounds(feature); + const width = right - left; + const height = bottom - top; + const x = (left + right) / 2; + const y = (top + bottom) / 2; + const padding = 20; + context.transform.zoomTo( + { x, y }, + { width: width + padding, height: height + padding } + ); + } }} /> - - {/each} - - - - {data.properties.name} - - - - -
-
- -

Canvas (projection transform)

- - -
- - - - - {#each states.features as feature} - { - if ( - selectedStateId === feature.id || - !states.features.some((f) => f.id == feature.id) // County selected - ) { - selectedStateId = null; - transform.reset(); - } else { - selectedStateId = feature.id; - tooltip.hide(); - const featureTransform = geoFitObjectTransform( - projection, - [width, height], - feature - ); - transform.setTranslate(featureTransform.translate); - transform.setScale(featureTransform.scale); - } - }} - /> - {/each} - - {#each selectedCountiesFeatures as feature (feature.id)} - { - selectedStateId = null; - transform.reset(); - }} - /> - {/each} - - - - {#if tooltip.data} - - {/if} - - - - {@const [longitude, latitude] = projection.invert?.([tooltip.x, tooltip.y]) ?? []} - {data.properties.name} - - - - - - -
-
- -

Canvas (canvas transform)

- - -
- - - - - {#each states.features as feature} - { - const geoPath = d3geoPath(projection); - - if ( - selectedStateId === feature.id || - !states.features.some((f) => f.id == feature.id) // County selected - ) { - selectedStateId = null; - transform.reset(); - } else { - selectedStateId = feature.id; - tooltip.hide(); - let [[left, top], [right, bottom]] = geoPath.bounds(feature); - let width = right - left; - let height = bottom - top; - let x = (left + right) / 2; - let y = (top + bottom) / 2; - const padding = 20; - - transform.zoomTo({ x, y }, { width: width + padding, height: height + padding }); - } - }} - /> - {/each} - - {#each selectedCountiesFeatures as feature (feature.id)} - { - selectedStateId = null; - transform.reset(); - }} - /> - {/each} - - - - - {#if tooltip.data} - - {/if} - - - - {data.properties.name} - - + + {#if context.tooltip.data && shared.renderContext === 'canvas'} + + {/if} + + + + {#snippet children({ data })} + {data.properties.name} + + - + {/snippet} + + {/snippet}
diff --git a/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.ts b/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.ts index af04f1af5..5a147d814 100644 --- a/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.ts @@ -13,6 +13,7 @@ export async function load({ fetch }) { }>, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/ZoomableTileMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/ZoomableTileMap/+page.svelte index 562219b3b..eec923cd0 100644 --- a/packages/layerchart/src/routes/docs/examples/ZoomableTileMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/ZoomableTileMap/+page.svelte @@ -3,216 +3,233 @@ import { geoMercator } from 'd3-geo'; import { feature } from 'topojson-client'; - import { Chart, GeoPath, GeoTile, Svg, Tooltip, geoFitObjectTransform } from 'layerchart'; + import { Chart, GeoPath, GeoTile, Layer, Tooltip, geoFitObjectTransform } from 'layerchart'; import TransformControls from '$lib/components/TransformControls.svelte'; - import { Field, RangeField, Switch } from 'svelte-ux'; + import { RangeField } from 'svelte-ux'; import GeoDebug from '$lib/docs/GeoDebug.svelte'; import Preview from '$lib/docs/Preview.svelte'; import TilesetField from '$lib/docs/TilesetField.svelte'; + import { shared } from '../../shared.svelte.js'; - export let data; - const states = feature(data.geojson, data.geojson.objects.states); + let { data } = $props(); - $: filteredStates = { + const states = $derived(feature(data.geojson, data.geojson.objects.states)); + + const filteredStates = $derived({ ...states, features: states.features.filter((d) => { // Contiguous states return Number(d.id) < 60 && d.properties.name !== 'Alaska' && d.properties.name !== 'Hawaii'; }), - }; + }); - let serviceUrl: ComponentProps['url']; - let zoomDelta = 0; - let debug = false; + let serviceUrl = $state['url']>(null!); + let zoomDelta = $state(0); + let debug = $derived(shared.debug); -
+
- - -

Examples

-

SVG

- - -
- - {#if debug} -
- -
- {/if} - - - - - - - {#each filteredStates.features as feature} - { - const featureTransform = geoFitObjectTransform(projection, [width, height], feature); - transform.setTranslate(featureTransform.translate); - transform.setScale(featureTransform.scale); - }} - /> - {/each} - - - - {@const [longitude, latitude] = projection.invert?.([tooltip.x, tooltip.y]) ?? []} - {data.properties.name} - - - - - -
-
-
- -

SVG with padding

- - -
- - {#if debug} -
- -
- {/if} - - - - - - - {#each filteredStates.features as feature} - { - const featureTransform = geoFitObjectTransform(projection, [width, height], feature); - transform.setTranslate(featureTransform.translate); - transform.setScale(featureTransform.scale); - }} - /> - {/each} - - - - {@const [longitude, latitude] = - projection.invert?.([tooltip.x - padding.left, tooltip.y - padding.top]) ?? []} - {data.properties.name} - - - - - -
-
-
- -

Seamless (multiple zoom layers)

- - -
- - {#if debug} -
- -
- {/if} - - - - - - - - - - - {#each filteredStates.features as feature} - { - const featureTransform = geoFitObjectTransform(projection, [width, height], feature); - transform.setTranslate(featureTransform.translate); - transform.setScale(featureTransform.scale); - }} - /> - {/each} - - - - {@const [longitude, latitude] = projection.invert?.([tooltip.x, tooltip.y]) ?? []} - {data.properties.name} - - - - - -
-
-
+

Basic

+ +{#if serviceUrl} + +
+ + {#snippet children({ context })} + {#if debug} +
+ +
+ {/if} + + + + + + + {#each filteredStates.features as feature} + { + if (!context.geo.projection) return; + const featureTransform = geoFitObjectTransform( + context.geo.projection, + [context.width, context.height], + feature + ); + context.transform.setTranslate(featureTransform.translate); + context.transform.setScale(featureTransform.scale); + }} + /> + {/each} + + + + {#snippet children({ data })} + {@const [longitude, latitude] = + context.geo.projection?.invert?.([context.tooltip.x, context.tooltip.y]) ?? []} + {data.properties.name} + + + + + {/snippet} + + {/snippet} +
+
+
+ +

With padding

+ + +
+ + {#snippet children({ context })} + {#if debug} +
+ +
+ {/if} + + + + + + + {#each filteredStates.features as feature} + { + if (!context.geo.projection) return; + const featureTransform = geoFitObjectTransform( + context.geo.projection, + [context.width, context.height], + feature + ); + context.transform.setTranslate(featureTransform.translate); + context.transform.setScale(featureTransform.scale); + }} + /> + {/each} + + + + {#snippet children({ data })} + {@const [longitude, latitude] = + context.geo.projection?.invert?.([ + context.tooltip.x - context.padding.left, + context.tooltip.y - context.padding.top, + ]) ?? []} + {data.properties.name} + + + + + {/snippet} + + {/snippet} +
+
+
+ +

Seamless (multiple zoom layers)

+ + +
+ + {#snippet children({ context })} + {#if debug} +
+ +
+ {/if} + + + + + + + + + + + {#each filteredStates.features as feature} + { + if (!context.geo.projection) return; + const featureTransform = geoFitObjectTransform( + context.geo.projection, + [context.width, context.height], + feature + ); + context.transform.setTranslate(featureTransform.translate); + context.transform.setScale(featureTransform.scale); + }} + /> + {/each} + + + + {#snippet children({ data })} + {@const [longitude, latitude] = + context.geo.projection?.invert?.([context.tooltip.x, context.tooltip.y]) ?? []} + {data.properties.name} + + + + + {/snippet} + + {/snippet} +
+
+
+{/if} diff --git a/packages/layerchart/src/routes/docs/examples/ZoomableTileMap/+page.ts b/packages/layerchart/src/routes/docs/examples/ZoomableTileMap/+page.ts index 84c432e95..6314ea7dd 100644 --- a/packages/layerchart/src/routes/docs/examples/ZoomableTileMap/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/ZoomableTileMap/+page.ts @@ -11,6 +11,7 @@ export async function load({ fetch }) { }>, meta: { pageSource, + supportedContexts: ['svg', 'canvas'], }, }; } diff --git a/packages/layerchart/src/routes/docs/performance/dimension_arrays/+page.svelte b/packages/layerchart/src/routes/docs/performance/dimension_arrays/+page.svelte index af3de4fb8..05c7b71d1 100644 --- a/packages/layerchart/src/routes/docs/performance/dimension_arrays/+page.svelte +++ b/packages/layerchart/src/routes/docs/performance/dimension_arrays/+page.svelte @@ -6,32 +6,37 @@ import { zip } from 'd3-array'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; const { data } = $props(); let example = $state<'single'>('single'); - let renderContext = $state<'svg' | 'canvas'>('svg'); + + let renderContext = $derived(shared.renderContext as 'svg' | 'canvas'); let motion = $state(true); + let show = $state(true); let chartProps = $derived['props']>({ xAxis: { format: (v) => format(new Date(v)) }, - yAxis: { format: 'metric' }, - tooltip: { root: { motion }, header: { format: (v) => format(new Date(v)) } }, - highlight: { motion }, + tooltip: { + root: { motion: motion ? 'spring' : 'none' }, + header: { format: (v) => format(new Date(v)) }, + }, + highlight: { motion: motion ? 'spring' : 'none' }, });
-
- - - Svg - Canvas +
+ + + Yes + No - - + + Yes No @@ -47,16 +52,18 @@ {#key chartProps} {#if example === 'single'} -
- d[0]} - y={(d) => d[1]} - props={chartProps} - brush - {renderContext} - profile - /> +
+ {#if show} + d[0]} + y={(d) => d[1]} + props={chartProps} + brush + {renderContext} + profile + /> + {/if}
{:else if example === 'series'} @@ -68,32 +75,34 @@ tcp: data.chartData.tcp[0], }} > -
- d[0]} - y={(d) => d[1]} - series={[ - { - key: 'cpu', - data: zip(data.chartData.date, data.chartData.cpu), - color: 'hsl(var(--color-danger))', - }, - { - key: 'ram', - data: zip(data.chartData.date, data.chartData.ram), - color: 'hsl(var(--color-warning))', - }, - { - key: 'tcp', - data: zip(data.chartData.date, data.chartData.tcp), - color: 'hsl(var(--color-success))', - }, - ]} - props={chartProps} - brush - {renderContext} - profile - /> +
+ {#if show} + d[0]} + y={(d) => d[1]} + series={[ + { + key: 'cpu', + data: zip(data.chartData.date, data.chartData.cpu), + color: 'var(--color-danger)', + }, + { + key: 'ram', + data: zip(data.chartData.date, data.chartData.ram), + color: 'var(--color-warning)', + }, + { + key: 'tcp', + data: zip(data.chartData.date, data.chartData.tcp), + color: 'var(--color-success)', + }, + ]} + props={chartProps} + brush + {renderContext} + profile + /> + {/if}
{/if} diff --git a/packages/layerchart/src/routes/docs/performance/dimension_arrays/+page.ts b/packages/layerchart/src/routes/docs/performance/dimension_arrays/+page.ts index 7307cb529..f276a082a 100644 --- a/packages/layerchart/src/routes/docs/performance/dimension_arrays/+page.ts +++ b/packages/layerchart/src/routes/docs/performance/dimension_arrays/+page.ts @@ -1,6 +1,6 @@ import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { chartData: (await fetch('/data/examples/bench/dimension_arrays/data.json').then((r) => r.json() @@ -13,6 +13,7 @@ export async function load() { meta: { description: 'Individual arrays per dimension, similar to uplot', pageSource, + supportedContexts: ['svg', 'canvas'], hideTableOfContents: true, }, }; diff --git a/packages/layerchart/src/routes/docs/performance/dimension_arrays_processed/+page.svelte b/packages/layerchart/src/routes/docs/performance/dimension_arrays_processed/+page.svelte index 0f331b6fc..64a7cb2a3 100644 --- a/packages/layerchart/src/routes/docs/performance/dimension_arrays_processed/+page.svelte +++ b/packages/layerchart/src/routes/docs/performance/dimension_arrays_processed/+page.svelte @@ -6,18 +6,23 @@ import { zip } from 'd3-array'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; const { data } = $props(); let example = $state<'single'>('single'); - let renderContext = $state<'svg' | 'canvas'>('svg'); + + let renderContext = $derived(shared.renderContext as 'svg' | 'canvas'); let motion = $state(true); + let show = $state(true); let chartProps = $derived['props']>({ xAxis: { format: (v) => format(new Date(v)) }, - yAxis: { format: 'metric' }, - tooltip: { root: { motion }, header: { format: (v) => format(new Date(v)) } }, - highlight: { motion }, + tooltip: { + root: { motion: motion ? 'spring' : 'none' }, + header: { format: (v) => format(new Date(v)) }, + }, + highlight: { motion: motion ? 'spring' : 'none' }, }); let chartData = $derived({ @@ -28,16 +33,16 @@
-
- - - Svg - Canvas +
+ + + Yes + No - - + + Yes No @@ -53,46 +58,50 @@ {#key chartProps} {#if example === 'single'} -
- d[0]} - y={(d) => d[1]} - props={chartProps} - brush - {renderContext} - profile - /> +
+ {#if show} + d[0]} + y={(d) => d[1]} + props={chartProps} + brush + {renderContext} + profile + /> + {/if}
{:else if example === 'series'} -
- d[0]} - y={(d) => d[1]} - series={[ - { - key: 'cpu', - data: chartData.cpu, - color: 'hsl(var(--color-danger))', - }, - { - key: 'ram', - data: chartData.ram, - color: 'hsl(var(--color-warning))', - }, - { - key: 'tcp', - data: chartData.tcp, - color: 'hsl(var(--color-success))', - }, - ]} - props={chartProps} - brush - {renderContext} - profile - /> +
+ {#if show} + d[0]} + y={(d) => d[1]} + series={[ + { + key: 'cpu', + data: chartData.cpu, + color: 'var(--color-danger)', + }, + { + key: 'ram', + data: chartData.ram, + color: 'var(--color-warning)', + }, + { + key: 'tcp', + data: chartData.tcp, + color: 'var(--color-success)', + }, + ]} + props={chartProps} + brush + {renderContext} + profile + /> + {/if}
{/if} diff --git a/packages/layerchart/src/routes/docs/performance/dimension_arrays_processed/+page.ts b/packages/layerchart/src/routes/docs/performance/dimension_arrays_processed/+page.ts index eaa3ddbf2..3a41720f5 100644 --- a/packages/layerchart/src/routes/docs/performance/dimension_arrays_processed/+page.ts +++ b/packages/layerchart/src/routes/docs/performance/dimension_arrays_processed/+page.ts @@ -1,6 +1,6 @@ import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { chartData: (await fetch('/data/examples/bench/dimension_arrays/data.json').then((r) => r.json() @@ -14,6 +14,7 @@ export async function load() { description: 'Individual arrays per dimension, similar to uplot. Pre-processed before passed to LineChart', pageSource, + supportedContexts: ['svg', 'canvas'], hideTableOfContents: true, }, }; diff --git a/packages/layerchart/src/routes/docs/performance/series_arrays/+page.svelte b/packages/layerchart/src/routes/docs/performance/series_arrays/+page.svelte index 540aefcab..27eff73da 100644 --- a/packages/layerchart/src/routes/docs/performance/series_arrays/+page.svelte +++ b/packages/layerchart/src/routes/docs/performance/series_arrays/+page.svelte @@ -5,32 +5,37 @@ import { format } from '@layerstack/utils'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; const { data } = $props(); let example = $state<'single'>('single'); - let renderContext = $state<'svg' | 'canvas'>('svg'); + + let renderContext = $derived(shared.renderContext as 'svg' | 'canvas'); let motion = $state(true); + let show = $state(true); let chartProps = $derived['props']>({ xAxis: { format: (v) => format(new Date(v)) }, - yAxis: { format: 'metric' }, - tooltip: { root: { motion }, header: { format: (v) => format(new Date(v)) } }, - highlight: { motion }, + tooltip: { + root: { motion: motion ? 'spring' : 'none' }, + header: { format: (v) => format(new Date(v)) }, + }, + highlight: { motion: motion ? 'spring' : 'none' }, });
-
- - - Svg - Canvas +
+ + + Yes + No - - + + Yes No @@ -46,34 +51,38 @@ {#key chartProps} {#if example === 'single'} -
- +
+ {#if show} + + {/if}
{:else if example === 'series'} -
- +
+ {#if show} + + {/if}
{/if} diff --git a/packages/layerchart/src/routes/docs/performance/series_arrays/+page.ts b/packages/layerchart/src/routes/docs/performance/series_arrays/+page.ts index 44f741c07..fbc7ab17b 100644 --- a/packages/layerchart/src/routes/docs/performance/series_arrays/+page.ts +++ b/packages/layerchart/src/routes/docs/performance/series_arrays/+page.ts @@ -1,6 +1,6 @@ import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { chartData: (await fetch('/data/examples/bench/series_arrays/data.json').then((r) => r.json() @@ -21,6 +21,7 @@ export async function load() { meta: { description: 'Array per series, each with `x` / `y` items', pageSource, + supportedContexts: ['svg', 'canvas'], hideTableOfContents: true, }, }; diff --git a/packages/layerchart/src/routes/docs/performance/streaming/+page.svelte b/packages/layerchart/src/routes/docs/performance/streaming/+page.svelte new file mode 100644 index 000000000..8d6f9c236 --- /dev/null +++ b/packages/layerchart/src/routes/docs/performance/streaming/+page.svelte @@ -0,0 +1,138 @@ + + +
+
+ + + Yes + No + + + + + + Yes + No + + +
+ +
+ + + + + + + + + +
+ +
+ {#if show && chartData.length} + + {/if} +
+ + data: {format(chartData.length)} points +
diff --git a/packages/layerchart/src/routes/docs/examples/DotPlot/+page.ts b/packages/layerchart/src/routes/docs/performance/streaming/+page.ts similarity index 63% rename from packages/layerchart/src/routes/docs/examples/DotPlot/+page.ts rename to packages/layerchart/src/routes/docs/performance/streaming/+page.ts index 0fcd704ec..e799c3dde 100644 --- a/packages/layerchart/src/routes/docs/examples/DotPlot/+page.ts +++ b/packages/layerchart/src/routes/docs/performance/streaming/+page.ts @@ -4,7 +4,8 @@ export async function load() { return { meta: { pageSource, - related: ['components/Points'], + supportedContexts: ['svg', 'canvas'], + hideTableOfContents: true, }, }; } diff --git a/packages/layerchart/src/routes/docs/performance/wide_data/+page.svelte b/packages/layerchart/src/routes/docs/performance/wide_data/+page.svelte index b5b1bae25..431158386 100644 --- a/packages/layerchart/src/routes/docs/performance/wide_data/+page.svelte +++ b/packages/layerchart/src/routes/docs/performance/wide_data/+page.svelte @@ -2,35 +2,40 @@ import { type ComponentProps } from 'svelte'; import { LineChart } from 'layerchart'; import { Field, ToggleGroup, ToggleOption } from 'svelte-ux'; - import { format, PeriodType } from '@layerstack/utils'; + import { format } from '@layerstack/utils'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; const { data } = $props(); let example = $state<'single'>('single'); - let renderContext = $state<'svg' | 'canvas'>('svg'); + let renderContext = $derived(shared.renderContext as 'svg' | 'canvas'); let motion = $state(true); + let show = $state(true); + let chartProps = $derived['props']>({ xAxis: { format: (v) => format(new Date(v * 60 * 1000)) }, - yAxis: { format: 'metric' }, - tooltip: { root: { motion }, header: { format: (v) => format(new Date(v * 60 * 1000)) } }, - highlight: { motion }, + tooltip: { + root: { motion: motion ? 'spring' : 'none' }, + header: { format: (v) => format(new Date(v * 60 * 1000)) }, + }, + highlight: { motion: motion ? 'spring' : 'none' }, });
-
- - - Svg - Canvas +
+ + + Yes + No - - + + Yes No @@ -46,38 +51,42 @@ {#key chartProps} {#if example === 'single'} -
- 100 - d.idl} - props={chartProps} - brush - {renderContext} - profile - /> +
+ {#if show} + 100 - d.idl} + props={chartProps} + brush + {renderContext} + profile + /> + {/if}
{:else if example === 'series'} -
- 100 - d.idl, color: 'hsl(var(--color-danger))' }, - { - key: 'ram', - value: (d) => (100 * d.writ) / (d.writ + d.used), - color: 'hsl(var(--color-warning))', - }, - { key: 'tcp', value: (d) => d.send, color: 'hsl(var(--color-success))' }, - ]} - props={chartProps} - brush - {renderContext} - profile - /> +
+ {#if show} + 100 - d.idl, color: 'var(--color-danger)' }, + { + key: 'ram', + value: (d) => (100 * d.writ) / (d.writ + d.used), + color: 'var(--color-warning)', + }, + { key: 'tcp', value: (d) => d.send, color: 'var(--color-success)' }, + ]} + props={chartProps} + brush + {renderContext} + profile + /> + {/if}
{/if} diff --git a/packages/layerchart/src/routes/docs/performance/wide_data/+page.ts b/packages/layerchart/src/routes/docs/performance/wide_data/+page.ts index 5cac933f9..411936f4e 100644 --- a/packages/layerchart/src/routes/docs/performance/wide_data/+page.ts +++ b/packages/layerchart/src/routes/docs/performance/wide_data/+page.ts @@ -1,6 +1,6 @@ import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { chartData: (await fetch('/data/examples/bench/wide_data/data.json').then((r) => r.json())) as { epoch: number; @@ -14,6 +14,7 @@ export async function load() { meta: { description: 'Wide data (property per series)', pageSource, + supportedContexts: ['svg', 'canvas'], hideTableOfContents: true, }, }; diff --git a/packages/layerchart/src/routes/docs/performance/wide_data_processed/+page.svelte b/packages/layerchart/src/routes/docs/performance/wide_data_processed/+page.svelte index c1c553ea2..145f3cd71 100644 --- a/packages/layerchart/src/routes/docs/performance/wide_data_processed/+page.svelte +++ b/packages/layerchart/src/routes/docs/performance/wide_data_processed/+page.svelte @@ -2,21 +2,22 @@ import { type ComponentProps } from 'svelte'; import { LineChart } from 'layerchart'; import { Field, ToggleGroup, ToggleOption } from 'svelte-ux'; - import { format, PeriodType } from '@layerstack/utils'; + import { format } from '@layerstack/utils'; import Preview from '$lib/docs/Preview.svelte'; + import { shared } from '../../shared.svelte.js'; const { data } = $props(); let example = $state<'single'>('single'); - let renderContext = $state<'svg' | 'canvas'>('svg'); + let renderContext = $derived(shared.renderContext as 'svg' | 'canvas'); let motion = $state(true); + let show = $state(true); + let chartProps = $derived['props']>({ - xAxis: { format: PeriodType.Day }, - yAxis: { format: 'metric' }, - tooltip: { root: { motion } }, - highlight: { motion }, + tooltip: { root: { motion: motion ? 'spring' : 'none' } }, + highlight: { motion: motion ? 'spring' : 'none' }, }); let chartData = $derived( @@ -32,16 +33,16 @@
-
- - - Svg - Canvas +
+ + + Yes + No - - + + Yes No @@ -57,37 +58,41 @@ {#key chartProps} {#if example === 'single'} -
- +
+ {#if show} + + {/if}
{:else if example === 'series'} -
- +
+ {#if show} + + {/if}
{/if} diff --git a/packages/layerchart/src/routes/docs/performance/wide_data_processed/+page.ts b/packages/layerchart/src/routes/docs/performance/wide_data_processed/+page.ts index f5ac9830a..daa8c576a 100644 --- a/packages/layerchart/src/routes/docs/performance/wide_data_processed/+page.ts +++ b/packages/layerchart/src/routes/docs/performance/wide_data_processed/+page.ts @@ -1,6 +1,6 @@ import pageSource from './+page.svelte?raw'; -export async function load() { +export async function load({ fetch }) { return { chartData: (await fetch('/data/examples/bench/wide_data/data.json').then((r) => r.json())) as { epoch: number; @@ -14,6 +14,7 @@ export async function load() { meta: { description: 'Wide data (property per series). Pre-processed before passed to LineChart', pageSource, + supportedContexts: ['svg', 'canvas'], hideTableOfContents: true, }, }; diff --git a/packages/layerchart/src/routes/docs/shared.svelte.ts b/packages/layerchart/src/routes/docs/shared.svelte.ts new file mode 100644 index 000000000..b8180d1c5 --- /dev/null +++ b/packages/layerchart/src/routes/docs/shared.svelte.ts @@ -0,0 +1,11 @@ +import type { ComponentProps } from 'svelte'; +import { Layer } from 'layerchart'; + +// Shared state for the docs layout +export const shared = $state<{ + renderContext: ComponentProps['type']; + debug: boolean; +}>({ + renderContext: 'svg', + debug: false, +}); diff --git a/packages/layerchart/src/routes/docs/tools/GeojsonPreview/+page.svelte b/packages/layerchart/src/routes/docs/tools/GeojsonPreview/+page.svelte index 82f5b65c7..9a8435ab3 100644 --- a/packages/layerchart/src/routes/docs/tools/GeojsonPreview/+page.svelte +++ b/packages/layerchart/src/routes/docs/tools/GeojsonPreview/+page.svelte @@ -15,7 +15,7 @@ import { schemeCategory10 } from 'd3-scale-chromatic'; import { color } from 'd3-color'; - import { Canvas, Chart, GeoPath, GeoTile, Svg, Tooltip } from 'layerchart'; + import { Chart, GeoPath, GeoTile, Layer, Tooltip } from 'layerchart'; import TransformControls from '$lib/components/TransformControls.svelte'; import { EmptyMessage, @@ -28,24 +28,27 @@ import TilesetField from '$lib/docs/TilesetField.svelte'; import Json from '$lib/docs/Json.svelte'; - - let geojsonStr = ''; - let geojson: GeoJSON.FeatureCollection; - let error = ''; - - let selectedTab: 'input' | 'geojson' = 'input'; - - $: if (geojsonStr) { - try { - geojson = JSON.parse(geojsonStr); - error = ''; - } catch (e) { - error = 'Invalid object'; - console.error(e); + import { shared } from '../../shared.svelte.js'; + + let geojsonStr = $state(''); + let geojson = $state(); + let error = $state(''); + + let selectedTab: 'input' | 'geojson' = $state('input'); + + $effect.pre(() => { + if (geojsonStr) { + try { + geojson = JSON.parse(geojsonStr); + error = ''; + } catch (e) { + error = 'Invalid object'; + console.error(e); + } } - } + }); - let projection = geoMercator; + let projection = $state(geoMercator); const projections = [ { label: 'Identity', value: geoIdentity as () => GeoProjection }, { label: 'Albers', value: geoAlbers }, @@ -57,8 +60,8 @@ { label: 'Orthographic', value: geoOrthographic }, ]; - let serviceUrl: ComponentProps['url']; - let zoomDelta = 0; + let serviceUrl = $state['url']>(); + let zoomDelta = $state(0); const colorScale = scaleOrdinal().range( schemeCategory10.map((hex) => { @@ -94,38 +97,43 @@ initialScrollMode: 'scale', }} padding={{ top: 8, bottom: 8, left: 8, right: 8 }} - let:tooltip > - {#if projection === geoMercator} - - - - - - - - {/if} - - - - - {#each geojson?.features as feature} - - {/each} - - - - - {#each Object.entries(data.properties) as [key, value]} - - {/each} - - + {#snippet children({ context })} + {#if projection === geoMercator && serviceUrl} + + + + + + + + {/if} + + + + + {#if geojson?.features} + {#each geojson?.features as feature} + + {/each} + {/if} + + + + {#snippet children({ data })} + + {#each Object.entries(data.properties) as [key, value]} + + {/each} + + {/snippet} + + {/snippet} {:else} Please enter input below diff --git a/packages/layerchart/src/routes/docs/tools/GeojsonPreview/+page.ts b/packages/layerchart/src/routes/docs/tools/GeojsonPreview/+page.ts index 6691e1376..a9ab2775f 100644 --- a/packages/layerchart/src/routes/docs/tools/GeojsonPreview/+page.ts +++ b/packages/layerchart/src/routes/docs/tools/GeojsonPreview/+page.ts @@ -4,6 +4,7 @@ export async function load({ url }) { return { meta: { pageSource, + supportedContexts: ['svg', 'canvas'], hideTableOfContents: true, }, }; diff --git a/packages/layerchart/src/routes/docs/tools/ShapefilePreview/+page.svelte b/packages/layerchart/src/routes/docs/tools/ShapefilePreview/+page.svelte index 2bed5980b..fbbca5099 100644 --- a/packages/layerchart/src/routes/docs/tools/ShapefilePreview/+page.svelte +++ b/packages/layerchart/src/routes/docs/tools/ShapefilePreview/+page.svelte @@ -11,7 +11,7 @@ type GeoProjection, } from 'd3-geo'; - import { Canvas, Chart, GeoPath } from 'layerchart'; + import { Chart, GeoPath, Layer } from 'layerchart'; import { Button, ButtonGroup, @@ -23,17 +23,17 @@ Toggle, } from 'svelte-ux'; - import { mdiChevronDown } from '@mdi/js'; + import LucideChevronDown from '~icons/lucide/chevron-down'; import { goto } from '$app/navigation'; + import { shared } from '../../shared.svelte.js'; + let { data } = $props(); - export let data; + const geojson = $derived(data.geojson); - $: geojson = data.geojson; + let file = $state(data.file); - let file = data.file; - - let projection = geoIdentity as unknown as () => GeoProjection; + let projection = $state(geoIdentity as unknown as () => GeoProjection); const projections = [ { label: 'Identity', value: geoIdentity as () => GeoProjection }, { label: 'Albers', value: geoAlbers }, @@ -61,8 +61,8 @@ - -