layerchart 0.74.0 → 0.75.1

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;
@@ -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);
@@ -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))}
@@ -329,12 +374,12 @@
329
374
  />
330
375
  {/each}
331
376
 
332
- {#if stackSeries}
377
+ {#if stackSeries && visibleSeries.length > 1}
333
378
  <Tooltip.Separator {...props.tooltip?.separator} />
334
379
 
335
380
  <Tooltip.Item
336
381
  label="total"
337
- value={sum(series, (s) => {
382
+ value={sum(visibleSeries, (s) => {
338
383
  const seriesTooltipData = s.data ? s.data.find((d) => x(d) === x(data)) : data;
339
384
  const valueAccessor = accessor(s.value ?? (s.data ? asAny(y) : s.key));
340
385
 
@@ -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';
@@ -38,6 +40,7 @@
38
40
  labels?: typeof labels;
39
41
  legend?: typeof legend;
40
42
  orientation?: typeof orientation;
43
+ profile?: typeof profile;
41
44
  props?: typeof props;
42
45
  rule?: typeof rule;
43
46
  series?: typeof series;
@@ -113,11 +116,11 @@
113
116
  $: if (seriesLayout === 'group') {
114
117
  if (isVertical) {
115
118
  x1Scale = scaleBand().padding(groupPadding);
116
- x1Domain = series.map((s) => s.key);
119
+ x1Domain = visibleSeries.map((s) => s.key);
117
120
  x1Range = ({ xScale }) => [0, xScale.bandwidth?.()];
118
121
  } else {
119
122
  y1Scale = scaleBand().padding(groupPadding);
120
- y1Domain = series.map((s) => s.key);
123
+ y1Domain = visibleSeries.map((s) => s.key);
121
124
  y1Range = ({ yScale }) => [0, yScale.bandwidth?.()];
122
125
  }
123
126
  }
@@ -142,7 +145,10 @@
142
145
 
143
146
  export let renderContext: 'svg' | 'canvas' = 'svg';
144
147
 
145
- $: allSeriesData = series
148
+ /** Log initial render performance using `console.time` */
149
+ export let profile = false;
150
+
151
+ $: allSeriesData = visibleSeries
146
152
  .flatMap((s) =>
147
153
  s.data?.map((d) => {
148
154
  return { seriesKey: s.key, ...d };
@@ -155,7 +161,7 @@
155
161
  >;
156
162
 
157
163
  $: if (stackSeries) {
158
- const seriesKeys = series.map((s) => s.key);
164
+ const seriesKeys = visibleSeries.map((s) => s.key);
159
165
  // const stackData = stack().keys(seriesKeys)(chartDataArray(data)) as any[];
160
166
 
161
167
  const offset =
@@ -180,9 +186,11 @@
180
186
  });
181
187
  }
182
188
 
189
+ let highlightSeriesKey: (typeof series)[number]['key'] | null = null;
190
+
183
191
  function getBarsProps(s: (typeof series)[number], i: number) {
184
192
  const isFirst = i == 0;
185
- const isLast = i == series.length - 1;
193
+ const isLast = i == visibleSeries.length - 1;
186
194
 
187
195
  const isStackLayout = seriesLayout.startsWith('stack');
188
196
 
@@ -212,11 +220,15 @@
212
220
  y: isVertical ? valueAccessor : undefined,
213
221
  x1: isVertical && groupSeries ? (d) => s.value ?? s.key : undefined,
214
222
  y1: !isVertical && groupSeries ? (d) => s.value ?? s.key : undefined,
215
- rounded: isStackLayout && i !== series.length - 1 ? 'none' : 'edge',
223
+ rounded: isStackLayout && i !== visibleSeries.length - 1 ? 'none' : 'edge',
216
224
  radius: 4,
217
225
  strokeWidth: 1,
218
226
  insets: stackInsets,
219
227
  fill: s.color,
228
+ class: cls(
229
+ 'transition-opacity',
230
+ highlightSeriesKey && highlightSeriesKey !== s.key && 'opacity-10'
231
+ ),
220
232
  onBarClick: (e) => onBarClick({ data: e.data, series: s }),
221
233
  ...props.bars,
222
234
  ...s.props,
@@ -224,14 +236,30 @@
224
236
 
225
237
  return barsProps;
226
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
+ }
227
255
  </script>
228
256
 
229
257
  <Chart
230
258
  data={chartData}
231
259
  x={x ??
232
260
  (stackSeries
233
- ? (d) => series.flatMap((s, i) => d.stackData[i])
234
- : series.map((s) => s.value ?? s.key))}
261
+ ? (d) => visibleSeries.flatMap((s, i) => d.stackData[i])
262
+ : visibleSeries.map((s) => s.value ?? s.key))}
235
263
  {xScale}
236
264
  {xBaseline}
237
265
  xNice={orientation === 'horizontal'}
@@ -240,8 +268,8 @@
240
268
  {x1Range}
241
269
  y={y ??
242
270
  (stackSeries
243
- ? (d) => series.flatMap((s, i) => d.stackData[i])
244
- : series.map((s) => s.value ?? s.key))}
271
+ ? (d) => visibleSeries.flatMap((s, i) => d.stackData[i])
272
+ : visibleSeries.map((s) => s.value ?? s.key))}
245
273
  {yScale}
246
274
  {yBaseline}
247
275
  yNice={orientation === 'vertical'}
@@ -294,7 +322,7 @@
294
322
  <slot name="belowMarks" {...slotProps} />
295
323
 
296
324
  <slot name="marks" {...slotProps}>
297
- {#each series as s, i (s.key)}
325
+ {#each visibleSeries as s, i (s.key)}
298
326
  <Bars {...getBarsProps(s, i)} />
299
327
  {/each}
300
328
  </slot>
@@ -359,11 +387,21 @@
359
387
  scale={isDefaultSeries
360
388
  ? undefined
361
389
  : scaleOrdinal(
362
- series.map((s) => s.label ?? s.key),
390
+ series.map((s) => s.key),
363
391
  series.map((s) => s.color)
364
392
  )}
393
+ tickFormat={(key) => series.find((s) => s.key === key)?.label ?? key}
365
394
  placement="bottom"
366
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
+ }}
367
405
  {...props.legend}
368
406
  {...typeof legend === 'object' ? legend : null}
369
407
  />
@@ -377,7 +415,7 @@
377
415
  >
378
416
  <Tooltip.List {...props.tooltip?.list}>
379
417
  <!-- Reverse series order so tooltip items match stacks -->
380
- {@const seriesItems = stackSeries ? [...series].reverse() : series}
418
+ {@const seriesItems = stackSeries ? [...visibleSeries].reverse() : visibleSeries}
381
419
  {#each seriesItems as s}
382
420
  {@const seriesTooltipData = s.data ? findRelatedData(s.data, data, x) : data}
383
421
  {@const valueAccessor = accessor(s.value ?? (s.data ? asAny(y) : s.key))}
@@ -391,12 +429,12 @@
391
429
  />
392
430
  {/each}
393
431
 
394
- {#if stackSeries || groupSeries}
432
+ {#if (stackSeries || groupSeries) && visibleSeries.length > 1}
395
433
  <Tooltip.Separator {...props.tooltip?.separator} />
396
434
 
397
435
  <Tooltip.Item
398
436
  label="total"
399
- value={sum(series, (s) => {
437
+ value={sum(visibleSeries, (s) => {
400
438
  const seriesTooltipData = s.data ? findRelatedData(s.data, data, x) : data;
401
439
  const valueAccessor = accessor(s.value ?? (s.data ? asAny(y) : s.key));
402
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
  />
@@ -171,18 +171,18 @@
171
171
 
172
172
  const referenceNode = (e.target as Element).closest('.layercake-container')!;
173
173
  const point = localPoint(referenceNode, e);
174
- const localX = point?.x ?? 0;
175
- const localY = point?.y ?? 0;
174
+ const pointerX = point?.x ?? 0;
175
+ const pointerY = point?.y ?? 0;
176
176
 
177
177
  if (
178
178
  // @ts-expect-error
179
- e.offsetX < e.currentTarget?.offsetLeft ||
179
+ pointerX < e.currentTarget?.offsetLeft ||
180
180
  // @ts-expect-error
181
- e.offsetX > e.currentTarget?.offsetLeft + e.currentTarget?.offsetWidth ||
181
+ pointerX > e.currentTarget?.offsetLeft + e.currentTarget?.offsetWidth ||
182
182
  // @ts-expect-error
183
- e.offsetY < e.currentTarget?.offsetTop ||
183
+ pointerY < e.currentTarget?.offsetTop ||
184
184
  // @ts-expect-error
185
- e.offsetY > e.currentTarget?.offsetTop + e.currentTarget?.offsetHeight
185
+ pointerY > e.currentTarget?.offsetTop + e.currentTarget?.offsetHeight
186
186
  ) {
187
187
  // Ignore if within padding of chart
188
188
  hideTooltip();
@@ -197,10 +197,10 @@
197
197
  let xValueAtPoint: any;
198
198
  if ($radial) {
199
199
  // Assume radial is always centered
200
- const { radians } = cartesianToPolar(localX - $width / 2, localY - $height / 2);
200
+ const { radians } = cartesianToPolar(pointerX - $width / 2, pointerY - $height / 2);
201
201
  xValueAtPoint = scaleInvert($xScale, radians);
202
202
  } else {
203
- xValueAtPoint = scaleInvert($xScale, localX - $padding.left);
203
+ xValueAtPoint = scaleInvert($xScale, pointerX - $padding.left);
204
204
  }
205
205
 
206
206
  const index = bisectX($flatData, xValueAtPoint, 1);
@@ -212,7 +212,7 @@
212
212
 
213
213
  case 'bisect-y': {
214
214
  // `y` value at pointer coordinate
215
- const yValueAtPoint = scaleInvert($yScale, localY - $padding.top);
215
+ const yValueAtPoint = scaleInvert($yScale, pointerY - $padding.top);
216
216
 
217
217
  const index = bisectY($flatData, yValueAtPoint, 1);
218
218
  const previousValue = $flatData[index - 1];
@@ -223,8 +223,8 @@
223
223
 
224
224
  case 'bisect-band': {
225
225
  // `x` and `y` values at pointer coordinate
226
- const xValueAtPoint = scaleInvert($xScale, localX);
227
- const yValueAtPoint = scaleInvert($yScale, localY);
226
+ const xValueAtPoint = scaleInvert($xScale, pointerX);
227
+ const yValueAtPoint = scaleInvert($yScale, pointerY);
228
228
 
229
229
  if (isScaleBand($xScale)) {
230
230
  // Find point closest to pointer within the x band
@@ -251,7 +251,7 @@
251
251
  }
252
252
 
253
253
  case 'quadtree': {
254
- tooltipData = quadtree.find(localX, localY, radius);
254
+ tooltipData = quadtree.find(pointerX, pointerY, radius);
255
255
  break;
256
256
  }
257
257
  }
@@ -264,8 +264,8 @@
264
264
 
265
265
  $tooltip = {
266
266
  ...$tooltip,
267
- x: localX,
268
- y: localY,
267
+ x: pointerX,
268
+ y: pointerY,
269
269
  data: tooltipData,
270
270
  };
271
271
  } else {
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.74.0",
7
+ "version": "0.75.1",
8
8
  "devDependencies": {
9
9
  "@changesets/cli": "^2.27.10",
10
10
  "@mdi/js": "^7.4.47",