From bf50ac207f6dd70186185f047ebc6eb233889003 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 11 Apr 2024 11:13:32 -0400 Subject: [PATCH 01/40] Brush WIP --- .../src/lib/components/Brush.svelte | 157 ++++++++++++++++++ .../layerchart/src/routes/_NavMenu.svelte | 1 + .../routes/docs/components/Brush/+page.svelte | 128 ++++++++++++++ .../src/routes/docs/components/Brush/+page.ts | 18 ++ 4 files changed, 304 insertions(+) create mode 100644 packages/layerchart/src/lib/components/Brush.svelte create mode 100644 packages/layerchart/src/routes/docs/components/Brush/+page.svelte create mode 100644 packages/layerchart/src/routes/docs/components/Brush/+page.ts diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte new file mode 100644 index 000000000..272b011b4 --- /dev/null +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -0,0 +1,157 @@ + + +
+ {#if min != null} +
+ +
+
+ {/if} +
diff --git a/packages/layerchart/src/routes/_NavMenu.svelte b/packages/layerchart/src/routes/_NavMenu.svelte index c95ea60c1..5ec180562 100644 --- a/packages/layerchart/src/routes/_NavMenu.svelte +++ b/packages/layerchart/src/routes/_NavMenu.svelte @@ -66,6 +66,7 @@ 'Threshold', ], Interactions: [ + 'Brush', 'Highlight', 'HitCanvas', 'Tooltip', diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte new file mode 100644 index 000000000..44f9c8c24 --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -0,0 +1,128 @@ + + +

Examples

+ +

Clip

+ + +
+
+ + + + + + + + + +
+ +
+ + + + + + { + if (e.detail.xDomain) { + xDomain = e.detail.xDomain; + } + }} + /> + +
+
+
+ +

Filter data

+ + +
+
+ + + + + + + +
+ +
+ + + + + + { + if (e.detail.xDomain) { + xDomain = e.detail.xDomain; + } + }} + /> + +
+
+
diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.ts b/packages/layerchart/src/routes/docs/components/Brush/+page.ts new file mode 100644 index 000000000..e5984947e --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.ts @@ -0,0 +1,18 @@ +import { parse } from 'svelte-ux'; + +import api from '$lib/components/Brush.svelte?raw&sveld'; +import source from '$lib/components/Brush.svelte?raw'; +import pageSource from './+page.svelte?raw'; + +export async function load() { + return { + appleStock: await fetch('/data/examples/date/apple-stock.json').then(async (r) => + parse(await r.text()) + ), + meta: { + api, + source, + pageSource, + }, + }; +} From 91dc0521eddbad085414d47f1a183c91ebe3018e Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 12 Apr 2024 22:23:13 -0400 Subject: [PATCH 02/40] [ChartClipPath] Rename `includePadding` to `full` to match `` --- .changeset/purple-tools-eat.md | 5 +++++ .../src/lib/components/ChartClipPath.svelte | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/purple-tools-eat.md diff --git a/.changeset/purple-tools-eat.md b/.changeset/purple-tools-eat.md new file mode 100644 index 000000000..197227d9b --- /dev/null +++ b/.changeset/purple-tools-eat.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +[ChartClipPath] Remove padding by default (opt-in with `full`) diff --git a/packages/layerchart/src/lib/components/ChartClipPath.svelte b/packages/layerchart/src/lib/components/ChartClipPath.svelte index 319f3c0a6..670908fb8 100644 --- a/packages/layerchart/src/lib/components/ChartClipPath.svelte +++ b/packages/layerchart/src/lib/components/ChartClipPath.svelte @@ -5,15 +5,15 @@ const { width, height, padding } = getContext('LayerCake'); - /** Whether clipping should include chart padding (ex. axis) */ - export let includePadding = false; + /** Include padding area (ex. axis) */ + export let full = false; From 90b889ba31ecb505193ca59c554021429e03e199 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 12 Apr 2024 22:26:35 -0400 Subject: [PATCH 03/40] [Frame] Expose `rectEl` and forward `mousedown` and `touchstart` events --- .changeset/five-phones-tease.md | 5 +++++ packages/layerchart/src/lib/components/Frame.svelte | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/five-phones-tease.md diff --git a/.changeset/five-phones-tease.md b/.changeset/five-phones-tease.md new file mode 100644 index 000000000..4d0110c38 --- /dev/null +++ b/.changeset/five-phones-tease.md @@ -0,0 +1,5 @@ +--- +"layerchart": patch +--- + +[Frame] Expose `rectEl` and forward `mousedown` and `touchstart` events diff --git a/packages/layerchart/src/lib/components/Frame.svelte b/packages/layerchart/src/lib/components/Frame.svelte index 1be6c57ff..8d9bb0dde 100644 --- a/packages/layerchart/src/lib/components/Frame.svelte +++ b/packages/layerchart/src/lib/components/Frame.svelte @@ -5,6 +5,9 @@ /** Include padding area */ export let full = false; + + /** Access underlying `` element */ + export let rectEl: SVGRectElement; From 87756a52f60749e7794ec41ab412e870dd06dd68 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 12 Apr 2024 22:47:24 -0400 Subject: [PATCH 04/40] [Brush] Forward `mousedown` and `touchstart` events --- .changeset/itchy-cameras-guess.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/itchy-cameras-guess.md diff --git a/.changeset/itchy-cameras-guess.md b/.changeset/itchy-cameras-guess.md new file mode 100644 index 000000000..1859769ec --- /dev/null +++ b/.changeset/itchy-cameras-guess.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +[Brush] Forward `mousedown` and `touchstart` events From 077712375b91b081c97f8c4ea068fa9ae13b89e0 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 12 Apr 2024 22:49:33 -0400 Subject: [PATCH 05/40] [Brush] Use SVG/rect instaed of HTML/div --- .../src/lib/components/Brush.svelte | 73 ++++++++++--------- .../routes/docs/components/Brush/+page.svelte | 32 ++++---- 2 files changed, 53 insertions(+), 52 deletions(-) diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte index 272b011b4..c414419e6 100644 --- a/packages/layerchart/src/lib/components/Brush.svelte +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -3,17 +3,21 @@ import { createEventDispatcher, getContext } from 'svelte'; import { clamp, cls } from 'svelte-ux'; + import Frame from './Frame.svelte'; + import Group from './Group.svelte'; let min: number | null = null; let max: number | null = null; - let brushEl: HTMLDivElement; + export let handleWidth = 5; + + let frameEl: SVGRectElement; const dispatch = createEventDispatcher<{ change: { xDomain?: [any, any]; yDomain?: [any, any] }; }>(); - const { xScale, padding } = getContext('LayerCake'); + const { xScale, width, height } = getContext('LayerCake'); export let classes: { root?: string; @@ -25,7 +29,7 @@ * Convert pixel value `x` to percent of element's width */ function pixelToPercent(x: number) { - const { left, right } = brushEl.getBoundingClientRect(); + const { left, right } = frameEl.getBoundingClientRect(); const scale = scaleLinear([left, right], [0, 1]).clamp(true); return scale(x); } @@ -60,7 +64,7 @@ if (e instanceof TouchEvent) { if (e.changedTouches.length !== 1) return; if (e.changedTouches[0].identifier !== startTouch?.identifier) return; - } else if (e.target === brushEl) { + } else if (e.target === frameEl) { clear(); } @@ -110,48 +114,47 @@ // Track last min/max to fix infinite loop let lastExtents: [number | null, number | null] = [null, null]; - $: console.log({ min, max, lastExtents }); $: if ( ((min == null && max == null) || min !== max) && (lastExtents[0] !== min || lastExtents[1] !== max) ) { lastExtents = [min, max]; - console.log('dispatching change'); dispatch('change', { xDomain: [domainScale.invert(min ?? 0), domainScale.invert(max ?? 1)] }); } - $: left = 100 * (min ?? 0); - $: right = 100 * (1 - (max ?? 1)); + $: left = $width * (min ?? 0); + $: right = $width * (max ?? 0); -
- {#if min != null} -
+ + + + + -
+ -
+ + + - {/if} -
+ + diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index 44f9c8c24..cfab3c427 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -24,7 +24,7 @@

Examples

-

Clip

+

Clip data

@@ -61,15 +61,14 @@ > + { + if (e.detail.xDomain) { + xDomain = e.detail.xDomain; + } + }} + /> - - { - if (e.detail.xDomain) { - xDomain = e.detail.xDomain; - } - }} - />
@@ -113,15 +112,14 @@ > + { + if (e.detail.xDomain) { + xDomain = e.detail.xDomain; + } + }} + /> - - { - if (e.detail.xDomain) { - xDomain = e.detail.xDomain; - } - }} - />
From b3c3cb0c404e2b0f72e05a19f72be835bb855202 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 12 Apr 2024 23:06:00 -0400 Subject: [PATCH 06/40] Forward `dblclick` event --- .changeset/five-phones-tease.md | 4 ++-- .changeset/pretty-vans-exist.md | 5 +++++ packages/layerchart/src/lib/components/Frame.svelte | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/pretty-vans-exist.md diff --git a/.changeset/five-phones-tease.md b/.changeset/five-phones-tease.md index 4d0110c38..d09476a1e 100644 --- a/.changeset/five-phones-tease.md +++ b/.changeset/five-phones-tease.md @@ -1,5 +1,5 @@ --- -"layerchart": patch +'layerchart': patch --- -[Frame] Expose `rectEl` and forward `mousedown` and `touchstart` events +[Frame] Expose `rectEl` and forward `mousedown`, `touchstart`, and `dblclick` events diff --git a/.changeset/pretty-vans-exist.md b/.changeset/pretty-vans-exist.md new file mode 100644 index 000000000..0a69ef836 --- /dev/null +++ b/.changeset/pretty-vans-exist.md @@ -0,0 +1,5 @@ +--- +"layerchart": patch +--- + +[Group] Forward `dblclick` event diff --git a/packages/layerchart/src/lib/components/Frame.svelte b/packages/layerchart/src/lib/components/Frame.svelte index 8d9bb0dde..737d7c09c 100644 --- a/packages/layerchart/src/lib/components/Frame.svelte +++ b/packages/layerchart/src/lib/components/Frame.svelte @@ -18,6 +18,7 @@ on:click on:mousedown on:touchstart + on:dblclick bind:this={rectEl} {...$$restProps} /> From 68b1ac86cabfd1bad33428341041d85a6f519fcc Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 12 Apr 2024 23:10:27 -0400 Subject: [PATCH 07/40] [Brush] Support double clicking on frame to select all, double clicking on range to clear. Move classes to Group contrainers --- .../src/lib/components/Brush.svelte | 29 +++++++++++++++---- .../routes/docs/components/Brush/+page.svelte | 28 +++--------------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte index c414419e6..560fd7e89 100644 --- a/packages/layerchart/src/lib/components/Brush.svelte +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -109,6 +109,11 @@ max = null; } + function selectAll() { + min = 0; + max = 1; + } + // Map percentage back to domain $: domainScale = $xScale.copy().range([0, 1]); @@ -131,30 +136,42 @@ class={cls('frame', 'fill-transparent')} on:mousedown={reset} on:touchstart={reset} + on:dblclick={() => selectAll()} bind:rectEl={frameEl} /> - + clear()} + > - + - + diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index cfab3c427..084da8991 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -50,22 +50,12 @@
- + { - if (e.detail.xDomain) { - xDomain = e.detail.xDomain; - } + xDomain = e.detail.xDomain; }} /> @@ -101,22 +91,12 @@
- + { - if (e.detail.xDomain) { - xDomain = e.detail.xDomain; - } + xDomain = e.detail.xDomain; }} /> From 9620b6fce5cce46e9e334f5cb442e735199cfe4f Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sat, 13 Apr 2024 01:15:41 -0400 Subject: [PATCH 08/40] [Brush] Use domain values for min/max instead of percentages to enable initializing selecction and reduce mapping (pixels <-> domain instead of pixels <-> percent <-> domain) --- .../src/lib/components/Brush.svelte | 81 +++++++++--------- .../routes/docs/components/Brush/+page.svelte | 84 ++++++++++++++++++- 2 files changed, 124 insertions(+), 41 deletions(-) diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte index 560fd7e89..35d436f29 100644 --- a/packages/layerchart/src/lib/components/Brush.svelte +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -1,23 +1,23 @@ diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index 084da8991..61532a5c8 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -8,13 +8,13 @@ import Area from '$lib/components/Area.svelte'; import Axis from '$lib/components/Axis.svelte'; import Brush from '$lib/components/Brush.svelte'; - import Points from '$lib/components/Points.svelte'; import ChartClipPath from '$lib/components/ChartClipPath.svelte'; export let data; let xDomain = [null, null]; + let xDomain2 = [new Date('2010-01-01'), new Date('2011-12-31')]; $: filteredData = xDomain[0] != null && xDomain[1] != null @@ -24,6 +24,48 @@

Examples

+

Initialize selection

+ + +
+
+ + + + + + + + + +
+ +
+ + + + { + xDomain2 = e.detail.xDomain; + }} + /> + + +
+
+
+

Clip data

@@ -104,3 +146,43 @@
+ + From 0e386a576cf946d70d9ecf9f85c91a0c6d70f091 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sat, 13 Apr 2024 01:16:05 -0400 Subject: [PATCH 09/40] Cleanup --- .../layerchart/src/routes/docs/components/Brush/+page.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index 61532a5c8..32b804044 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -1,5 +1,4 @@ @@ -156,7 +161,7 @@ width={handleWidth} height={$height} class={cls('fill-transparent cursor-ew-resize select-none')} - on:dblclick={() => (min = xDomainMin)} + on:dblclick={() => (xDomain[0] = xDomainMin)} /> @@ -170,7 +175,7 @@ width={handleWidth} height={$height} class={cls('fill-transparent cursor-ew-resize select-none')} - on:dblclick={() => (max = xDomainMax)} + on:dblclick={() => (xDomain[1] = xDomainMax)} /> diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index 32b804044..733047ea8 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -52,13 +52,7 @@ - { - xDomain2 = e.detail.xDomain; - }} - /> +
From 9cfe1a5e282ec0240d1c49b42a286b6b6d74940e Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 14 Apr 2024 11:12:50 -0400 Subject: [PATCH 14/40] Update Brush examples to use Svelte UX's to localize xDomain variable per example, and use for Area fill --- .../layerchart/src/lib/components/index.ts | 1 + .../routes/docs/components/Brush/+page.svelte | 217 +++++++++--------- 2 files changed, 105 insertions(+), 113 deletions(-) diff --git a/packages/layerchart/src/lib/components/index.ts b/packages/layerchart/src/lib/components/index.ts index 8c930a3c3..5042f5034 100644 --- a/packages/layerchart/src/lib/components/index.ts +++ b/packages/layerchart/src/lib/components/index.ts @@ -8,6 +8,7 @@ export { default as Axis } from './Axis.svelte'; export { default as Bar } from './Bar.svelte'; export { default as Bars } from './Bars.svelte'; export { default as Blur } from './Blur.svelte'; +export { default as Brush } from './Brush.svelte'; export { default as Bounds } from './Bounds.svelte'; export { default as Calendar } from './Calendar.svelte'; export { default as Canvas } from './layout/Canvas.svelte'; diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index 733047ea8..67655179b 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -1,5 +1,6 @@

Examples

-

Initialize selection

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

Clip data

-
- - - - - + +
+ + + + + + + + + + + +
+ +
+ + - - - -
- -
- - - - { - xDomain = e.detail.xDomain; - }} - /> - - -
+ { + set(e.detail.xDomain); + }} + /> + +
+
+
@@ -103,44 +82,58 @@
-
- - - - - - - -
- -
- - - - { - xDomain = e.detail.xDomain; - }} - /> - - -
+ +
+ + (xDomain[0] == null || d.date >= xDomain[0]) && + (xDomain[1] == null || d.date <= xDomain[1]) + )} + x="date" + xScale={scaleTime()} + y="value" + yDomain={[0, null]} + yNice + padding={{ left: 16, bottom: 24 }} + > + + + + + + + + +
+ +
+ + + + { + set(e.detail.xDomain); + }} + /> + + +
+
- + From 3c84c0ebfd4fbe67c6549cb6eb5f899460da2e80 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 14 Apr 2024 12:41:21 -0400 Subject: [PATCH 15/40] Add randomWalk() util --- packages/layerchart/src/lib/utils/genData.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/layerchart/src/lib/utils/genData.ts b/packages/layerchart/src/lib/utils/genData.ts index 90705e7de..857a3cacb 100644 --- a/packages/layerchart/src/lib/utils/genData.ts +++ b/packages/layerchart/src/lib/utils/genData.ts @@ -1,4 +1,7 @@ import { addMinutes, startOfDay, startOfToday, subDays } from 'date-fns'; +import { cumsum } from 'd3-array'; +import { randomNormal } from 'd3-random'; + import { degreesToRadians, radiansToDegrees } from './math.js'; /** @@ -19,6 +22,14 @@ export function getRandomInteger(min: number, max: number, includeMax = true) { return Math.floor(Math.random() * (max - min + (includeMax ? 1 : 0)) + min); } +/** + * @see: https://observablehq.com/@d3/d3-cumsum + */ +export function randomWalk(options?: { count?: number }) { + const random = randomNormal(); + return Array.from(cumsum({ length: options?.count ?? 100 }, random)); +} + export function createSeries(options: { count?: number; min: number; From b2cc2f4b065fc48049d6389511390647fd171a41 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 14 Apr 2024 12:43:31 -0400 Subject: [PATCH 16/40] Add sync'd brush example (using ) --- .../routes/docs/components/Brush/+page.svelte | 101 ++++++++++-------- 1 file changed, 58 insertions(+), 43 deletions(-) diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index 67655179b..081e3215d 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -1,6 +1,7 @@ diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index 984cb2415..a08c9f91f 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -35,6 +35,44 @@

Examples

+

Integrated brush

+ + +
+ +
+ + + + + + + + + + + { + set(e.detail.xDomain); + }} + /> + + +
+
+
+
+

Clip data

@@ -139,6 +177,7 @@
+

Sync brushes and bind to xDomain

From f7e92edfca89f63892f2a1da71807d0bd3911db9 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 2 Jun 2024 10:13:11 -0400 Subject: [PATCH 22/40] [Brush] Add `y` and `both` (area) along with `x` (default) --- .../src/lib/components/Brush.svelte | 122 +++++++++++++----- .../routes/docs/components/Brush/+page.svelte | 122 ++++++++++++++++++ 2 files changed, 215 insertions(+), 29 deletions(-) diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte index 2196a74c9..0236bbf91 100644 --- a/packages/layerchart/src/lib/components/Brush.svelte +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -7,7 +7,12 @@ import Group from './Group.svelte'; import { localPoint } from '$lib/utils/event.js'; - const { xScale, xDomain: xDomainContext, width, height, padding } = getContext('LayerCake'); + /* + TODO: + - [ ] Handle resetting back to original domain (ex. yDomain={[0, null]}) + */ + + const { xScale, yScale, width, height, padding } = getContext('LayerCake'); const dispatch = createEventDispatcher<{ change: { xDomain?: [any, any]; yDomain?: [any, any] }; @@ -15,13 +20,16 @@ brushEnd: { xDomain?: [any, any]; yDomain?: [any, any] }; }>(); - // export let axis: 'x' | 'y' | 'both' = 'x'; - export let handleWidth = 5; + export let axis: 'x' | 'y' | 'both' = 'x'; + + /** Size of draggable handles (width/height) */ + export let handleSize = 5; /** Only show range while actively brushing. Useful with `brushEnd` event */ export let clearOnEnd = false; export let xDomain: [number | null, number | null] = [null, null]; + export let yDomain: [number | null, number | null] = [null, null]; export let classes: { root?: string; @@ -31,26 +39,42 @@ let frameEl: SVGRectElement; - $: [xDomainMin, xDomainMax] = extent($xDomainContext); + $: [xDomainMin, xDomainMax] = extent($xScale.domain()); + $: [yDomainMin, yDomainMax] = extent($yScale.domain()); function handler( - fn: (start: { xDomain: [number, number]; value: number }, value: number) => void + fn: ( + start: { + xDomain: [number, number]; + yDomain: [number, number]; + value: { x: number; y: number }; + }, + value: { x: number; y: number } + ) => void ) { return (e: PointerEvent) => { const start = { xDomain: [xDomain[0] ?? xDomainMin, xDomain[1] ?? xDomainMax], - value: $xScale.invert(localPoint(frameEl, e)?.x - $padding.left), + yDomain: [yDomain[0] ?? yDomainMin, yDomain[1] ?? yDomainMax], + value: { + x: $xScale.invert(localPoint(frameEl, e)?.x - $padding.left), + y: $yScale.invert(localPoint(frameEl, e)?.y - $padding.top), + }, }; - dispatch('brushStart', { xDomain }); + dispatch('brushStart', { xDomain, yDomain }); const onPointerMove = (e: PointerEvent) => { - fn(start, $xScale.invert(localPoint(frameEl, e)?.x - $padding.left)); + fn(start, { + x: $xScale.invert(localPoint(frameEl, e)?.x - $padding.left), + y: $yScale.invert(localPoint(frameEl, e)?.y - $padding.top), + }); - if (xDomain[0] === xDomain[1]) { + if (xDomain[0] === xDomain[1] || yDomain[0] === yDomain[1]) { // Ignore? + // TODO: What about when using `x` or `y` axis? } else { - dispatch('change', { xDomain }); + dispatch('change', { xDomain, yDomain }); } }; @@ -59,7 +83,7 @@ clear(); } - dispatch('brushEnd', { xDomain }); + dispatch('brushEnd', { xDomain, yDomain }); if (clearOnEnd) { clear(); @@ -76,44 +100,77 @@ const reset = handler((start, value) => { xDomain = [ - clamp(Math.min(start.value, value), xDomainMin, xDomainMax), - clamp(Math.max(start.value, value), xDomainMin, xDomainMax), + clamp(Math.min(start.value.x, value.x), xDomainMin, xDomainMax), + clamp(Math.max(start.value.x, value.x), xDomainMin, xDomainMax), + ]; + + yDomain = [ + clamp(Math.min(start.value.y, value.y), yDomainMin, yDomainMax), + clamp(Math.max(start.value.y, value.y), yDomainMin, yDomainMax), ]; }); const adjustRange = handler((start, value) => { - const d = clamp( - value - start.value, + const dx = clamp( + value.x - start.value.x, xDomainMin - start.xDomain[0], xDomainMax - start.xDomain[1] ); - xDomain = [Number(start.xDomain[0]) + d, Number(start.xDomain[1]) + d]; + xDomain = [Number(start.xDomain[0]) + dx, Number(start.xDomain[1]) + dx]; + + const dy = clamp( + value.y - start.value.y, + yDomainMin - start.yDomain[0], + yDomainMax - start.yDomain[1] + ); + yDomain = [Number(start.yDomain[0]) + dy, Number(start.yDomain[1]) + dy]; }); const adjustMin = handler((start, value) => { xDomain = [ - clamp(value > start.xDomain[1] ? start.xDomain[1] : value, xDomainMin, xDomainMax), - clamp(value > start.xDomain[1] ? value : start.xDomain[1], xDomainMin, xDomainMax), + clamp(value.x > start.xDomain[1] ? start.xDomain[1] : value.x, xDomainMin, xDomainMax), + clamp(value.x > start.xDomain[1] ? value.x : start.xDomain[1], xDomainMin, xDomainMax), + ]; + + yDomain = [ + clamp(value.y > start.yDomain[1] ? start.yDomain[1] : value.y, yDomainMin, yDomainMax), + clamp(value.y > start.yDomain[1] ? value.y : start.yDomain[1], yDomainMin, yDomainMax), ]; }); const adjustMax = handler((start, value) => { xDomain = [ - clamp(value < start.xDomain[0] ? value : start.xDomain[0], xDomainMin, xDomainMax), - clamp(value < start.xDomain[0] ? start.xDomain[0] : value, xDomainMin, xDomainMax), + clamp(value.x < start.xDomain[0] ? value.x : start.xDomain[0], xDomainMin, xDomainMax), + clamp(value.x < start.xDomain[0] ? start.xDomain[0] : value.x, xDomainMin, xDomainMax), + ]; + + yDomain = [ + clamp(value.y < start.yDomain[0] ? value.y : start.yDomain[0], yDomainMin, yDomainMax), + clamp(value.y < start.yDomain[0] ? start.yDomain[0] : value.y, yDomainMin, yDomainMax), ]; }); function clear() { xDomain = [null, null]; + yDomain = [null, null]; } function selectAll() { xDomain = [xDomainMin, xDomainMax]; + yDomain = [yDomainMin, yDomainMax]; } + $: top = $yScale(yDomain[1]); + $: bottom = $yScale(yDomain[0]); $: left = $xScale(xDomain[0]); $: right = $xScale(xDomain[1]); + + $: console.log({ top, bottom, left, right }); + + $: rangeTop = axis === 'both' || axis === 'y' ? top : 0; + $: rangeLeft = axis === 'both' || axis === 'x' ? left : 0; + $: rangeWidth = axis === 'both' || axis === 'x' ? right - left : $width; + $: rangeHeight = axis === 'both' || axis === 'y' ? bottom - top : $height; @@ -124,27 +181,34 @@ bind:rectEl={frameEl} /> - clear()}> + clear()} + > - + + (xDomain[0] = xDomainMin)} /> - + (xDomain[1] = xDomainMax)} /> diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index a08c9f91f..fae9d81f6 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -35,6 +35,128 @@

Examples

+

X-axis

+ + +
+ +
+ + + + + + + + + + + { + console.log(e.detail.xDomain); + set(e.detail.xDomain); + }} + /> + + +
+
+
+
+ +

Y-axis

+ + +
+ +
+ + + + + + + + + + + { + console.log(e.detail.yDomain); + set(e.detail.yDomain); + }} + /> + + +
+
+
+
+ +

Both (area)

+ + +
+ +
+ + + + + + + + + + + { + console.log(e.detail.yDomain, e.detail.yDomain); + set({ + xDomain: e.detail.xDomain, + yDomain: e.detail.yDomain, + }); + }} + /> + + +
+
+
+
+

Integrated brush

From 9cbd0491e658124bbc99f7f8c4d73019f007c78a Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 2 Jun 2024 18:45:04 -0400 Subject: [PATCH 23/40] Rename `reset` => `createRange` and `clear` => `reset` --- .../layerchart/src/lib/components/Brush.svelte | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte index 0236bbf91..4d8079437 100644 --- a/packages/layerchart/src/lib/components/Brush.svelte +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -80,13 +80,13 @@ const onPointerUp = (e: PointerEvent) => { if (e.target === frameEl) { - clear(); + reset(); } dispatch('brushEnd', { xDomain, yDomain }); if (clearOnEnd) { - clear(); + reset(); } window.removeEventListener('pointermove', onPointerMove); @@ -98,7 +98,7 @@ }; } - const reset = handler((start, value) => { + const createRange = handler((start, value) => { xDomain = [ clamp(Math.min(start.value.x, value.x), xDomainMin, xDomainMax), clamp(Math.max(start.value.x, value.x), xDomainMin, xDomainMax), @@ -150,7 +150,7 @@ ]; }); - function clear() { + function reset() { xDomain = [null, null]; yDomain = [null, null]; } @@ -165,8 +165,6 @@ $: left = $xScale(xDomain[0]); $: right = $xScale(xDomain[1]); - $: console.log({ top, bottom, left, right }); - $: rangeTop = axis === 'both' || axis === 'y' ? top : 0; $: rangeLeft = axis === 'both' || axis === 'x' ? left : 0; $: rangeWidth = axis === 'both' || axis === 'x' ? right - left : $width; @@ -176,7 +174,7 @@ selectAll()} bind:rectEl={frameEl} /> @@ -186,7 +184,7 @@ x={rangeLeft} y={rangeTop} on:pointerdown={adjustRange} - on:dblclick={() => clear()} + on:dblclick={() => reset()} > Date: Mon, 3 Jun 2024 00:49:34 -0400 Subject: [PATCH 24/40] [Brush] Reset back to original domain (not just null/null). Add top/bottom handles. Rename `clearOnEnd` to `resetOnEnd` --- .../src/lib/components/Brush.svelte | 156 ++++++++++++------ .../routes/docs/components/Brush/+page.svelte | 77 +++++---- 2 files changed, 147 insertions(+), 86 deletions(-) diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte index 4d8079437..214379e7f 100644 --- a/packages/layerchart/src/lib/components/Brush.svelte +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -26,7 +26,7 @@ export let handleSize = 5; /** Only show range while actively brushing. Useful with `brushEnd` event */ - export let clearOnEnd = false; + export let resetOnEnd = false; export let xDomain: [number | null, number | null] = [null, null]; export let yDomain: [number | null, number | null] = [null, null]; @@ -39,6 +39,10 @@ let frameEl: SVGRectElement; + // Capture original domains for reset() + const originalXDomain = $xScale.domain(); + const originalYDomain = $yScale.domain(); + $: [xDomainMin, xDomainMax] = extent($xScale.domain()); $: [yDomainMin, yDomainMax] = extent($yScale.domain()); @@ -70,22 +74,23 @@ y: $yScale.invert(localPoint(frameEl, e)?.y - $padding.top), }); - if (xDomain[0] === xDomain[1] || yDomain[0] === yDomain[1]) { - // Ignore? - // TODO: What about when using `x` or `y` axis? - } else { - dispatch('change', { xDomain, yDomain }); - } + // if (xDomain[0] === xDomain[1] || yDomain[0] === yDomain[1]) { + // // Ignore? + // // TODO: What about when using `x` or `y` axis? + // } else { + dispatch('change', { xDomain, yDomain }); + // } }; const onPointerUp = (e: PointerEvent) => { if (e.target === frameEl) { reset(); + dispatch('change', { xDomain, yDomain }); } dispatch('brushEnd', { xDomain, yDomain }); - if (clearOnEnd) { + if (resetOnEnd) { reset(); } @@ -99,6 +104,8 @@ } const createRange = handler((start, value) => { + isActive = true; + xDomain = [ clamp(Math.min(start.value.x, value.x), xDomainMin, xDomainMax), clamp(Math.max(start.value.x, value.x), xDomainMin, xDomainMax), @@ -126,33 +133,39 @@ yDomain = [Number(start.yDomain[0]) + dy, Number(start.yDomain[1]) + dy]; }); - const adjustMin = handler((start, value) => { - xDomain = [ - clamp(value.x > start.xDomain[1] ? start.xDomain[1] : value.x, xDomainMin, xDomainMax), - clamp(value.x > start.xDomain[1] ? value.x : start.xDomain[1], xDomainMin, xDomainMax), - ]; - + const adjustBottom = handler((start, value) => { yDomain = [ clamp(value.y > start.yDomain[1] ? start.yDomain[1] : value.y, yDomainMin, yDomainMax), clamp(value.y > start.yDomain[1] ? value.y : start.yDomain[1], yDomainMin, yDomainMax), ]; }); - const adjustMax = handler((start, value) => { + const adjustTop = handler((start, value) => { + yDomain = [ + clamp(value.y < start.yDomain[1] ? value.y : start.yDomain[0], yDomainMin, yDomainMax), + clamp(value.y < start.yDomain[1] ? start.yDomain[0] : value.y, yDomainMin, yDomainMax), + ]; + }); + + const adjustLeft = handler((start, value) => { xDomain = [ - clamp(value.x < start.xDomain[0] ? value.x : start.xDomain[0], xDomainMin, xDomainMax), - clamp(value.x < start.xDomain[0] ? start.xDomain[0] : value.x, xDomainMin, xDomainMax), + clamp(value.x > start.xDomain[1] ? start.xDomain[1] : value.x, xDomainMin, xDomainMax), + clamp(value.x > start.xDomain[1] ? value.x : start.xDomain[1], xDomainMin, xDomainMax), ]; + }); - yDomain = [ - clamp(value.y < start.yDomain[0] ? value.y : start.yDomain[0], yDomainMin, yDomainMax), - clamp(value.y < start.yDomain[0] ? start.yDomain[0] : value.y, yDomainMin, yDomainMax), + const adjustRight = handler((start, value) => { + xDomain = [ + clamp(value.x < start.xDomain[0] ? value.x : start.xDomain[0], xDomainMin, xDomainMax), + clamp(value.x < start.xDomain[0] ? start.xDomain[0] : value.x, xDomainMin, xDomainMax), ]; }); function reset() { - xDomain = [null, null]; - yDomain = [null, null]; + isActive = false; + + xDomain = originalXDomain; + yDomain = originalYDomain; } function selectAll() { @@ -169,6 +182,8 @@ $: rangeLeft = axis === 'both' || axis === 'x' ? left : 0; $: rangeWidth = axis === 'both' || axis === 'x' ? right - left : $width; $: rangeHeight = axis === 'both' || axis === 'y' ? bottom - top : $height; + + let isActive = false; @@ -179,36 +194,69 @@ bind:rectEl={frameEl} /> - reset()} - > - - - - - - (xDomain[0] = xDomainMin)} - /> - - - - (xDomain[1] = xDomainMax)} - /> - + {#if isActive} + reset()} + > + + + + {#if axis === 'both' || axis === 'y'} + + (yDomain[0] = yDomainMin)} + /> + + + + (yDomain[1] = yDomainMax)} + /> + + {/if} + + {#if axis === 'both' || axis === 'x'} + + (xDomain[0] = xDomainMin)} + /> + + + + (xDomain[1] = xDomainMax)} + /> + + {/if} + {/if} diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index fae9d81f6..dd4245367 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -35,7 +35,7 @@

Examples

-

X-axis

+

Integrated brush (x-axis)

@@ -62,7 +62,7 @@ { console.log(e.detail.xDomain); set(e.detail.xDomain); @@ -75,7 +75,7 @@
-

Y-axis

+

Integrated brush (y-axis)

@@ -101,7 +101,7 @@ { console.log(e.detail.yDomain); set(e.detail.yDomain); @@ -114,7 +114,7 @@
-

Both (area)

+

Integrated brush (both axis / area)

@@ -141,7 +141,7 @@ { console.log(e.detail.yDomain, e.detail.yDomain); set({ @@ -157,7 +157,7 @@
-

Integrated brush

+

Clip data

@@ -181,10 +181,23 @@ + + +
+
+ + + { + on:change={(e) => { + console.log('xDomain', e.detail.xDomain); set(e.detail.xDomain); }} /> @@ -195,49 +208,49 @@
-

Clip data

+

Clip data (y-axis)

-
- -
+
+ +
- - - - - - - + + { + set(e.detail.yDomain); + }} + />
-
+
- - { - set(e.detail.xDomain); - }} - /> + + + + + + +
From 06286d1d050124b2600c9574a73b76f8d801843d Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 3 Jun 2024 09:35:31 -0400 Subject: [PATCH 25/40] Add TODO --- packages/layerchart/src/lib/components/Brush.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte index 214379e7f..ced0d9e7a 100644 --- a/packages/layerchart/src/lib/components/Brush.svelte +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -258,5 +258,7 @@ /> {/if} + + {/if} From eec69246ff70042301157c555d6583d67837bec6 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 3 Jun 2024 10:35:41 -0400 Subject: [PATCH 26/40] Remove `yNice` for more deterministic results --- .../src/routes/docs/components/Brush/+page.svelte | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index dd4245367..7d4e62d06 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -48,7 +48,6 @@ {xDomain} y="value" yDomain={[0, null]} - yNice padding={{ left: 16, bottom: 24 }} > @@ -64,7 +63,6 @@ axis="x" resetOnEnd on:brushEnd={(e) => { - console.log(e.detail.xDomain); set(e.detail.xDomain); }} /> @@ -87,7 +85,6 @@ xScale={scaleTime()} y="value" {yDomain} - yNice padding={{ left: 16, bottom: 24 }} > @@ -103,7 +100,6 @@ axis="y" resetOnEnd on:brushEnd={(e) => { - console.log(e.detail.yDomain); set(e.detail.yDomain); }} /> @@ -127,7 +123,6 @@ xDomain={value.xDomain} y="value" yDomain={value.yDomain} - yNice padding={{ left: 16, bottom: 24 }} > @@ -143,7 +138,6 @@ axis="both" resetOnEnd on:brushEnd={(e) => { - console.log(e.detail.yDomain, e.detail.yDomain); set({ xDomain: e.detail.xDomain, yDomain: e.detail.yDomain, @@ -170,7 +164,6 @@ {xDomain} y="value" yDomain={[0, null]} - yNice padding={{ left: 16, bottom: 24 }} > @@ -197,7 +190,6 @@ { - console.log('xDomain', e.detail.xDomain); set(e.detail.xDomain); }} /> @@ -240,7 +232,6 @@ xScale={scaleTime()} y="value" {yDomain} - yNice padding={{ left: 16, bottom: 24 }} > @@ -274,7 +265,6 @@ xScale={scaleTime()} y="value" yDomain={[0, null]} - yNice padding={{ left: 16, bottom: 24 }} > @@ -313,7 +303,7 @@
-

Sync brushes and bind to xDomain

+

Sync brushes with `bind:xDomain`

{@const colorScale = scaleOrdinal([ @@ -333,7 +323,6 @@ {xDomain} y="value" yBaseline={0} - yNice padding={{ left: 16, bottom: 24 }} > From 0bda07e0ac125227910571bbd8ddc2d928bf5ba1 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 3 Jun 2024 10:56:15 -0400 Subject: [PATCH 27/40] Set `isActive` reactively to handle cases where `xDomain`/`yDomain` are set externally (ex. `bind:xDomain`) --- .../src/lib/components/Brush.svelte | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte index ced0d9e7a..3b0532125 100644 --- a/packages/layerchart/src/lib/components/Brush.svelte +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -28,8 +28,15 @@ /** Only show range while actively brushing. Useful with `brushEnd` event */ export let resetOnEnd = false; - export let xDomain: [number | null, number | null] = [null, null]; - export let yDomain: [number | null, number | null] = [null, null]; + export let xDomain: [number | null, number | null] = $xScale.domain(); + export let yDomain: [number | null, number | null] = $yScale.domain(); + + // Capture original domains for reset() + const originalXDomain = $xScale.domain(); + const originalYDomain = $yScale.domain(); + + $: [xDomainMin, xDomainMax] = extent($xScale.domain()); + $: [yDomainMin, yDomainMax] = extent($yScale.domain()); export let classes: { root?: string; @@ -39,13 +46,6 @@ let frameEl: SVGRectElement; - // Capture original domains for reset() - const originalXDomain = $xScale.domain(); - const originalYDomain = $yScale.domain(); - - $: [xDomainMin, xDomainMax] = extent($xScale.domain()); - $: [yDomainMin, yDomainMax] = extent($yScale.domain()); - function handler( fn: ( start: { @@ -183,7 +183,12 @@ $: rangeWidth = axis === 'both' || axis === 'x' ? right - left : $width; $: rangeHeight = axis === 'both' || axis === 'y' ? bottom - top : $height; - let isActive = false; + // Set reactively to handle cases where xDomain/yDomain are set externally (ex. `bind:xDomain`) + $: isActive = + xDomain[0]?.valueOf() !== originalXDomain[0]?.valueOf() || + xDomain[1]?.valueOf() !== originalXDomain[1]?.valueOf() || + yDomain[0]?.valueOf() !== originalYDomain[0]?.valueOf() || + yDomain[1]?.valueOf() !== originalYDomain[1]?.valueOf(); From 1114b2be90d9a58117797797bef2ee120acec3fb Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 3 Jun 2024 10:56:49 -0400 Subject: [PATCH 28/40] Do not set original xDomain example outside of data --- .../layerchart/src/routes/docs/components/Brush/+page.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index 7d4e62d06..bc411208e 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -18,7 +18,7 @@ export let data; const now = new Date(); - let xDomain = [subDays(now, 100), subDays(now, 80)]; + let xDomain = [subDays(now, 60), subDays(now, 30)]; const seriesData = [ randomWalk({ count: 100 }).map((value, i) => ({ date: subDays(now, i), value: 10 + value })), @@ -312,6 +312,7 @@ 'var(--color-warning-500)', 'var(--color-danger-500)', ])} +
{#each seriesData as data, i}
From 33356064bb70f7cb79bf5eeee25028f98c769b19 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 3 Jun 2024 14:54:18 -0400 Subject: [PATCH 29/40] Add Tooltip interop WIP --- .../routes/docs/components/Brush/+page.svelte | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index bc411208e..f45b34756 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -10,8 +10,10 @@ import Axis from '$lib/components/Axis.svelte'; import Brush from '$lib/components/Brush.svelte'; import ChartClipPath from '$lib/components/ChartClipPath.svelte'; + import Highlight from '$lib/components/Highlight.svelte'; import LinearGradient from '$lib/components/LinearGradient.svelte'; import Rule from '$lib/components/Rule.svelte'; + import Tooltip from '$lib/components/Tooltip.svelte'; import { randomWalk } from '$lib/utils/genData.js'; @@ -35,6 +37,49 @@

Examples

+ +

Tooltip interop (WIP)

+ + +
+ +
+ + + + + + + + + + + + { + set(e.detail.xDomain); + }} + /> + + + 'TODO'} /> + +
+
+
+
+

Integrated brush (x-axis)

From 4ef6f10110e62b2a4df9b207ddb7e5f687a4e76d Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Wed, 5 Jun 2024 19:00:15 -0400 Subject: [PATCH 30/40] Disable user select to remove text selection on drag --- packages/layerchart/src/lib/components/Brush.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte index 3b0532125..561ac4dc0 100644 --- a/packages/layerchart/src/lib/components/Brush.svelte +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -191,7 +191,7 @@ yDomain[1]?.valueOf() !== originalYDomain[1]?.valueOf(); - + Date: Fri, 7 Jun 2024 12:20:26 -0400 Subject: [PATCH 31/40] Fix quadtree debug not allowing tooltip interactions --- packages/layerchart/src/lib/components/TooltipContext.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/layerchart/src/lib/components/TooltipContext.svelte b/packages/layerchart/src/lib/components/TooltipContext.svelte index 8d5ef46aa..feff4311d 100644 --- a/packages/layerchart/src/lib/components/TooltipContext.svelte +++ b/packages/layerchart/src/lib/components/TooltipContext.svelte @@ -370,7 +370,7 @@ {/if} {#if mode === 'quadtree' && debug} - + {#each quadtreeRects(quadtree, false) as rect} From c45bdf42135977a57fca06fb137c5aa98b76345e Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 7 Jun 2024 12:31:28 -0400 Subject: [PATCH 32/40] Allow pointer events from Brush to bubble up to TooltipContext to allow interop by restructing slot location and layout for bisect and quadtree modes (does not fix voronoi or bounds/band cases) --- .../src/lib/components/TooltipContext.svelte | 54 ++++++++++++------- .../routes/docs/components/Brush/+page.svelte | 35 ++++++++---- 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/packages/layerchart/src/lib/components/TooltipContext.svelte b/packages/layerchart/src/lib/components/TooltipContext.svelte index feff4311d..63ea7349d 100644 --- a/packages/layerchart/src/lib/components/TooltipContext.svelte +++ b/packages/layerchart/src/lib/components/TooltipContext.svelte @@ -149,6 +149,17 @@ const localX = point?.x ?? 0; const localY = point?.y ?? 0; + if ( + e.offsetX < e.currentTarget.offsetLeft || + e.offsetX > e.currentTarget.offsetLeft + e.currentTarget.offsetWidth || + e.offsetY < e.currentTarget.offsetTop || + e.offsetY > e.currentTarget.offsetTop + e.currentTarget.offsetHeight + ) { + // Ignore if within padding of chart + hideTooltip(); + return; + } + // If tooltipData not provided already (voronoi, etc), attempt to find it // TODO: When using bisect-x/y/band, values should be sorted. Tyipcally are for `x`, but not `y` (and band depends on if x or y scale) if (tooltipData == null) { @@ -316,26 +327,30 @@ } - - {#if ['bisect-x', 'bisect-y', 'bisect-band', 'quadtree'].includes(mode)} - -
{ - onClick({ data: $tooltip?.data }); - }} - /> - +
{ + onClick({ data: $tooltip?.data }); + }} + > + +
+ +
+
{:else if mode === 'voronoi'} + showTooltip(e.detail.event, e.detail.data)} @@ -348,6 +363,7 @@ /> {:else if mode === 'bounds' || mode === 'band'} + {#each rects as rect} @@ -367,6 +383,8 @@ {/each} +{:else} + {/if} {#if mode === 'quadtree' && debug} diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index f45b34756..3d34865fc 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -1,6 +1,6 @@

Examples

- -

Tooltip interop (WIP)

+

Tooltip interop

@@ -53,6 +47,8 @@ yDomain={[0, null]} padding={{ left: 16, bottom: 24 }} tooltip={{ mode: 'bisect-x' }} + let:height + let:padding > @@ -73,7 +69,28 @@ /> - 'TODO'} /> + + {format(data.value, 'currency')} + + + + {format(data.date, PeriodType.Day)} +
From 2f822b035b8717d3e6753a1e297187b173455a82 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 7 Jun 2024 12:44:05 -0400 Subject: [PATCH 33/40] Move Tooltip interop example to bottom --- .../routes/docs/components/Brush/+page.svelte | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index 3d34865fc..5daa0922f 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -32,71 +32,6 @@

Examples

-

Tooltip interop

- - -
- -
- - - - - - - - - - - - { - set(e.detail.xDomain); - }} - /> - - - - {format(data.value, 'currency')} - - - - {format(data.date, PeriodType.Day)} - - -
-
-
-
-

Integrated brush (x-axis)

@@ -423,3 +358,68 @@ {/each}
+ +

Tooltip interop

+ + +
+ +
+ + + + + + + + + + + + { + set(e.detail.xDomain); + }} + /> + + + + {format(data.value, 'currency')} + + + + {format(data.date, PeriodType.Day)} + + +
+
+
+
From e95d3723d0c7788f7c86b6e3cbeb73d4d7a568e4 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sat, 8 Jun 2024 16:48:33 -0400 Subject: [PATCH 34/40] Simplify markup --- .../src/lib/components/Brush.svelte | 80 ++++++++----------- 1 file changed, 35 insertions(+), 45 deletions(-) diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte index 561ac4dc0..ee894da49 100644 --- a/packages/layerchart/src/lib/components/Brush.svelte +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -200,68 +200,58 @@ /> {#if isActive} - reset()} - > + /> + + {#if axis === 'both' || axis === 'y'} (yDomain[0] = yDomainMin)} /> - - {#if axis === 'both' || axis === 'y'} - - (yDomain[0] = yDomainMin)} - /> - - - - (yDomain[1] = yDomainMax)} - /> - + on:dblclick={() => (yDomain[1] = yDomainMax)} + /> {/if} {#if axis === 'both' || axis === 'x'} - - (xDomain[0] = xDomainMin)} - /> - - - (xDomain[0] = xDomainMin)} + /> + + - (xDomain[1] = xDomainMax)} - /> - + on:dblclick={() => (xDomain[1] = xDomainMax)} + /> {/if} From c1f61d8bcf552f703b210a4b819549f2823f991c Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sat, 8 Jun 2024 18:34:52 -0400 Subject: [PATCH 35/40] Support passing classes and props to underlying range/handles/etc and add examples --- .../src/lib/components/Brush.svelte | 42 +++++++++++++------ .../layerchart/src/lib/components/Rect.svelte | 1 + .../routes/docs/components/Brush/+page.svelte | 32 ++++++++++++++ 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte index ee894da49..1b53b351a 100644 --- a/packages/layerchart/src/lib/components/Brush.svelte +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -1,17 +1,12 @@ - + selectAll()} bind:rectEl={frameEl} @@ -205,9 +207,15 @@ y={rangeTop} width={rangeWidth} height={rangeHeight} - class={cls('range', 'fill-surface-content/10 cursor-move select-none')} + class={cls( + 'range', + 'cursor-move select-none', + range?.fill == null && 'fill-surface-content/10', + classes.range + )} on:pointerdown={adjustRange} on:dblclick={() => reset()} + {...range} /> {#if axis === 'both' || axis === 'y'} @@ -216,9 +224,10 @@ y={rangeTop} width={rangeWidth} height={handleSize} - class={cls('handle top', 'fill-transparent cursor-ns-resize select-none')} + class={cls('handle top', 'fill-transparent cursor-ns-resize select-none', classes.handle)} on:pointerdown={adjustTop} on:dblclick={() => (yDomain[0] = yDomainMin)} + {...handle} /> (yDomain[1] = yDomainMax)} + {...handle} /> {/if} @@ -238,9 +252,10 @@ y={rangeTop} width={handleSize} height={rangeHeight} - class={cls('handle left', 'fill-transparent cursor-ew-resize select-none')} + class={cls('handle left', 'fill-transparent cursor-ew-resize select-none', classes.handle)} on:pointerdown={adjustLeft} on:dblclick={() => (xDomain[0] = xDomainMin)} + {...handle} /> (xDomain[1] = xDomainMax)} + {...handle} /> {/if} diff --git a/packages/layerchart/src/lib/components/Rect.svelte b/packages/layerchart/src/lib/components/Rect.svelte index 196ba8ab7..0af826b02 100644 --- a/packages/layerchart/src/lib/components/Rect.svelte +++ b/packages/layerchart/src/lib/components/Rect.svelte @@ -50,4 +50,5 @@ on:pointermove on:pointerout on:pointerleave + on:dblclick /> diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index 5daa0922f..1e5e154d9 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -12,10 +12,12 @@ import ChartClipPath from '$lib/components/ChartClipPath.svelte'; import Highlight from '$lib/components/Highlight.svelte'; import LinearGradient from '$lib/components/LinearGradient.svelte'; + import Pattern from '$lib/components/Pattern.svelte'; import Rule from '$lib/components/Rule.svelte'; import Tooltip from '$lib/components/Tooltip.svelte'; import { randomWalk } from '$lib/utils/genData.js'; + import { url } from 'svelte-ux/utils/routing'; export let data; @@ -32,6 +34,36 @@

Examples

+

Styling via classes

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

Styling via props

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

Integrated brush (x-axis)

From 9b95abf03fd05fe552c6648f583eaa5261a43d90 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sat, 8 Jun 2024 18:37:42 -0400 Subject: [PATCH 36/40] Dispatch change event when double clicking on handle to expand to min/max --- .../src/lib/components/Brush.svelte | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte index 1b53b351a..ed33e2e23 100644 --- a/packages/layerchart/src/lib/components/Brush.svelte +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -226,7 +226,10 @@ height={handleSize} class={cls('handle top', 'fill-transparent cursor-ns-resize select-none', classes.handle)} on:pointerdown={adjustTop} - on:dblclick={() => (yDomain[0] = yDomainMin)} + on:dblclick={() => { + yDomain[0] = yDomainMin; + dispatch('change', { xDomain, yDomain }); + }} {...handle} /> @@ -241,7 +244,9 @@ classes.handle )} on:pointerdown={adjustBottom} - on:dblclick={() => (yDomain[1] = yDomainMax)} + on:dblclick={() => { + yDomain[1] = yDomainMax; + }} {...handle} /> {/if} @@ -254,7 +259,10 @@ height={rangeHeight} class={cls('handle left', 'fill-transparent cursor-ew-resize select-none', classes.handle)} on:pointerdown={adjustLeft} - on:dblclick={() => (xDomain[0] = xDomainMin)} + on:dblclick={() => { + xDomain[0] = xDomainMin; + dispatch('change', { xDomain, yDomain }); + }} {...handle} /> @@ -265,7 +273,10 @@ height={rangeHeight} class={cls('handle right', 'fill-transparent cursor-ew-resize select-none', classes.handle)} on:pointerdown={adjustRight} - on:dblclick={() => (xDomain[1] = xDomainMax)} + on:dblclick={() => { + xDomain[1] = xDomainMax; + dispatch('change', { xDomain, yDomain }); + }} {...handle} /> {/if} From 3678d71c989cfcb3bbc2bf6c974ed1f607dac967 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 9 Jun 2024 10:41:26 -0400 Subject: [PATCH 37/40] Add selection example --- .../routes/docs/components/Brush/+page.svelte | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index 1e5e154d9..b923341aa 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -1,6 +1,6 @@

Examples

@@ -180,7 +187,7 @@
-

Clip data

+

Separate chart (clip data)

@@ -229,7 +236,7 @@
-

Clip data (y-axis)

+

Separate chart (clip data: y-axis)

@@ -278,7 +285,7 @@
-

Filter data

+

Separate chart (filter data)

@@ -455,3 +462,55 @@
+ +

Selection

+ + + +
+ + + + + + + {#each points as point} + {@const isSelected = + (value.xDomain[0] == null || value.xDomain[0] <= point.data.x) && + (value.xDomain[1] == null || point.data.x <= value.xDomain[1]) && + (value.yDomain[0] == null || value.yDomain[0] <= point.data.y) && + (value.yDomain[1] == null || point.data.y <= value.yDomain[1])} + + + {/each} + + + { + set({ + xDomain: e.detail.xDomain, + yDomain: e.detail.yDomain, + }); + }} + /> + + +
+
+
From adfbedf31a72a44eee066231d9bfd7b5535bdfc2 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 9 Jun 2024 10:43:25 -0400 Subject: [PATCH 38/40] Refine pattern styling --- .../layerchart/src/routes/docs/components/Brush/+page.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index b923341aa..a67eed140 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -61,8 +61,8 @@ - - + + From 19981c85b964cf1f639f31d366034bf396f4c902 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 9 Jun 2024 23:14:27 -0400 Subject: [PATCH 39/40] [Brush] Support passing "range" and "handle" slots for full control. Useful to add icons, etc --- .../src/lib/components/Brush.svelte | 113 +++++++++++------- .../routes/docs/components/Brush/+page.svelte | 34 +++++- 2 files changed, 99 insertions(+), 48 deletions(-) diff --git a/packages/layerchart/src/lib/components/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte index ed33e2e23..d1a457677 100644 --- a/packages/layerchart/src/lib/components/Brush.svelte +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -6,6 +6,7 @@ import { clamp, cls } from 'svelte-ux'; import Frame from './Frame.svelte'; import { localPoint } from '$lib/utils/event.js'; + import Group from './Group.svelte'; const { xScale, yScale, width, height, padding } = getContext('LayerCake'); @@ -15,6 +16,7 @@ brushEnd: { xDomain?: [any, any]; yDomain?: [any, any] }; }>(); + /** Axis to apply brushing */ export let axis: 'x' | 'y' | 'both' = 'x'; /** Size of draggable handles (width/height) */ @@ -202,83 +204,104 @@ /> {#if isActive} - reset()} - {...range} - /> + + + reset()} + {...range} + /> + + {#if axis === 'both' || axis === 'y'} - { yDomain[0] = yDomainMin; dispatch('change', { xDomain, yDomain }); }} - {...handle} - /> - - + + + + + + { yDomain[1] = yDomainMax; }} - {...handle} - /> + > + + + + {/if} {#if axis === 'both' || axis === 'x'} - { xDomain[0] = xDomainMin; dispatch('change', { xDomain, yDomain }); }} - {...handle} - /> - - + + + + + + { xDomain[1] = xDomainMax; dispatch('change', { xDomain, yDomain }); }} - {...handle} - /> + > + + + + {/if} diff --git a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte index a67eed140..d3b39c52d 100644 --- a/packages/layerchart/src/routes/docs/components/Brush/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -1,7 +1,9 @@ + +
+
+ +

Styling via slots

+ + +
+ + + + + + + + + + + +
From 41ad80ef570f3cad3ddddbf4cf29be66ce095477 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 9 Jun 2024 23:18:46 -0400 Subject: [PATCH 40/40] Update changeset --- .changeset/itchy-cameras-guess.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/itchy-cameras-guess.md b/.changeset/itchy-cameras-guess.md index 1859769ec..6d0802f88 100644 --- a/.changeset/itchy-cameras-guess.md +++ b/.changeset/itchy-cameras-guess.md @@ -1,5 +1,5 @@ --- -'layerchart': patch +'layerchart': minor --- -[Brush] Forward `mousedown` and `touchstart` events +Add Brush component