- {@render children?.({ brushContext })}
+ {@render children?.({ brushContext: brushState })}
- {#if brushContext.isActive}
+ {#if brushState.active}
reset()}
@@ -498,36 +446,36 @@
{#if axis === 'both' || axis === 'y'}
{
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 });
}
}}
>
@@ -536,36 +484,36 @@
{#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 });
}
}}
>
{
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 cce083856..87d103b9f 100644
--- a/packages/layerchart/src/lib/components/Chart.svelte
+++ b/packages/layerchart/src/lib/components/Chart.svelte
@@ -40,7 +40,8 @@
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 from './BrushContext.svelte';
+ import { type BrushState } from '$lib/states/brush.svelte.js';
import type { TimeInterval } from 'd3-time';
const defaultPadding = { top: 0, right: 0, bottom: 0, left: 0 };
@@ -156,7 +157,7 @@
radial: boolean;
tooltip: TooltipContextValue
;
geo: GeoContextValue;
- brush: BrushContextValue;
+ brush: BrushState;
transform: TransformContextValue;
};
@@ -1092,7 +1093,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 61d1e4c9c..6c04e8b2f 100644
--- a/packages/layerchart/src/lib/components/charts/AreaChart.svelte
+++ b/packages/layerchart/src/lib/components/charts/AreaChart.svelte
@@ -97,6 +97,7 @@
import { setTooltipMetaContext } from '../tooltip/tooltipMetaContext.js';
import DefaultTooltip from './DefaultTooltip.svelte';
import ChartAnnotations from './ChartAnnotations.svelte';
+ import type { BrushDomainType } from '../../states/brush.svelte.js';
let {
data = [],
@@ -423,10 +424,10 @@
? {
axis: 'x',
resetOnEnd: true,
- xDomain,
+ x: xDomain as BrushDomainType,
...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 e035095e9..f995f4a75 100644
--- a/packages/layerchart/src/lib/components/charts/BarChart.svelte
+++ b/packages/layerchart/src/lib/components/charts/BarChart.svelte
@@ -114,6 +114,7 @@
import { setTooltipMetaContext } from '../tooltip/tooltipMetaContext.js';
import DefaultTooltip from './DefaultTooltip.svelte';
import ChartAnnotations from './ChartAnnotations.svelte';
+ import type { BrushDomainType } from '../../states/brush.svelte.js';
let {
data = [],
@@ -458,11 +459,18 @@
? {
axis: 'x',
resetOnEnd: true,
- xDomain,
+ x: xDomain as BrushDomainType,
...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 4c296de25..6152eb1ea 100644
--- a/packages/layerchart/src/lib/components/charts/LineChart.svelte
+++ b/packages/layerchart/src/lib/components/charts/LineChart.svelte
@@ -104,6 +104,7 @@
import DefaultTooltip from './DefaultTooltip.svelte';
import ChartAnnotations from './ChartAnnotations.svelte';
import { isScaleTime } from '../../utils/scales.svelte.js';
+ import type { BrushDomainType } from '../../states/brush.svelte.js';
let {
data = [],
@@ -342,10 +343,10 @@
? {
axis: 'x',
resetOnEnd: true,
- xDomain,
+ x: xDomain as BrushDomainType,
...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 c15a46ed9..5ce19d872 100644
--- a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte
+++ b/packages/layerchart/src/lib/components/charts/ScatterChart.svelte
@@ -38,8 +38,8 @@
'radial'
> & {
props?: ScatterChartPropsObjProp;
- yDomain?: ComponentProps['yDomain'];
- yScale?: AnyScale;
+ // yDomain?: ComponentProps['yDomain'];
+ // yScale?: AnyScale;
};
@@ -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';
@@ -65,6 +64,7 @@
import { asAny } from '../../utils/types.js';
import { SeriesState } from '$lib/states/series.svelte.js';
import { createLegendProps } from './utils.svelte.js';
+ import type { BrushDomainType } from '../../states/brush.svelte.js';
let {
data = [],
@@ -235,12 +235,12 @@
? {
axis: 'both',
resetOnEnd: true,
- xDomain,
- yDomain,
+ x: xDomain as BrushDomainType,
+ y: yDomain as BrushDomainType,
...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/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/lib/states/brush.svelte.ts b/packages/layerchart/src/lib/states/brush.svelte.ts
new file mode 100644
index 000000000..33f426ce8
--- /dev/null
+++ b/packages/layerchart/src/lib/states/brush.svelte.ts
@@ -0,0 +1,58 @@
+import { getChartContext } from '$lib/components/Chart.svelte';
+
+// TODO: Should we support the full `DomainType` (`string`, etc)
+// type BrushDomainType = NonNullable;
+export type BrushDomainType = Array;
+
+export type BrushRange = {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+};
+
+export class BrushState {
+ ctx: ReturnType | null;
+
+ x = $state([null, null]);
+ y = $state([null, null]);
+ active = $state();
+ axis = $state<'x' | 'y' | 'both'>('x');
+ handleSize = $state(0);
+
+ constructor(
+ ctx: typeof this.ctx,
+ options?: {
+ x?: BrushDomainType;
+ y?: BrushDomainType;
+ active?: boolean;
+ axis?: 'x' | 'y' | 'both';
+ }
+ ) {
+ this.ctx = ctx;
+
+ this.x = options?.x ?? [null, null];
+ this.y = options?.y ?? [null, null];
+ // this.active = options?.active ?? (this.x !== null || this.y !== null);
+ this.active = options?.active;
+ this.axis = options?.axis ?? 'x';
+ }
+
+ get range() {
+ if (!this.ctx) {
+ return { x: 0, y: 0, width: 0, height: 0 };
+ }
+
+ const top = this.ctx.yScale(this.y?.[1]);
+ const bottom = this.ctx.yScale(this.y?.[0]);
+ const left = this.ctx.xScale(this.x?.[0]);
+ const right = this.ctx.xScale(this.x?.[1]);
+
+ return {
+ x: this.axis === 'both' || this.axis === 'x' ? left : 0,
+ y: this.axis === 'both' || this.axis === 'y' ? top : 0,
+ width: this.axis === 'both' || this.axis === 'x' ? right - left : this.ctx.width,
+ height: this.axis === 'both' || this.axis === 'y' ? bottom - top : this.ctx.height,
+ };
+ }
+}
diff --git a/packages/layerchart/src/lib/states/series.svelte.ts b/packages/layerchart/src/lib/states/series.svelte.ts
index 8cbe491be..fd1346e88 100644
--- a/packages/layerchart/src/lib/states/series.svelte.ts
+++ b/packages/layerchart/src/lib/states/series.svelte.ts
@@ -3,14 +3,6 @@ import type { SeriesData } from '../components/charts/types.js';
import { SelectionState } from '@layerstack/svelte-state';
-class HighlightKey {
- current = $state['key'] | null>(null);
-
- set = (seriesKey: typeof this.current) => {
- this.current = seriesKey;
- };
-}
-
export class SeriesState {
#series = $state.raw[]>([]);
selectedKeys = new SelectionState();
@@ -68,3 +60,11 @@ export class SeriesState {
>;
}
}
+
+class HighlightKey {
+ current = $state['key'] | null>(null);
+
+ set = (seriesKey: typeof this.current) => {
+ this.current = seriesKey;
+ };
+}
diff --git a/packages/layerchart/src/lib/utils/types.ts b/packages/layerchart/src/lib/utils/types.ts
index dfb41385d..cf1f3a714 100644
--- a/packages/layerchart/src/lib/utils/types.ts
+++ b/packages/layerchart/src/lib/utils/types.ts
@@ -19,12 +19,22 @@ export function asAny(x: any): any {
*
* @template T - The base object type from which properties will be omitted.
* @template U - The object type whose properties will be omitted from 'T'.
+ *
* @example
* type Result = Without<{ a: number; b: string; }, { b: string; }>;
* // Result type will be { a: number; }
*/
export type Without = Omit;
+/**
+ * Extracts the non-nullable array type from a type that may include nulls.
+ *
+ * @example
+ * type Result = NonNullArray<(number | null)[]>;
+ * // Result type will be number[].
+ */
+export type NonNullArray = T extends (infer U | null)[] ? U[] : never;
+
export type AxisKey = 'x' | 'y' | 'z' | 'r';
export type Extents = {
diff --git a/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte b/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte
index b726efe71..004f42b28 100644
--- a/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte
+++ b/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte
@@ -1292,7 +1292,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 },
@@ -1307,8 +1307,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 281efa7eb..f028927cc 100644
--- a/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte
+++ b/packages/layerchart/src/routes/docs/components/BrushContext/+page.svelte
@@ -1,9 +1,9 @@
Examples
+Html click
+
+
+
+
+
{
+ console.log('onBrushEnd', e);
+ xDomain2 = e.brush.x;
+ },
+ }}
+ >
+ {#snippet children({ context })}
+
+ {#each dataSeriesData as d}
+ {@const start = d.date}
+ {@const end = endOfInterval('day', d.date)}
+
+ {@const x = context.xScale(start)}
+ {@const width = context.xScale(end) - x}
+ {@const height = context.height}
+
+
+ {
+ console.log('clicked', start);
+ }}
+ onpointerenter={(e) => {
+ // console.log(start);
+ }}
+ >
+ {/each}
+
+ {/snippet}
+
+
+
+
+Band scale (WIP)
+
+
+
+ {
+ console.log(e);
+ },
+ }}
+ >
+
+
+
+
+
+
+
Basic
@@ -132,7 +212,7 @@
- {#if context.brush.isActive}
+ {#if context.brush.active}
- {#if context.brush.isActive}
+ {#if context.brush.active}
@@ -204,11 +284,11 @@
- {#if context.brush.isActive}
+ {#if context.brush.active}
@@ -245,7 +325,7 @@
resetOnEnd: true,
onBrushEnd: (e) => {
// @ts-expect-error
- set(e.xDomain);
+ set(e.brush.x);
},
}}
>
@@ -283,7 +363,7 @@
resetOnEnd: true,
onBrushEnd: (e) => {
// @ts-expect-error
- set(e.yDomain);
+ set(e.brush.y);
},
}}
>
@@ -323,9 +403,9 @@
onBrushEnd: (e) => {
set({
// @ts-expect-error
- xDomain: e.xDomain,
+ xDomain: e.brush.x,
// @ts-expect-error
- yDomain: e.yDomain,
+ yDomain: e.brush.y,
});
},
}}
@@ -384,7 +464,7 @@
brush={{
onChange: (e) => {
// @ts-expect-error
- set(e.xDomain);
+ set(e.brush.x);
},
}}
>
@@ -412,7 +492,7 @@
axis: 'y',
onChange: (e) => {
// @ts-expect-error
- set(e.yDomain);
+ set(e.brush.y);
},
}}
>
@@ -494,7 +574,7 @@
brush={{
onChange: (e) => {
// @ts-expect-error
- set(e.xDomain);
+ set(e.brush.x);
},
}}
>
@@ -555,9 +635,9 @@
padding={{ left: 16 }}
brush={{
mode: 'separated',
- xDomain,
- onChange: (e) => (xDomain = e.xDomain),
- onReset: (e) => (xDomain = null),
+ x: xDomain,
+ onChange: (e) => (xDomain = e.brush.x),
+ onReset: (e) => (xDomain = [null, null]),
}}
>
@@ -565,7 +645,6 @@
line={{ class: 'stroke-2 stroke-(--chart-color)' }}
class="fill-(--chart-color) opacity-20"
/>
-
@@ -592,7 +671,7 @@
resetOnEnd: true,
onBrushEnd: (e) => {
// @ts-expect-error
- set(e.xDomain);
+ set(e.brush.x);
},
}}
>
@@ -658,9 +737,9 @@
onChange: (e) => {
set({
// @ts-expect-error
- xDomain: e.xDomain,
+ xDomain: e.brush.x,
// @ts-expect-error
- yDomain: e.yDomain,
+ yDomain: e.brush.y,
});
},
}}
@@ -717,9 +796,9 @@
onBrushEnd: (e) => {
set({
// @ts-expect-error
- xDomain: e.xDomain,
+ xDomain: e.brush.x,
// @ts-expect-error
- yDomain: e.yDomain,
+ yDomain: e.brush.y,
});
},
}}
@@ -744,14 +823,14 @@
brush={{
axis: 'both',
mode: 'separated',
- xDomain: value?.xDomain,
- yDomain: value?.yDomain,
+ x: value?.xDomain,
+ y: value?.yDomain,
onChange: (e) => {
set({
// @ts-expect-error
- xDomain: e.xDomain,
+ xDomain: e.brush.x,
// @ts-expect-error
- yDomain: e.yDomain,
+ yDomain: e.brush.y,
});
},
}}
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;
},
}}
>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e99c12151..6146b81ee 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -297,8 +297,8 @@ importers:
specifier: 2.0.0-next.17
version: 2.0.0-next.17
'@layerstack/utils':
- specifier: 2.0.0-next.14
- version: 2.0.0-next.14
+ specifier: 2.0.0-next.15
+ version: 2.0.0-next.15
d3-array:
specifier: ^3.2.4
version: 3.2.4
@@ -4205,7 +4205,7 @@ snapshots:
'@layerstack/tailwind@2.0.0-next.17':
dependencies:
- '@layerstack/utils': 2.0.0-next.14
+ '@layerstack/utils': 2.0.0-next.15
clsx: 2.1.1
d3-array: 3.2.4
lodash-es: 4.17.21