Skip to content

LayerChart 2.0 (next)#449

Open
techniq wants to merge 473 commits intomainfrom
next
Open

LayerChart 2.0 (next)#449
techniq wants to merge 473 commits intomainfrom
next

Conversation

@techniq
Copy link
Copy Markdown
Owner

@techniq techniq commented Mar 10, 2025

No description provided.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 10, 2025

built with Refined Cloudflare Pages Action

⚡ Cloudflare Pages Deployment

Name Status Preview Last Commit
layerchart ✅ Ready (View Log) Visit Preview 753b19b

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Mar 10, 2025

Open in StackBlitz

npm i https://pkg.pr.new/layerchart@449

commit: 753b19b

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 12, 2025

🦋 Changeset detected

Latest commit: 753b19b

The changes in this PR will be included in the next version bump.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@HicaroD
Copy link
Copy Markdown

HicaroD commented May 9, 2025

Hey, @techniq.
First of all, thank you for your amazing work, this package is actually incredible!

I have a question: When are you planning to merge this PR so we can finally get the Svelte 5 support? 🤗

@techniq
Copy link
Copy Markdown
Owner Author

techniq commented May 9, 2025

Hey @HicaroD, thanks for the kind words 🫶. Sorry, I don't have a rough release date set yet. There are a good number of improvements I would like to include in 2.0.0 (see milestone although not guaranteed to cover everything or have everything).

With that said, you 100% can use it today via layerchart@next and docs at https://next.layerchart.com/. I do this already for Github Analysis, Strava Analysis, and my main work project. It's also be used to build out shadcn-svelte Charts (issue).

I could see pushing some items to 2.1 but want to make sure if we find any breaking changes (mostly API refinements) we get them all in 2.0. I'm anticipating very little (if any), but still don't want to rush it.

Related: I also need to migrate Svelte UX fully over to Svelte 5 (it's Svelte 3-5 compat with Svelte 3/4 state/syntax). Both libraries have already been migrated to Tailwind 4, but the move to Svelte 5 state runes/snippets and regressions checking will take some concentrated efforts and time. I don't know how close I'll try to sync their release schedules though (and the benefit of having @next releases in place so they can be incrementally adopted).

@cycle4passion
Copy link
Copy Markdown

All the examples give 500 errors on https://next.layerchart.com/

@techniq
Copy link
Copy Markdown
Owner Author

techniq commented May 9, 2025

All the examples give 500 errors on https://next.layerchart.com/

@cycle4passion Odd... working here

image

Maybe try a hard refresh? Which browser?

@cycle4passion
Copy link
Copy Markdown

mobile only, does not work in arc (chromium), native chrome, or Safari browser.

@techniq
Copy link
Copy Markdown
Owner Author

techniq commented May 9, 2025

mobile only, does not work in arc (chromium), native chrome, or Safari browser.

Really odd... I can't reproduce

image

techniq and others added 21 commits April 22, 2026 11:16
The `{#await}` block doesn't anchor a remote `query` to SSR's hydratable serialization, so on hydration the client threw `hydratable_missing_but_required` looking for `getStats/`. Switch to the `query.loading`/`error`/`current` pattern, which is the supported path for remote queries and also avoids the async-mode `<svelte:boundary>` commit that triggered a "Batch has scheduled roots" invariant.
* Initialize bundle analyzer package

* update bundle baseline
- Sum entry chunk + all chunks reachable via static imports (so the reported size reflects true up-front cost; lazy chunks excluded)
- Add `--visualize` flag (and `bundle:visualize` script) that emits an interactive treemap HTML per scenario via `rollup-plugin-visualizer`
- Regenerate `latest.json` baseline with the new methodology
* Initialize bundle analyzer package

* update bundle baseline

* Prepare ChartChildren and Chart for tree-shaking

- Add `"sideEffects": ["**/*.css"]` to layerchart/package.json so downstream bundlers can prune unused barrel re-exports
- Convert ChartChildren's value imports of components only referenced in `ComponentProps<typeof X>` to `import type` (Area, Arc, Bars, BrushContext, Group, Line, Pie, Spline, TooltipContext)
- Inline `geoFitObjectTransform` into Chart.svelte to drop the static import edge through `$lib/utils/geo.js` (which transitively imports d3-geo)

No visible change in the bundle analyzer (it already does aggressive treeshaking), but unlocks the dynamic-import refactor in the next commit and protects consumers whose bundlers tree-shake less aggressively.

* Lazy-load conditionally-rendered components in Chart

Convert statically-imported components to `{#await import(...)}` so they only ship to users who opt in via the corresponding prop:
- ChartChildren: ChartAnnotations (when annotations.length > 0), DefaultTooltip (tooltipContext truthy), Labels, Legend, Points
- TooltipContext: Voronoi (mode === 'voronoi'), Arc (radial bounds/band)
Voronoi alone removes d3-geo-voronoi and its transitive d3-geo from the always-loaded bundle.
Update the bundle analyzer to sum the entry chunk plus all chunks reachable via static imports (lazy chunks excluded), so the reported size reflects up-front cost rather than total feature surface.

Result: `core` (`Chart` + `Svg`) drops 154.94 → 109.95 KB gz (-29%). Comparable savings on every scenario except `all` (which exercises every lazy path).

* Add `pnpm bundle:visualize` to generate treemaps using rollup-plugin-visualizer

* Fix BarChart legend toggle test for lazy-loaded Legend
The test queried `.lc-legend-swatch-button` synchronously, but `Legend` is now dynamically imported inside `ChartChildren` and isn't in the DOM until the chunk resolves. Wrap the query in `vi.waitFor` so the test waits for the buttons to mount before clicking.

* Fix analyzer footgun: don't overwrite latest.json on filtered runs
A filtered run (e.g. `pnpm bundle:visualize -- core`) was overwriting `bundle-reports/latest.json` with just the filtered scenarios, causing the PR comparison comment to show "0 KB" for every scenario the filtered run didn't cover. Now `latest.json` is only updated when no `--components`, scenario, or component filters are passed; filtered runs still get a timestamped report. Also regenerate the full baseline against the current lazy-loaded code.

* Fix CI test failure by avoiding tooltip namespace barrel in DefaultTooltip

DefaultTooltip is dynamically imported from ChartChildren. It was using `import * as Tooltip from '../tooltip/index.js'`, which dragged the entire tooltip barrel — including TooltipContext.svelte (already in the static graph via Chart.svelte) — into its lazy chunk. Under CI's resource-constrained dev server this broke the DefaultTooltip browser test ("Failed to fetch dynamically imported module"). Replace the namespace import with explicit named imports of just the 5 components actually used (no Context). The local `const Tooltip = { Root, Header, List, Item, Separator }` keeps the existing template syntax (`<Tooltip.Root>`, etc.) unchanged. Bonus: tightens tree-shaking — `all` scenario drops ~3 KB gz.

* Revert DefaultTooltip lazy-load to fix vitest-browser CI test
The dynamic import of `DefaultTooltip` from `ChartChildren` caused a CI-only "Failed to fetch dynamically imported module" failure in `DefaultTooltip.svelte.test.ts`. Local tests passed; only the Linux/playwright runner reproduced. Switching the inner `import * as Tooltip` namespace to named imports (commit 7e5d6e7) didn't help. The savings were small (~5 KB gz on `core`) and not worth the test instability — the other lazy-loads (Voronoi, Arc, ChartAnnotations, Labels, Legend, Points) remain. Net Phase 2 gain on `core` is now -39 KB gz (-25%) instead of -45 KB gz (-29%).

* Revert ChartChildren lazy-loads; keep TooltipContext lazy-loads

The DefaultTooltip vitest-browser test still failed in CI ("Failed to fetch dynamically imported module") even after reverting just DefaultTooltip's lazy-load and after switching the inner namespace import to named imports. Rather than continue narrowing, revert all 4 ChartChildren lazy-loads (Labels, Legend, Points, ChartAnnotations). The TooltipContext lazy-loads (Voronoi, Arc) stay — they're the biggest win (~17 KB gz on core from removing d3-geo-voronoi + d3-geo from the static graph) and aren't in the test failure path. Net Phase 2 gain on `core` is now -17 KB gz (-11%).

* Switch ChartChildren lazy-loads from `{#await}` to `$effect`
The `{#await import('./X.svelte')}` template pattern broke `DefaultTooltip.svelte.test.ts` in CI ("Failed to fetch dynamically imported module" on the test file). Local tests passed; only the Linux/playwright runner reproduced. Move the dynamic imports back into `$effect` blocks (script-side `import()`), which Vite/vitest-browser appears to handle differently. Same chunks, same bundle savings, but the dynamic imports live in regular JS rather than Svelte template syntax. TooltipContext keeps `{#await}` for Voronoi/Arc — those weren't in the test failure path. Net Phase 2 gain on `core` recovered to -40 KB gz (-25%).

* Refactor lazy-loading to a `<Lazy>` wrapper with prop-spread + `then` snippet

Replace per-component `$state`/`$effect`/conditional-render boilerplate in ChartChildren with a generic `<Lazy>` component that takes a `load` factory and either spreads remaining props to the loaded component (single-render case: ChartAnnotations, Legend) or passes it via a `then` snippet (loop case: Points, Labels). Conditional gating uses standard `{#if}` outside `<Lazy>` rather than a `when` prop. The snippet is named `then` (not `children`) to avoid collisions with loaded components that have their own `children` prop. Same bundle behavior and CI-friendly `$effect`-under-the-hood as the previous explicit pattern; ~half the lines per lazy-load.

* Use `{#await import(...)}` for lazy-loads; remove `<Lazy>` wrapper

Now that `optimizeDeps.include: ['d3-interpolate']` prevents the mid-test Vite reload (the actual cause of the CI flake), the cleaner inline `{#await import('./X.svelte') then { default: X }}` pattern works fine in CI. Revert ChartChildren back to that pattern, matching what TooltipContext already does. Remove `Lazy.svelte` since it's no longer needed. Same chunks, same ~25% savings on `core` (115.60 KB gz).

* Add `build` script alias so CI's `build:packages` actually builds layerchart

The bundle analysis CI workflow runs `pnpm build:packages` (which calls `pnpm --filter './packages/*' build`) before `pnpm bundle:analyze`. layerchart only had a `package` script (svelte-package convention), no `build`, so the filter call did nothing in CI — the analyzer ran against an empty `dist/` and produced 0-byte sizes for every scenario. That's why the PR comment showed "0.00 KB" for "New". Add a `build` alias for `svelte-package` (kept alongside existing `package` for back-compat) so CI's existing build step now actually rebuilds layerchart's `dist/`.

* Add percentage change to bundle analysis PR comment
The script already computed `sizePercent` and `gzipSizePercent`; now display them inline in the Change column alongside the raw KB deltas (e.g. `-160.00 KB (-25.1%)`). Easier to scan relative impact across scenarios than raw byte counts alone. Same for the Individual Components table.

* Group bundle analysis scenarios in PR comment by category

Add a `group` field to scenarios (Foundation, Cartesian charts, Geo, Hierarchy, Graph / network, Worst case) and render the PR comment with one sub-table per group instead of one alphabetized list. Reorder `define-scenarios.ts` to put `core` first and scenarios within their category. Remove the alphabetical sort in `analyzeChanges` so the comment preserves the natural order.

* Move heavy-dep components into sub-path exports

Remove from root `layerchart`: `Geo*` + `Graticule` + `TileImage`, `Tree`/`Treemap`/`Pack`/`Partition`, `ForceSimulation`, `Dagre`/`Sankey`/`Chord`/`Ribbon`. Each group now lives in its own folder + sub-path entry: `layerchart/geo`, `layerchart/hierarchy`, `layerchart/force`, `layerchart/graph`.

Defends against bundlers that don't tree-shake the root barrel cleanly — `@dagrejs/dagre` (~22 KB), `d3-geo` (~15 KB), `d3-force` (~7 KB), `d3-hierarchy` (~6 KB), `d3-sankey` (~6 KB), and `d3-chord` (~2 KB) are now reachable only via opt-in imports. Per-scenario bundle sizes are unchanged for already-good consumers; the worst-case `all` scenario drops 241.8 → 235.5 KB gz.

`Voronoi`/`Hull` stay at root (already lazy via `TooltipContext`). `Contour`/`Density`/`Raster`/`BoxPlot`/`Violin`/`Threshold` also stay (not category-specific). High-level charts (`LineChart`, `BarChart`, etc.) remain at root.

Breaking: imports for the moved components must move to the new sub-paths.

* Lazy-load opt-in features in core path

Three components that everyone pays for in `core` today, but only some users actually need:

- `Spline` in `Grid` (radial linear grid lines only — non-radial users never render it)
- `Bar` in `Highlight` (only when user sets `bar` prop, default `false`)
- `BrushContext` in `Chart` (only when user sets `brush` prop, default `undefined`) — required splitting the inner `<TooltipContext>` tree across the brush/no-brush branches; brush tests now wait for the lazy chunk via a new `awaitBrushReady` helper

Saves ~4 KB gz on `core` (115.60 → 111.31 KB) and similar on every cartesian/geo/graph/hierarchy scenario. ~28% total reduction on `core` vs the pre-Phase-1 baseline.

Also switch `@layerstack/svelte-actions` imports from the barrel to sub-paths (`/styles`, `/portal`). No bundle effect since tree-shaking already stripped the unused `popover.js`, but it stops the Svelte REPL/CDN from eagerly fetching `@floating-ui/dom` (popover's transitive dep) when users load `layerchart` from a CDN.

* add changeset for lazy loading opt-in features (brush, radial spline, etc)

* lazy load d3-quadtree and DefaultTooltip based on usage

* Render chart subtree during BrushContext lazy load

The previous structure put `<TooltipContext>` + `<ChartChildren>` *inside* the `{#await import('./BrushContext.svelte')}` block, so on slow networks the entire chart was blocked on the chunk fetch (~300-1000ms on Fast 4G). Move the same subtree into the `{#await}`'s pending branch as well so the chart paints immediately; the `{:then}` branch then re-mounts it inside `BrushContext` once the chunk arrives.

Trade-off: brief one-time re-mount of `TooltipContext` + `ChartChildren` (~50ms) when the chunk lands. Acceptable because it happens before any user interaction (no tooltip/series state to lose) and brush is opt-in. Bundle savings preserved (core +0.1 KB from the duplicated template, since the actual modules are deduped).

The other lazy-load sites (`Voronoi`/`Arc` in `TooltipContext`, `DefaultTooltip`, `Bar`, `Points`/`Labels`/`Legend`/`ChartAnnotations` in `ChartChildren`, `Spline` in `Grid`) don't need the same treatment — none of them block visible chart content from rendering.
Co-authored-by: github-actions[bot] <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* split Circle.svelte into 3 layer-specific components (Circle.svg.svelte, etc) along with CircleState (Circle.shared.svelte.ts).  Keep Circle.svelte and delegate to underlying type

* split Text.svelte into 3 layer-specific components

* Organize into component directories

* split Rect, Line, and Path into 3 layer-specific components

* split Ellipse, Polygon, Group, Image, ClipPath, Pattern, LinearGradient, and RadialGradient into 3 layer-specific components

* Add changeset for per-layer primitive

* split Axis into 3 layer-specific components

* Add full-chart layer examples (to monitor progress)

* split Grid and Rule into 3 layer-specific components

* split Chart and ChartChildren into 3 layer-specific components, and alias Layer based on layer type (Layer within layerchart/svg => Svg)

* refine bundle scenarios

* refine bundle scenarios

* Add more foundation examples

Co-authored-by: Copilot <copilot@github.com>

* improve bundle comment

* split Highlight, ChartClipPath, RectClipPath into 3 layer-specific components

* split Arc, Area, and Spline into 3 layer-specific components

* split ArcLabel, Bar, Bars, Labels, Pie, and Points into 3 layer-specific components

* split Frame, Cell, Threshold, AnnotationLine, AnnotationPoint, and Trail into 3 layer-specific components

* split AnnotationRange, CircleClipPath, Vector, Link, Hull, Density, and Calendar into 3 layer-specific components

* split BoxPlot, Violin, Raster, Month, Contour, and Voronoi into 3 layer-specific components

* split geo components (GeoPath, GeoSpline, etc) into 3 layer-specific components

* split Ribbon into 3 layer-specific components.  Re-export all layout/helper components

* split high-level charts (BarChart, LineChart, etc) into 3 layer-specific components.  Re-export all layout/helper components

* update changeset and docs

* Update bundle size PR comment with collapsible sections

* Remove `Svg` from core (just Chart)

* Split `Foundation` scenarios into `Core (agnostic)` and `Core (layer-specific)`

* update bundle report

* Lazy load transform context (and only when used) similar to brush context

* Add ChartCore.  Fix TransformContext test

* update bundle sizes in docs

* move core section above base

* Fix primitive fill/stroke for canvas primitives

* fix(Text): Render on `<Svg>` layer when only one of `x`/`y` is set

The static-mode render guard required both `x` and `y` to be explicit, so
`<Text y={-6}>` inside a positioned `<Group>` (e.g. tooltip labels in the
GeoPoint world-capitals example) was skipped on the Svg layer — Canvas
worked because it doesn't gate on coordinate validity.

Change `&&` to `||` so Text renders when either coordinate is set; the
missing one falls through to the existing `motionX`/`motionY` default of 0,
matching SVG's natural "missing coord = 0" behavior and the Canvas variant.

* fix components missing html impls (Calendar, Bars, Annotation*, etc)

* Fix Text within group

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: github-actions[bot] <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@github-actions github-actions Bot mentioned this pull request Apr 30, 2026
techniq and others added 7 commits April 30, 2026 13:40
* perf: Skip motion container allocation when `motion` prop is `undefined`

* perf: Skip mark-info `$effect` for pixel-mode primitives

* perf: Reduce per-tick reactive overhead in `Path` / `Link` (force-simulation graphs)

* fix(Arc, RectClipPath, ChartClipPath): Restore on-mount tween animations

* Force SVG for lattice example to verify delegation perf issue

* Revert lattice svg force

* Improve bundle size warnings

* sort bundle scenarios by size desc

* sort warnings by change desc

* Revert "perf: Skip motion container allocation when `motion` prop is `undefined`"

This reverts b45f47a. Empirical measurements on the lattice (n=20,
760 links) and tree force-simulation examples showed the call-site
gating produced no measurable FPS difference vs. an unmodified
`createMotion` — the fast path at `motion.svelte.ts:197-213`
(passthrough returned when `motionProp === undefined`) already
covers the no-motion case.

  | Example  | Phase       | Before    | After (revert) |
  |----------|-------------|-----------|---------------:|
  | Lattice  | steady sim  | 6.46-6.53 | 6.52-6.58      |
  | Tree     | active sim  | 16.88-17.28 | 17.46-17.51  |

The 728-line, 13-file diff added per-call-site gates and null-check
fallbacks for an optimization that was already happening one layer
down. Reverting restores the simpler unconditional construction.

The Path.shared.svelte.ts merge keeps ee6b332's `#getPathData`
hot-path getter (which is independent of the motion-alloc question)
and switches the initial-pathData resolution to `resolvePathData()`
to handle the polymorphic `string | () => string` form added by
that commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(SeriesState): Avoid `derived_inert` crash when chart unmounts under a `<svelte:boundary>`

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <action@github.com>
Co-authored-by: github-actions[bot] <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants