layerchart 0.73.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -59,9 +59,9 @@
59
59
  export let stroke: string | undefined = undefined;
60
60
  export let strokeWidth: number | undefined = undefined;
61
61
 
62
- const xAccessor = x ? accessor(x) : $contextX;
63
- const y0Accessor = y0 ? accessor(y0) : (d: any) => min($yDomain);
64
- const y1Accessor = y1 ? accessor(y1) : $y;
62
+ $: xAccessor = x ? accessor(x) : $contextX;
63
+ $: y0Accessor = y0 ? accessor(y0) : (d: any) => min($yDomain);
64
+ $: y1Accessor = y1 ? accessor(y1) : $y;
65
65
 
66
66
  $: xOffset = isScaleBand($xScale) ? $xScale.bandwidth() / 2 : 0;
67
67
  $: yOffset = isScaleBand($yScale) ? $yScale.bandwidth() / 2 : 0;
@@ -5,7 +5,7 @@
5
5
  import Rect from './Rect.svelte';
6
6
  import Spline from './Spline.svelte';
7
7
 
8
- import { createDimensionGetter } from '../utils/rect.js';
8
+ import { createDimensionGetter, type Insets } from '../utils/rect.js';
9
9
  import { isScaleBand } from '../utils/scales.js';
10
10
  import { accessor, type Accessor } from '../utils/common.js';
11
11
  import { greatestAbs } from '@layerstack/utils';
@@ -54,7 +54,7 @@
54
54
  | 'bottom-left'
55
55
  | 'bottom-right' = 'all';
56
56
 
57
- export let inset = 0;
57
+ export let insets: Insets | undefined = undefined;
58
58
 
59
59
  export let spring: ComponentProps<Rect>['spring'] = undefined;
60
60
  export let tweened: ComponentProps<Rect>['tweened'] = undefined;
@@ -66,7 +66,7 @@
66
66
  y,
67
67
  x1,
68
68
  y1,
69
- inset,
69
+ insets,
70
70
  });
71
71
  $: dimensions = $getDimensions(bar) ?? { x: 0, y: 0, width: 0, height: 0 };
72
72
 
@@ -5,6 +5,7 @@
5
5
  import Bar from './Bar.svelte';
6
6
  import Rect from './Rect.svelte';
7
7
  import { chartDataArray, type Accessor } from '../utils/common.js';
8
+ import type { Insets } from '../index.js/utils/rect.js';
8
9
 
9
10
  const { data: contextData, cGet, config } = chartContext();
10
11
 
@@ -39,7 +40,7 @@
39
40
  export let fill: string | undefined = undefined;
40
41
 
41
42
  /** Inset the rect for amount of padding. Useful with multiple bars (bullet, overlap, etc) */
42
- export let inset = 0;
43
+ export let insets: Insets | undefined = undefined;
43
44
 
44
45
  /** Define unique value for {#each} `(key)` expressions to improve transitions. `index` position used by default */
45
46
  export let key: (d: any, index: number) => any = (d, i) => i;
@@ -66,7 +67,7 @@
66
67
  {stroke}
67
68
  {strokeWidth}
68
69
  {radius}
69
- {inset}
70
+ {insets}
70
71
  {spring}
71
72
  {tweened}
72
73
  on:click={() => onBarClick({ data: d })}
@@ -78,6 +78,7 @@
78
78
  {...$$restProps}
79
79
  on:click
80
80
  on:pointermove
81
+ on:pointerenter
81
82
  on:pointerleave
82
83
  />
83
84
  {/if}
@@ -68,7 +68,10 @@
68
68
 
69
69
  export let onAreaClick: (e: { data: any }) => void = () => {};
70
70
  export let onBarClick: (e: { data: any }) => void = () => {};
71
+
71
72
  export let onPointClick: (e: { point: (typeof _points)[number]; data: any }) => void = () => {};
73
+ export let onPointEnter: (e: { point: (typeof _points)[number]; data: any }) => void = () => {};
74
+ export let onPointLeave: (e: { point: (typeof _points)[number]; data: any }) => void = () => {};
72
75
 
73
76
  const _x = accessor(x);
74
77
  const _y = accessor(y);
@@ -361,7 +364,7 @@
361
364
  spring={motion}
362
365
  x={typeof bar === 'object' ? bar.x : undefined}
363
366
  y={typeof bar === 'object' ? bar.y : undefined}
364
- inset={typeof bar === 'object' ? bar.inset : undefined}
367
+ insets={typeof bar === 'object' ? bar.insets : undefined}
365
368
  stroke={typeof bar === 'object' ? bar.stroke : undefined}
366
369
  strokeWidth={typeof bar === 'object' ? bar.strokeWidth : undefined}
367
370
  radius={typeof bar === 'object' ? bar.radius : undefined}
@@ -411,6 +414,8 @@
411
414
  typeof points === 'object' ? points.class : null
412
415
  )}
413
416
  on:click={() => onPointClick({ point, data: highlightData })}
417
+ on:pointerenter={() => onPointEnter({ point, data: highlightData })}
418
+ on:pointerleave={() => onPointLeave({ point, data: highlightData })}
414
419
  />
415
420
  {/each}
416
421
  </slot>
@@ -35,7 +35,9 @@
35
35
  export let placement: Placement | undefined = undefined;
36
36
  export let orientation: 'horizontal' | 'vertical' = 'horizontal';
37
37
 
38
- export let onClick: ((tick: any) => any) | undefined = undefined;
38
+ export let onClick: ((item: any) => any) | undefined = undefined;
39
+ export let onPointerEnter: ((item: any) => any) | undefined = undefined;
40
+ export let onPointerLeave: ((item: any) => any) | undefined = undefined;
39
41
 
40
42
  /** Determine display ramp (individual color swatches or continuous ramp)*/
41
43
  export let variant: 'ramp' | 'swatches' = 'ramp';
@@ -47,6 +49,7 @@
47
49
  tick?: string;
48
50
  swatches?: string;
49
51
  swatch?: string;
52
+ item?: (item: any) => string;
50
53
  } = {};
51
54
 
52
55
  $: _scale = scale ?? (cScale ? $cScale : null);
@@ -137,6 +140,7 @@
137
140
  {...$$restProps}
138
141
  class={cls(
139
142
  'inline-block',
143
+ 'z-[1]', // stack above tooltip context layers (band rects, voronoi, ...)
140
144
  placement && [
141
145
  'absolute',
142
146
  {
@@ -208,9 +212,12 @@
208
212
  >
209
213
  {#each tickValues ?? xScale?.ticks?.(ticks) ?? [] as tick}
210
214
  {@const color = _scale(tick)}
215
+ {@const item = { value: tick, color }}
211
216
  <button
212
- class={cls('flex gap-1', !onClick && 'cursor-auto')}
213
- on:click={() => onClick?.({ tick, color })}
217
+ class={cls('flex gap-1', !onClick && 'cursor-auto', classes.item?.(item))}
218
+ on:click={() => onClick?.(item)}
219
+ on:pointerenter={() => onPointerEnter?.(item)}
220
+ on:pointerleave={() => onPointerLeave?.(item)}
214
221
  >
215
222
  <div
216
223
  class={cls('h-4 w-4 rounded-full', classes.swatch)}
@@ -14,7 +14,9 @@ declare const __propDef: {
14
14
  tickLength?: number | undefined;
15
15
  placement?: ("center" | "bottom" | "left" | "right" | "top" | "top-left" | "top-right" | "bottom-left" | "bottom-right") | undefined;
16
16
  orientation?: "horizontal" | "vertical" | undefined;
17
- onClick?: ((tick: any) => any) | undefined | undefined;
17
+ onClick?: ((item: any) => any) | undefined | undefined;
18
+ onPointerEnter?: ((item: any) => any) | undefined | undefined;
19
+ onPointerLeave?: ((item: any) => any) | undefined | undefined;
18
20
  variant?: "ramp" | "swatches" | undefined;
19
21
  classes?: {
20
22
  root?: string;
@@ -23,6 +25,7 @@ declare const __propDef: {
23
25
  tick?: string;
24
26
  swatches?: string;
25
27
  swatch?: string;
28
+ item?: (item: any) => string;
26
29
  } | undefined;
27
30
  };
28
31
  events: {
@@ -101,8 +101,8 @@
101
101
  }
102
102
  }
103
103
 
104
- const xAccessor = x ? accessor(x) : $contextX;
105
- const yAccessor = y ? accessor(y) : $contextY;
104
+ $: xAccessor = x ? accessor(x) : $contextX;
105
+ $: yAccessor = y ? accessor(y) : $contextY;
106
106
 
107
107
  $: xOffset = isScaleBand($xScale) ? $xScale.bandwidth() / 2 : 0;
108
108
  $: yOffset = isScaleBand($yScale) ? $yScale.bandwidth() / 2 : 0;
@@ -1,9 +1,11 @@
1
1
  <script lang="ts" generics="TData">
2
- import { type ComponentProps } from 'svelte';
2
+ import { onMount, type ComponentProps } from 'svelte';
3
3
  import { scaleLinear, scaleOrdinal, scaleTime } from 'd3-scale';
4
4
  import { stack, stackOffsetDiverging, stackOffsetExpand, stackOffsetNone } from 'd3-shape';
5
5
  import { sum } from 'd3-array';
6
6
  import { format } from '@layerstack/utils';
7
+ import { cls } from '@layerstack/tailwind';
8
+ import { selectionStore } from '@layerstack/svelte-stores';
7
9
 
8
10
  import Area from '../Area.svelte';
9
11
  import Axis from '../Axis.svelte';
@@ -34,6 +36,7 @@
34
36
  labels?: typeof labels;
35
37
  legend?: typeof legend;
36
38
  points?: typeof points;
39
+ profile?: typeof profile;
37
40
  props?: typeof props;
38
41
  rule?: typeof rule;
39
42
  series?: typeof series;
@@ -103,7 +106,10 @@
103
106
 
104
107
  export let renderContext: 'svg' | 'canvas' = 'svg';
105
108
 
106
- $: allSeriesData = series
109
+ /** Log initial render performance using `console.time` */
110
+ export let profile = false;
111
+
112
+ $: allSeriesData = visibleSeries
107
113
  .flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d })))
108
114
  .filter((d) => d) as Array<TData & { stackData?: any }>;
109
115
 
@@ -112,13 +118,14 @@
112
118
  >;
113
119
 
114
120
  $: if (stackSeries) {
115
- const seriesKeys = series.map((s) => s.key);
121
+ const seriesKeys = visibleSeries.map((s) => s.key);
116
122
  const offset =
117
123
  seriesLayout === 'stackExpand'
118
124
  ? stackOffsetExpand
119
125
  : seriesLayout === 'stackDiverging'
120
126
  ? stackOffsetDiverging
121
127
  : stackOffsetNone;
128
+
122
129
  const stackData = stack()
123
130
  .keys(seriesKeys)
124
131
  .value((d, key) => {
@@ -139,6 +146,8 @@
139
146
  $: xScale =
140
147
  $$props.xScale ?? (accessor(x)(chartData[0]) instanceof Date ? scaleTime() : scaleLinear());
141
148
 
149
+ let highlightSeriesKey: (typeof series)[number]['key'] | null = null;
150
+
142
151
  function getAreaProps(s: (typeof series)[number], i: number) {
143
152
  const lineProps = {
144
153
  ...props.line,
@@ -156,10 +165,18 @@
156
165
  : (s.value ?? (s.data ? undefined : s.key)),
157
166
  fill: s.color,
158
167
  fillOpacity: 0.3,
168
+ class: cls(
169
+ 'transition-opacity',
170
+ highlightSeriesKey && highlightSeriesKey !== s.key && 'opacity-10'
171
+ ),
159
172
  ...props.area,
160
173
  ...s.props,
161
174
  line: {
162
- class: !('stroke-width' in lineProps) ? 'stroke-2' : '',
175
+ class: cls(
176
+ !('stroke-width' in lineProps) && 'stroke-2',
177
+ 'transition-opacity',
178
+ highlightSeriesKey && highlightSeriesKey !== s.key && 'opacity-10'
179
+ ),
163
180
  stroke: s.color,
164
181
  ...lineProps,
165
182
  },
@@ -167,6 +184,22 @@
167
184
 
168
185
  return areaProps;
169
186
  }
187
+
188
+ const selectedSeries = selectionStore();
189
+ $: visibleSeries = series.filter((s) => {
190
+ return (
191
+ // @ts-expect-error
192
+ $selectedSeries.selected.length === 0 || $selectedSeries.isSelected(s.key)
193
+ // || highlightSeriesKey == s.key
194
+ );
195
+ });
196
+
197
+ if (profile) {
198
+ console.time('AreaChart render');
199
+ onMount(() => {
200
+ console.timeEnd('AreaChart render');
201
+ });
202
+ }
170
203
  </script>
171
204
 
172
205
  <Chart
@@ -175,8 +208,8 @@
175
208
  {xScale}
176
209
  y={y ??
177
210
  (stackSeries
178
- ? (d) => series.flatMap((s, i) => d.stackData[i])
179
- : series.map((s) => s.value ?? s.key))}
211
+ ? (d) => visibleSeries.flatMap((s, i) => d.stackData[i])
212
+ : visibleSeries.map((s) => s.value ?? s.key))}
180
213
  yBaseline={0}
181
214
  yNice
182
215
  {radial}
@@ -220,7 +253,7 @@
220
253
  <slot name="belowMarks" {...slotProps} />
221
254
 
222
255
  <slot name="marks" {...slotProps}>
223
- {#each series as s, i (s.key)}
256
+ {#each visibleSeries as s, i (s.key)}
224
257
  <Area {...getAreaProps(s, i)} />
225
258
  {/each}
226
259
  </slot>
@@ -260,7 +293,7 @@
260
293
  </slot>
261
294
 
262
295
  {#if points}
263
- {#each series as s}
296
+ {#each visibleSeries as s}
264
297
  <Points
265
298
  data={s.data}
266
299
  fill={s.color}
@@ -272,7 +305,7 @@
272
305
  {/if}
273
306
 
274
307
  <slot name="highlight" {...slotProps}>
275
- {#each series as s, i (s.key)}
308
+ {#each visibleSeries as s, i (s.key)}
276
309
  {@const seriesTooltipData =
277
310
  s.data && tooltip.data ? findRelatedData(s.data, tooltip.data, x) : null}
278
311
 
@@ -282,6 +315,8 @@
282
315
  points={{ fill: s.color }}
283
316
  lines={i == 0}
284
317
  onPointClick={(e) => onPointClick({ ...e, series: s })}
318
+ onPointEnter={() => (highlightSeriesKey = s.key)}
319
+ onPointLeave={() => (highlightSeriesKey = null)}
285
320
  {...props.highlight}
286
321
  />
287
322
  {/each}
@@ -298,11 +333,21 @@
298
333
  scale={isDefaultSeries
299
334
  ? undefined
300
335
  : scaleOrdinal(
301
- series.map((s) => s.label ?? s.key),
336
+ series.map((s) => s.key),
302
337
  series.map((s) => s.color)
303
338
  )}
339
+ tickFormat={(key) => series.find((s) => s.key === key)?.label ?? key}
304
340
  placement="bottom"
305
341
  variant="swatches"
342
+ onClick={(item) => $selectedSeries.toggleSelected(item.value)}
343
+ onPointerEnter={(item) => (highlightSeriesKey = item.value)}
344
+ onPointerLeave={(item) => (highlightSeriesKey = null)}
345
+ classes={{
346
+ item: (item) =>
347
+ visibleSeries.length && !visibleSeries.some((s) => s.key === item.value)
348
+ ? 'opacity-50'
349
+ : '',
350
+ }}
306
351
  {...props.legend}
307
352
  {...typeof legend === 'object' ? legend : null}
308
353
  />
@@ -314,7 +359,7 @@
314
359
  <Tooltip.Header {...props.tooltip?.header}>{format(x(data))}</Tooltip.Header>
315
360
  <Tooltip.List {...props.tooltip?.list}>
316
361
  <!-- Reverse series order so tooltip items match stacks -->
317
- {@const seriesItems = stackSeries ? [...series].reverse() : series}
362
+ {@const seriesItems = stackSeries ? [...visibleSeries].reverse() : visibleSeries}
318
363
  {#each seriesItems as s}
319
364
  {@const seriesTooltipData = s.data ? findRelatedData(s.data, data, x) : data}
320
365
  {@const valueAccessor = accessor(s.value ?? (s.data ? asAny(y) : s.key))}
@@ -1,9 +1,11 @@
1
1
  <script lang="ts" generics="TData">
2
- import { type ComponentProps } from 'svelte';
2
+ import { onMount, type ComponentProps } from 'svelte';
3
3
  import { scaleBand, scaleOrdinal, scaleLinear } from 'd3-scale';
4
4
  import { stack, stackOffsetDiverging, stackOffsetExpand, stackOffsetNone } from 'd3-shape';
5
5
  import { sum } from 'd3-array';
6
6
  import { format } from '@layerstack/utils';
7
+ import { cls } from '@layerstack/tailwind';
8
+ import { selectionStore } from '@layerstack/svelte-stores';
7
9
 
8
10
  import Axis from '../Axis.svelte';
9
11
  import Bars from '../Bars.svelte';
@@ -25,6 +27,7 @@
25
27
  type Accessor,
26
28
  } from '../../utils/common.js';
27
29
  import { asAny } from '../../utils/types.js';
30
+ import type { Insets } from '../../index.js/utils/rect.js';
28
31
 
29
32
  type ChartProps = ComponentProps<Chart<TData>>;
30
33
 
@@ -33,9 +36,11 @@
33
36
  grid?: typeof grid;
34
37
  bandPadding?: typeof bandPadding;
35
38
  groupPadding?: typeof groupPadding;
39
+ stackPadding?: typeof stackPadding;
36
40
  labels?: typeof labels;
37
41
  legend?: typeof legend;
38
42
  orientation?: typeof orientation;
43
+ profile?: typeof profile;
39
44
  props?: typeof props;
40
45
  rule?: typeof rule;
41
46
  series?: typeof series;
@@ -84,6 +89,8 @@
84
89
  export let bandPadding = 0.4;
85
90
  /** Padding between group/series items when using 'seriesLayout="group"', applied to scaleBand().padding() */
86
91
  export let groupPadding = 0;
92
+ /** Padding between series items within bars when using 'seriesLayout="stack"' */
93
+ export let stackPadding = 0;
87
94
 
88
95
  /** Event dispatched with current tooltip data */
89
96
  export let onTooltipClick: (e: { data: any }) => void = () => {};
@@ -109,11 +116,11 @@
109
116
  $: if (seriesLayout === 'group') {
110
117
  if (isVertical) {
111
118
  x1Scale = scaleBand().padding(groupPadding);
112
- x1Domain = series.map((s) => s.key);
119
+ x1Domain = visibleSeries.map((s) => s.key);
113
120
  x1Range = ({ xScale }) => [0, xScale.bandwidth?.()];
114
121
  } else {
115
122
  y1Scale = scaleBand().padding(groupPadding);
116
- y1Domain = series.map((s) => s.key);
123
+ y1Domain = visibleSeries.map((s) => s.key);
117
124
  y1Range = ({ yScale }) => [0, yScale.bandwidth?.()];
118
125
  }
119
126
  }
@@ -138,7 +145,10 @@
138
145
 
139
146
  export let renderContext: 'svg' | 'canvas' = 'svg';
140
147
 
141
- $: allSeriesData = series
148
+ /** Log initial render performance using `console.time` */
149
+ export let profile = false;
150
+
151
+ $: allSeriesData = visibleSeries
142
152
  .flatMap((s) =>
143
153
  s.data?.map((d) => {
144
154
  return { seriesKey: s.key, ...d };
@@ -151,7 +161,7 @@
151
161
  >;
152
162
 
153
163
  $: if (stackSeries) {
154
- const seriesKeys = series.map((s) => s.key);
164
+ const seriesKeys = visibleSeries.map((s) => s.key);
155
165
  // const stackData = stack().keys(seriesKeys)(chartDataArray(data)) as any[];
156
166
 
157
167
  const offset =
@@ -176,20 +186,49 @@
176
186
  });
177
187
  }
178
188
 
189
+ let highlightSeriesKey: (typeof series)[number]['key'] | null = null;
190
+
179
191
  function getBarsProps(s: (typeof series)[number], i: number) {
180
- const valueAccesor = stackSeries
192
+ const isFirst = i == 0;
193
+ const isLast = i == visibleSeries.length - 1;
194
+
195
+ const isStackLayout = seriesLayout.startsWith('stack');
196
+
197
+ let stackInsets: Insets | undefined = undefined;
198
+
199
+ if (isStackLayout) {
200
+ const stackInset = stackPadding / 2;
201
+ if (isVertical) {
202
+ stackInsets = {
203
+ bottom: isFirst ? undefined : stackInset,
204
+ top: isLast ? undefined : stackInset,
205
+ };
206
+ } else {
207
+ stackInsets = {
208
+ left: isFirst ? undefined : stackInset,
209
+ right: isLast ? undefined : stackInset,
210
+ };
211
+ }
212
+ }
213
+
214
+ const valueAccessor = stackSeries
181
215
  ? (d: any) => d.stackData[i]
182
216
  : (s.value ?? (s.data ? undefined : s.key));
183
217
  const barsProps: ComponentProps<Bars> = {
184
218
  data: s.data,
185
- x: !isVertical ? valueAccesor : undefined,
186
- y: isVertical ? valueAccesor : undefined,
219
+ x: !isVertical ? valueAccessor : undefined,
220
+ y: isVertical ? valueAccessor : undefined,
187
221
  x1: isVertical && groupSeries ? (d) => s.value ?? s.key : undefined,
188
222
  y1: !isVertical && groupSeries ? (d) => s.value ?? s.key : undefined,
189
- rounded: seriesLayout.startsWith('stack') && i !== series.length - 1 ? 'none' : 'edge',
223
+ rounded: isStackLayout && i !== visibleSeries.length - 1 ? 'none' : 'edge',
190
224
  radius: 4,
191
225
  strokeWidth: 1,
226
+ insets: stackInsets,
192
227
  fill: s.color,
228
+ class: cls(
229
+ 'transition-opacity',
230
+ highlightSeriesKey && highlightSeriesKey !== s.key && 'opacity-10'
231
+ ),
193
232
  onBarClick: (e) => onBarClick({ data: e.data, series: s }),
194
233
  ...props.bars,
195
234
  ...s.props,
@@ -197,14 +236,30 @@
197
236
 
198
237
  return barsProps;
199
238
  }
239
+
240
+ const selectedSeries = selectionStore();
241
+ $: visibleSeries = series.filter((s) => {
242
+ return (
243
+ // @ts-expect-error
244
+ $selectedSeries.selected.length === 0 || $selectedSeries.isSelected(s.key)
245
+ // || highlightSeriesKey == s.key
246
+ );
247
+ });
248
+
249
+ if (profile) {
250
+ console.time('BarChart render');
251
+ onMount(() => {
252
+ console.timeEnd('BarChart render');
253
+ });
254
+ }
200
255
  </script>
201
256
 
202
257
  <Chart
203
258
  data={chartData}
204
259
  x={x ??
205
260
  (stackSeries
206
- ? (d) => series.flatMap((s, i) => d.stackData[i])
207
- : series.map((s) => s.value ?? s.key))}
261
+ ? (d) => visibleSeries.flatMap((s, i) => d.stackData[i])
262
+ : visibleSeries.map((s) => s.value ?? s.key))}
208
263
  {xScale}
209
264
  {xBaseline}
210
265
  xNice={orientation === 'horizontal'}
@@ -213,8 +268,8 @@
213
268
  {x1Range}
214
269
  y={y ??
215
270
  (stackSeries
216
- ? (d) => series.flatMap((s, i) => d.stackData[i])
217
- : series.map((s) => s.value ?? s.key))}
271
+ ? (d) => visibleSeries.flatMap((s, i) => d.stackData[i])
272
+ : visibleSeries.map((s) => s.value ?? s.key))}
218
273
  {yScale}
219
274
  {yBaseline}
220
275
  yNice={orientation === 'vertical'}
@@ -267,7 +322,7 @@
267
322
  <slot name="belowMarks" {...slotProps} />
268
323
 
269
324
  <slot name="marks" {...slotProps}>
270
- {#each series as s, i (s.key)}
325
+ {#each visibleSeries as s, i (s.key)}
271
326
  <Bars {...getBarsProps(s, i)} />
272
327
  {/each}
273
328
  </slot>
@@ -332,11 +387,21 @@
332
387
  scale={isDefaultSeries
333
388
  ? undefined
334
389
  : scaleOrdinal(
335
- series.map((s) => s.label ?? s.key),
390
+ series.map((s) => s.key),
336
391
  series.map((s) => s.color)
337
392
  )}
393
+ tickFormat={(key) => series.find((s) => s.key === key)?.label ?? key}
338
394
  placement="bottom"
339
395
  variant="swatches"
396
+ onClick={(item) => $selectedSeries.toggleSelected(item.value)}
397
+ onPointerEnter={(item) => (highlightSeriesKey = item.value)}
398
+ onPointerLeave={(item) => (highlightSeriesKey = null)}
399
+ classes={{
400
+ item: (item) =>
401
+ visibleSeries.length && !visibleSeries.some((s) => s.key === item.value)
402
+ ? 'opacity-50'
403
+ : '',
404
+ }}
340
405
  {...props.legend}
341
406
  {...typeof legend === 'object' ? legend : null}
342
407
  />
@@ -350,7 +415,7 @@
350
415
  >
351
416
  <Tooltip.List {...props.tooltip?.list}>
352
417
  <!-- Reverse series order so tooltip items match stacks -->
353
- {@const seriesItems = stackSeries ? [...series].reverse() : series}
418
+ {@const seriesItems = stackSeries ? [...visibleSeries].reverse() : visibleSeries}
354
419
  {#each seriesItems as s}
355
420
  {@const seriesTooltipData = s.data ? findRelatedData(s.data, data, x) : data}
356
421
  {@const valueAccessor = accessor(s.value ?? (s.data ? asAny(y) : s.key))}
@@ -364,12 +429,12 @@
364
429
  />
365
430
  {/each}
366
431
 
367
- {#if stackSeries || groupSeries}
432
+ {#if (stackSeries || groupSeries) && visibleSeries.length > 1}
368
433
  <Tooltip.Separator {...props.tooltip?.separator} />
369
434
 
370
435
  <Tooltip.Item
371
436
  label="total"
372
- value={sum(series, (s) => {
437
+ value={sum(visibleSeries, (s) => {
373
438
  const seriesTooltipData = s.data ? findRelatedData(s.data, data, x) : data;
374
439
  const valueAccessor = accessor(s.value ?? (s.data ? asAny(y) : s.key));
375
440
  return valueAccessor(seriesTooltipData);
@@ -1,7 +1,9 @@
1
1
  <script lang="ts" generics="TData">
2
- import { type ComponentProps } from 'svelte';
2
+ import { onMount, type ComponentProps } from 'svelte';
3
3
  import { scaleLinear, scaleOrdinal, scaleTime } from 'd3-scale';
4
4
  import { format } from '@layerstack/utils';
5
+ import { cls } from '@layerstack/tailwind';
6
+ import { selectionStore } from '@layerstack/svelte-stores';
5
7
 
6
8
  import Axis from '../Axis.svelte';
7
9
  import Canvas from '../layout/Canvas.svelte';
@@ -31,6 +33,7 @@
31
33
  labels?: typeof labels;
32
34
  legend?: typeof legend;
33
35
  points?: typeof points;
36
+ profile?: typeof profile;
34
37
  props?: typeof props;
35
38
  rule?: typeof rule;
36
39
  series?: typeof series;
@@ -94,6 +97,9 @@
94
97
 
95
98
  export let renderContext: 'svg' | 'canvas' = 'svg';
96
99
 
100
+ /** Log initial render performance using `console.time` */
101
+ export let profile = false;
102
+
97
103
  $: allSeriesData = series
98
104
  .flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d })))
99
105
  .filter((d) => d) as Array<TData & { stackData?: any }>;
@@ -106,11 +112,16 @@
106
112
  $: xScale =
107
113
  $$props.xScale ?? (accessor(x)(chartData[0]) instanceof Date ? scaleTime() : scaleLinear());
108
114
 
115
+ let highlightSeriesKey: (typeof series)[number]['key'] | null = null;
116
+
109
117
  function getSplineProps(s: (typeof series)[number], i: number) {
110
118
  const splineProps: ComponentProps<Spline> = {
111
119
  data: s.data,
112
120
  y: s.value ?? (s.data ? undefined : s.key),
113
- class: 'stroke-2',
121
+ class: cls(
122
+ 'stroke-2 transition-opacity',
123
+ highlightSeriesKey && highlightSeriesKey !== s.key && 'opacity-10'
124
+ ),
114
125
  stroke: s.color,
115
126
  ...props.spline,
116
127
  ...s.props,
@@ -118,6 +129,22 @@
118
129
 
119
130
  return splineProps;
120
131
  }
132
+
133
+ const selectedSeries = selectionStore();
134
+ $: visibleSeries = series.filter((s) => {
135
+ return (
136
+ // @ts-expect-error
137
+ $selectedSeries.selected.length === 0 || $selectedSeries.isSelected(s.key)
138
+ // || highlightSeriesKey == s.key
139
+ );
140
+ });
141
+
142
+ if (profile) {
143
+ console.time('LineChart render');
144
+ onMount(() => {
145
+ console.timeEnd('LineChart render');
146
+ });
147
+ }
121
148
  </script>
122
149
 
123
150
  <Chart
@@ -167,7 +194,7 @@
167
194
  <slot name="belowMarks" {...slotProps} />
168
195
 
169
196
  <slot name="marks" {...slotProps}>
170
- {#each series as s, i (s.key)}
197
+ {#each visibleSeries as s, i (s.key)}
171
198
  <Spline {...getSplineProps(s, i)} />
172
199
  {/each}
173
200
  </slot>
@@ -201,7 +228,7 @@
201
228
  </slot>
202
229
 
203
230
  {#if points}
204
- {#each series as s}
231
+ {#each visibleSeries as s}
205
232
  <Points
206
233
  data={s.data}
207
234
  fill={s.color}
@@ -217,7 +244,7 @@
217
244
  {/if}
218
245
 
219
246
  <slot name="highlight" {...slotProps}>
220
- {#each series as s, i (s.key)}
247
+ {#each visibleSeries as s, i (s.key)}
221
248
  {@const seriesTooltipData =
222
249
  s.data && tooltip.data ? findRelatedData(s.data, tooltip.data, x) : null}
223
250
  <Highlight
@@ -226,6 +253,8 @@
226
253
  points={{ fill: s.color }}
227
254
  lines={i === 0}
228
255
  onPointClick={(e) => onPointClick({ ...e, series: s })}
256
+ onPointEnter={() => (highlightSeriesKey = s.key)}
257
+ onPointLeave={() => (highlightSeriesKey = null)}
229
258
  {...props.highlight}
230
259
  />
231
260
  {/each}
@@ -238,11 +267,21 @@
238
267
  scale={isDefaultSeries
239
268
  ? undefined
240
269
  : scaleOrdinal(
241
- series.map((s) => s.label ?? s.key),
270
+ series.map((s) => s.key),
242
271
  series.map((s) => s.color)
243
272
  )}
273
+ tickFormat={(key) => series.find((s) => s.key === key)?.label ?? key}
244
274
  placement="bottom"
245
275
  variant="swatches"
276
+ onClick={(item) => $selectedSeries.toggleSelected(item.value)}
277
+ onPointerEnter={(item) => (highlightSeriesKey = item.value)}
278
+ onPointerLeave={(item) => (highlightSeriesKey = null)}
279
+ classes={{
280
+ item: (item) =>
281
+ visibleSeries.length && !visibleSeries.some((s) => s.key === item.value)
282
+ ? 'opacity-50'
283
+ : '',
284
+ }}
246
285
  {...props.legend}
247
286
  {...typeof legend === 'object' ? legend : null}
248
287
  />
@@ -253,7 +292,7 @@
253
292
  <Tooltip.Root {...props.tooltip?.root} let:data>
254
293
  <Tooltip.Header {...props.tooltip?.header}>{format(x(data))}</Tooltip.Header>
255
294
  <Tooltip.List {...props.tooltip?.list}>
256
- {#each series as s}
295
+ {#each visibleSeries as s}
257
296
  {@const seriesTooltipData = s.data ? findRelatedData(s.data, data, x) : data}
258
297
  {@const valueAccessor = accessor(s.value ?? (s.data ? asAny(y) : s.key))}
259
298
 
@@ -1,7 +1,9 @@
1
1
  <script lang="ts" generics="TData">
2
- import { type ComponentProps } from 'svelte';
2
+ import { onMount, type ComponentProps } from 'svelte';
3
3
  import { sum } from 'd3-array';
4
4
  import { format } from '@layerstack/utils';
5
+ import { cls } from '@layerstack/tailwind';
6
+ import { selectionStore } from '@layerstack/svelte-stores';
5
7
 
6
8
  import Arc from '../Arc.svelte';
7
9
  import Canvas from '../layout/Canvas.svelte';
@@ -27,6 +29,7 @@
27
29
  padAngle?: typeof padAngle;
28
30
  center?: typeof center;
29
31
  placement?: typeof placement;
32
+ profile?: typeof profile;
30
33
  props?: typeof props;
31
34
  range?: typeof range;
32
35
  series?: typeof series;
@@ -118,6 +121,9 @@
118
121
 
119
122
  export let renderContext: 'svg' | 'canvas' = 'svg';
120
123
 
124
+ /** Log initial render performance using `console.time` */
125
+ export let profile = false;
126
+
121
127
  $: allSeriesData = series
122
128
  .flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d })))
123
129
  .filter((d) => d) as Array<TData>;
@@ -125,13 +131,33 @@
125
131
  $: chartData = (allSeriesData.length ? allSeriesData : chartDataArray(data)) as Array<TData>;
126
132
 
127
133
  $: seriesColors = series.map((s) => s.color).filter((d) => d != null);
134
+
135
+ let highlightKey: (typeof series)[number]['key'] | null = null;
136
+
137
+ const selectedKeys = selectionStore();
138
+ $: visibleData = chartData.filter((d) => {
139
+ const dataKey = keyAccessor(d);
140
+ return (
141
+ // @ts-expect-error
142
+ $selectedKeys.selected.length === 0 || $selectedKeys.isSelected(dataKey)
143
+ // || highlightKey == dataKey
144
+ );
145
+ });
146
+
147
+ if (profile) {
148
+ console.time('PieChart render');
149
+ onMount(() => {
150
+ console.timeEnd('PieChart render');
151
+ });
152
+ }
128
153
  </script>
129
154
 
130
155
  <Chart
131
- data={chartData}
156
+ data={visibleData}
132
157
  x={value}
133
158
  y={key}
134
159
  c={key}
160
+ cDomain={chartData.map(keyAccessor)}
135
161
  cRange={seriesColors.length
136
162
  ? seriesColors
137
163
  : [
@@ -205,6 +231,10 @@
205
231
  // Workaround for `tooltip={{ mode: 'manual' }}
206
232
  onTooltipClick({ data: d });
207
233
  }}
234
+ class={cls(
235
+ 'transition-opacity',
236
+ highlightKey && highlightKey !== keyAccessor(d) && 'opacity-50'
237
+ )}
208
238
  {...props.arc}
209
239
  {...s.props}
210
240
  />
@@ -235,6 +265,10 @@
235
265
  // Workaround for `tooltip={{ mode: 'manual' }}
236
266
  onTooltipClick({ data: arc.data });
237
267
  }}
268
+ class={cls(
269
+ 'transition-opacity',
270
+ highlightKey && highlightKey !== keyAccessor(arc.data) && 'opacity-50'
271
+ )}
238
272
  {...props.arc}
239
273
  {...s.props}
240
274
  />
@@ -257,6 +291,15 @@
257
291
  }}
258
292
  placement="bottom"
259
293
  variant="swatches"
294
+ onClick={(item) => $selectedKeys.toggleSelected(item.value)}
295
+ onPointerEnter={(item) => (highlightKey = item.value)}
296
+ onPointerLeave={(item) => (highlightKey = null)}
297
+ classes={{
298
+ item: (item) =>
299
+ visibleData.length && !visibleData.some((d) => keyAccessor(d) === item.value)
300
+ ? 'opacity-50'
301
+ : '',
302
+ }}
260
303
  {...props.legend}
261
304
  {...typeof legend === 'object' ? legend : null}
262
305
  />
@@ -210,7 +210,9 @@ declare class __sveltets_Render<TData> {
210
210
  tickLength?: number | undefined;
211
211
  placement?: ("center" | "bottom" | "left" | "right" | "top" | "top-left" | "top-right" | "bottom-left" | "bottom-right") | undefined;
212
212
  orientation?: "horizontal" | "vertical" | undefined;
213
- onClick?: ((tick: any) => any) | undefined | undefined;
213
+ onClick?: ((item: any) => any) | undefined | undefined;
214
+ onPointerEnter?: ((item: any) => any) | undefined | undefined;
215
+ onPointerLeave?: ((item: any) => any) | undefined | undefined;
214
216
  variant?: "ramp" | "swatches" | undefined;
215
217
  classes?: {
216
218
  root?: string;
@@ -219,6 +221,7 @@ declare class __sveltets_Render<TData> {
219
221
  tick?: string;
220
222
  swatches?: string;
221
223
  swatch?: string;
224
+ item?: (item: any) => string;
222
225
  } | undefined;
223
226
  };
224
227
  maxValue?: number | undefined;
@@ -226,6 +229,7 @@ declare class __sveltets_Render<TData> {
226
229
  padAngle?: number;
227
230
  center?: boolean;
228
231
  placement?: "center" | "left" | "right";
232
+ profile?: boolean;
229
233
  props?: {
230
234
  pie?: Partial<ComponentProps<Pie>>;
231
235
  group?: Partial<ComponentProps<Group>>;
@@ -1,7 +1,9 @@
1
1
  <script lang="ts" generics="TData">
2
- import { type ComponentProps } from 'svelte';
2
+ import { onMount, type ComponentProps } from 'svelte';
3
3
  import { scaleLinear, scaleOrdinal, scaleTime } from 'd3-scale';
4
4
  import { format } from '@layerstack/utils';
5
+ import { cls } from '@layerstack/tailwind';
6
+ import { selectionStore } from '@layerstack/svelte-stores';
5
7
 
6
8
  import Axis from '../Axis.svelte';
7
9
  import Canvas from '../layout/Canvas.svelte';
@@ -27,6 +29,7 @@
27
29
  grid?: typeof grid;
28
30
  labels?: typeof labels;
29
31
  legend?: typeof legend;
32
+ profile?: typeof profile;
30
33
  props?: typeof props;
31
34
  series?: typeof series;
32
35
  renderContext?: typeof renderContext;
@@ -75,6 +78,9 @@
75
78
 
76
79
  export let renderContext: 'svg' | 'canvas' = 'svg';
77
80
 
81
+ /** Log initial render performance using `console.time` */
82
+ export let profile = false;
83
+
78
84
  // Default xScale based on first data's `x` value
79
85
  $: xScale =
80
86
  $$props.xScale ??
@@ -85,22 +91,44 @@
85
91
  $$props.yScale ??
86
92
  (accessor(y)(chartDataArray(data)[0]) instanceof Date ? scaleTime() : scaleLinear());
87
93
 
88
- $: chartData = series
94
+ $: chartData = visibleSeries
89
95
  .flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d })))
90
96
  .filter((d) => d) as Array<TData>;
91
97
 
98
+ let highlightSeriesKey: (typeof series)[number]['key'] | null = null;
99
+
92
100
  function getPointsProps(s: (typeof series)[number], i: number) {
93
101
  const pointsProps: ComponentProps<Points> = {
94
102
  data: s.data,
95
103
  stroke: s.color,
96
104
  fill: s.color,
97
105
  fillOpacity: 0.3,
106
+ class: cls(
107
+ 'transition-opacity',
108
+ highlightSeriesKey && highlightSeriesKey !== s.key && 'opacity-10'
109
+ ),
98
110
  ...props.points,
99
111
  ...s.props,
100
112
  };
101
113
 
102
114
  return pointsProps;
103
115
  }
116
+
117
+ const selectedSeries = selectionStore();
118
+ $: visibleSeries = series.filter((s) => {
119
+ return (
120
+ // @ts-expect-error
121
+ $selectedSeries.selected.length === 0 || $selectedSeries.isSelected(s.key)
122
+ // || highlightSeriesKey == s.key
123
+ );
124
+ });
125
+
126
+ if (profile) {
127
+ console.time('ScatterChart render');
128
+ onMount(() => {
129
+ console.timeEnd('ScatterChart render');
130
+ });
131
+ }
104
132
  </script>
105
133
 
106
134
  <Chart
@@ -142,7 +170,7 @@
142
170
  <slot name="belowMarks" {...slotProps} />
143
171
 
144
172
  <slot name="marks" {...slotProps}>
145
- {#each series as s, i (s.key)}
173
+ {#each visibleSeries as s, i (s.key)}
146
174
  <Points {...getPointsProps(s, i)} />
147
175
  {/each}
148
176
  </slot>
@@ -194,11 +222,21 @@
194
222
  scale={isDefaultSeries
195
223
  ? undefined
196
224
  : scaleOrdinal(
197
- series.map((s) => s.label ?? s.key),
225
+ series.map((s) => s.key),
198
226
  series.map((s) => s.color)
199
227
  )}
228
+ tickFormat={(key) => series.find((s) => s.key === key)?.label ?? key}
200
229
  placement="bottom"
201
230
  variant="swatches"
231
+ onClick={(item) => $selectedSeries.toggleSelected(item.value)}
232
+ onPointerEnter={(item) => (highlightSeriesKey = item.value)}
233
+ onPointerLeave={(item) => (highlightSeriesKey = null)}
234
+ classes={{
235
+ item: (item) =>
236
+ visibleSeries.length && !visibleSeries.some((s) => s.key === item.value)
237
+ ? 'opacity-50'
238
+ : '',
239
+ }}
202
240
  {...props.legend}
203
241
  {...typeof legend === 'object' ? legend : null}
204
242
  />
@@ -1,5 +1,22 @@
1
1
  import type { ChartContext } from '../components/ChartContext.svelte';
2
2
  import { type Accessor } from './common.js';
3
+ /** A set of inset distances, applied to a rectangle to shrink or expand the area represented by that rectangle. */
4
+ export type Insets = {
5
+ /** Applies an inset all sides of a rectangle: `left`, `right`, `bottom`, and `top` */
6
+ all?: number;
7
+ /** Applies an inset all horizontal sides of a rectangle: `left`, and `right`, overriding `all` */
8
+ x?: number;
9
+ /** Applies an inset all vertical sides of a rectangle: `top`, and `bottom`, overriding `all` */
10
+ y?: number;
11
+ /** Applies an inset the left side of a rectangle, overriding `x` */
12
+ left?: number;
13
+ /** Applies an inset the right side of a rectangle, overriding `x` */
14
+ right?: number;
15
+ /** Applies an inset the top side of a rectangle, overriding `y` */
16
+ top?: number;
17
+ /** Applies an inset the bottom side of a rectangle, overriding `y` */
18
+ bottom?: number;
19
+ };
3
20
  type DimensionGetterOptions = {
4
21
  /** Override `x` accessor from context */
5
22
  x?: Accessor;
@@ -9,7 +26,7 @@ type DimensionGetterOptions = {
9
26
  x1?: Accessor;
10
27
  /** Override `y1` accessor from context */
11
28
  y1?: Accessor;
12
- inset?: number;
29
+ insets?: Insets;
13
30
  };
14
31
  export declare function createDimensionGetter<TData>(context: ChartContext<TData>, options?: DimensionGetterOptions): import("svelte/store").Readable<(item: any) => {
15
32
  x: any;
@@ -4,8 +4,8 @@ import { isScaleBand } from './scales.js';
4
4
  import { accessor } from './common.js';
5
5
  export function createDimensionGetter(context, options) {
6
6
  const { xScale, yScale, x: xAccessor, y: yAccessor, x1: x1Accessor, y1: y1Accessor, x1Scale, y1Scale, } = context;
7
- const inset = options?.inset ?? 0;
8
7
  return derived([xScale, x1Scale, yScale, y1Scale, xAccessor, yAccessor, x1Accessor, y1Accessor], ([$xScale, $x1Scale, $yScale, $y1Scale, $xAccessor, $yAccessor, $x1Accessor, $y1Accessor]) => {
8
+ const insets = resolveInsets(options?.insets);
9
9
  // Use `xscale.domain()` instead of `$xDomain` to include `nice()` being applied
10
10
  const [minXDomain, maxXDomain] = $xScale.domain();
11
11
  const [minYDomain, maxYDomain] = $yScale.domain();
@@ -17,9 +17,11 @@ export function createDimensionGetter(context, options) {
17
17
  return function getter(item) {
18
18
  if (isScaleBand($yScale)) {
19
19
  // Horizontal band
20
- const y = firstValue($yScale(_y(item)) ?? 0) + ($y1Scale ? $y1Scale(_y1(item)) : 0) + inset / 2;
20
+ const y = firstValue($yScale(_y(item)) ?? 0) + ($y1Scale ? $y1Scale(_y1(item)) : 0) + insets.top;
21
21
  const height = Math.max(0, $yScale.bandwidth
22
- ? ($y1Scale ? ($y1Scale.bandwidth?.() ?? 0) : $yScale.bandwidth()) - inset
22
+ ? ($y1Scale ? ($y1Scale.bandwidth?.() ?? 0) : $yScale.bandwidth()) -
23
+ insets.bottom -
24
+ insets.top
23
25
  : 0);
24
26
  const xValue = _x(item);
25
27
  let left = 0;
@@ -44,18 +46,17 @@ export function createDimensionGetter(context, options) {
44
46
  left = xValue;
45
47
  right = min([0, maxXDomain]);
46
48
  }
47
- return {
48
- x: $xScale(left),
49
- y,
50
- width: $xScale(right) - $xScale(left),
51
- height,
52
- };
49
+ const x = $xScale(left) + insets.left;
50
+ const width = Math.max(0, $xScale(right) - $xScale(left) - insets.left - insets.right);
51
+ return { x, y, width, height };
53
52
  }
54
53
  else {
55
54
  // Vertical band or linear
56
- const x = firstValue($xScale(_x(item))) + ($x1Scale ? $x1Scale(_x1(item)) : 0) + inset / 2;
55
+ const x = firstValue($xScale(_x(item))) + ($x1Scale ? $x1Scale(_x1(item)) : 0) + insets.left;
57
56
  const width = Math.max(0, $xScale.bandwidth
58
- ? ($x1Scale ? ($x1Scale.bandwidth?.() ?? 0) : $xScale.bandwidth()) - inset
57
+ ? ($x1Scale ? ($x1Scale.bandwidth?.() ?? 0) : $xScale.bandwidth()) -
58
+ insets.left -
59
+ insets.right
59
60
  : 0);
60
61
  const yValue = _y(item);
61
62
  let top = 0;
@@ -80,12 +81,9 @@ export function createDimensionGetter(context, options) {
80
81
  top = min([0, maxYDomain]);
81
82
  bottom = yValue;
82
83
  }
83
- return {
84
- x,
85
- y: $yScale(top),
86
- width,
87
- height: $yScale(bottom) - $yScale(top),
88
- };
84
+ const y = $yScale(top) + insets.top;
85
+ const height = $yScale(bottom) - $yScale(top) - insets.bottom - insets.top;
86
+ return { x, y, width, height };
89
87
  }
90
88
  };
91
89
  });
@@ -97,3 +95,13 @@ export function createDimensionGetter(context, options) {
97
95
  export function firstValue(value) {
98
96
  return Array.isArray(value) ? value[0] : value;
99
97
  }
98
+ function resolveInsets(insets) {
99
+ const all = insets?.all ?? 0;
100
+ const x = insets?.x ?? all;
101
+ const y = insets?.y ?? all;
102
+ const left = insets?.left ?? x;
103
+ const right = insets?.right ?? x;
104
+ const top = insets?.top ?? y;
105
+ const bottom = insets?.bottom ?? y;
106
+ return { left, right, bottom, top };
107
+ }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "author": "Sean Lynch <techniq35@gmail.com>",
5
5
  "license": "MIT",
6
6
  "repository": "techniq/layerchart",
7
- "version": "0.73.0",
7
+ "version": "0.75.0",
8
8
  "devDependencies": {
9
9
  "@changesets/cli": "^2.27.10",
10
10
  "@mdi/js": "^7.4.47",