From 31e28ad74c955fe7ea07ee87fc2b0a0bde486b5a Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 20 Jun 2025 21:23:09 -0400 Subject: [PATCH 01/20] BrushState WIP --- .../src/lib/components/BrushContext.svelte | 427 ++++++++++-------- .../src/lib/components/Chart.svelte | 6 +- .../lib/components/charts/AreaChart.svelte | 4 +- .../src/lib/components/charts/BarChart.svelte | 11 +- .../lib/components/charts/LineChart.svelte | 4 +- .../lib/components/charts/ScatterChart.svelte | 8 +- .../docs/components/BrushContext/+page.svelte | 126 ++++-- 7 files changed, 355 insertions(+), 231 deletions(-) diff --git a/packages/layerchart/src/lib/components/BrushContext.svelte b/packages/layerchart/src/lib/components/BrushContext.svelte index b31e2404c..3d30971a2 100644 --- a/packages/layerchart/src/lib/components/BrushContext.svelte +++ b/packages/layerchart/src/lib/components/BrushContext.svelte @@ -1,7 +1,32 @@ @@ -153,13 +181,16 @@ const ctx = getChartContext(); let { + // xDomain, + // yDomain, + x, + y, brushContext: brushContextProp = $bindable(), + axis = 'x', handleSize = 5, resetOnEnd = false, ignoreResetClick = false, - xDomain: xDomain, - yDomain: yDomain, mode = 'integrated', disabled = false, range = {}, @@ -174,89 +205,106 @@ let rootEl = $state(); - if (xDomain === undefined) { - xDomain = ctx.xScale.domain(); - } - if (yDomain === undefined) { - yDomain = ctx.yScale.domain(); - } - - $effect.pre(() => { - if (xDomain !== undefined) return; - xDomain = ctx.xScale.domain(); - }); - - $effect.pre(() => { - if (yDomain !== undefined) return; - yDomain = ctx.yScale.domain(); - }); - - const ogXDomain = xDomain; - const ogYDomain = yDomain; - const originalXDomain = ctx.config.xDomain; - const originalYDomain = ctx.config.yDomain; - - const xDomainMinMax = $derived(extent(ctx.xScale.domain()) as [number, number]); - const xDomainMin = $derived(xDomainMinMax[0]); - const xDomainMax = $derived(xDomainMinMax[1]); - - const yDomainMinMax = $derived(extent(ctx.yScale.domain()) as [number, number]); - const yDomainMin = $derived(yDomainMinMax[0]); - const yDomainMax = $derived(yDomainMinMax[1]); - - const top = $derived(ctx.yScale(yDomain?.[1])); - const bottom = $derived(ctx.yScale(yDomain?.[0])); - const left = $derived(ctx.xScale(xDomain?.[0])); - const right = $derived(ctx.xScale(xDomain?.[1])); - - const _range = $derived({ - x: axis === 'both' || axis === 'x' ? left : 0, - y: axis === 'both' || axis === 'y' ? top : 0, - width: axis === 'both' || axis === 'x' ? right - left : ctx.width, - height: axis === 'both' || axis === 'y' ? bottom - top : ctx.height, + const brushState = new BrushState({ x, y }); + + // if (xDomain === undefined) { + // xDomain = ctx.xScale.domain(); + // } + // if (yDomain === undefined) { + // yDomain = ctx.yScale.domain(); + // } + + // $effect.pre(() => { + // if (xDomain !== undefined) return; + // xDomain = ctx.xScale.domain(); + // }); + + // $effect.pre(() => { + // if (yDomain !== undefined) return; + // yDomain = ctx.yScale.domain(); + // }); + + // const ogXDomain = xDomain; + // const ogYDomain = yDomain; + // const originalXDomain = ctx.config.xDomain; + // const originalYDomain = ctx.config.yDomain; + + // const xDomainMinMax = $derived(extent(ctx.xScale.domain()) as [number, number]); + // const xDomainMin = $derived(xDomainMinMax[0]); + // const xDomainMax = $derived(xDomainMinMax[1]); + const [xDomainMin, xDomainMax] = $derived(ctx.xScale.domain()); + + // const yDomainMinMax = $derived(extent(ctx.yScale.domain()) as [number, number]); + // const yDomainMin = $derived(yDomainMinMax[0]); + // const yDomainMax = $derived(yDomainMinMax[1]); + const [yDomainMin, yDomainMax] = $derived(ctx.yScale.domain()); + + const top = $derived(ctx.yScale(brushState.y?.[1])); + const bottom = $derived(ctx.yScale(brushState.y?.[0])); + const left = $derived(ctx.xScale(brushState.x?.[0])); + const right = $derived(ctx.xScale(brushState.x?.[1])); + + $effect(() => { + brushState.range = { + x: axis === 'both' || axis === 'x' ? left : 0, + y: axis === 'both' || axis === 'y' ? top : 0, + width: axis === 'both' || axis === 'x' ? right - left : ctx.width, + height: axis === 'both' || axis === 'y' ? bottom - top : ctx.height, + }; + brushState.handleSize = handleSize; }); - let isActive = $state(false); - - const brushContext = { - get xDomain() { - return xDomain!; - }, - set xDomain(v: DomainType) { - xDomain = v; - }, - get yDomain() { - return yDomain!; - }, - set yDomain(v: DomainType) { - yDomain = v; - }, - get isActive() { - return isActive; - }, - set isActive(v: boolean) { - isActive = v; - }, - get range() { - return _range; - }, - get handleSize() { - return handleSize; - }, - }; - - brushContextProp = brushContext; - - setBrushContext(brushContext); + // const _range = $derived({ + // x: axis === 'both' || axis === 'x' ? left : 0, + // y: axis === 'both' || axis === 'y' ? top : 0, + // width: axis === 'both' || axis === 'x' ? right - left : ctx.width, + // height: axis === 'both' || axis === 'y' ? bottom - top : ctx.height, + // }); + + // let isActive = $state(false); + + // const brushContext = { + // get xDomain() { + // return xDomain!; + // }, + // set xDomain(v: DomainType) { + // xDomain = v; + // }, + // get yDomain() { + // return yDomain!; + // }, + // set yDomain(v: DomainType) { + // yDomain = v; + // }, + // get isActive() { + // return isActive; + // }, + // set isActive(v: boolean) { + // isActive = v; + // }, + // get range() { + // return _range; + // }, + // get handleSize() { + // return handleSize; + // }, + // }; + + // brushContextProp = brushContext; + brushContextProp = brushState; + + // setBrushContext(brushContext); + setBrushContext(brushState); const logger = new Logger('BrushContext'); const RESET_THRESHOLD = 1; // size of pointer delta to ignore function handler( + /** Callback on pointer move */ fn: ( start: { - xDomain: [number, number]; - yDomain: [number, number]; + x: DomainType; + y: DomainType; value: { x: number; y: number }; }, value: { x: number; y: number } @@ -284,15 +332,21 @@ } const start = { - xDomain: [xDomain?.[0] ?? xDomainMin, xDomain?.[1] ?? xDomainMax] as [number, number], - yDomain: [yDomain?.[0] ?? yDomainMin, yDomain?.[1] ?? yDomainMax] as [number, number], + x: [ + brushState.x[0] ?? ctx.xScale.domain()[0], + brushState.x[1] ?? ctx.xScale.domain()[1], + ] as DomainType, + y: [ + brushState.y[0] ?? ctx.yScale.domain()[0], + brushState.y[1] ?? ctx.yScale.domain()[1], + ] as DomainType, value: { x: scaleInvert(ctx.xScale, startPoint?.x ?? 0), y: scaleInvert(ctx.yScale, startPoint?.y ?? 0), }, }; - onBrushStart({ xDomain, yDomain }); + onBrushStart({ brush: brushState }); const onPointerMove = (e: PointerEvent) => { const currentPoint = localPoint(e, rootEl); @@ -301,7 +355,7 @@ y: scaleInvert(ctx.yScale, currentPoint?.y ?? 0), }); - onChange({ xDomain, yDomain }); + onChange({ brush: brushState }); }; const onPointerUp = (e: PointerEvent) => { @@ -316,8 +370,8 @@ if ( (isClickOutside && xPointDelta < RESET_THRESHOLD && yPointDelta < RESET_THRESHOLD) || - _range.width < RESET_THRESHOLD || - _range.height < RESET_THRESHOLD + brushState.range.width < RESET_THRESHOLD || + brushState.range.height < RESET_THRESHOLD ) { // Clicked on frame, or pointer delta was less than threshold (default: 1px) if (ignoreResetClick) { @@ -325,24 +379,24 @@ } else { logger.debug('resetting due to frame click'); reset(); - onChange({ xDomain, yDomain }); + onChange({ brush: brushState }); } } else { logger.debug('drag end', { target: e.target, xPointDelta, yPointDelta, - rangeWidth: _range.width, - rangeHeight: _range.height, + rangeWidth: brushState.range.width, + rangeHeight: brushState.range.height, }); } - onBrushEnd({ xDomain, yDomain }); + onBrushEnd({ brush: brushState }); if (resetOnEnd) { if (ignoreResetClick) { // Still hide brush, but do not reset domain - brushContext.isActive = false; + brushState.active = false; } else { reset(); } @@ -359,9 +413,9 @@ const createRange = handler((start, value) => { logger.debug('createRange'); - brushContext.isActive = true; + brushState.active = true; - xDomain = [ + brushState.x = [ // @ts-expect-error clamp(min([start.value.x, value.x]), xDomainMin, xDomainMax), // @ts-expect-error @@ -369,7 +423,7 @@ ]; // xDomain = [start.value.x, value.x]; - yDomain = [ + brushState.y = [ // @ts-expect-error clamp(min([start.value.y, value.y]), yDomainMin, yDomainMax), // @ts-expect-error @@ -379,89 +433,84 @@ const adjustRange = handler((start, value) => { logger.debug('adjustRange'); - const dx = clamp( - value.x - start.value.x, - xDomainMin - start.xDomain[0], - xDomainMax - start.xDomain[1] - ); - xDomain = [add(start.xDomain[0], dx), add(start.xDomain[1], dx)]; - - const dy = clamp( - value.y - start.value.y, - yDomainMin - start.yDomain[0], - yDomainMax - start.yDomain[1] - ); - yDomain = [add(start.yDomain[0], dy), add(start.yDomain[1], dy)]; + const dx = clamp(value.x - start.value.x, xDomainMin - start.x[0], xDomainMax - start.x[1]); + brushState.x = [add(start.x[0], dx), add(start.x[1], dx)]; + + const dy = clamp(value.y - start.value.y, yDomainMin - start.y[0], yDomainMax - start.y[1]); + brushState.y = [add(start.y[0], dy), add(start.y[1], dy)]; }); const adjustTop = handler((start, value) => { logger.debug('adjustTop'); - 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), + brushState.y = [ + clamp(value.y < start.y[0] ? value.y : start.y[0], yDomainMin, yDomainMax), + clamp(value.y < start.y[0] ? start.y[0] : value.y, yDomainMin, yDomainMax), ]; }); const adjustBottom = handler((start, value) => { logger.debug('adjustBottom'); - 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), + brushState.y = [ + clamp(value.y > start.y[1] ? start.y[1] : value.y, yDomainMin, yDomainMax), + clamp(value.y > start.y[1] ? value.y : start.y[1], yDomainMin, yDomainMax), ]; }); const adjustLeft = handler((start, value) => { logger.debug('adjustLeft'); - 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), + brushState.x = [ + clamp(value.x > start.x[1] ? start.x[1] : value.x, xDomainMin, xDomainMax), + clamp(value.x > start.x[1] ? value.x : start.x[1], xDomainMin, xDomainMax), ]; }); const adjustRight = handler((start, value) => { logger.debug('adjustRight'); - 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), + brushState.x = [ + clamp(value.x < start.x[0] ? value.x : start.x[0], xDomainMin, xDomainMax), + clamp(value.x < start.x[0] ? start.x[0] : value.x, xDomainMin, xDomainMax), ]; }); function reset() { logger.debug('reset'); - brushContext.isActive = false; + brushState.active = false; - onReset({ xDomain, yDomain }); + onReset({ brush: brushState }); - xDomain = ogXDomain; - yDomain = ogYDomain; + // xDomain = ogXDomain; + // yDomain = ogYDomain; + // brushState.x = [ctx.xScale.domain()[0], ctx.xScale.domain()[1]]; + // brushState.y = [ctx.yScale.domain()[0], ctx.yScale.domain()[1]]; + brushState.x = [null, null]; + brushState.y = [null, null]; } function selectAll() { logger.debug('selectedAll'); - xDomain = [xDomainMin, xDomainMax]; - yDomain = [yDomainMin, yDomainMax]; + brushState.x = [xDomainMin, xDomainMax]; + brushState.y = [yDomainMin, yDomainMax]; } $effect.pre(() => { if (mode === 'separated') { // Set reactively to handle cases where xDomain/yDomain are set externally (ex. `bind:xDomain`) - const isXAxisActive = - xDomain?.[0]?.valueOf() !== originalXDomain?.[0]?.valueOf() || - xDomain?.[1]?.valueOf() !== originalXDomain?.[1]?.valueOf(); - - const isYAxisActive = - yDomain?.[0]?.valueOf() !== originalYDomain?.[0]?.valueOf() || - yDomain?.[1]?.valueOf() !== originalYDomain?.[1]?.valueOf(); - - const result = - axis === 'x' ? isXAxisActive : axis == 'y' ? isYAxisActive : isXAxisActive || isYAxisActive; - brushContext.isActive = result; + // TODO: Update + // const isXAxisActive = + // brushState.x[0]?.valueOf() !== originalXDomain?.[0]?.valueOf() || + // brushState.x[1]?.valueOf() !== originalXDomain?.[1]?.valueOf(); + // const isYAxisActive = + // brushState.y[0]?.valueOf() !== originalYDomain?.[0]?.valueOf() || + // brushState.y[1]?.valueOf() !== originalYDomain?.[1]?.valueOf(); + // const result = + // axis === 'x' ? isXAxisActive : axis == 'y' ? isYAxisActive : isXAxisActive || isYAxisActive; + // brushState.active = result; } }); {#if disabled} - {@render children?.({ brushContext })} + {@render children?.({ brushContext: brushState })} {:else} {@const handleClass = layerClass('brush-handle')} @@ -482,16 +531,16 @@ style:width="{ctx.containerWidth}px" style:height="{ctx.containerHeight}px" > - {@render children?.({ brushContext })} + {@render children?.({ brushContext: brushState })} - {#if brushContext.isActive} + {#if brushState.active}
{ e.stopPropagation(); - if (yDomain) { - yDomain[0] = yDomainMin; - onChange({ xDomain, yDomain }); + if (brushState.y[0]) { + brushState.y[0] = ctx.yScale.domain()[0]; + onChange({ brush: brushState }); } }} >
{ e.stopPropagation(); - if (yDomain) { - yDomain[1] = yDomainMax; - onChange({ xDomain, yDomain }); + if (brushState.y[1]) { + brushState.y[1] = ctx.yScale.domain()[1]; + onChange({ brush: brushState }); } }} >
@@ -559,10 +608,10 @@ {#if axis === 'both' || axis === 'x'}
{ e.stopPropagation(); - if (xDomain) { - xDomain[0] = xDomainMin; - onChange({ xDomain, yDomain }); + if (brushState.x[0]) { + brushState.x[0] = ctx.xScale.domain()[0]; + onChange({ brush: brushState }); } }} >
@@ -585,9 +634,9 @@
{ e.stopPropagation(); - if (xDomain) { - xDomain[1] = xDomainMax; - onChange({ xDomain: xDomain, yDomain: yDomain }); + if (brushState.x[1]) { + brushState.x[1] = ctx.xScale.domain()[1]; + onChange({ brush: brushState }); } }} >
diff --git a/packages/layerchart/src/lib/components/Chart.svelte b/packages/layerchart/src/lib/components/Chart.svelte index ffc617384..7d824edf4 100644 --- a/packages/layerchart/src/lib/components/Chart.svelte +++ b/packages/layerchart/src/lib/components/Chart.svelte @@ -38,7 +38,7 @@ import { unique } from '@layerstack/utils'; import { geoFitObjectTransform } from '$lib/utils/geo.js'; import TransformContext, { type TransformContextValue } from './TransformContext.svelte'; - import BrushContext, { type BrushContextValue } from './BrushContext.svelte'; + import BrushContext, { type BrushState } from './BrushContext.svelte'; import { layerClass } from '$lib/utils/attributes.js'; const defaultPadding = { top: 0, right: 0, bottom: 0, left: 0 }; @@ -152,7 +152,7 @@ radial: boolean; tooltip: TooltipContextValue; geo: GeoContextValue; - brush: BrushContextValue; + brush: BrushState; transform: TransformContextValue; }; @@ -1049,7 +1049,7 @@ let geoContext = $state(null!); let transformContext = $state(null!); let tooltipContext = $state(null!); - let brushContext = $state(null!); + let brushContext = $state(null!); const context: ChartContextValue = { get activeGetters() { diff --git a/packages/layerchart/src/lib/components/charts/AreaChart.svelte b/packages/layerchart/src/lib/components/charts/AreaChart.svelte index 2e2636f81..a94385e91 100644 --- a/packages/layerchart/src/lib/components/charts/AreaChart.svelte +++ b/packages/layerchart/src/lib/components/charts/AreaChart.svelte @@ -452,10 +452,10 @@ ? { axis: 'x', resetOnEnd: true, - xDomain, + x: xDomain, ...brushProps, onBrushEnd: (e) => { - xDomain = e.xDomain; + xDomain = e.brush.x; brushProps.onBrushEnd?.(e); }, } diff --git a/packages/layerchart/src/lib/components/charts/BarChart.svelte b/packages/layerchart/src/lib/components/charts/BarChart.svelte index a43652b7e..e2ad87867 100644 --- a/packages/layerchart/src/lib/components/charts/BarChart.svelte +++ b/packages/layerchart/src/lib/components/charts/BarChart.svelte @@ -460,11 +460,18 @@ ? { axis: 'x', resetOnEnd: true, - xDomain, + x: xDomain, ...brushProps, onBrushEnd: (e) => { // TOOD: This should set xRange instead of xDomain, and/or xDomain should be all values, not just bounds of brush range - xDomain = e.xDomain; + // const values = context?.xScale.domain() ?? []; + // console.log('domain', values, e.xDomain); + // const i0 = values?.indexOf(e.xDomain[0]); + // const i1 = values?.indexOf(e.xDomain[1]); + // xDomain = values.slice(i0, i1); + + xDomain = e.brush.x; + brushProps.onBrushEnd?.(e); }, } diff --git a/packages/layerchart/src/lib/components/charts/LineChart.svelte b/packages/layerchart/src/lib/components/charts/LineChart.svelte index de1944013..e382a3aa4 100644 --- a/packages/layerchart/src/lib/components/charts/LineChart.svelte +++ b/packages/layerchart/src/lib/components/charts/LineChart.svelte @@ -352,10 +352,10 @@ ? { axis: 'x', resetOnEnd: true, - xDomain, + x: xDomain, ...brushProps, onBrushEnd: (e) => { - xDomain = e.xDomain; + xDomain = e.brush.x; brushProps.onBrushEnd?.(e); }, } diff --git a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte b/packages/layerchart/src/lib/components/charts/ScatterChart.svelte index 2516c491e..deec38267 100644 --- a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte +++ b/packages/layerchart/src/lib/components/charts/ScatterChart.svelte @@ -262,12 +262,12 @@ ? { axis: 'both', resetOnEnd: true, - xDomain, - yDomain, + x: xDomain, + y: yDomain, ...brushProps, onBrushEnd: (e) => { - xDomain = e.xDomain; - yDomain = e.yDomain; + xDomain = e.brush.x; + yDomain = e.brush.y; brushProps.onBrushEnd?.(e); }, } diff --git a/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte b/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte index 4cb84c8e7..14f3068a8 100644 --- a/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte @@ -2,8 +2,8 @@ import { scaleBand, scaleOrdinal, scaleTime } from 'd3-scale'; import { range } from 'd3-array'; import { timeDay } from 'd3-time'; - import { State } from 'svelte-ux'; - import { format } from '@layerstack/utils'; + import { Button, State } from 'svelte-ux'; + import { endOfInterval, format } from '@layerstack/utils'; import { cls } from '@layerstack/tailwind'; import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'; @@ -66,21 +66,90 @@ value: 'integer', keys: ['value', 'baseline'], }); + + let xDomain2 = $state([null, null]);

Examples

- +

Basic

@@ -145,7 +214,7 @@ - {#if context.brush.isActive} + {#if context.brush.active} - {#if context.brush.isActive} + {#if context.brush.active} @@ -234,11 +303,11 @@ - {#if context.brush.isActive} + {#if context.brush.active} @@ -276,7 +345,7 @@ resetOnEnd: true, onBrushEnd: (e) => { // @ts-expect-error - set(e.xDomain); + set(e.brush.x); }, }} > @@ -315,7 +384,7 @@ resetOnEnd: true, onBrushEnd: (e) => { // @ts-expect-error - set(e.yDomain); + set(e.brush.y); }, }} > @@ -356,9 +425,9 @@ onBrushEnd: (e) => { set({ // @ts-expect-error - xDomain: e.xDomain, + xDomain: e.brush.x, // @ts-expect-error - yDomain: e.yDomain, + yDomain: e.brush.y, }); }, }} @@ -419,7 +488,7 @@ brush={{ onChange: (e) => { // @ts-expect-error - set(e.xDomain); + set(e.brush.x); }, }} > @@ -448,7 +517,7 @@ axis: 'y', onChange: (e) => { // @ts-expect-error - set(e.yDomain); + set(e.brush.y); }, }} > @@ -533,7 +602,7 @@ brush={{ onChange: (e) => { // @ts-expect-error - set(e.xDomain); + set(e.brush.x); }, }} > @@ -596,8 +665,8 @@ padding={{ left: 16 }} brush={{ mode: 'separated', - xDomain, - onChange: (e) => (xDomain = e.xDomain), + x: xDomain, + onChange: (e) => (xDomain = e.brush.x), onReset: (e) => (xDomain = null), }} > @@ -606,7 +675,6 @@ line={{ class: 'stroke-2 stroke-(--chart-color)' }} class="fill-(--chart-color) opacity-20" /> - @@ -634,7 +702,7 @@ resetOnEnd: true, onBrushEnd: (e) => { // @ts-expect-error - set(e.xDomain); + set(e.brush.x); }, }} > @@ -700,9 +768,9 @@ onChange: (e) => { set({ // @ts-expect-error - xDomain: e.xDomain, + xDomain: e.brush.x, // @ts-expect-error - yDomain: e.yDomain, + yDomain: e.brush.y, }); }, }} @@ -759,9 +827,9 @@ onBrushEnd: (e) => { set({ // @ts-expect-error - xDomain: e.xDomain, + xDomain: e.brush.x, // @ts-expect-error - yDomain: e.yDomain, + yDomain: e.brush.y, }); }, }} @@ -791,9 +859,9 @@ onChange: (e) => { set({ // @ts-expect-error - xDomain: e.xDomain, + xDomain: e.brush.x, // @ts-expect-error - yDomain: e.yDomain, + yDomain: e.brush.y, }); }, }} From d3947458d1e4eefb3391417cc0480f35dac2c478 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 11 Sep 2025 07:52:45 -0400 Subject: [PATCH 02/20] Add missing import after merge --- .../src/routes/docs/components/BrushContext/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte b/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte index b182ad309..735be6126 100644 --- a/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte @@ -1,5 +1,5 @@ @@ -48,7 +48,6 @@ import { cls } from '@layerstack/tailwind'; import Axis from '../Axis.svelte'; - import BrushContext from '../BrushContext.svelte'; import Chart from '../Chart.svelte'; import ChartAnnotations from './ChartAnnotations.svelte'; import ChartClipPath from '../ChartClipPath.svelte'; @@ -61,9 +60,10 @@ import Rule from '../Rule.svelte'; import * as Tooltip from '../tooltip/index.js'; - import { accessor, chartDataArray, defaultChartPadding } from '../../utils/common.js'; + import { chartDataArray, defaultChartPadding } from '../../utils/common.js'; import { asAny } from '../../utils/types.js'; import { createLegendProps, SeriesState } from './utils.svelte.js'; + import type { BrushDomainType } from '../BrushContext.svelte'; let { data = [], @@ -246,8 +246,8 @@ ? { axis: 'both', resetOnEnd: true, - x: xDomain, - y: yDomain, + x: xDomain as BrushDomainType, + y: yDomain as BrushDomainType, ...brushProps, onBrushEnd: (e) => { xDomain = e.brush.x; diff --git a/packages/layerchart/src/lib/components/charts/types.ts b/packages/layerchart/src/lib/components/charts/types.ts index e3120fc1f..913f8f881 100644 --- a/packages/layerchart/src/lib/components/charts/types.ts +++ b/packages/layerchart/src/lib/components/charts/types.ts @@ -137,12 +137,6 @@ export type BaseChartProps< */ y?: Accessor; - xScale?: AnyScale; - /** - * The x domain to be used for the chart. - * - */ - xDomain?: ComponentProps['xDomain']; /** * Use radial instead of cartesian coordinates, mapping `x` to `angle` and `y`` to * radial. Radial lines are positioned relative to the origin, use transform @@ -151,18 +145,21 @@ export type BaseChartProps< * @default false */ radial?: boolean; + /** * The series data to be used for the chart. * * @default [{ key: 'default', value: y, color: 'var(--color-primary)' }] */ series?: SeriesData[]; + /** * The layout of the series. * * @default 'overlap' */ seriesLayout?: 'overlap' | 'stack' | 'stackExpand' | 'stackDiverging'; + /** * The axis to be used for the chart. * @@ -174,6 +171,7 @@ export type BaseChartProps< | 'y' | boolean | SimplifiedChartSnippet; + /** * The brush to be used for the chart. * @@ -194,6 +192,7 @@ export type BaseChartProps< * @default false */ labels?: ComponentProps> | boolean | ChartSnippet; + /** * The legend to be used for the chart. * diff --git a/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte b/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte index 98dbed674..99ba2add5 100644 --- a/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte @@ -1289,7 +1289,7 @@ x="date" y="value" {xDomain} - brush={{ onBrushEnd: (e) => (xDomain = e.xDomain) }} + brush={{ onBrushEnd: (e) => (xDomain = e.brush.x) }} props={{ area: { motion: { type: 'tween', duration: 200 } }, xAxis: { motion: { type: 'tween', duration: 200 }, tickMultiline: true }, @@ -1304,8 +1304,7 @@ data={denseDateSeriesData2} x="date" y="value" - {xDomain} - brush={{ onBrushEnd: (e) => (xDomain = e.xDomain) }} + brush={{ onBrushEnd: (e) => (xDomain = e.brush.x) }} props={{ area: { motion: { type: 'tween', duration: 200 } }, xAxis: { motion: { type: 'tween', duration: 200 }, tickMultiline: true }, diff --git a/packages/layerchart/src/routes/docs/components/Axis/+page.svelte b/packages/layerchart/src/routes/docs/components/Axis/+page.svelte index 405bbeebf..3c5ca0d88 100644 --- a/packages/layerchart/src/routes/docs/components/Axis/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Axis/+page.svelte @@ -848,7 +848,7 @@ brush={{ resetOnEnd: true, onBrushEnd: (e) => { - xDomain = asAny(e.xDomain); + xDomain = asAny(e.brush.x); }, }} > @@ -865,9 +865,9 @@ padding={{ top: 20, bottom: 20, left: 20, right: 20 }} brush={{ mode: 'separated', - xDomain, + x: xDomain, onChange: (e) => { - xDomain = asAny(e.xDomain); + xDomain = asAny(e.brush.x); }, }} > @@ -901,7 +901,7 @@ brush={{ resetOnEnd: true, onBrushEnd: (e) => { - xDomain = asAny(e.xDomain); + xDomain = asAny(e.brush.x); }, }} > @@ -918,9 +918,9 @@ padding={{ top: 20, bottom: 20, left: 20, right: 20 }} brush={{ mode: 'separated', - xDomain, + x: xDomain, onChange: (e) => { - xDomain = asAny(e.xDomain); + xDomain = asAny(e.brush.x); }, }} > diff --git a/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte b/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte index 735be6126..9f59032a0 100644 --- a/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte @@ -28,15 +28,13 @@ import Preview from '$lib/docs/Preview.svelte'; import { createDateSeries, randomWalk } from '$lib/utils/genData.js'; import { asAny } from '$lib/utils/types.js'; - import type { DomainType } from '$lib/utils/scales.svelte.js'; + import type { BrushDomainType } from '$lib/components/BrushContext.svelte'; import { shared } from '../../shared.svelte.js'; let { data } = $props(); const now = new Date(); - let xDomain = $state([timeDay.offset(now, -60), timeDay.offset(now, -30)]) as - | DomainType - | undefined; + let xDomain = $state([timeDay.offset(now, -60), timeDay.offset(now, -30)]) as BrushDomainType; const seriesData = [ randomWalk({ count: 100 }).map((value, i) => ({ @@ -69,7 +67,7 @@ keys: ['value', 'baseline'], }); - let xDomain2 = $state([null, null]); + let xDomain2 = $state([null, null]);

Examples

@@ -114,6 +112,7 @@ {@const width = context.xScale(end) - x} {@const height = context.height} +
-

Band scale

+

Band scale (WIP)

@@ -638,7 +637,7 @@ mode: 'separated', x: xDomain, onChange: (e) => (xDomain = e.brush.x), - onReset: (e) => (xDomain = null), + onReset: (e) => (xDomain = [null, null]), }} > @@ -824,8 +823,8 @@ brush={{ axis: 'both', mode: 'separated', - xDomain: value?.xDomain, - yDomain: value?.yDomain, + x: value?.xDomain, + y: value?.yDomain, onChange: (e) => { set({ // @ts-expect-error diff --git a/packages/layerchart/src/routes/docs/examples/Candlestick/+page.svelte b/packages/layerchart/src/routes/docs/examples/Candlestick/+page.svelte index eb1623753..965dbbda9 100644 --- a/packages/layerchart/src/routes/docs/examples/Candlestick/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Candlestick/+page.svelte @@ -108,7 +108,7 @@ yNice brush={{ onChange: (e) => { - xDomain = e.xDomain; + xDomain = e.brush.x; }, }} > From d9c45a4c3373bebff5cac60acf8668dc8e59976e Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 12 Sep 2025 08:15:07 -0400 Subject: [PATCH 04/20] Update @layerstack/utils with improved clamp() types. Fix remaining type warnings --- packages/layerchart/package.json | 2 +- .../src/lib/components/BrushContext.svelte | 35 +++++++++---------- packages/layerchart/src/lib/utils/types.ts | 10 ++++++ pnpm-lock.yaml | 16 +++++++-- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/packages/layerchart/package.json b/packages/layerchart/package.json index d9b9b5235..45ee414e1 100644 --- a/packages/layerchart/package.json +++ b/packages/layerchart/package.json @@ -84,7 +84,7 @@ "@layerstack/svelte-actions": "1.0.1-next.14", "@layerstack/svelte-state": "0.1.0-next.19", "@layerstack/tailwind": "2.0.0-next.17", - "@layerstack/utils": "2.0.0-next.14", + "@layerstack/utils": "2.0.0-next.15", "d3-array": "^3.2.4", "d3-color": "^3.1.0", "d3-delaunay": "^6.0.4", diff --git a/packages/layerchart/src/lib/components/BrushContext.svelte b/packages/layerchart/src/lib/components/BrushContext.svelte index 06d8c9d50..e4af0d9ea 100644 --- a/packages/layerchart/src/lib/components/BrushContext.svelte +++ b/packages/layerchart/src/lib/components/BrushContext.svelte @@ -1,4 +1,5 @@

Examples

diff --git a/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte index 911ed1ec2..5825ddd85 100644 --- a/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte @@ -13,7 +13,7 @@ Graticule, Layer, Tooltip, - type ChartContextValue, + type ChartState, } from 'layerchart'; import { Button, ButtonGroup } from 'svelte-ux'; import { sortFunc } from '@layerstack/utils'; @@ -31,7 +31,7 @@ const countries = feature(data.geojson, data.geojson.objects.countries); - let context = $state(null!); + let context = $state(null!); let selectedFeature: (typeof countries.features)[0] | null = $state(null); diff --git a/packages/layerchart/src/routes/docs/examples/EarthquakeGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/EarthquakeGlobe/+page.svelte index 3875fcb7d..c3bee5e41 100644 --- a/packages/layerchart/src/routes/docs/examples/EarthquakeGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/EarthquakeGlobe/+page.svelte @@ -13,7 +13,7 @@ Graticule, Layer, Tooltip, - type ChartContextValue, + type ChartState, } from 'layerchart'; import Preview from '$lib/docs/Preview.svelte'; @@ -24,7 +24,7 @@ const countries = feature(data.geojson, data.geojson.objects.countries); - let context = $state>(); + let context = $state>(); let velocity = $state(3); diff --git a/packages/layerchart/src/routes/docs/examples/EclipsesGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/EclipsesGlobe/+page.svelte index aee9bc7ae..e9a9b10fa 100644 --- a/packages/layerchart/src/routes/docs/examples/EclipsesGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/EclipsesGlobe/+page.svelte @@ -5,15 +5,7 @@ import { interpolateGreens, interpolatePurples } from 'd3-scale-chromatic'; import { feature } from 'topojson-client'; - import { - Chart, - GeoPath, - Graticule, - Legend, - Layer, - Tooltip, - type ChartContextValue, - } from 'layerchart'; + import { Chart, GeoPath, Graticule, Legend, Layer, Tooltip, type ChartState } from 'layerchart'; import { Button, ButtonGroup, Field, RangeField } from 'svelte-ux'; import { format } from '@layerstack/utils'; @@ -27,7 +19,7 @@ const countries = feature(data.geojson, data.geojson.objects.countries); const eclipses = feature(data.eclipses, data.eclipses.objects.eclipses); - let context = $state(null!); + let context = $state(null!); let velocity = $state(3); const timer = new TimerState({ diff --git a/packages/layerchart/src/routes/docs/examples/Pack/+page.svelte b/packages/layerchart/src/routes/docs/examples/Pack/+page.svelte index cca61c884..ef9bdcba6 100644 --- a/packages/layerchart/src/routes/docs/examples/Pack/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Pack/+page.svelte @@ -14,7 +14,7 @@ Layer, Text, findAncestor, - type ChartContextValue, + type ChartState, } from 'layerchart'; import { Breadcrumb, Button, Field, RangeField, ToggleGroup, ToggleOption } from 'svelte-ux'; import { format, sortFunc } from '@layerstack/utils'; @@ -32,7 +32,7 @@ let padding = $state(3); let nodes = $state.raw[]>([]); let selected = $state.raw>(); - let context = $state(null!); + let context = $state(null!); $effect(() => { if (context?.transform && selected) { diff --git a/packages/layerchart/src/routes/docs/examples/SketchyGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/SketchyGlobe/+page.svelte index d2c002338..3ed2ffb99 100644 --- a/packages/layerchart/src/routes/docs/examples/SketchyGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/SketchyGlobe/+page.svelte @@ -4,7 +4,7 @@ import { feature } from 'topojson-client'; import { presimplify, simplify } from 'topojson-simplify'; - import { Chart, GeoPath, Graticule, Layer, type ChartContextValue } from 'layerchart'; + import { Chart, GeoPath, Graticule, Layer, type ChartState } from 'layerchart'; import { Button, ButtonGroup, Field, RangeField } from 'svelte-ux'; import { TimerState } from '@layerstack/svelte-state'; @@ -19,7 +19,7 @@ const geojson = $derived(simplify(presimplify(data.geojson), Math.pow(10, 2 - minArea))); const land = $derived(feature(geojson, data.geojson.objects.land)); - let context = $state(null!); + let context = $state(null!); let velocity = $state(1); const timer = new TimerState({ diff --git a/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.svelte index b2223df77..7f797a072 100644 --- a/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/SubmarineCablesGlobe/+page.svelte @@ -14,7 +14,7 @@ Graticule, Layer, Tooltip, - type ChartContextValue, + type ChartState, } from 'layerchart'; import Preview from '$lib/docs/Preview.svelte'; @@ -24,7 +24,7 @@ const countries = feature(data.geojson, data.geojson.objects.countries); - let context = $state(); + let context = $state(); let velocity = $state(3); const timer = new TimerState({ diff --git a/packages/layerchart/src/routes/docs/examples/TranslucentGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/TranslucentGlobe/+page.svelte index 1cb031df3..222372bcc 100644 --- a/packages/layerchart/src/routes/docs/examples/TranslucentGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/TranslucentGlobe/+page.svelte @@ -9,7 +9,7 @@ Graticule, Layer, Tooltip, - type ChartContextValue, + type ChartState, } from 'layerchart'; import { Button, ButtonGroup, Field, RangeField } from 'svelte-ux'; import { TimerState } from '@layerstack/svelte-state'; @@ -20,7 +20,7 @@ const countries = feature(data.geojson, data.geojson.objects.countries); - let context = $state(); + let context = $state(); let velocity = $state(3); const timer = new TimerState({ From 4e0b0de089f131d93da965ae27edad9c3b0a9629 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 19 Oct 2025 01:35:58 -0400 Subject: [PATCH 11/20] Fix prop reactivity (brushing, etc) --- .../src/lib/components/Chart.svelte | 4 +- .../layerchart/src/lib/states/chart.svelte.ts | 62 ++++++++++--------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/packages/layerchart/src/lib/components/Chart.svelte b/packages/layerchart/src/lib/components/Chart.svelte index 61a111a14..b1b5310c1 100644 --- a/packages/layerchart/src/lib/components/Chart.svelte +++ b/packages/layerchart/src/lib/components/Chart.svelte @@ -636,11 +636,11 @@ brush, } = $derived(props); - const chartState = new ChartState({ + const chartState = new ChartState(() => ({ ref: refProp, context: contextProp, ...props, - }); + })); let ref = $state(); diff --git a/packages/layerchart/src/lib/states/chart.svelte.ts b/packages/layerchart/src/lib/states/chart.svelte.ts index df9b397a3..775987798 100644 --- a/packages/layerchart/src/lib/states/chart.svelte.ts +++ b/packages/layerchart/src/lib/states/chart.svelte.ts @@ -35,8 +35,11 @@ export class ChartState< XScale extends AnyScale = AnyScale, YScale extends AnyScale = AnyScale, > { - // Props - initialized via constructor parameter - props!: ChartPropsWithoutHTML; + // Props getter function - set in constructor + private _propsGetter!: () => ChartPropsWithoutHTML; + + // Props - accessed via getter function for fine-grained reactivity + props = $derived(this._propsGetter()); // Context references geoContext = $state(null!); @@ -57,9 +60,10 @@ export class ChartState< // Meta data (mutable from context) meta = $state>({}); - constructor(props: ChartPropsWithoutHTML) { - this.props = props; - this.meta = props.meta ?? {}; + constructor(propsGetter: () => ChartPropsWithoutHTML) { + this._propsGetter = propsGetter; + const initialProps = propsGetter(); + this.meta = initialProps.meta ?? {}; } // Use $derived fields instead of getters for caching @@ -442,27 +446,29 @@ export class ChartState< return this.transformContext; } - config = $derived({ - x: this.props.x, - y: this.props.y, - z: this.props.z, - r: this.props.r, - c: this.props.c, - x1: this.props.x1, - y1: this.props.y1, - xDomain: this._xDomain, - yDomain: this._yDomain, - zDomain: this.props.zDomain, - rDomain: this.props.rDomain, - x1Domain: this.props.x1Domain, - y1Domain: this.props.y1Domain, - cDomain: this.props.cDomain, - xRange: this.props.xRange, - yRange: this.props.yRange, - zRange: this.props.zRange, - rRange: this.props.rRange, - cRange: this.props.cRange, - x1Range: this.props.x1Range, - y1Range: this.props.y1Range, - }); + get config() { + return { + x: this.props.x, + y: this.props.y, + z: this.props.z, + r: this.props.r, + c: this.props.c, + x1: this.props.x1, + y1: this.props.y1, + xDomain: this._xDomain, + yDomain: this._yDomain, + zDomain: this.props.zDomain, + rDomain: this.props.rDomain, + x1Domain: this.props.x1Domain, + y1Domain: this.props.y1Domain, + cDomain: this.props.cDomain, + xRange: this.props.xRange, + yRange: this.props.yRange, + zRange: this.props.zRange, + rRange: this.props.rRange, + cRange: this.props.cRange, + x1Range: this.props.x1Range, + y1Range: this.props.y1Range, + }; + } } From 3e320ec1cff210d6d5c2bf1877594849f26e825a Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 19 Oct 2025 01:48:19 -0400 Subject: [PATCH 12/20] Cleanup --- .../src/lib/components/Chart.svelte | 70 +------------------ .../layerchart/src/lib/states/chart.svelte.ts | 54 ++++++++++++-- 2 files changed, 48 insertions(+), 76 deletions(-) diff --git a/packages/layerchart/src/lib/components/Chart.svelte b/packages/layerchart/src/lib/components/Chart.svelte index b1b5310c1..922189958 100644 --- a/packages/layerchart/src/lib/components/Chart.svelte +++ b/packages/layerchart/src/lib/components/Chart.svelte @@ -1,12 +1,10 @@ -{@render children({ - geoContext, +{@render props.children({ + geoState, })} diff --git a/packages/layerchart/src/lib/contexts/geo.ts b/packages/layerchart/src/lib/contexts/geo.ts index c1f9947cc..72cb165d7 100644 --- a/packages/layerchart/src/lib/contexts/geo.ts +++ b/packages/layerchart/src/lib/contexts/geo.ts @@ -1,19 +1,17 @@ import { Context } from 'runed'; -import { type GeoProjection } from 'd3-geo'; +import type { GeoState } from '$lib/states/geo.svelte.js'; -export type GeoContextValue = { - projection: GeoProjection | undefined; -}; +export type { GeoState }; /** * Access or set the current GeoContext. */ -const _GeoContext = new Context('GeoContext'); +const _GeoContext = new Context('GeoContext'); export function getGeoContext() { - return _GeoContext.getOr({ projection: undefined } as GeoContextValue); + return _GeoContext.getOr({ projection: undefined } as GeoState); } -export function setGeoContext(geo: GeoContextValue) { +export function setGeoContext(geo: GeoState) { return _GeoContext.set(geo); } diff --git a/packages/layerchart/src/lib/states/chart.svelte.ts b/packages/layerchart/src/lib/states/chart.svelte.ts index 1fbf4c157..e9daca869 100644 --- a/packages/layerchart/src/lib/states/chart.svelte.ts +++ b/packages/layerchart/src/lib/states/chart.svelte.ts @@ -19,7 +19,7 @@ import { filterObject } from '$lib/utils/filterObject.js'; import { calcDomain, calcScaleExtents, createGetter, createChartScale } from '$lib/utils/chart.js'; import { printDebug } from '$lib/utils/debug.js'; -import type { GeoContextValue } from '$lib/contexts/geo.js'; +import type { GeoState } from '$lib/contexts/geo.js'; import type { TransformContextValue } from '$lib/contexts/transform.js'; import type { TooltipContextValue } from '$lib/contexts/tooltip.js'; import type { BrushContextValue } from '$lib/components/BrushContext.svelte'; @@ -43,7 +43,7 @@ export class ChartState< props = $derived(this._propsGetter()); // Context references - geoContext = $state(null!); + geoContext = $state(null!); transformContext = $state(null!); tooltipContext = $state(null!); brushContext = $state(null!); diff --git a/packages/layerchart/src/lib/states/geo.svelte.ts b/packages/layerchart/src/lib/states/geo.svelte.ts new file mode 100644 index 000000000..19266e275 --- /dev/null +++ b/packages/layerchart/src/lib/states/geo.svelte.ts @@ -0,0 +1,111 @@ +import type { GeoProjection } from 'd3-geo'; +import type { GeoContextProps } from '$lib/components/GeoContext.svelte'; + +export class GeoState { + // Props getter function - set in constructor + private _propsGetter!: () => GeoContextProps; + + // Props - accessed via getter function for fine-grained reactivity + props = $derived(this._propsGetter()); + + // Context references + chartWidth = $state(100); + chartHeight = $state(100); + transformScale = $state(1); + transformTranslateX = $state(0); + transformTranslateY = $state(0); + + // The actual projection instance + projection = $state(undefined); + + constructor(propsGetter: () => GeoContextProps) { + this._propsGetter = propsGetter; + + // Main effect to build and configure the projection + $effect.pre(() => { + if (!this.props.projection) return; + + const _projection = this.props.projection(); + + // Apply fitSize if fitGeojson is provided + if (this.props.fitGeojson && 'fitSize' in _projection) { + _projection.fitSize(this.fitSizeRange, this.props.fitGeojson); + } + + // Apply scale + if ('scale' in _projection) { + if (this.props.scale) { + _projection.scale(this.props.scale); + } + + if (this.props.applyTransform?.includes('scale')) { + _projection.scale(this.transformScale); + } + } + + // Apply rotate + if ('rotate' in _projection) { + if (this.props.rotate) { + _projection.rotate([ + this.props.rotate.yaw, + this.props.rotate.pitch, + this.props.rotate.roll, + ]); + } + + if (this.props.applyTransform?.includes('rotate')) { + _projection.rotate([ + this.transformTranslateX, // yaw + this.transformTranslateY, // pitch + // TODO: `roll` from `transformContext`? + ]); + } + } + + // Apply translate + if ('translate' in _projection) { + if (this.props.translate) { + _projection.translate(this.props.translate); + } + + if (this.props.applyTransform?.includes('translate')) { + _projection.translate([this.transformTranslateX, this.transformTranslateY]); + } + } + + // Apply center + if (this.props.center && 'center' in _projection) { + _projection.center(this.props.center); + } + + // Apply reflectX + if (this.props.reflectX) { + _projection.reflectX(this.props.reflectX); + } + + // Apply reflectY + if (this.props.reflectY) { + _projection.reflectY(this.props.reflectY); + } + + // Apply clipAngle + if (this.props.clipAngle && 'clipAngle' in _projection) { + _projection.clipAngle(this.props.clipAngle); + } + + // Apply clipExtent + if (this.props.clipExtent && 'clipExtent' in _projection) { + _projection.clipExtent(this.props.clipExtent); + } + + this.projection = _projection; + }); + } + + // Derived properties + fitSizeRange = $derived( + this.props.fixedAspectRatio + ? [100, 100 / this.props.fixedAspectRatio] + : [this.chartWidth, this.chartHeight] + ) as [number, number]; +} From 5ac9391a8c898189524ea6dc5d49548b1da26dc0 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 20 Oct 2025 08:28:20 -0400 Subject: [PATCH 15/20] Fix import --- packages/layerchart/src/lib/components/Chart.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/layerchart/src/lib/components/Chart.svelte b/packages/layerchart/src/lib/components/Chart.svelte index cad0fd211..b72d1c7ab 100644 --- a/packages/layerchart/src/lib/components/Chart.svelte +++ b/packages/layerchart/src/lib/components/Chart.svelte @@ -21,7 +21,7 @@ import BrushContext from './BrushContext.svelte'; import { setChartContext } from '$lib/contexts/chart.js'; - import { ChartState } from 'layerchart/states/chart.svelte.js'; + import { ChartState } from '$lib/states/chart.svelte.js'; export type ChartResizeDetail = { width: number; From 9a863be44c6762e7951f205c78d66d6c32b00dc0 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Wed, 12 Nov 2025 23:03:17 -0500 Subject: [PATCH 16/20] Update .gitignore --- docs/.gitignore | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/.gitignore b/docs/.gitignore index 0f7fc9912..45e973bb2 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -24,4 +24,12 @@ vite.config.js.timestamp-* vite.config.ts.timestamp-* # Content Collections -.content-collections \ No newline at end of file +.content-collections + +# Generated API documentation +src/generated/api.json +src/generated/api/*.json +!src/generated/api/README.md + +# Generated StackBlitz files +static/stackblitz-files.json \ No newline at end of file From d74ff78032b678d9822452cc7256534d2e60ec90 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Wed, 12 Nov 2025 23:28:04 -0500 Subject: [PATCH 17/20] Fix type --- .../src/routes/docs/examples/Treemap/+page.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/layerchart/src/routes/docs/examples/Treemap/+page.svelte b/packages/layerchart/src/routes/docs/examples/Treemap/+page.svelte index a3c690cb8..59d705451 100644 --- a/packages/layerchart/src/routes/docs/examples/Treemap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Treemap/+page.svelte @@ -44,7 +44,7 @@ .sum((d) => d.value) .sort(sortFunc('value', 'desc')); - let selectedCarNode = $state>(); + let selectedCarNode = $state>(); let isFiltered = $state(false); const groupedCars = $derived( @@ -70,7 +70,7 @@ // d => d.year, ) ); - let groupedHierarchy = $state>(); + let groupedHierarchy = $state>(hierarchy(groupedCars).count()); $effect.pre(() => { untrack(() => { selectedCarNode = groupedHierarchy; @@ -78,7 +78,7 @@ }); $effect.pre(() => { - groupedHierarchy = hierarchy(groupedCars).count() as HierarchyRectangularNode; + groupedHierarchy = hierarchy(groupedCars).count() as HierarchyNode; }); let tile: ComponentProps['tile'] = $state('squarify'); From 3fe2d4877746374c10c561603efa7b4e97499c39 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 27 Nov 2025 23:48:16 -0500 Subject: [PATCH 18/20] Extract TransformState from TransformContext component --- .../lib/components/TransformContext.svelte | 346 +++--------------- packages/layerchart/src/lib/contexts/index.ts | 1 + .../layerchart/src/lib/contexts/transform.ts | 106 +----- .../layerchart/src/lib/states/chart.svelte.ts | 4 +- .../src/lib/states/transform.svelte.ts | 322 ++++++++++++++-- .../examples/EarthquakeGlobe/+page.svelte | 2 +- .../docs/examples/EclipsesGlobe/+page.svelte | 2 +- .../docs/examples/SketchyGlobe/+page.svelte | 2 +- .../SubmarineCablesGlobe/+page.svelte | 2 +- .../examples/TranslucentGlobe/+page.svelte | 2 +- 10 files changed, 359 insertions(+), 430 deletions(-) diff --git a/packages/layerchart/src/lib/components/TransformContext.svelte b/packages/layerchart/src/lib/components/TransformContext.svelte index 6b8e578ff..1c449197a 100644 --- a/packages/layerchart/src/lib/components/TransformContext.svelte +++ b/packages/layerchart/src/lib/components/TransformContext.svelte @@ -1,74 +1,27 @@
- {@render children?.({ transformContext: transformContext })} + {@render children?.({ transformState })}