diff --git a/.changeset/five-phones-tease.md b/.changeset/five-phones-tease.md new file mode 100644 index 000000000..d09476a1e --- /dev/null +++ b/.changeset/five-phones-tease.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +[Frame] Expose `rectEl` and forward `mousedown`, `touchstart`, and `dblclick` events 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 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/.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/Brush.svelte b/packages/layerchart/src/lib/components/Brush.svelte new file mode 100644 index 000000000..7793d0cae --- /dev/null +++ b/packages/layerchart/src/lib/components/Brush.svelte @@ -0,0 +1,164 @@ + + + + selectAll()} + bind:rectEl={frameEl} + /> + + clear()}> + + + + + (xDomain[0] = xDomainMin)} + /> + + + + (xDomain[1] = xDomainMax)} + /> + + 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; diff --git a/packages/layerchart/src/lib/components/Frame.svelte b/packages/layerchart/src/lib/components/Frame.svelte index 1be6c57ff..f1146fa9e 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; 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/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; 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..984cb2415 --- /dev/null +++ b/packages/layerchart/src/routes/docs/components/Brush/+page.svelte @@ -0,0 +1,199 @@ + + +Examples + +Clip data + + + + + + + + + + + + + + + + + + + + + + + { + set(e.detail.xDomain); + }} + /> + + + + + + + +Filter data + + + + + + + (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); + }} + /> + + + + + + +Sync brushes and bind to xDomain + + + {@const colorScale = scaleOrdinal([ + 'var(--color-success-500)', + 'var(--color-info-500)', + 'var(--color-warning-500)', + 'var(--color-danger-500)', + ])} + + {#each seriesData as data, i} + + + + + + format(v, PeriodType.Day, { variant: 'short' })} + /> + + + + + + + + + + + + + + + + + + + + {/each} + + 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, + }, + }; +}