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/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/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/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/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/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/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/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-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/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/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/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-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/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/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/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/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-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/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/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 index 7a9c31a93..01dcd5b01 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -2,65 +2,106 @@ "mode": "pre", "tag": "next", "initialVersions": { - "layerchart": "1.0.7" + "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", + "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", @@ -69,17 +110,21 @@ "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", @@ -96,6 +141,8 @@ "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-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/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/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/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/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/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 93b9e553f..60059423c 100644 --- a/package.json +++ b/package.json @@ -4,13 +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", @@ -18,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.20.0" + "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 26e8c4f43..cb163223c 100644 --- a/packages/layerchart/CHANGELOG.md +++ b/packages/layerchart/CHANGELOG.md @@ -1,5 +1,157 @@ # LayerChart +## 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 index 64ecb37a1..ca6140a72 100644 --- a/packages/layerchart/src/lib/components/AnnotationPoint.svelte +++ b/packages/layerchart/src/lib/components/AnnotationPoint.svelte @@ -88,25 +88,34 @@ ? 'start' : 'middle', }); - - { + function onPointerMove(e: PointerEvent | MouseEvent | TouchEvent) { if (details) { e.stopPropagation(); ctx.tooltip.show(e, { annotation: { label, details } }); } - }} - onpointerleave={() => { + } + + function onPointerLeave(e: PointerEvent | MouseEvent | TouchEvent) { if (details) { + e.stopPropagation(); ctx.tooltip.hide(); } - }} + } + + + {#if label} @@ -114,6 +123,15 @@ value={label} {...labelProps} {...props?.label} - class={cls('text-xs pointer-events-none', props?.label?.class)} + class={cls('lc-annotation-point-label', props?.label?.class)} /> {/if} + + diff --git a/packages/layerchart/src/lib/components/AnnotationRange.svelte b/packages/layerchart/src/lib/components/AnnotationRange.svelte index f1ba599e6..af2aeba8b 100644 --- a/packages/layerchart/src/lib/components/AnnotationRange.svelte +++ b/packages/layerchart/src/lib/components/AnnotationRange.svelte @@ -118,7 +118,12 @@ {#if fill || className} - + {/if} {#if gradient} @@ -142,6 +147,15 @@ value={label} {...labelProps} {...props?.label} - class={cls('text-xs pointer-events-none', props?.label?.class)} + class={cls('lc-annotation-range-label', props?.label?.class)} /> {/if} + + diff --git a/packages/layerchart/src/lib/components/Arc.svelte b/packages/layerchart/src/lib/components/Arc.svelte index 4c285e649..aec9b0754 100644 --- a/packages/layerchart/src/lib/components/Arc.svelte +++ b/packages/layerchart/src/lib/components/Arc.svelte @@ -189,7 +189,7 @@ import { degreesToRadians } from '$lib/utils/math.js'; import { getChartContext } from './Chart.svelte'; - import { extractLayerProps, layerClass } from '$lib/utils/attributes.js'; + import { extractLayerProps } from '$lib/utils/attributes.js'; import { cls } from '@layerstack/tailwind'; import { max } from 'd3-array'; import { @@ -402,7 +402,7 @@ pathData={trackArc()} stroke="none" bind:pathRef={trackRef} - {...extractLayerProps(track, 'arc-track')} + {...extractLayerProps(track, 'lc-arc-track')} /> {/if} @@ -416,7 +416,7 @@ stroke-width={strokeWidth} {opacity} {...restProps} - class={cls(layerClass('arc-line'), className)} + class={cls('lc-arc-line', className)} onpointerenter={onPointerEnter} onpointermove={onPointerMove} onpointerleave={onPointerLeave} diff --git a/packages/layerchart/src/lib/components/Area.svelte b/packages/layerchart/src/lib/components/Area.svelte index 4826b799e..456ead7ab 100644 --- a/packages/layerchart/src/lib/components/Area.svelte +++ b/packages/layerchart/src/lib/components/Area.svelte @@ -239,7 +239,15 @@ {#if line} - + {/if} {#if renderCtx === 'svg'} @@ -251,6 +259,6 @@ {stroke} stroke-width={strokeWidth} {opacity} - {...extractLayerProps(restProps, 'area-path')} + {...extractLayerProps(restProps, 'lc-area-path')} /> {/if} diff --git a/packages/layerchart/src/lib/components/Axis.svelte b/packages/layerchart/src/lib/components/Axis.svelte index ea270347b..d31bd5257 100644 --- a/packages/layerchart/src/lib/components/Axis.svelte +++ b/packages/layerchart/src/lib/components/Axis.svelte @@ -44,7 +44,7 @@ ticks?: TicksConfig; /** - * Width or height of each tick in pxiels (responsive reduce) + * Width or height of each tick in pixels (enabling responsive count) */ tickSpacing?: number; @@ -96,7 +96,7 @@ transitionInParams?: TransitionParams; /** - * Scale for the axis + * Override scale for the axis */ scale?: any; @@ -125,8 +125,17 @@ import { extent } from 'd3-array'; import { pointRadial } from 'd3-shape'; - - import { type FormatType, type FormatConfig } from '@layerstack/utils'; + import { + timeDay, + timeHour, + timeMillisecond, + timeMinute, + timeMonth, + timeSecond, + timeYear, + } from 'd3-time'; + + import { type FormatType, type FormatConfig, unique, PeriodType } from '@layerstack/utils'; import { cls } from '@layerstack/tailwind'; import Group, { type GroupProps } from './Group.svelte'; @@ -136,9 +145,9 @@ import { isScaleBand } from '$lib/utils/scales.svelte.js'; import { getChartContext } from './Chart.svelte'; - import { extractLayerProps, layerClass } from '$lib/utils/attributes.js'; + import { extractLayerProps } from '$lib/utils/attributes.js'; import { type MotionProp } from '$lib/utils/motion.svelte.js'; - import { resolveTickFormat, resolveTickVals, type TicksConfig } from '$lib/utils/ticks.js'; + import { autoTickVals, autoTickFormat, type TicksConfig } from '$lib/utils/ticks.js'; let { placement, @@ -183,6 +192,9 @@ const scale = $derived( scaleProp ?? (['horizontal', 'angle'].includes(orientation) ? ctx.xScale : ctx.yScale) ); + const interval = $derived( + ['horizontal', 'angle'].includes(orientation) ? ctx.xInterval : ctx.yInterval + ); const xRangeMinMax = $derived(extent(ctx.xRange)) as [number, number]; const yRangeMinMax = $derived(extent(ctx.yRange)) as [number, number]; @@ -206,9 +218,46 @@ ? Math.round(ctxSize / tickSpacing) : undefined ); - const tickVals = $derived(resolveTickVals(scale, ticks, tickCount)); + const tickVals = $derived.by(() => { + let tickVals = autoTickVals(scale, ticks, tickCount); + + if (interval != null) { + // Remove last tick when interval is provided (such as for bar charts with center aligned (offset) ticks) + tickVals.pop(); + } + + // Use format to filter ticks (helpful to keep ticks above a threshold for wide charts or short durations) + const formatType = typeof format === 'object' ? format?.type : format; + + if (formatType === 'integer') { + tickVals = tickVals.filter(Number.isInteger); + } else if (formatType === 'year' || formatType === PeriodType.CalendarYear) { + tickVals = tickVals.filter((val) => +timeYear.floor(val) === +val); + } else if ( + formatType === 'month' || + formatType === PeriodType.Month || + formatType === PeriodType.MonthYear + ) { + // tickVals = tickVals.filter((val) => +timeMonth.floor(val) === +val); + tickVals = tickVals.filter((val) => val.getDate() < 7); // first week of the month + } else if (formatType === 'day' || formatType === PeriodType.Day) { + tickVals = tickVals.filter((val) => +timeDay.floor(val) === +val); + } else if (formatType === 'hour' || formatType === PeriodType.Hour) { + tickVals = tickVals.filter((val) => +timeHour.floor(val) === +val); + } else if (formatType === 'minute' || formatType === PeriodType.Minute) { + tickVals = tickVals.filter((val) => +timeMinute.floor(val) === +val); + } else if (formatType === 'second' || formatType === PeriodType.Second) { + tickVals = tickVals.filter((val) => +timeSecond.floor(val) === +val); + } else if (formatType === 'millisecond' || formatType === PeriodType.Millisecond) { + tickVals = tickVals.filter((val) => +timeMillisecond.floor(val) === +val); + } + + // Remove any duplicates (manually added) + return unique(tickVals); + }); + const tickFormat = $derived( - resolveTickFormat({ + autoTickFormat({ scale, ticks, count: tickCount, @@ -221,27 +270,29 @@ function getCoords(tick: any) { switch (placement) { case 'top': - return { - x: scale(tick) + (isScaleBand(scale) ? scale.bandwidth() / 2 : 0), - y: yRangeMinMax[0], - }; - case 'bottom': return { - x: scale(tick) + (isScaleBand(scale) ? scale.bandwidth() / 2 : 0), - y: yRangeMinMax[1], + x: + scale(tick) + + (isScaleBand(scale) + ? scale.bandwidth() / 2 + : ctx.xInterval + ? (scale(ctx.xInterval.offset(tick)) - scale(tick)) / 2 // offset 1/2 width of time interval + : 0), + y: placement === 'top' ? yRangeMinMax[0] : yRangeMinMax[1], }; case 'left': - return { - x: xRangeMinMax[0], - y: scale(tick) + (isScaleBand(scale) ? scale.bandwidth() / 2 : 0), - }; - case 'right': return { - x: xRangeMinMax[1], - y: scale(tick) + (isScaleBand(scale) ? scale.bandwidth() / 2 : 0), + x: placement === 'left' ? xRangeMinMax[0] : xRangeMinMax[1], + y: + scale(tick) + + (isScaleBand(scale) + ? scale.bandwidth() / 2 + : ctx.yInterval + ? (scale(ctx.yInterval.offset(tick)) - scale(tick)) / 2 // offset 1/2 height of time interval + : 0), }; case 'angle': @@ -361,36 +412,31 @@ }); const resolvedLabelProps = $derived({ - value: typeof label === 'function' ? '' : undefined, + value: typeof label === 'function' ? '' : label, x: resolvedLabelX, y: resolvedLabelY, textAnchor: resolvedLabelTextAnchor, verticalAnchor: resolvedLabelVerticalAnchor, rotate: orientation === 'vertical' && labelPlacement === 'middle' ? -90 : 0, - capHeight: '.5rem', // text-[10px] + // complement 10px text (until Text supports custom styles) + capHeight: '7px', + lineHeight: '11px', ...labelProps, - class: cls( - layerClass('axis-label'), - 'text-[10px] stroke-surface-100 [stroke-width:2px] font-light', - classes.label, - labelProps?.class - ), + class: cls('lc-axis-label', classes.label, labelProps?.class), }) satisfies ComponentProps; {#if rule !== false} - {@const ruleProps = extractLayerProps(rule, 'axis-rule')} {/if} @@ -400,7 +446,7 @@ {/if} - {#each tickVals as tick, index (tick)} + {#each tickVals as tick, index (tick.valueOf())} {@const tickCoords = getCoords(tick)} {@const [radialTickCoordsX, radialTickCoordsY] = pointRadial(tickCoords.x, tickCoords.y)} {@const [radialTickMarkCoordsX, radialTickMarkCoordsY] = pointRadial( @@ -417,32 +463,21 @@ capHeight: '7px', lineHeight: '11px', ...tickLabelProps, - class: cls( - layerClass('axis-tick-label'), - '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 = extractLayerProps(grid, 'axis-grid')} {/if} {#if tickMarks} - {@const tickClasses = cls( - layerClass('axis-tick'), - 'stroke-surface-content/50', - classes.tick - )} + {@const tickClasses = cls('lc-axis-tick', classes.tick)} {#if orientation === 'horizontal'} {/each} + + diff --git a/packages/layerchart/src/lib/components/Bar.svelte b/packages/layerchart/src/lib/components/Bar.svelte index 98f6458af..f9cb0f318 100644 --- a/packages/layerchart/src/lib/components/Bar.svelte +++ b/packages/layerchart/src/lib/components/Bar.svelte @@ -71,7 +71,8 @@ Without< Omit, 'width' | 'height' | 'x' | 'y' | 'offset'>, BarPropsWithoutHTML - >; + > & + CommonEvents; - + {#if children} {@render children()} {:else} @@ -64,7 +64,7 @@ {stroke} fill={fill ?? (ctx.config.c ? ctx.cGet(d) : null)} onclick={(e) => onBarClick(e, { data: d })} - {...extractLayerProps(restProps, 'bars-bar')} + {...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 fd76b7f31..d7485adb4 100644 --- a/packages/layerchart/src/lib/components/Blur.svelte +++ b/packages/layerchart/src/lib/components/Blur.svelte @@ -24,7 +24,6 @@ import type { Snippet } from 'svelte'; import { getRenderContext } from './Chart.svelte'; import { createId } from '$lib/utils/createId.js'; - import { layerClass } from '$lib/utils/attributes.js'; const uid = $props.id(); @@ -35,13 +34,13 @@ {#if renderContext === 'svg'} - + {#if children} - + {@render children()} {/if} diff --git a/packages/layerchart/src/lib/components/BrushContext.svelte b/packages/layerchart/src/lib/components/BrushContext.svelte index b31e2404c..1eaee348e 100644 --- a/packages/layerchart/src/lib/components/BrushContext.svelte +++ b/packages/layerchart/src/lib/components/BrushContext.svelte @@ -148,7 +148,6 @@ import type { HTMLAttributes } from 'svelte/elements'; import { getChartContext } from './Chart.svelte'; import type { Snippet } from 'svelte'; - import { layerClass } from '$lib/utils/attributes.js'; const ctx = getChartContext(); @@ -463,7 +462,6 @@ {#if disabled} {@render children?.({ brushContext })} {:else} - {@const handleClass = layerClass('brush-handle')}
selectAll()} >
reset()} >
@@ -511,14 +503,7 @@ style:width="{_range.width}px" style:height="{handleSize}px" data-position="top" - class={cls( - handleClass, - 'cursor-ns-resize select-none', - 'range absolute', - 'z-10', - classes.handle, - handle?.class - )} + class={cls('lc-brush-handle', classes.handle, handle?.class)} onpointerdown={adjustTop} ondblclick={(e) => { e.stopPropagation(); @@ -536,15 +521,7 @@ style:width="{_range.width}px" style:height="{handleSize}px" data-position="bottom" - class={cls( - handleClass, - 'handle bottom', - 'cursor-ns-resize select-none', - 'range absolute', - 'z-10', - classes.handle, - handle?.class - )} + class={cls('lc-brush-handle', classes.handle, handle?.class)} onpointerdown={adjustBottom} ondblclick={(e) => { e.stopPropagation(); @@ -564,14 +541,7 @@ style:width="{handleSize}px" style:height="{_range.height}px" data-position="left" - class={cls( - handleClass, - 'cursor-ew-resize select-none', - 'range absolute', - 'z-10', - classes.handle, - handle?.class - )} + class={cls('lc-brush-handle', classes.handle, handle?.class)} onpointerdown={adjustLeft} ondblclick={(e) => { e.stopPropagation(); @@ -589,14 +559,7 @@ style:width="{handleSize}px" style:height="{_range.height}px" data-position="right" - class={cls( - handleClass, - 'cursor-ew-resize select-none', - 'range absolute', - 'z-10', - classes.handle, - handle?.class - )} + class={cls('lc-brush-handle', classes.handle, handle?.class)} onpointerdown={adjustRight} ondblclick={(e) => { e.stopPropagation(); @@ -610,3 +573,40 @@ {/if}
{/if} + + diff --git a/packages/layerchart/src/lib/components/Calendar.svelte b/packages/layerchart/src/lib/components/Calendar.svelte index f67e127f2..a67197bc5 100644 --- a/packages/layerchart/src/lib/components/Calendar.svelte +++ b/packages/layerchart/src/lib/components/Calendar.svelte @@ -107,7 +107,7 @@ yearDays.map((date) => { const cellData = dataByDate.get(date) ?? { date }; return { - x: timeWeek.count(timeYear(date), date) * cellSize[0], + x: timeWeek.count(start, date) * cellSize[0], y: date.getDay() * cellSize[1], color: ctx.config.c ? ctx.cGet(cellData) : 'transparent', data: cellData, @@ -128,24 +128,46 @@ fill={cell.color} onpointermove={(e) => tooltip?.show(e, cell.data)} onpointerleave={(e) => tooltip?.hide()} - {...extractLayerProps(restProps, 'calendar-cell', 'stroke-surface-content/5')} + 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 ffc617384..cce083856 100644 --- a/packages/layerchart/src/lib/components/Chart.svelte +++ b/packages/layerchart/src/lib/components/Chart.svelte @@ -1,12 +1,14 @@
(styles = _styles)} >
{@render children?.({ styles })} + + diff --git a/packages/layerchart/src/lib/components/Connector.svelte b/packages/layerchart/src/lib/components/Connector.svelte index 9d47fc1b5..8c3dc2b2a 100644 --- a/packages/layerchart/src/lib/components/Connector.svelte +++ b/packages/layerchart/src/lib/components/Connector.svelte @@ -141,7 +141,7 @@ marker-start={markerStartId ? `url(#${markerStartId})` : undefined} marker-mid={markerMidId ? `url(#${markerMidId})` : undefined} marker-end={markerEndId ? `url(#${markerEndId})` : undefined} - {...extractLayerProps(restProps, 'connector')} + {...extractLayerProps(restProps, 'lc-connector')} {...restProps} /> diff --git a/packages/layerchart/src/lib/components/Ellipse.svelte b/packages/layerchart/src/lib/components/Ellipse.svelte index 3bde17ec2..e3947e2ea 100644 --- a/packages/layerchart/src/lib/components/Ellipse.svelte +++ b/packages/layerchart/src/lib/components/Ellipse.svelte @@ -82,7 +82,6 @@ import { renderEllipse, type ComputedStylesOptions } from '$lib/utils/canvas.js'; import type { SVGAttributes } from 'svelte/elements'; import { createKey } from '$lib/utils/key.svelte.js'; - import { layerClass } from '$lib/utils/attributes.js'; let { cx = 0, @@ -133,7 +132,7 @@ ? merge({ styles: { strokeWidth } }, styleOverrides) : { styles: { fill, fillOpacity, stroke, strokeWidth, opacity }, - classes: className, + classes: cls('lc-ellipse', className), } ); } @@ -181,7 +180,49 @@ {stroke} stroke-width={strokeWidth} {opacity} - class={cls(layerClass('ellipse'), fill == null && 'fill-surface-content', className)} + class={cls('lc-ellipse', className)} {...restProps} /> +{:else if renderCtx === 'html'} +
{/if} + + diff --git a/packages/layerchart/src/lib/components/ForceSimulation.svelte b/packages/layerchart/src/lib/components/ForceSimulation.svelte index 8a53bb0ee..1aded4a0a 100644 --- a/packages/layerchart/src/lib/components/ForceSimulation.svelte +++ b/packages/layerchart/src/lib/components/ForceSimulation.svelte @@ -31,7 +31,7 @@ > = { alpha: number; alphaTarget: number; - simulation: SimulationFor; + simulation: Simulation; }; export type OnTickEvent< @@ -40,9 +40,9 @@ > = { alpha: number; alphaTarget: number; - nodes: NodeDatumFor[]; - links: LinkDatumFor[]; - simulation: SimulationFor; + nodes: NodeDatum[]; + links: LinkDatum[]; + simulation: Simulation; }; export type OnEndEvent< @@ -51,7 +51,18 @@ > = { alpha: number; alphaTarget: number; - simulation: SimulationFor; + simulation: Simulation; + }; + + export type OnNodesChangeEvent< + NodeDatum extends SimulationNodeDatum, + LinkDatum extends SimulationLinkDatum | undefined, + > = { + alpha: number; + alphaTarget: number; + nodes: NodeDatum[]; + links: LinkDatum[]; + simulation: Simulation; }; /** @@ -81,16 +92,6 @@ */ export const DEFAULT_VELOCITY_DECAY: number = 0.4; - type NodeDatumFor = NodeDatum & SimulationNodeDatum; - - type LinkDatumFor = LinkDatum & - SimulationLinkDatum>; - - type SimulationFor = Simulation< - NodeDatumFor, - LinkDatumFor - >; - export type ForceSimulationProps< NodeDatum extends SimulationNodeDatum, LinkDatum extends SimulationLinkDatum | undefined, @@ -159,6 +160,11 @@ */ onStart?: (e: OnStartEvent) => void; + /** + * Callback function triggered right before nodes get passed to the simulation + */ + onNodesChange?: (e: OnNodesChangeEvent) => void; + /** * Callback function triggered on each simulation tick */ @@ -172,10 +178,10 @@ children?: Snippet< [ { - nodes: NodeDatumFor[]; - links: LinkDatumFor[]; + nodes: NodeDatum[]; + links: LinkDatum[]; linkPositions: LinkPosition[]; - simulation: SimulationFor; + simulation: Simulation; }, ] >; @@ -200,6 +206,7 @@ stopped = false, static: staticProp, onStart: onStartProp, + onNodesChange: onNodesChangeProp, onTick: onTickProp, onEnd: onEndProp, children, @@ -211,15 +218,13 @@ // MARK: Private Props let linkPositions: LinkPosition[] = $state([]); - let simulatedNodes: NodeDatumFor[] = $state([]); - let simulatedLinks: LinkDatumFor[] = $derived( - (data.links ?? []) as LinkDatumFor[] - ); + let simulatedNodes: NodeDatum[] = $state([]); + let simulatedLinks: LinkDatum[] = $derived(data.links ?? []); // This casting is unfortunately necessary, due to unfortunate // overloading choices made, over at `@typed/d3-force`: - const simulation: SimulationFor = ( - forceSimulation() as SimulationFor + const simulation: Simulation = ( + forceSimulation() as Simulation ).stop(); // d3.Simulation does not provide a `.forces()` getter, so we need to @@ -263,6 +268,7 @@ () => { // Any time the `nodes` prop, or the `data` store gets changed // we pass them to the internal d3 simulation object: + onNodesChange(); pushNodesToSimulation(data.nodes); runOrResumeSimulation(); } @@ -498,6 +504,16 @@ }); } + function onNodesChange() { + onNodesChangeProp?.({ + alpha, + alphaTarget, + nodes: data.nodes, + links: data.links ?? [], + simulation, + }); + } + $effect(() => { return () => { simulation.stop(); diff --git a/packages/layerchart/src/lib/components/Frame.svelte b/packages/layerchart/src/lib/components/Frame.svelte index 01cc160f4..112ffa0ce 100644 --- a/packages/layerchart/src/lib/components/Frame.svelte +++ b/packages/layerchart/src/lib/components/Frame.svelte @@ -37,5 +37,5 @@ width={ctx.width + (full ? (ctx.padding?.left ?? 0) + (ctx.padding?.right ?? 0) : 0)} height={ctx.height + (full ? (ctx.padding?.top ?? 0) + (ctx.padding?.bottom ?? 0) : 0)} bind:ref - {...extractLayerProps(restProps, 'frame')} + {...extractLayerProps(restProps, 'lc-frame')} /> diff --git a/packages/layerchart/src/lib/components/GeoCircle.svelte b/packages/layerchart/src/lib/components/GeoCircle.svelte index 42a8afe7f..6b4cf7b5a 100644 --- a/packages/layerchart/src/lib/components/GeoCircle.svelte +++ b/packages/layerchart/src/lib/components/GeoCircle.svelte @@ -34,4 +34,4 @@ const geojson = $derived(geoCircle().radius(radius).center(center).precision(precision)()); - + diff --git a/packages/layerchart/src/lib/components/GeoEdgeFade.svelte b/packages/layerchart/src/lib/components/GeoEdgeFade.svelte index 8a5e049ff..ea1c848ab 100644 --- a/packages/layerchart/src/lib/components/GeoEdgeFade.svelte +++ b/packages/layerchart/src/lib/components/GeoEdgeFade.svelte @@ -56,6 +56,6 @@ const opacity = $derived(opacityProp ?? clamper(fade(distance))); - + {@render children?.()} diff --git a/packages/layerchart/src/lib/components/GeoPath.svelte b/packages/layerchart/src/lib/components/GeoPath.svelte index 11003b66f..9df491200 100644 --- a/packages/layerchart/src/lib/components/GeoPath.svelte +++ b/packages/layerchart/src/lib/components/GeoPath.svelte @@ -66,7 +66,6 @@ - + {#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 64e004c91..eec7034be 100644 --- a/packages/layerchart/src/lib/components/Grid.svelte +++ b/packages/layerchart/src/lib/components/Grid.svelte @@ -98,8 +98,8 @@ import Rule from './Rule.svelte'; import Spline from './Spline.svelte'; import { getChartContext } from './Chart.svelte'; - import { extractLayerProps, layerClass } from '$lib/utils/attributes.js'; - import { resolveTickVals, type TicksConfig } from '$lib/utils/ticks.js'; + import { extractLayerProps } from '$lib/utils/attributes.js'; + import { autoTickVals, type TicksConfig } from '$lib/utils/ticks.js'; const ctx = getChartContext(); @@ -131,8 +131,8 @@ const transitionIn = $derived((transitionInProp ?? tweenConfig?.options) ? fade : () => ({})); - const xTickVals = $derived(resolveTickVals(ctx.xScale, xTicks)); - const yTickVals = $derived(resolveTickVals(ctx.yScale, yTicks)); + const xTickVals = $derived(autoTickVals(ctx.xScale, xTicks)); + const yTickVals = $derived(autoTickVals(ctx.yScale, yTicks)); const xBandOffset = $derived( isScaleBand(ctx.xScale) @@ -151,11 +151,11 @@ ); - + {#if x} - {@const splineProps = extractLayerProps(x, 'grid-x-line')} + {@const splineProps = extractLayerProps(x, 'lc-grid-x-line')} - + {#each xTickVals as x (x)} {#if ctx.radial} {@const [x1, y1] = pointRadial(ctx.xScale(x), ctx.yRange[0])} @@ -167,12 +167,7 @@ {y2} motion={tweenConfig} {...splineProps} - class={cls( - layerClass('grid-x-radial-line'), - 'stroke-surface-content/10', - classes.line, - splineProps?.class - )} + class={cls('lc-grid-x-radial-line', classes.line, splineProps?.class)} /> {:else} {/if} {/each} {#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 = extractLayerProps(y, 'grid-y-line')} - + {@const splineProps = extractLayerProps(y, 'lc-grid-y-line')} + {#each yTickVals as y (y)} {#if ctx.radial} {#if radialY === 'circle'} @@ -218,12 +204,7 @@ r={ctx.yScale(y) + yBandOffset} {motion} {...splineProps} - class={cls( - layerClass('grid-y-radial-circle'), - 'fill-none stroke-surface-content/10', - classes.line, - splineProps?.class - )} + class={cls('lc-grid-y-radial-circle', classes.line, splineProps?.class)} /> {:else} {/if} {:else} - {/if} {/each} @@ -264,28 +237,52 @@ r={ctx.yScale(yTickVals[yTickVals.length - 1])! + ctx.yScale.step() + yBandOffset} {motion} {...splineProps} - class={cls( - layerClass('grid-y-radial-circle'), - 'fill-none stroke-surface-content/10', - classes.line, - splineProps?.class - )} + class={cls('lc-grid-y-radial-circle', classes.line, splineProps?.class)} /> {:else} - {/if} {/if} {/if} + + diff --git a/packages/layerchart/src/lib/components/Group.svelte b/packages/layerchart/src/lib/components/Group.svelte index 8d756fd7f..cbfbdaaa8 100644 --- a/packages/layerchart/src/lib/components/Group.svelte +++ b/packages/layerchart/src/lib/components/Group.svelte @@ -83,13 +83,10 @@ import { fade } from 'svelte/transition'; import { cubicIn } from 'svelte/easing'; - import { cls } from '@layerstack/tailwind'; - import { getRenderContext } from './Chart.svelte'; import { registerCanvasComponent } from './layout/Canvas.svelte'; import { getChartContext } from './Chart.svelte'; - import { layerClass } from '$lib/utils/attributes.js'; const ctx = getChartContext(); @@ -178,7 +175,7 @@ {:else if renderCtx === 'svg'} {@render children?.()} -{:else} +{:else if renderCtx === 'html'}
{@render children?.()}
{/if} + + diff --git a/packages/layerchart/src/lib/components/Highlight.svelte b/packages/layerchart/src/lib/components/Highlight.svelte index a576b067c..00551c312 100644 --- a/packages/layerchart/src/lib/components/Highlight.svelte +++ b/packages/layerchart/src/lib/components/Highlight.svelte @@ -101,6 +101,11 @@ */ motion?: MotionProp; + /** + * The opacity of the element. (0 to 1) + */ + opacity?: number; + onAreaClick?: (e: MouseEvent, detail: { data: any }) => void; onBarClick?: (e: MouseEvent, detail: { data: any }) => void; @@ -114,9 +119,8 @@ import { max, min } from 'd3-array'; import { pointRadial, type Series, type SeriesPoint } from 'd3-shape'; import { notNull } from '@layerstack/utils'; - import { cls } from '@layerstack/tailwind'; - import { isScaleBand } from '$lib/utils/scales.svelte.js'; + import { isScaleBand, isScaleTime } from '$lib/utils/scales.svelte.js'; import { asAny } from '$lib/utils/types.js'; import { getChartContext } from './Chart.svelte'; import { getTooltipContext } from './tooltip/TooltipContext.svelte'; @@ -136,6 +140,7 @@ lines: linesProp = false, area = false, bar = false, + opacity, motion = 'spring', onAreaClick, onBarClick, @@ -158,7 +163,9 @@ Array.isArray(yValue) ? yValue.map((v) => ctx.yScale(v)) : ctx.yScale(yValue) ); const yOffset = $derived(isScaleBand(ctx.yScale) && !ctx.radial ? ctx.yScale.bandwidth() / 2 : 0); - const axis = $derived(axisProp == null ? (isScaleBand(ctx.yScale) ? 'y' : 'x') : axisProp); + const axis = $derived( + axisProp == null ? (isScaleBand(ctx.yScale) || isScaleTime(ctx.yScale) ? 'y' : 'x') : axisProp + ); const _lines: { x1: number; y1: number; x2: number; y2: number }[] = $derived.by(() => { let tmpLines: { x1: number; y1: number; x2: number; y2: number }[] = []; @@ -249,6 +256,7 @@ height: 0, }; if (!highlightData) return tmpArea; + if (axis === 'x' || axis === 'both') { // x area if (Array.isArray(xCoord)) { @@ -284,9 +292,9 @@ tmpArea.height = ctx.yScale.step(); } else { // Find width to next data point - const index = ctx.flatData.findIndex((d) => Number(x(d)) === Number(x(highlightData))); + const index = ctx.flatData.findIndex((d) => Number(y(d)) === Number(y(highlightData))); const isLastPoint = index + 1 === ctx.flatData.length; - const nextDataPoint = isLastPoint ? max(ctx.yDomain) : x(ctx.flatData[index + 1]); + const nextDataPoint = isLastPoint ? max(ctx.yDomain) : y(ctx.flatData[index + 1]); tmpArea.height = (ctx.yScale(nextDataPoint) ?? 0) - (yCoord ?? 0); } @@ -432,11 +440,6 @@ return tmpPoints; } ); - - const areaProps = $derived(extractLayerProps(area, 'highlight-area')); - const barProps = $derived(extractLayerProps(bar, 'highlight-bar')); - const linesProps = $derived(extractLayerProps(linesProp, 'highlight-line')); - const pointsProps = $derived(extractLayerProps(points, 'highlight-point')); {#if highlightData} @@ -451,15 +454,16 @@ endAngle={_area.x + _area.width} innerRadius={_area.y} outerRadius={_area.y + _area.height} - class={cls(!areaProps.fill && 'fill-surface-content/5', areaProps.class)} + {opacity} + class="lc-highlight-area" onclick={onAreaClick && ((e) => onAreaClick(e, { data: highlightData }))} /> {:else} onAreaClick(e, { data: highlightData }))} /> {/if} @@ -472,8 +476,8 @@ onBarClick(e, { data: highlightData }))} /> {/if} @@ -490,11 +494,8 @@ y1={line.y1} x2={line.x2} y2={line.y2} - {...linesProps} - class={cls( - 'stroke-surface-content/20 stroke-2 [stroke-dasharray:2,2] pointer-events-none', - linesProps.class - )} + {opacity} + {...extractLayerProps(linesProp, 'lc-highlight-line')} /> {/each} {/if} @@ -512,12 +513,8 @@ fill={point.fill} r={4} strokeWidth={6} - {...pointsProps} - class={cls( - 'stroke-white [paint-order:stroke] drop-shadow-sm', - !point.fill && (typeof points === 'boolean' || !points.fill) && 'fill-primary', - pointsProps.class - )} + {opacity} + {...extractLayerProps(points, 'lc-highlight-point')} onpointerdown={onPointClick && ((e) => { // Do not propagate `pointerdown` event to `BrushContext` if `onclick` is provided @@ -543,3 +540,33 @@ {/if} {/if} {/if} + + diff --git a/packages/layerchart/src/lib/components/Hull.svelte b/packages/layerchart/src/lib/components/Hull.svelte index 48166604d..516a21247 100644 --- a/packages/layerchart/src/lib/components/Hull.svelte +++ b/packages/layerchart/src/lib/components/Hull.svelte @@ -65,7 +65,6 @@ import Spline from './Spline.svelte'; import { getChartContext } from './Chart.svelte'; import { getGeoContext } from './GeoContext.svelte'; - import { layerClass } from '$lib/utils/attributes.js'; let { data, @@ -104,13 +103,13 @@ ); - + {#if geoCtx.projection} {@const polygon = geoVoronoi().hull(points)} onclick?.(e, { points, polygon })} onpointermove={(e) => onpointermove?.(e, { points, polygon })} {onpointerleave} @@ -123,10 +122,18 @@ x={(d) => d[0]} y={(d) => d[1]} {curve} - class={cls(layerClass('hull-class'), '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 3f31ca58f..404ae1b44 100644 --- a/packages/layerchart/src/lib/components/Labels.svelte +++ b/packages/layerchart/src/lib/components/Labels.svelte @@ -74,7 +74,7 @@ import { isScaleBand } from '$lib/utils/scales.svelte.js'; import { getChartContext } from './Chart.svelte'; import Group from './Group.svelte'; - import { extractLayerProps, layerClass } from '$lib/utils/attributes.js'; + import { extractLayerProps } from '$lib/utils/attributes.js'; const ctx = getChartContext(); @@ -174,28 +174,38 @@ } - + {#snippet children({ points })} {#each points as point, i (key(point.data, i))} - {@const textProps = extractLayerProps(getTextProps(point), 'labels-text')} + {@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 35fcd1883..998b78360 100644 --- a/packages/layerchart/src/lib/components/Legend.svelte +++ b/packages/layerchart/src/lib/components/Legend.svelte @@ -65,10 +65,6 @@ */ orientation?: 'horizontal' | 'vertical'; - onclick?: (e: MouseEvent, detail: LegendItem) => any; - onpointerenter?: (e: MouseEvent, detail: LegendItem) => any; - onpointerleave?: (e: MouseEvent, detail: LegendItem) => any; - /** * Determine display ramp (individual color swatches or continuous ramp) * @@ -76,6 +72,11 @@ */ variant?: 'ramp' | 'swatches'; + /** + * An array of selected items. If provided, the legend fades unselected items. + */ + selected?: string[]; + /** * Classes to apply to the elements. * @@ -91,6 +92,10 @@ item?: string | ((item: LegendItem) => string); }; + onclick?: (e: MouseEvent, detail: LegendItem) => any; + onpointerenter?: (e: MouseEvent, detail: LegendItem) => any; + onpointerleave?: (e: MouseEvent, detail: LegendItem) => any; + /** * A bindable reference to the wrapping `
` element. * @@ -116,7 +121,8 @@ import { cls } from '@layerstack/tailwind'; import type { AnyScale } from '$lib/utils/scales.svelte.js'; import { getChartContext } from './Chart.svelte'; - import { extractLayerProps, layerClass } from '$lib/utils/attributes.js'; + import { extractLayerProps } from '$lib/utils/attributes.js'; + import { resolveMaybeFn } from '$lib/utils/common.js'; let { scale: scaleProp, @@ -134,6 +140,7 @@ onpointerenter, onpointerleave, variant = 'ramp', + selected = [], classes = {}, ref: refProp = $bindable(), class: className, @@ -296,29 +303,9 @@ bind:this={ref} {...restProps} data-placement={placement} - class={cls( - layerClass('legend-container'), - 'inline-block', - 'z-1', // stack above tooltip context layers (band rects, voronoi, ...) - placement && [ - 'absolute', - { - 'top-left': 'top-0 left-0', - top: 'top-0 left-1/2 -translate-x-1/2', - 'top-right': 'top-0 right-0', - left: 'top-1/2 left-0 -translate-y-1/2', - center: 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2', - right: 'top-1/2 right-0 -translate-y-1/2', - 'bottom-left': 'bottom-0 left-0', - bottom: 'bottom-0 left-1/2 -translate-x-1/2', - 'bottom-right': 'bottom-0 right-0', - }[placement], - ], - className, - classes.root - )} + class={cls('lc-legend-container', className, classes.root)} > -
+
{title}
{#if children} @@ -331,35 +318,31 @@ {width} height={height + tickLengthProp + tickFontSize} viewBox="0 0 {width} {height + tickLengthProp + tickFontSize}" - class={cls(layerClass('legend-ramp-svg'), 'overflow-visible')} + class={cls('lc-legend-ramp-svg')} > - + {#if scaleConfig.interpolator} {:else if scaleConfig.swatches} {#each scaleConfig.swatches as swatch, i} - + {/each} {/if} - + {#each tickValuesProp ?? scaleConfig.xScale?.ticks?.(ticks) ?? [] as tick, i} {tickFormatProp ? format(tick, asAny(tickFormatProp)) : tick} @@ -371,50 +354,26 @@ y1={0} x2={scaleConfig.xScale?.(tick)} y2={height + tickLengthProp} - class={cls(layerClass('legend-tick-line'), 'stroke-surface-content', classes.tick)} + class={cls('lc-legend-tick-line', classes.tick)} /> {/if} {/each} {:else if variant === 'swatches'} -
+
{#each scaleConfig.tickValues ?? scaleConfig.xScale?.ticks?.(ticks) ?? [] as tick} {@const color = scale?.(tick) ?? ''} {@const item = { value: tick, color }}
+ + diff --git a/packages/layerchart/src/lib/components/Line.svelte b/packages/layerchart/src/lib/components/Line.svelte index 57bf344b2..66c185915 100644 --- a/packages/layerchart/src/lib/components/Line.svelte +++ b/packages/layerchart/src/lib/components/Line.svelte @@ -4,6 +4,7 @@ import { renderPathData, type ComputedStylesOptions } from '$lib/utils/canvas.js'; import MarkerWrapper, { type MarkerOptions } from './MarkerWrapper.svelte'; import type { CommonStyleProps, Without } from '$lib/utils/types.js'; + import { pointsToAngleAndLength } from '$lib/utils/math.js'; export type LinePropsWithoutHTML = { /** @@ -98,7 +99,6 @@ import { createKey } from '$lib/utils/key.svelte.js'; import { createId } from '$lib/utils/createId.js'; - import { layerClass } from '$lib/utils/attributes.js'; const uid = $props.id(); @@ -148,7 +148,7 @@ ? merge({ styles: { strokeWidth } }, styleOverrides) : { styles: { fill, stroke, strokeWidth, opacity }, - classes: className, + classes: cls('lc-line', className), } ); } @@ -195,10 +195,47 @@ marker-start={markerStartId ? `url(#${markerStartId})` : undefined} marker-mid={markerMidId ? `url(#${markerMidId})` : undefined} marker-end={markerEndId ? `url(#${markerEndId})` : undefined} - class={cls(layerClass('line'), stroke === undefined && 'stroke-surface-content', className)} + class={cls('lc-line', className)} {...restProps} /> +{: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 f3b9f2fba..1e1068a05 100644 --- a/packages/layerchart/src/lib/components/LinearGradient.svelte +++ b/packages/layerchart/src/lib/components/LinearGradient.svelte @@ -81,7 +81,7 @@ import { createLinearGradient, getComputedStyles } from '../utils/canvas.js'; import { parsePercent } from '../utils/math.js'; import { createId } from '$lib/utils/createId.js'; - import { extractLayerProps, layerClass } from '$lib/utils/attributes.js'; + import { extractLayerProps } from '$lib/utils/attributes.js'; import { cls } from '@layerstack/tailwind'; const uid = $props.id(); @@ -113,6 +113,35 @@ let canvasGradient = $state(); + function createCSSGradient(): string { + if (!stops?.length) return ''; + + let direction: string; + if (rotate !== undefined) { + // Convert SVG rotation to CSS linear-gradient angle + // SVG: rotate(0) on horizontal gradient = left-to-right = CSS 90deg + // SVG: rotate(0) on vertical gradient = top-to-bottom = CSS 180deg + const baseAngle = vertical ? 180 : 90; + const cssAngle = baseAngle + rotate; + direction = `${cssAngle}deg`; + } else { + // Use direction keywords when no rotation is specified + direction = vertical ? 'to bottom' : 'to right'; + } + + const cssStops = stops + .map((stop, i) => { + if (Array.isArray(stop)) { + return `${stop[1]} ${stop[0]}`; + } else { + return `${stop} ${i * (100 / (stops.length - 1))}%`; + } + }) + .join(', '); + + return `linear-gradient(${direction}, ${cssStops})`; + } + function render(_ctx: CanvasRenderingContext2D) { // Use `getComputedStyles()` to convert each stop (if using CSS variables and/or classes) to color values const _stops = stops.map((stop, i) => { @@ -170,7 +199,7 @@ {y2} gradientTransform={rotate ? `rotate(${rotate})` : ''} gradientUnits={units} - {...extractLayerProps(restProps, 'linear-gradient')} + {...extractLayerProps(restProps, 'lc-linear-gradient')} > {#if stopsContent} {@render stopsContent?.()} @@ -180,13 +209,13 @@ {:else} {/if} {/each} @@ -195,4 +224,6 @@ {@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 7ce96773d..3b88987d0 100644 --- a/packages/layerchart/src/lib/components/Link.svelte +++ b/packages/layerchart/src/lib/components/Link.svelte @@ -173,5 +173,5 @@ TODO: {type} {curve} {sweep} - {...extractLayerProps(restProps, 'link')} + {...extractLayerProps(restProps, 'lc-link')} /> diff --git a/packages/layerchart/src/lib/components/Marker.svelte b/packages/layerchart/src/lib/components/Marker.svelte index d3f356eef..2efb45266 100644 --- a/packages/layerchart/src/lib/components/Marker.svelte +++ b/packages/layerchart/src/lib/components/Marker.svelte @@ -73,7 +73,6 @@ {#if children} {@render children({ points })} {:else} - {#if links} - {#each _links as link} - - {/each} - {/if} - {#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 index b1a65706a..24b5dd159 100644 --- a/packages/layerchart/src/lib/components/Polygon.svelte +++ b/packages/layerchart/src/lib/components/Polygon.svelte @@ -144,7 +144,6 @@ import { registerCanvasComponent } from './layout/Canvas.svelte'; import { renderPathData, type ComputedStylesOptions } from '$lib/utils/canvas.js'; import { createKey } from '$lib/utils/key.svelte.js'; - import { layerClass } from '$lib/utils/attributes.js'; import { polygon } from '$lib/utils/shape.js'; import { roundedPolygonPath } from '$lib/utils/path.js'; @@ -234,7 +233,7 @@ ? merge({ styles: { strokeWidth } }, styleOverrides) : { styles: { fill, fillOpacity, stroke, strokeWidth, opacity }, - classes: cls(layerClass('polygon'), fill == null && 'fill-surface-content', className), + classes: cls('lc-polygon', className), } ); } @@ -278,8 +277,33 @@ {stroke} stroke-width={strokeWidth} {opacity} - class={cls(layerClass('polygon'), fill == null && 'fill-surface-content', className)} + class={cls('lc-polygon', className)} {...restProps} bind:this={ref} /> {/if} + + diff --git a/packages/layerchart/src/lib/components/RadialGradient.svelte b/packages/layerchart/src/lib/components/RadialGradient.svelte index b6314b369..371977a57 100644 --- a/packages/layerchart/src/lib/components/RadialGradient.svelte +++ b/packages/layerchart/src/lib/components/RadialGradient.svelte @@ -89,7 +89,7 @@ import { parsePercent } from '../utils/math.js'; import { getChartContext } from './Chart.svelte'; import { createId } from '$lib/utils/createId.js'; - import { extractLayerProps, layerClass } from '$lib/utils/attributes.js'; + import { extractLayerProps } from '$lib/utils/attributes.js'; import { cls } from '@layerstack/tailwind'; const uid = $props.id(); @@ -167,12 +167,12 @@ {spreadMethod} gradientTransform={transform} gradientUnits={units} - {...extractLayerProps({ ...restProps, class: className }, 'radial-gradient')} + {...extractLayerProps({ ...restProps, class: className }, 'lc-radial-gradient')} > {#if stopsContent} {@render stopsContent()} {:else if stops} - {@const stopClass = cls(layerClass('radial-gradient-stop'), className)} + {@const stopClass = cls('lc-radial-gradient-stop', className)} {#each stops as stop, i} {#if Array.isArray(stop)} diff --git a/packages/layerchart/src/lib/components/Rect.svelte b/packages/layerchart/src/lib/components/Rect.svelte index 3615d76ea..88153923e 100644 --- a/packages/layerchart/src/lib/components/Rect.svelte +++ b/packages/layerchart/src/lib/components/Rect.svelte @@ -1,5 +1,5 @@ - - {#if showRule(x, 'x')} - {@const xCoord = - x === true || x === 'left' - ? xRangeMinMax[0] - : x === 'right' - ? xRangeMinMax[1] - : ctx.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 ctx.radial} - {@const [x1, y1] = pointRadial(xCoord, Number(yRangeMinMax[0]))} - {@const [x2, y2] = pointRadial(xCoord, Number(yRangeMinMax[1]))} + // 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; + + 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 + }); + } + } - - {:else} - - {/if} - {/if} + // 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 showRule(y, 'y')} {#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/Spline.svelte b/packages/layerchart/src/lib/components/Spline.svelte index 33953342b..c5fb4cf0e 100644 --- a/packages/layerchart/src/lib/components/Spline.svelte +++ b/packages/layerchart/src/lib/components/Spline.svelte @@ -132,7 +132,6 @@ import { getChartContext } from './Chart.svelte'; import { createKey } from '$lib/utils/key.svelte.js'; import { createId } from '$lib/utils/createId.js'; - import { layerClass } from '$lib/utils/attributes.js'; const ctx = getChartContext(); @@ -274,7 +273,7 @@ ? merge({ styles: { strokeWidth } }, styleOverrides) : { styles: { fill, fillOpacity, stroke, strokeWidth, opacity }, - classes: className, + classes: cls('lc-spline-path', className), } ); } @@ -362,12 +361,7 @@ {#if startContent && startPoint} - + {@render startContent({ point: startPoint, value: { @@ -396,7 +390,7 @@ {/if} {#if endContent && endPoint.current} - + {@render endContent({ point: endPoint.current, value: { @@ -408,3 +402,28 @@ {/if} {/key} {/if} + + diff --git a/packages/layerchart/src/lib/components/Text.svelte b/packages/layerchart/src/lib/components/Text.svelte index b993cc910..fb09924db 100644 --- a/packages/layerchart/src/lib/components/Text.svelte +++ b/packages/layerchart/src/lib/components/Text.svelte @@ -188,7 +188,6 @@ import { getComputedStyles, renderText, type ComputedStylesOptions } from '../utils/canvas.js'; import { createKey } from '$lib/utils/key.svelte.js'; - import { layerClass } from '$lib/utils/attributes.js'; import { degreesToRadians } from '$lib/utils/math.js'; import { createId } from '$lib/utils/createId.js'; @@ -411,7 +410,7 @@ paintOrder: 'stroke', textAnchor, }, - classes: cls(fill === undefined && 'fill-surface-content', className), + classes: cls('lc-text', className), }; const computedStyles = getComputedStyles(ctx.canvas, styles); @@ -473,13 +472,7 @@ {#if renderCtx === 'svg'} - + {#if path} {#key path} @@ -496,14 +489,14 @@ stroke-width={strokeWidth} {opacity} transform={transformProp} - class={cls(layerClass('text'), fill === undefined && 'fill-surface-content', className)} + class={['lc-text', className]} > {wordsByLines.map((line) => line.words.join(' ')).join()} @@ -522,13 +515,13 @@ {stroke} stroke-width={strokeWidth} {opacity} - class={cls(layerClass('text'), fill === undefined && 'fill-surface-content', className)} + class={['lc-text', className]} > {#each wordsByLines as line, index} {line.words.join(' ')} @@ -536,4 +529,57 @@ {/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/TileImage.svelte b/packages/layerchart/src/lib/components/TileImage.svelte index 90d2ab822..62899cdcb 100644 --- a/packages/layerchart/src/lib/components/TileImage.svelte +++ b/packages/layerchart/src/lib/components/TileImage.svelte @@ -135,7 +135,7 @@ y={(y + ty) * scale - 0.5} width={scale + 1} height={scale + 1} - {...extractLayerProps(restProps, 'tile-image-lower')} + {...extractLayerProps(restProps, 'lc-tile-image-lower')} /> {/key} {#if debug} @@ -152,7 +152,7 @@ y={(y + ty) * scale} width={scale} height={scale} - class="stroke-danger/50 fill-none" + class="lc-tile-image-debug-rect" /> {/if} + + diff --git a/packages/layerchart/src/lib/components/TransformContext.svelte b/packages/layerchart/src/lib/components/TransformContext.svelte index fdba70095..d5502c8f2 100644 --- a/packages/layerchart/src/lib/components/TransformContext.svelte +++ b/packages/layerchart/src/lib/components/TransformContext.svelte @@ -216,8 +216,6 @@ import type { Without } from '$lib/utils/types.js'; import { getChartContext } from './Chart.svelte'; import type { Snippet } from 'svelte'; - import { cls } from '@layerstack/tailwind'; - import { layerClass } from '$lib/utils/attributes.js'; import { createControlledMotion, createMotionTracker, @@ -519,9 +517,17 @@ onpointerup={onPointerUp} ondblclick={onDoubleClick} onclickcapture={onClick} - class={cls(layerClass('transform-context'), 'h-full', className)} + 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 594c718a7..74dba7937 100644 --- a/packages/layerchart/src/lib/components/TransformControls.svelte +++ b/packages/layerchart/src/lib/components/TransformControls.svelte @@ -32,20 +32,15 @@ - + {#if geo.projection} {@const polygons = geoVoronoi().polygons(points)} {#each polygons.features as feature} @@ -131,11 +129,7 @@ > onclick?.(e, { data: feature.properties.site.data, feature })} onpointerenter={(e) => onpointerenter?.(e, { data: feature.properties.site.data, feature })} @@ -158,11 +152,7 @@ onclick?.(e, { data: point.data, point })} onpointerenter={(e) => onpointerenter?.(e, { data: point.data, point })} onpointermove={(e) => onpointermove?.(e, { data: point.data, point })} @@ -178,3 +168,12 @@ {/each} {/if} + + diff --git a/packages/layerchart/src/lib/components/charts/ArcChart.svelte b/packages/layerchart/src/lib/components/charts/ArcChart.svelte index d6d3ca755..a23b2b55f 100644 --- a/packages/layerchart/src/lib/components/charts/ArcChart.svelte +++ b/packages/layerchart/src/lib/components/charts/ArcChart.svelte @@ -124,7 +124,6 @@ import { sum } from 'd3-array'; import { format } from '@layerstack/utils'; import { cls } from '@layerstack/tailwind'; - import { SelectionState } from '@layerstack/svelte-state'; import Arc, { type ArcPropsWithoutHTML } from '../Arc.svelte'; import Chart from '../Chart.svelte'; @@ -133,7 +132,13 @@ import Legend from '../Legend.svelte'; import * as Tooltip from '../tooltip/index.js'; - import { accessor, chartDataArray, type Accessor } from '../../utils/common.js'; + import { + accessor, + chartDataArray, + getObjectOrNull, + resolveMaybeFn, + type Accessor, + } from '../../utils/common.js'; import { asAny } from '../../utils/types.js'; import type { SeriesData, @@ -141,7 +146,8 @@ SimplifiedChartPropsObject, SimplifiedChartSnippet, } from './types.js'; - import { HighlightKey } from './utils.svelte.js'; + import { SeriesState } from '$lib/states/series.svelte.js'; + import { createLegendProps } from './utils.svelte.js'; import { setTooltipMetaContext } from '../tooltip/tooltipMetaContext.js'; import { getColorIfDefined } from '$lib/utils/color.js'; @@ -223,63 +229,32 @@ }); }); - const selectedSeries = new SelectionState(); - - const visibleSeries = $derived( - series.filter((s) => selectedSeries.isEmpty() || selectedSeries.isSelected(s.key)) - ); - - const allSeriesData = $derived( - visibleSeries - .flatMap((s) => - s.data?.map((d) => { - return { seriesKey: s.key, ...d }; - }) - ) - .filter((d) => d) as Array - ); + const seriesState = new SeriesState(() => series); const chartData = $derived( - allSeriesData.length ? allSeriesData : chartDataArray(data) + seriesState.allSeriesData.length ? seriesState.allSeriesData : chartDataArray(data) ) as Array; - const seriesColors = $derived(series.map((s) => s.color).filter((d) => d != null)); - - const highlightKey = new HighlightKey(); - const selectedKeys = new SelectionState(); - const visibleData = $derived( chartData.filter((d) => { const dataKey = keyAccessor(d); - return selectedKeys.isEmpty() || selectedKeys.isSelected(dataKey); + return seriesState.selectedKeys.isEmpty() || seriesState.selectedKeys.isSelected(dataKey); }) ); function getLegendProps(): ComponentProps { - return { - tickFormat: (tick) => { - const item = chartData.find((d) => keyAccessor(d) === tick); - return item ? (labelAccessor(item) ?? tick) : tick; - }, - placement: 'bottom', - variant: 'swatches', - onclick: (e, item) => { - selectedKeys.toggle(item.value); - selectedSeries.toggle(item.value); - }, - onpointerenter: (e, item) => (highlightKey.current = item.value), - onpointerleave: (e) => (highlightKey.current = 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), + return createLegendProps({ + seriesState, + props: { + tickFormat: (tick) => { + // Use data label instead of series label + const item = chartData.find((d) => keyAccessor(d) === tick); + return item ? (labelAccessor(item) ?? tick) : tick; + }, + ...props.legend, + ...getObjectOrNull(legend), }, - }; + }); } function getGroupProps(): ComponentProps { @@ -317,6 +292,7 @@ multiSeries && (trackOuterRadius ?? 0) < 0 ? i * (trackOuterRadius ?? 0) : trackOuterRadius, fill: s.color ?? context.cScale?.(context.c(d)), track: { fill: s.color ?? context.cScale?.(context.c(d)), fillOpacity: 0.1 }, + opacity: seriesState.isHighlighted(keyAccessor(d), true) ? 1 : 0.1, tooltipContext: context.tooltip, data: d, onclick: (e) => { @@ -326,12 +302,7 @@ }, ...props.arc, ...s.props, - class: cls( - 'transition-opacity', - highlightKey.current && highlightKey.current !== keyAccessor(d) && 'opacity-50', - props.arc?.class, - s.props?.class - ), + class: cls(props.arc?.class, s.props?.class), }; } @@ -357,7 +328,7 @@ return key; }, get visibleSeries() { - return visibleSeries; + return seriesState.visibleSeries; }, }); @@ -367,22 +338,21 @@ bind:context data={visibleData} x={value} - y={key} {c} cDomain={chartData.map(keyAccessor)} - cRange={seriesColors.length - ? seriesColors + cRange={seriesState.allSeriesColors.length + ? seriesState.allSeriesColors : c !== key ? chartData.map((d) => cAccessor(d)) : [ - 'var(--color-primary)', - 'var(--color-secondary)', - 'var(--color-info)', - 'var(--color-success)', - 'var(--color-warning)', - 'var(--color-danger)', + '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 === true ? 32 : 0 }} + padding={{ bottom: legend ? 32 : 0 }} {...restProps} tooltip={tooltip === false ? false @@ -396,10 +366,10 @@ color: cAccessor, context, series, - visibleSeries, + visibleSeries: seriesState.visibleSeries, visibleData, - highlightKey: highlightKey.current, - setHighlightKey: highlightKey.set, + highlightKey: seriesState.highlightKey.current, + setHighlightKey: seriesState.highlightKey.set, getLegendProps, getGroupProps, getArcProps, @@ -453,8 +423,8 @@ value={valueAccessor(data)} color={context.cScale?.(context.c(data))} {format} - onpointerenter={() => (highlightKey.current = keyAccessor(data))} - onpointerleave={() => (highlightKey.current = null)} + onpointerenter={() => (seriesState.highlightKey.current = keyAccessor(data))} + onpointerleave={() => (seriesState.highlightKey.current = null)} {...props.tooltip?.item} /> diff --git a/packages/layerchart/src/lib/components/charts/AreaChart.svelte b/packages/layerchart/src/lib/components/charts/AreaChart.svelte index 85c2f10f3..61d1e4c9c 100644 --- a/packages/layerchart/src/lib/components/charts/AreaChart.svelte +++ b/packages/layerchart/src/lib/components/charts/AreaChart.svelte @@ -67,7 +67,6 @@ @@ -392,22 +361,23 @@ bind:context data={visibleData} x={value} - y={key} c={key} cDomain={chartData.map(keyAccessor)} - cRange={seriesColors.length - ? seriesColors + cRange={seriesState.allSeriesColors.length + ? seriesState.allSeriesColors : c !== key ? chartData.map((d) => cAccessor(d)) : [ - 'var(--color-primary)', - 'var(--color-secondary)', - 'var(--color-info)', - 'var(--color-success)', - 'var(--color-warning)', - '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 }} + padding={{ + bottom: legend === true || getObjectOrNull(legend)?.placement?.includes('bottom') ? 32 : 0, + }} {...restProps} tooltip={tooltip === false ? false @@ -421,10 +391,10 @@ color: cAccessor, context, series, - visibleSeries, + visibleSeries: seriesState.visibleSeries, visibleData, - highlightKey: highlightKey.current, - setHighlightKey: highlightKey.set, + highlightKey: seriesState.highlightKey.current, + setHighlightKey: seriesState.highlightKey.set, getLegendProps, getGroupProps, }} @@ -445,7 +415,8 @@ {@render marks(snippetProps)} {:else} - {#each visibleSeries as s, seriesIdx (s.key)} + + {#each series as s, seriesIdx (s.key)} {#if typeof pie === 'function'} {@render pie({ ...snippetProps, @@ -497,8 +468,8 @@ value={valueAccessor(data)} color={context.cScale?.(context.c(data))} {format} - onpointerenter={() => (highlightKey.current = keyAccessor(data))} - onpointerleave={() => (highlightKey.current = null)} + onpointerenter={() => (seriesState.highlightKey.current = keyAccessor(data))} + onpointerleave={() => (seriesState.highlightKey.current = null)} {...props.tooltip?.item} /> diff --git a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte b/packages/layerchart/src/lib/components/charts/ScatterChart.svelte index ba275278a..c15a46ed9 100644 --- a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte +++ b/packages/layerchart/src/lib/components/charts/ScatterChart.svelte @@ -44,7 +44,6 @@
{#if color}
{/if} @@ -118,3 +106,28 @@ {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 4466ff77d..b67a3ca2a 100644 --- a/packages/layerchart/src/lib/components/tooltip/TooltipItem.svelte +++ b/packages/layerchart/src/lib/components/tooltip/TooltipItem.svelte @@ -78,7 +78,6 @@ import { format as formatUtil, type FormatType, type FormatConfig } from '@layerstack/utils'; import { cls } from '@layerstack/tailwind'; import type { Snippet } from 'svelte'; - import { layerClass } from '$lib/utils/attributes.js'; let { ref: refProp = $bindable(), @@ -129,37 +128,19 @@
{#if color}
@@ -174,17 +155,8 @@
{#if children} {@render children()} @@ -194,3 +166,39 @@ {/if}
+ + diff --git a/packages/layerchart/src/lib/components/tooltip/TooltipList.svelte b/packages/layerchart/src/lib/components/tooltip/TooltipList.svelte index 2f6223ac2..c0fdf818d 100644 --- a/packages/layerchart/src/lib/components/tooltip/TooltipList.svelte +++ b/packages/layerchart/src/lib/components/tooltip/TooltipList.svelte @@ -1,6 +1,5 @@ -
+
{@render children?.()}
+ + diff --git a/packages/layerchart/src/lib/components/tooltip/TooltipSeparator.svelte b/packages/layerchart/src/lib/components/tooltip/TooltipSeparator.svelte index 3ebe5e38a..ce78ebb65 100644 --- a/packages/layerchart/src/lib/components/tooltip/TooltipSeparator.svelte +++ b/packages/layerchart/src/lib/components/tooltip/TooltipSeparator.svelte @@ -1,6 +1,5 @@ -
+
{@render children?.()}
+ + diff --git a/packages/layerchart/src/lib/docs/Blockquote.svelte b/packages/layerchart/src/lib/docs/Blockquote.svelte index 9cfefe486..00c1a4bd5 100644 --- a/packages/layerchart/src/lib/docs/Blockquote.svelte +++ b/packages/layerchart/src/lib/docs/Blockquote.svelte @@ -1,8 +1,8 @@ @@ -12,6 +12,6 @@ '[&>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 c7ad9c592..5651de976 100644 --- a/packages/layerchart/src/lib/docs/Code.svelte +++ b/packages/layerchart/src/lib/docs/Code.svelte @@ -1,46 +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/Preview.svelte b/packages/layerchart/src/lib/docs/Preview.svelte index 864cebb06..cb361d925 100644 --- a/packages/layerchart/src/lib/docs/Preview.svelte +++ b/packages/layerchart/src/lib/docs/Preview.svelte @@ -1,34 +1,32 @@ -
+
- {@render children?.()} + {@render children()}
{#if code && showCode} -
- +
+
{/if}
{#if code} View data import type { ComponentProps } from 'svelte'; import { Button, Dialog, Toggle, Tooltip } from 'svelte-ux'; - import { mdiGithub } from '@mdi/js'; + import LucideGithub from '~icons/lucide/github.svelte'; import Code from './Code.svelte'; @@ -29,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/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/utils/attributes.ts b/packages/layerchart/src/lib/utils/attributes.ts index 14cfc49a7..b0f696cc4 100644 --- a/packages/layerchart/src/lib/utils/attributes.ts +++ b/packages/layerchart/src/lib/utils/attributes.ts @@ -1,18 +1,5 @@ import { cls } from '@layerstack/tailwind'; - -/** - * Creates a string containing a class name that can be used by - * developers to target a specific layer/element within a LayerChart. - * - * This is a function so that the class names remain consistent and the - * prefix/structure can be changed in the future if needed - * - * @param layerName - the name of the layer to be appended to the generated class name - * @returns a string to be used as a class on an element - */ -export function layerClass(layerName: string) { - return `lc-${layerName}`; -} +import type { ClassValue } from 'svelte/elements'; type ExtractObjectType = T extends object ? (T extends Function ? never : T) : never; type WithClass = T & { class?: string }; @@ -29,25 +16,23 @@ function isObjectWithClass(val: any): val is { class?: string } { * 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 layerName The name of the layer used to apply a layer classname for targeting styling + * @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, - layerName: string, - extraClasses?: string + className: string, + ...extraClasses: ClassValue[] ): WithClass extends never ? DefaultProps : ExtractObjectType> { - const className = layerClass(layerName); - if (isObjectWithClass(props)) { return { ...props, - class: cls(className, props.class ?? '', extraClasses), + class: cls(className, ...extraClasses, props.class), } as WithClass>; } return { - class: cls(className, extraClasses), + class: cls(className, ...extraClasses), } as WithClass extends never ? DefaultProps : ExtractObjectType>; } 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 8b26a2ce3..6983fd93e 100644 --- a/packages/layerchart/src/lib/utils/common.ts +++ b/packages/layerchart/src/lib/utils/common.ts @@ -51,7 +51,7 @@ export function defaultChartPadding( + 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/genData.ts b/packages/layerchart/src/lib/utils/genData.ts index 769818881..87f0177a2 100644 --- a/packages/layerchart/src/lib/utils/genData.ts +++ b/packages/layerchart/src/lib/utils/genData.ts @@ -58,19 +58,22 @@ export function createSeries(options: { }); } -export function createDateSeries(options: { - count?: number; - min: number; - max: number; - keys?: TKey[]; - value?: 'number' | 'integer'; -}) { +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 { @@ -79,7 +82,7 @@ export function createDateSeries(options: { keys.map((key) => { return [ key, - options.value === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max), + valueType === 'integer' ? getRandomInteger(min, max) : getRandomNumber(min, max), ]; }) ), @@ -87,17 +90,20 @@ 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 = timeDay.floor(new Date()); @@ -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), ]; }) ), diff --git a/packages/layerchart/src/lib/utils/graph/dagre.ts b/packages/layerchart/src/lib/utils/graph/dagre.ts index 0b98bcd7c..5b7adf70d 100644 --- a/packages/layerchart/src/lib/utils/graph/dagre.ts +++ b/packages/layerchart/src/lib/utils/graph/dagre.ts @@ -109,14 +109,14 @@ export function dagreGraph( } /** - * Get all upstream predecessors for dagre nodeId + * Get all upstream predecessors ids for dagre nodeId */ export function dagreAncestors( graph: dagre.graphlib.Graph, nodeId: string, maxDepth = Infinity, currentDepth = 0 -): dagre.Node[] { +): string[] { if (currentDepth === maxDepth) { return []; } @@ -124,28 +124,26 @@ export function dagreAncestors( const predecessors = graph.predecessors(nodeId) ?? []; return [ ...predecessors, - // @ts-expect-error: Types from dagre appear incorrect ...predecessors.flatMap((pId) => dagreAncestors(graph, pId, maxDepth, currentDepth + 1)), ]; } /** - * Get all downstream descendants for dagre nodeId + * Get all downstream descendants ids for dagre nodeId */ export function dagreDescendants( graph: dagre.graphlib.Graph, nodeId: string, maxDepth = Infinity, currentDepth = 0 -): dagre.Node[] { +): string[] { if (currentDepth === maxDepth) { return []; } - const predecessors = graph.successors(nodeId) ?? []; + const successors = graph.successors(nodeId) ?? []; return [ - ...predecessors, - // @ts-expect-error: Types from dagre appear incorrect - ...predecessors.flatMap((pId) => dagreDescendants(graph, pId, maxDepth, currentDepth + 1)), + ...successors, + ...successors.flatMap((pId) => dagreDescendants(graph, pId, maxDepth, currentDepth + 1)), ]; } 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 index 570b7a03e..628852e03 100644 --- a/packages/layerchart/src/lib/utils/motion.svelte.ts +++ b/packages/layerchart/src/lib/utils/motion.svelte.ts @@ -162,7 +162,7 @@ function setupTracking( if (options.controlled) return; $effect(() => { - motion.set(getValue()); + motion.set(getValue(), { instant: motion.target == null }); }); } 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 index 916bfbb18..f0be8ee80 100644 --- a/packages/layerchart/src/lib/utils/rect.svelte.ts +++ b/packages/layerchart/src/lib/utils/rect.svelte.ts @@ -116,7 +116,7 @@ export function createDimensionGetter( const width = Math.max(0, ctx.xScale(right) - ctx.xScale(left) - insets.left - insets.right); return { x, y, width, height }; - } else { + } 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; @@ -152,9 +152,82 @@ export function createDimensionGetter( 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 }; } }; @@ -164,6 +237,6 @@ export function createDimensionGetter( * 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[]) { +export function firstValue(value: number | number[] | undefined) { return Array.isArray(value) ? value[0] : value; } diff --git a/packages/layerchart/src/lib/utils/scales.svelte.ts b/packages/layerchart/src/lib/utils/scales.svelte.ts index 89e80711f..598e7eef9 100644 --- a/packages/layerchart/src/lib/utils/scales.svelte.ts +++ b/packages/layerchart/src/lib/utils/scales.svelte.ts @@ -1,5 +1,5 @@ import { unique } from '@layerstack/utils'; -import { scaleBand, type ScaleBand, type ScaleTime } from 'd3-scale'; +import { scaleBand, scaleLinear, scaleTime, type ScaleBand, type ScaleTime } from 'd3-scale'; import { createControlledMotion, type MotionProp, @@ -8,7 +8,7 @@ import { type TweenOptions, } from '$lib/utils/motion.svelte.js'; import { Spring, Tween } from 'svelte/motion'; -import type { Accessor } from './common.js'; +import { accessor, type Accessor } from './common.js'; import type { OnlyObjects } from './types.js'; import type { TimeInterval } from 'd3-time'; @@ -53,6 +53,11 @@ export function isScaleTime(scale: AnyScale): scale is ScaleTime): 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(); @@ -157,6 +162,44 @@ export function createScale( 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) 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/ticks.test.ts b/packages/layerchart/src/lib/utils/ticks.test.ts index ec9512896..d0b33fcc4 100644 --- a/packages/layerchart/src/lib/utils/ticks.test.ts +++ b/packages/layerchart/src/lib/utils/ticks.test.ts @@ -1,22 +1,22 @@ import { describe, it, expect, vi } from 'vitest'; -import { resolveTickVals } from './ticks.js'; +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('resolveTickVals', () => { +describe('autoTickVals', () => { it('returns array ticks directly', () => { const ticks = [1, 2, 3]; const scale = { ticks: mockTicksFn } as any; - expect(resolveTickVals(scale, ticks)).toEqual([1, 2, 3]); + 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(resolveTickVals(scale, fnTicks)).toEqual([4, 5, 6]); + expect(autoTickVals(scale, fnTicks)).toEqual([4, 5, 6]); expect(fnTicks).toHaveBeenCalledWith(scale); }); @@ -24,46 +24,46 @@ describe('resolveTickVals', () => { const interval = { every: vi.fn() } as unknown as TimeInterval; const ticksConfig = { interval }; const scale = { ticks: vi.fn(() => [7, 8, 9]) } as any; - expect(resolveTickVals(scale, ticksConfig)).toEqual([7, 8, 9]); + 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(resolveTickVals(scale, ticksConfig)).toEqual([]); + expect(autoTickVals(scale, ticksConfig)).toEqual([]); }); it('filters band scale domain with number ticks', () => { const scale = { domain: mockDomain, bandwidth: vi.fn() } as any; - expect(resolveTickVals(scale, 2)).toEqual(['a', 'c', 'e']); + 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(resolveTickVals(scale)).toEqual(['a', 'b', 'c', 'd', 'e']); + 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(resolveTickVals(scale, undefined, undefined)).toEqual([1, 2]); + 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(resolveTickVals(scale, 5)).toEqual([10, 20]); + 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(resolveTickVals(scale, 5)).toEqual([]); + expect(autoTickVals(scale, 5)).toEqual([]); }); it('handles null ticks with placement', () => { const scale = { domain: mockDomain, ticks: vi.fn(() => [1, 2, 3]) } as any; - expect(resolveTickVals(scale, null, undefined)).toEqual([1, 2, 3]); + 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 b0dc44284..bf2073a84 100644 --- a/packages/layerchart/src/lib/utils/ticks.ts +++ b/packages/layerchart/src/lib/utils/ticks.ts @@ -122,7 +122,7 @@ export type TicksConfig = | { interval: TimeInterval | null } | null; -export function resolveTickVals(scale: AnyScale, ticks?: TicksConfig, count?: number): any[] { +export function autoTickVals(scale: AnyScale, ticks?: TicksConfig, count?: number): any[] { // Explicit ticks if (Array.isArray(ticks)) return ticks; @@ -152,7 +152,7 @@ export function resolveTickVals(scale: AnyScale, ticks?: TicksConfig, count?: nu return []; } -export function resolveTickFormat(options: { +export function autoTickFormat(options: { scale: AnyScale; ticks?: TicksConfig; count?: number; diff --git a/packages/layerchart/src/lib/utils/types.ts b/packages/layerchart/src/lib/utils/types.ts index 69b0e83ea..dfb41385d 100644 --- a/packages/layerchart/src/lib/utils/types.ts +++ b/packages/layerchart/src/lib/utils/types.ts @@ -1,7 +1,9 @@ +import type { MouseEventHandler, PointerEventHandler } from 'svelte/elements'; +import type { TransitionConfig } from 'svelte/transition'; import type { HierarchyNode } from 'd3-hierarchy'; -import type { AnyScale } from './scales.svelte.js'; import type { SankeyGraph } from 'd3-sankey'; -import type { TransitionConfig } from 'svelte/transition'; + +import type { AnyScale } from './scales.svelte.js'; /** * Useful to workaround Svelte 3/4 markup type issues @@ -98,6 +100,19 @@ export type CommonStyleProps = { 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; diff --git a/packages/layerchart/src/routes/+layout.svelte b/packages/layerchart/src/routes/+layout.svelte index 4b12bf148..00f302d38 100644 --- a/packages/layerchart/src/routes/+layout.svelte +++ b/packages/layerchart/src/routes/+layout.svelte @@ -3,8 +3,6 @@ import posthog from 'posthog-js'; import { watch } from 'runed'; - import { mdiArrowTopRight, mdiDotsVertical, mdiGithub, mdiTwitter } from '@mdi/js'; - import 'prism-themes/themes/prism-vsc-dark-plus.css'; import { AppBar, AppLayout, @@ -20,6 +18,12 @@ import { sortFunc } from '@layerstack/utils'; import { MediaQueryPresets } from '@layerstack/svelte-state'; + import LucideArrowUpRight from '~icons/lucide/arrow-up-right'; + import LucideEllipsisVertical from '~icons/lucide/ellipsis-vertical'; + import LucideGithub from '~icons/lucide/github'; + import CustomBluesky from '~icons/custom-brands/bluesky'; + import CustomDiscord from '~icons/custom-brands/discord'; + import { dev } from '$app/environment'; import { afterNavigate, goto } from '$app/navigation'; import { page } from '$app/state'; @@ -141,7 +145,7 @@
+

Vertical with rotation

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

Horizontal

diff --git a/packages/layerchart/src/routes/docs/components/AnnotationLine/+page.ts b/packages/layerchart/src/routes/docs/components/AnnotationLine/+page.ts index f365ac6b7..2f58fc972 100644 --- a/packages/layerchart/src/routes/docs/components/AnnotationLine/+page.ts +++ b/packages/layerchart/src/routes/docs/components/AnnotationLine/+page.ts @@ -15,7 +15,7 @@ export async function load({ fetch }) { api, source, pageSource, - supportedContexts: ['svg', 'canvas'], + supportedContexts: ['svg', 'canvas', 'html'], related: ['components/AnnotationPoint', 'components/AnnotationRange'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/AnnotationPoint/+page.ts b/packages/layerchart/src/routes/docs/components/AnnotationPoint/+page.ts index 61e8def41..40e902423 100644 --- a/packages/layerchart/src/routes/docs/components/AnnotationPoint/+page.ts +++ b/packages/layerchart/src/routes/docs/components/AnnotationPoint/+page.ts @@ -15,7 +15,7 @@ export async function load({ fetch }) { api, source, pageSource, - supportedContexts: ['svg', 'canvas'], + supportedContexts: ['svg', 'canvas', 'html'], related: ['components/AnnotationLine', 'components/AnnotationRange'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/AnnotationRange/+page.ts b/packages/layerchart/src/routes/docs/components/AnnotationRange/+page.ts index ceeceb0df..3ba45b059 100644 --- a/packages/layerchart/src/routes/docs/components/AnnotationRange/+page.ts +++ b/packages/layerchart/src/routes/docs/components/AnnotationRange/+page.ts @@ -15,7 +15,7 @@ export async function load({ fetch }) { api, source, pageSource, - supportedContexts: ['svg', 'canvas'], + supportedContexts: ['svg', 'canvas', 'html'], related: ['components/AnnotationLine', 'components/AnnotationPoint'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte b/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte index 98dbed674..b726efe71 100644 --- a/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte @@ -634,6 +634,9 @@ xAxis: { format: 'month', tickMarks: false }, yAxis: { ticks: 4, format: (v) => v + '° F' }, highlight: { points: false }, + tooltip: { + context: { mode: 'bisect-x' }, + }, }} series={[ { diff --git a/packages/layerchart/src/routes/docs/components/Axis/+page.svelte b/packages/layerchart/src/routes/docs/components/Axis/+page.svelte index 886e6efa0..405bbeebf 100644 --- a/packages/layerchart/src/routes/docs/components/Axis/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Axis/+page.svelte @@ -1,4 +1,5 @@ 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 index 0ae4cd316..845a912e3 100644 --- a/packages/layerchart/src/routes/docs/components/Ellipse/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Ellipse/+page.svelte @@ -6,20 +6,98 @@

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 index aaa350556..bc49fffce 100644 --- a/packages/layerchart/src/routes/docs/components/Ellipse/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Ellipse/+page.ts @@ -9,7 +9,7 @@ export async function load() { source, pageSource, description: '`` element with tweened properties using `motionStore`', - supportedContexts: ['svg', 'canvas'], + supportedContexts: ['svg', 'canvas', 'html'], }, }; } diff --git a/packages/layerchart/src/routes/docs/components/Grid/+page.svelte b/packages/layerchart/src/routes/docs/components/Grid/+page.svelte index ba5f1055d..5380cb447 100644 --- a/packages/layerchart/src/routes/docs/components/Grid/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Grid/+page.svelte @@ -1,5 +1,4 @@

Examples

@@ -130,6 +213,21 @@
+

Vertical

+ + +
+ +
+
+

Series

@@ -148,6 +246,38 @@
+

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)

@@ -178,6 +308,68 @@
+

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)

@@ -368,7 +560,6 @@ +

Axis labels inside

+ + +
+ +
+
+

Legend

diff --git a/packages/layerchart/src/routes/docs/components/LinearGradient/+page.ts b/packages/layerchart/src/routes/docs/components/LinearGradient/+page.ts index 171c0cfc4..509d4e126 100644 --- a/packages/layerchart/src/routes/docs/components/LinearGradient/+page.ts +++ b/packages/layerchart/src/routes/docs/components/LinearGradient/+page.ts @@ -8,7 +8,7 @@ export async function load() { api, source, pageSource, - supportedContexts: ['svg', 'canvas'], + supportedContexts: ['svg', 'canvas', 'html'], related: ['components/RadialGradient', 'components/Pattern'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/PieChart/+page.svelte b/packages/layerchart/src/routes/docs/components/PieChart/+page.svelte index a899c6273..96e20ef52 100644 --- a/packages/layerchart/src/routes/docs/components/PieChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/PieChart/+page.svelte @@ -180,6 +180,7 @@ innerRadius={-20} cornerRadius={4} padAngle={0.02} + tooltip={false} {renderContext} {debug} > diff --git a/packages/layerchart/src/routes/docs/components/Point/+page.svelte b/packages/layerchart/src/routes/docs/components/Point/+page.svelte index 663f1fd20..f9bafc5e1 100644 --- a/packages/layerchart/src/routes/docs/components/Point/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Point/+page.svelte @@ -16,19 +16,21 @@ 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 88b528d40..c99343062 100644 --- a/packages/layerchart/src/routes/docs/components/Point/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Point/+page.ts @@ -9,7 +9,7 @@ export async function load() { source, pageSource, description: 'Convenient way to translate a data item to SVG x/y coordinates', - supportedContexts: ['svg', 'canvas'], + supportedContexts: ['svg', 'canvas', 'html'], related: ['examples/Area'], }, }; diff --git a/packages/layerchart/src/routes/docs/components/Rect/+page.svelte b/packages/layerchart/src/routes/docs/components/Rect/+page.svelte index ac1cf99bf..2ed03e59d 100644 --- a/packages/layerchart/src/routes/docs/components/Rect/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Rect/+page.svelte @@ -6,15 +6,82 @@

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 48fe70a36..643927949 100644 --- a/packages/layerchart/src/routes/docs/components/Rect/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Rect/+page.ts @@ -9,7 +9,7 @@ export async function load() { source, pageSource, description: '`` element with tweened properties using `motionStore`', - supportedContexts: ['svg', 'canvas'], + supportedContexts: ['svg', 'canvas', 'html'], related: [ 'components/Bars', 'components/Highlight', diff --git a/packages/layerchart/src/routes/docs/components/Rule/+page.svelte b/packages/layerchart/src/routes/docs/components/Rule/+page.svelte index 44c8055d3..85f6bf2a1 100644 --- a/packages/layerchart/src/routes/docs/components/Rule/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Rule/+page.svelte @@ -1,10 +1,16 @@

Examples

@@ -39,7 +45,7 @@ - +
@@ -86,7 +92,6 @@
+ +

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 1730f9ea5..e58e8af90 100644 --- a/packages/layerchart/src/routes/docs/components/Rule/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Rule/+page.ts @@ -1,15 +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, supportedContexts: ['svg', 'canvas'], - related: ['components/Axis', 'components/Line'], + related: ['components/Axis', 'components/Line', 'examples/Candlestick', 'examples/Duration'], }, }; } diff --git a/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte b/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte index b30c2a1ea..1b517322b 100644 --- a/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte @@ -50,10 +50,6 @@
-
- See also: AnnotationRange for more examples -
-

Domain padding

diff --git a/packages/layerchart/src/routes/docs/components/Text/+page.svelte b/packages/layerchart/src/routes/docs/components/Text/+page.svelte index 811600335..98152daef 100644 --- a/packages/layerchart/src/routes/docs/components/Text/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Text/+page.svelte @@ -5,6 +5,7 @@ import { Field, RangeField, Switch, TextField, ToggleGroup, ToggleOption } from 'svelte-ux'; import Preview from 'layerchart/docs/Preview.svelte'; import { shared } from '../../shared.svelte.js'; + import { toTitleCase } from '@layerstack/utils'; const config = $state({ x: 0, @@ -105,44 +106,27 @@
-
-
-

SVG

-
-
- - - - {#if config.showAnchor} - - {/if} - - -
-
-
- -
-

Canvas

-
-
- - - - {#if config.showAnchor} - - {/if} - - +
+ {#each ['svg', 'canvas', 'html'] as const as type} +
+

{toTitleCase(type)}

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

Examples

diff --git a/packages/layerchart/src/routes/docs/components/Text/+page.ts b/packages/layerchart/src/routes/docs/components/Text/+page.ts index 4a31bc978..875f03bb0 100644 --- a/packages/layerchart/src/routes/docs/components/Text/+page.ts +++ b/packages/layerchart/src/routes/docs/components/Text/+page.ts @@ -8,7 +8,7 @@ export async function load() { api, source, pageSource, - supportedContexts: ['svg', 'canvas'], + supportedContexts: ['svg', 'canvas', 'html'], features: [ 'Adjustable anchor/origin point (center horizontally and vertically)', 'Rotate (based on origin)', diff --git a/packages/layerchart/src/routes/docs/components/Tooltip/+page.svelte b/packages/layerchart/src/routes/docs/components/Tooltip/+page.svelte index 5d7506e02..71672c40d 100644 --- a/packages/layerchart/src/routes/docs/components/Tooltip/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Tooltip/+page.svelte @@ -1,6 +1,6 @@

Examples

- +

Basic

+
(d.close < d.open ? 'desc' : 'asc')} - cScale={scaleOrdinal()} cDomain={['desc', 'asc']} - cRange={['#e41a1c', '#4daf4a']} - padding={{ left: 16, bottom: 24 }} + cRange={['var(--color-danger)', 'var(--color-success)']} + padding={{ left: 20, bottom: 32 }} tooltip={{ mode: 'quadtree-x' }} > - {#snippet children({ context })} + + + + + + + + + + {#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' }} + > - - ''} /> - - [d.open, d.close]} radius={2} /> - + + + + + - + + {#snippet children({ data })} @@ -46,7 +95,106 @@ {/snippet} - {/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' }} + > + + + + + + + + + + {#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 ec9188a54..4dde51d0b 100644 --- a/packages/layerchart/src/routes/docs/examples/Candlestick/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Candlestick/+page.ts @@ -11,13 +11,7 @@ export async function load({ fetch }) { meta: { pageSource, supportedContexts: ['svg', 'canvas'], - related: [ - 'components/Bars', - 'components/Points', - 'examples/Bars', - 'examples/Histogram', - 'examples/Sparkbar', - ], + related: ['components/Rule', 'components/Bars'], }, }; } diff --git a/packages/layerchart/src/routes/docs/examples/Columns/+page.svelte b/packages/layerchart/src/routes/docs/examples/Columns/+page.svelte index a633e7078..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, @@ -119,7 +120,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -141,7 +142,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -163,7 +164,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'band' }} > @@ -195,7 +196,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'band' }} > @@ -227,7 +228,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'band' }} > @@ -270,7 +271,7 @@ x="date" xScale={scaleBand().padding(0.4)} y="value" - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -291,7 +292,7 @@ x="date" xScale={scaleBand().padding(0.4)} y="value" - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -313,7 +314,7 @@ xScale={scaleBand().padding(0.4)} y="value" yBaseline={0} - yNice={4} + yNice yPadding={[16, 16]} padding={{ left: 16, bottom: 24 }} > @@ -338,7 +339,7 @@ xScale={scaleBand().padding(0.4)} y="value" yBaseline={0} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -362,7 +363,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -384,7 +385,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -410,7 +411,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -436,7 +437,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -466,7 +467,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -496,7 +497,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -523,7 +524,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > {#snippet children({ context })} @@ -561,7 +562,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -583,7 +584,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -605,7 +606,7 @@ xScale={scaleBand().padding(0.4)} y={['value', 'baseline']} yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'bisect-x' }} > @@ -638,7 +639,7 @@ x="date" xScale={scaleBand().padding(0.4)} y={['value', (d) => -d.baseline]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'bisect-x' }} > @@ -663,6 +664,75 @@
+

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

@@ -680,7 +750,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -721,7 +791,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -763,7 +833,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -807,7 +877,7 @@ xScale={scaleBand().padding(0.4)} y="value" yDomain={[0, null]} - yNice={4} + yNice padding={{ left: 16, bottom: 24 }} > @@ -843,15 +913,13 @@ [0, xScale.bandwidth()]} padding={{ left: 16, bottom: 24 }} @@ -902,11 +970,10 @@ yScale.domain()} padding={{ top: 24, bottom: 24, left: 24, right: 24 }} tooltip={{ mode: 'quadtree-x' }} @@ -247,9 +246,8 @@ padding={{ left: 32, right: 32, bottom: 20 }} props={{ bars: { - // TODO: Determine why non-rounded Rect within Bar is not working for inverted range - // rounded: 'none', - class: 'stroke-none fill-blue-500', + rounded: 'none', + class: '_stroke-none fill-blue-500', }, }} {renderContext} diff --git a/packages/layerchart/src/routes/docs/examples/Duration/+page.svelte b/packages/layerchart/src/routes/docs/examples/Duration/+page.svelte index 820c06050..24da8d308 100644 --- a/packages/layerchart/src/routes/docs/examples/Duration/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Duration/+page.svelte @@ -3,7 +3,7 @@ import { timeMinute, timeDay } from 'd3-time'; import { Duration } from 'svelte-ux'; - import { BarChart, Points, Tooltip } from 'layerchart'; + import { BarChart, Points, Rule, Tooltip } from 'layerchart'; import Preview from '$lib/docs/Preview.svelte'; import { getRandomInteger } from '$lib/utils/genData.js'; @@ -27,6 +27,10 @@ }; }); + function formatYear(number: number): string { + return Math.sign(number) === -1 ? Math.abs(number) + ' BC' : number + ' AD'; + } + let renderContext = $derived(shared.renderContext as 'svg' | 'canvas'); @@ -257,7 +261,8 @@ {renderContext} > {#snippet marks()} - + + {/snippet} {#snippet tooltip({ context })} @@ -316,7 +321,8 @@ {renderContext} > {#snippet marks()} - + + {/snippet} {#snippet tooltip({ context })} @@ -345,3 +351,108 @@
+ +

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 index 34d38b74c..b630c13dd 100644 --- a/packages/layerchart/src/routes/docs/examples/Duration/+page.ts +++ b/packages/layerchart/src/routes/docs/examples/Duration/+page.ts @@ -1,6 +1,8 @@ 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 { @@ -17,6 +19,15 @@ export async function load() { }; }); }), + 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'], diff --git a/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte b/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte index b0a0f6615..1b64810a0 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte @@ -7,7 +7,6 @@ import Preview from '$lib/docs/Preview.svelte'; import { shared } from '../../shared.svelte.js'; - // @ts-expect-error import LucideStar from '~icons/lucide/star'; let { data } = $props(); diff --git a/packages/layerchart/src/routes/docs/examples/Line/+page.svelte b/packages/layerchart/src/routes/docs/examples/Line/+page.svelte index 882a1c934..2621f5265 100644 --- a/packages/layerchart/src/routes/docs/examples/Line/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Line/+page.svelte @@ -1,5 +1,5 @@ + +

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/PunchCard/+page.svelte b/packages/layerchart/src/routes/docs/examples/PunchCard/+page.svelte index 4f8366d81..4a1064252 100644 --- a/packages/layerchart/src/routes/docs/examples/PunchCard/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/PunchCard/+page.svelte @@ -40,6 +40,7 @@ tooltip: { context: { mode: 'band' } }, }} {renderContext} + debug={shared.debug} > {#snippet highlight()} diff --git a/packages/layerchart/src/routes/docs/examples/RadialLine/+page.svelte b/packages/layerchart/src/routes/docs/examples/RadialLine/+page.svelte index 99a20cb39..4d779cb45 100644 --- a/packages/layerchart/src/routes/docs/examples/RadialLine/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/RadialLine/+page.svelte @@ -1,5 +1,5 @@