@rokkit/chart 1.3.6 → 1.3.7

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.
@@ -141,5 +141,7 @@
141
141
  }
142
142
  [data-plot-axis-line] {
143
143
  stroke: currentColor;
144
+ /* Decoration only — never intercept clicks meant for data marks beneath it. */
145
+ pointer-events: none;
144
146
  }
145
147
  </style>
@@ -2,26 +2,44 @@
2
2
  import { getContext } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
4
 
5
+ type Props = {
6
+ lines?: 'auto' | 'x' | 'y' | 'both'
7
+ xTicks?: number
8
+ yTicks?: number
9
+ }
10
+ let { lines = 'auto', xTicks = 6, yTicks = 6 }: Props = $props()
11
+
5
12
  const state = getContext<PlotState>('plot-state')
6
13
 
7
- const xGridLines = $derived.by(() => {
8
- const s = state.xScale
9
- if (!s || typeof s.bandwidth !== 'function') return []
10
- return s.domain().map((val: unknown) => ({ pos: (s(val) ?? 0) + s.bandwidth() / 2 }))
11
- })
14
+ const isBand = (s: unknown) =>
15
+ !!s && typeof (s as { bandwidth?: unknown }).bandwidth === 'function'
12
16
 
13
- const yGridLines = $derived.by(() => {
17
+ const showX = $derived(
18
+ lines === 'x' || lines === 'both' || (lines === 'auto' && isBand(state.xScale))
19
+ )
20
+ const showY = $derived(lines === 'auto' || lines === 'y' || lines === 'both')
21
+
22
+ const yLines = $derived.by(() => {
14
23
  const s = state.yScale
15
- if (!s || typeof s.ticks !== 'function') return []
16
- return s.ticks(6).map((val: number) => ({ pos: s(val) }))
24
+ if (!showY || !s || typeof s.ticks !== 'function') return []
25
+ return s.ticks(yTicks).map((val: number) => ({ pos: s(val) }))
26
+ })
27
+
28
+ const xLines = $derived.by(() => {
29
+ const s = state.xScale
30
+ if (!showX || !s) return []
31
+ if (isBand(s))
32
+ return s.domain().map((val: unknown) => ({ pos: (s(val) ?? 0) + s.bandwidth() / 2 }))
33
+ if (typeof s.ticks !== 'function') return []
34
+ return s.ticks(xTicks).map((val: number) => ({ pos: s(val) }))
17
35
  })
18
36
  </script>
19
37
 
20
38
  <g class="grid" data-plot-grid>
21
- {#each yGridLines as line (line.pos)}
22
- <line x1="0" y1={line.pos} x2={state.innerWidth} y2={line.pos} data-plot-grid-line />
39
+ {#each yLines as line (line.pos)}
40
+ <line x1="0" y1={line.pos} x2={state.innerWidth} y2={line.pos} data-plot-grid-line="y" />
23
41
  {/each}
24
- {#each xGridLines as line (line.pos)}
42
+ {#each xLines as line (line.pos)}
25
43
  <line x1={line.pos} y1="0" x2={line.pos} y2={state.innerHeight} data-plot-grid-line="x" />
26
44
  {/each}
27
45
  </g>
@@ -29,7 +47,8 @@
29
47
  <style>
30
48
  [data-plot-grid-line] {
31
49
  stroke: var(--chart-grid-color, currentColor);
32
- opacity: 0.15;
33
- stroke-dasharray: 2 4;
50
+ stroke-width: var(--chart-grid-width, 1);
51
+ stroke-dasharray: var(--chart-grid-dash, 2 4);
52
+ opacity: var(--chart-grid-opacity, 0.15);
34
53
  }
35
54
  </style>
@@ -1,3 +1,8 @@
1
- declare const Grid: import("svelte").Component<Record<string, never>, {}, "">;
1
+ type Props = {
2
+ lines?: 'auto' | 'x' | 'y' | 'both';
3
+ xTicks?: number;
4
+ yTicks?: number;
5
+ };
6
+ declare const Grid: import("svelte").Component<Props, {}, "">;
2
7
  type Grid = ReturnType<typeof Grid>;
3
8
  export default Grid;
package/dist/Plot.svelte CHANGED
@@ -21,8 +21,11 @@
21
21
  import Waterfall from './geoms/Waterfall.svelte'
22
22
  import Hexbin from './geoms/Hexbin.svelte'
23
23
  import Ribbon from './geoms/Ribbon.svelte'
24
+ import Highlight from './geoms/Highlight.svelte'
25
+ import Trend from './geoms/Trend.svelte'
24
26
 
25
27
  type Row = Record<string, unknown>
28
+ type Method = string | number | { type: string; [k: string]: unknown }
26
29
  type Margin = { top: number; right: number; bottom: number; left: number }
27
30
  type ChartPresetCtx = { current: typeof defaultPreset }
28
31
 
@@ -33,7 +36,7 @@
33
36
  width?: number
34
37
  height?: number
35
38
  mode?: 'light' | 'dark'
36
- grid?: boolean
39
+ grid?: boolean | 'x' | 'y' | 'both'
37
40
  axes?: boolean
38
41
  margin?: Margin
39
42
  legend?: boolean
@@ -46,6 +49,14 @@
46
49
  xTicks?: number
47
50
  yTicks?: number
48
51
  minorTicks?: boolean
52
+ x?: string
53
+ y?: string
54
+ highlight?: 'first' | 'last' | 'min' | 'max' | number | ((row: Row, i: number) => boolean)
55
+ label?: boolean | string | ((row: Row) => unknown)
56
+ trend?: Method | Method[]
57
+ onselect?: (detail: unknown) => void
58
+ selectable?: boolean
59
+ selected?: Row[]
49
60
  children?: Snippet
50
61
  }
51
62
 
@@ -69,6 +80,14 @@
69
80
  xTicks = undefined,
70
81
  yTicks = undefined,
71
82
  minorTicks = false,
83
+ x = undefined,
84
+ y = undefined,
85
+ highlight = undefined,
86
+ label = false,
87
+ trend = undefined,
88
+ onselect = undefined,
89
+ selectable = false,
90
+ selected = $bindable([]),
72
91
  children
73
92
  }: Props = $props()
74
93
 
@@ -92,7 +111,11 @@
92
111
 
93
112
  const effectiveWidth = $derived(observedWidth > 0 ? observedWidth : (spec?.width ?? width))
94
113
  const svgHeight = $derived(spec?.height ?? height)
95
- const showGrid = $derived(spec?.grid ?? grid)
114
+ const gridValue = $derived(spec?.grid ?? grid)
115
+ const showGrid = $derived(gridValue !== false)
116
+ const gridLines = $derived(
117
+ gridValue === true || gridValue === false ? 'auto' : gridValue
118
+ )
96
119
  const showLegend = $derived(spec?.legend ?? legend)
97
120
  const chartTitle = $derived(spec?.title ?? title)
98
121
  const chartSummary = $derived(spec?.summary ?? summary)
@@ -125,7 +148,9 @@
125
148
  colorScheme: spec?.colorScheme,
126
149
  colorMidpoint: spec?.colorMidpoint,
127
150
  orientation: spec?.orientation,
128
- chartPreset
151
+ chartPreset,
152
+ onselect,
153
+ selectable
129
154
  }
130
155
  }
131
156
 
@@ -140,6 +165,17 @@
140
165
  plotState.update(buildPlotConfig())
141
166
  })
142
167
 
168
+ // external control → state
169
+ $effect(() => {
170
+ plotState.setSelected(selected)
171
+ })
172
+ // clicks (state) → prop, guarded to avoid an update loop
173
+ $effect(() => {
174
+ const rows = plotState.selectedRows
175
+ const same = rows.length === selected.length && rows.every((r, i) => r === selected[i])
176
+ if (!same) selected = rows
177
+ })
178
+
143
179
  $effect(() => {
144
180
  if (!zoom || !svgEl) return
145
181
  // d3-zoom/d3-selection are client-only; load them dynamically so they are
@@ -169,6 +205,9 @@
169
205
  // Geoms from spec (spec-driven API)
170
206
  const specGeoms = $derived(spec?.geoms ?? [])
171
207
 
208
+ const overlayX = $derived(spec?.x ?? x)
209
+ const overlayY = $derived(spec?.y ?? y)
210
+
172
211
  // Geom component resolver for spec-driven mode
173
212
  const GEOM_COMPONENTS = {
174
213
  bar: Bar,
@@ -217,7 +256,7 @@
217
256
  >
218
257
  <!-- Grid (behind everything) -->
219
258
  {#if showGrid}
220
- <Grid />
259
+ <Grid lines={gridLines} {xTicks} {yTicks} />
221
260
  {/if}
222
261
 
223
262
  <!-- Declarative children (geom components) -->
@@ -250,6 +289,14 @@
250
289
  <Axis type="x" label={spec?.labels?.[spec?.x ?? ''] ?? ''} format={xFormat} ticks={xTicks} {minorTicks} />
251
290
  <Axis type="y" label={spec?.labels?.[spec?.y ?? ''] ?? ''} format={yFormat} ticks={yTicks} {minorTicks} />
252
291
  {/if}
292
+
293
+ {#if trend !== null && trend !== undefined}
294
+ <Trend x={overlayX} y={overlayY} {trend} />
295
+ {/if}
296
+
297
+ {#if (highlight !== null && highlight !== undefined) || selectable || selected.length}
298
+ <Highlight x={overlayX} y={overlayY} {highlight} {label} />
299
+ {/if}
253
300
  </g>
254
301
  </svg>
255
302
 
@@ -1,6 +1,10 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { PlotSpec, PlotHelpers } from './lib/plot/types.js';
3
3
  type Row = Record<string, unknown>;
4
+ type Method = string | number | {
5
+ type: string;
6
+ [k: string]: unknown;
7
+ };
4
8
  type Margin = {
5
9
  top: number;
6
10
  right: number;
@@ -14,7 +18,7 @@ type Props = {
14
18
  width?: number;
15
19
  height?: number;
16
20
  mode?: 'light' | 'dark';
17
- grid?: boolean;
21
+ grid?: boolean | 'x' | 'y' | 'both';
18
22
  axes?: boolean;
19
23
  margin?: Margin;
20
24
  legend?: boolean;
@@ -27,8 +31,16 @@ type Props = {
27
31
  xTicks?: number;
28
32
  yTicks?: number;
29
33
  minorTicks?: boolean;
34
+ x?: string;
35
+ y?: string;
36
+ highlight?: 'first' | 'last' | 'min' | 'max' | number | ((row: Row, i: number) => boolean);
37
+ label?: boolean | string | ((row: Row) => unknown);
38
+ trend?: Method | Method[];
39
+ onselect?: (detail: unknown) => void;
40
+ selectable?: boolean;
41
+ selected?: Row[];
30
42
  children?: Snippet;
31
43
  };
32
- declare const Plot: import("svelte").Component<Props, {}, "">;
44
+ declare const Plot: import("svelte").Component<Props, {}, "selected">;
33
45
  type Plot = ReturnType<typeof Plot>;
34
46
  export default Plot;
@@ -40,6 +40,7 @@ export class PlotState {
40
40
  patterns: any;
41
41
  symbols: any;
42
42
  };
43
+ get data(): any[];
43
44
  get margin(): {
44
45
  top: number;
45
46
  right: number;
@@ -71,8 +72,14 @@ export class PlotState {
71
72
  symbols: string[];
72
73
  };
73
74
  get hovered(): null;
75
+ get interactive(): boolean;
76
+ get selectedRows(): any[];
74
77
  setHovered(data: any): void;
75
78
  clearHovered(): void;
79
+ isSelected(row: any): boolean;
80
+ setSelected(rows: any): void;
81
+ clearSelected(): void;
82
+ handleSelect(detail: any): void;
76
83
  applyZoom(transform: any): void;
77
84
  resetZoom(): void;
78
85
  #private;
@@ -39,6 +39,9 @@ export class PlotState {
39
39
  #mode = $state('light')
40
40
  #chartPreset = $state(defaultPreset)
41
41
  #hovered = $state(null)
42
+ #selected = $state(new SvelteSet())
43
+ #onselect = $state(undefined)
44
+ #selectable = $state(false)
42
45
  #orientationOverride = $state(undefined)
43
46
 
44
47
  axisOrigin = $state([undefined, undefined])
@@ -274,6 +277,9 @@ export class PlotState {
274
277
  this.#height = config.height ?? 400
275
278
  this.#mode = config.mode ?? 'light'
276
279
  this.#chartPreset = config.chartPreset ?? defaultPreset
280
+ this.#onselect = config.onselect
281
+ this.#selectable = config.selectable ?? false
282
+ if (config.selected) this.#selected = new SvelteSet(config.selected)
277
283
  this.#axisOffset = config.axisOffset ?? 0
278
284
  this.#marginOverride = config.margin ?? undefined
279
285
  this.#orientationOverride = config.orientation ?? undefined
@@ -298,6 +304,8 @@ export class PlotState {
298
304
  if (config.height !== undefined) this.#height = config.height
299
305
  if (config.mode !== undefined) this.#mode = config.mode
300
306
  if (config.chartPreset !== undefined) this.#chartPreset = config.chartPreset
307
+ if (config.onselect !== undefined) this.#onselect = config.onselect
308
+ if (config.selectable !== undefined) this.#selectable = config.selectable
301
309
  if (config.axisOffset !== undefined) this.#axisOffset = config.axisOffset
302
310
  this.#marginOverride = config.margin ?? undefined
303
311
  this.#orientationOverride = config.orientation ?? undefined
@@ -345,6 +353,13 @@ export class PlotState {
345
353
  return resolvePreset(this.#presetName, this.#helpers)
346
354
  }
347
355
 
356
+ get data() {
357
+ // Return #rawData (not #data): geoms read their rows via geomData() which
358
+ // returns #rawData, so overlays and index lookups (plotState.data.indexOf(datum))
359
+ // must use the SAME proxy identity. #data and #rawData proxy the same source
360
+ // array separately, so their elements are not ===. Same content, aligned identity.
361
+ return this.#rawData
362
+ }
348
363
  get margin() {
349
364
  return this.#effectiveMargin
350
365
  }
@@ -363,6 +378,12 @@ export class PlotState {
363
378
  get hovered() {
364
379
  return this.#hovered
365
380
  }
381
+ get interactive() {
382
+ return Boolean(this.#onselect) || this.#selectable
383
+ }
384
+ get selectedRows() {
385
+ return [...this.#selected]
386
+ }
366
387
 
367
388
  setHovered(data) {
368
389
  this.#hovered = data
@@ -370,6 +391,22 @@ export class PlotState {
370
391
  clearHovered() {
371
392
  this.#hovered = null
372
393
  }
394
+ isSelected(row) {
395
+ return this.#selected.has(row)
396
+ }
397
+ setSelected(rows) {
398
+ this.#selected = new SvelteSet(rows ?? [])
399
+ }
400
+ clearSelected() {
401
+ this.#selected = new SvelteSet()
402
+ }
403
+ handleSelect(detail) {
404
+ this.#onselect?.(detail)
405
+ if (this.#selectable && detail?.datum !== undefined) {
406
+ if (this.#selected.has(detail.datum)) this.#selected.delete(detail.datum)
407
+ else this.#selected.add(detail.datum)
408
+ }
409
+ }
373
410
 
374
411
  applyZoom(transform) {
375
412
  this.#zoomTransform = transform
@@ -3,6 +3,7 @@
3
3
  import Area from '../geoms/Area.svelte'
4
4
 
5
5
  type Row = Record<string, unknown>
6
+ type Method = string | number | { type: string; [k: string]: unknown }
6
7
  type Format = (v: unknown) => string
7
8
 
8
9
  type Props = {
@@ -17,13 +18,18 @@
17
18
  width?: number
18
19
  height?: number
19
20
  mode?: 'light' | 'dark'
20
- grid?: boolean
21
+ grid?: boolean | 'x' | 'y' | 'both'
21
22
  legend?: boolean
23
+ highlight?: 'first' | 'last' | 'min' | 'max' | number | ((row: Row, i: number) => boolean)
24
+ trend?: Method | Method[]
22
25
  xFormat?: Format
23
26
  yFormat?: Format
24
27
  xTicks?: number
25
28
  yTicks?: number
26
29
  minorTicks?: boolean
30
+ onselect?: (detail: unknown) => void
31
+ selectable?: boolean
32
+ selected?: Row[]
27
33
  }
28
34
 
29
35
  let {
@@ -40,14 +46,38 @@
40
46
  mode = 'light',
41
47
  grid = true,
42
48
  legend = false,
49
+ highlight = undefined,
50
+ trend = undefined,
43
51
  xFormat = undefined,
44
52
  yFormat = undefined,
45
53
  xTicks = undefined,
46
54
  yTicks = undefined,
47
- minorTicks = false
55
+ minorTicks = false,
56
+ onselect = undefined,
57
+ selectable = false,
58
+ selected = $bindable([])
48
59
  }: Props = $props()
49
60
  </script>
50
61
 
51
- <Plot {data} {width} {height} {mode} {grid} {legend} {xFormat} {yFormat} {xTicks} {yTicks} {minorTicks}>
62
+ <Plot
63
+ {data}
64
+ {x}
65
+ {y}
66
+ {width}
67
+ {height}
68
+ {mode}
69
+ {grid}
70
+ {legend}
71
+ {highlight}
72
+ {trend}
73
+ {xFormat}
74
+ {yFormat}
75
+ {xTicks}
76
+ {yTicks}
77
+ {minorTicks}
78
+ {onselect}
79
+ {selectable}
80
+ bind:selected
81
+ >
52
82
  <Area {x} {y} color={fill} {pattern} {stat} options={{ curve, stack }} />
53
83
  </Plot>
@@ -1,4 +1,8 @@
1
1
  type Row = Record<string, unknown>;
2
+ type Method = string | number | {
3
+ type: string;
4
+ [k: string]: unknown;
5
+ };
2
6
  type Format = (v: unknown) => string;
3
7
  type Props = {
4
8
  data?: Row[];
@@ -12,14 +16,19 @@ type Props = {
12
16
  width?: number;
13
17
  height?: number;
14
18
  mode?: 'light' | 'dark';
15
- grid?: boolean;
19
+ grid?: boolean | 'x' | 'y' | 'both';
16
20
  legend?: boolean;
21
+ highlight?: 'first' | 'last' | 'min' | 'max' | number | ((row: Row, i: number) => boolean);
22
+ trend?: Method | Method[];
17
23
  xFormat?: Format;
18
24
  yFormat?: Format;
19
25
  xTicks?: number;
20
26
  yTicks?: number;
21
27
  minorTicks?: boolean;
28
+ onselect?: (detail: unknown) => void;
29
+ selectable?: boolean;
30
+ selected?: Row[];
22
31
  };
23
- declare const AreaChart: import("svelte").Component<Props, {}, "">;
32
+ declare const AreaChart: import("svelte").Component<Props, {}, "selected">;
24
33
  type AreaChart = ReturnType<typeof AreaChart>;
25
34
  export default AreaChart;
@@ -3,6 +3,7 @@
3
3
  import Line from '../geoms/Line.svelte'
4
4
 
5
5
  type Row = Record<string, unknown>
6
+ type Method = string | number | { type: string; [k: string]: unknown }
6
7
  type Format = (v: unknown) => string
7
8
 
8
9
  type Props = {
@@ -18,13 +19,18 @@
18
19
  width?: number
19
20
  height?: number
20
21
  mode?: 'light' | 'dark'
21
- grid?: boolean
22
+ grid?: boolean | 'x' | 'y' | 'both'
22
23
  legend?: boolean
24
+ highlight?: 'first' | 'last' | 'min' | 'max' | number | ((row: Row, i: number) => boolean)
25
+ trend?: Method | Method[]
23
26
  xFormat?: Format
24
27
  yFormat?: Format
25
28
  xTicks?: number
26
29
  yTicks?: number
27
30
  minorTicks?: boolean
31
+ onselect?: (detail: unknown) => void
32
+ selectable?: boolean
33
+ selected?: Row[]
28
34
  }
29
35
 
30
36
  let {
@@ -42,14 +48,39 @@
42
48
  mode = 'light',
43
49
  grid = true,
44
50
  legend = false,
51
+ highlight = undefined,
52
+ trend = undefined,
45
53
  xFormat = undefined,
46
54
  yFormat = undefined,
47
55
  xTicks = undefined,
48
56
  yTicks = undefined,
49
- minorTicks = false
57
+ minorTicks = false,
58
+ onselect = undefined,
59
+ selectable = false,
60
+ selected = $bindable([])
50
61
  }: Props = $props()
51
62
  </script>
52
63
 
53
- <Plot {data} {width} {height} {mode} {grid} {legend} {tooltip} {xFormat} {yFormat} {xTicks} {yTicks} {minorTicks}>
64
+ <Plot
65
+ {data}
66
+ {x}
67
+ {y}
68
+ {width}
69
+ {height}
70
+ {mode}
71
+ {grid}
72
+ {legend}
73
+ {tooltip}
74
+ {highlight}
75
+ {trend}
76
+ {xFormat}
77
+ {yFormat}
78
+ {xTicks}
79
+ {yTicks}
80
+ {minorTicks}
81
+ {onselect}
82
+ {selectable}
83
+ bind:selected
84
+ >
54
85
  <Line {x} {y} {color} {symbol} {label} {stat} options={{ curve }} />
55
86
  </Plot>
@@ -1,4 +1,8 @@
1
1
  type Row = Record<string, unknown>;
2
+ type Method = string | number | {
3
+ type: string;
4
+ [k: string]: unknown;
5
+ };
2
6
  type Format = (v: unknown) => string;
3
7
  type Props = {
4
8
  data?: Row[];
@@ -13,14 +17,19 @@ type Props = {
13
17
  width?: number;
14
18
  height?: number;
15
19
  mode?: 'light' | 'dark';
16
- grid?: boolean;
20
+ grid?: boolean | 'x' | 'y' | 'both';
17
21
  legend?: boolean;
22
+ highlight?: 'first' | 'last' | 'min' | 'max' | number | ((row: Row, i: number) => boolean);
23
+ trend?: Method | Method[];
18
24
  xFormat?: Format;
19
25
  yFormat?: Format;
20
26
  xTicks?: number;
21
27
  yTicks?: number;
22
28
  minorTicks?: boolean;
29
+ onselect?: (detail: unknown) => void;
30
+ selectable?: boolean;
31
+ selected?: Row[];
23
32
  };
24
- declare const LineChart: import("svelte").Component<Props, {}, "">;
33
+ declare const LineChart: import("svelte").Component<Props, {}, "selected">;
25
34
  type LineChart = ReturnType<typeof LineChart>;
26
35
  export default LineChart;
@@ -2,6 +2,9 @@
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
4
  import { buildAreas, buildStackedAreas } from './lib/areas.js'
5
+ import { buildSelectDetail } from '../lib/select.js'
6
+
7
+ type Row = Record<string, unknown>
5
8
 
6
9
  type Options = {
7
10
  stack?: boolean
@@ -23,6 +26,13 @@
23
26
  const plotState = getContext<PlotState>('plot-state')
24
27
  let id = $state<string | null>(null)
25
28
 
29
+ function selectPoint(d: Row, event: MouseEvent | KeyboardEvent) {
30
+ if (plotState.interactive)
31
+ plotState.handleSelect(
32
+ buildSelectDetail(d, plotState.data.indexOf(d), { x, y }, 'area', color ? d[color] : undefined, event)
33
+ )
34
+ }
35
+
26
36
  onMount(() => {
27
37
  id = plotState.registerGeom({
28
38
  type: 'area',
@@ -79,16 +89,23 @@
79
89
  {#each data as d, i (`hover::${i}`)}
80
90
  {@const px = typeof xScale?.bandwidth === 'function' ? (xScale(d[x]) ?? 0) + xScale.bandwidth() / 2 : (xScale?.(d[x]) ?? 0)}
81
91
  {@const py = yScale?.(d[y]) ?? 0}
92
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
82
93
  <circle
83
94
  cx={px}
84
95
  cy={py}
85
96
  r="8"
86
97
  fill="transparent"
87
98
  stroke="none"
88
- role="presentation"
99
+ role={plotState.interactive ? 'button' : 'presentation'}
100
+ tabindex={plotState.interactive ? 0 : undefined}
101
+ style:cursor={plotState.interactive ? 'pointer' : undefined}
89
102
  data-plot-element="area-hover"
90
103
  onmouseenter={() => plotState.setHovered(d)}
91
104
  onmouseleave={() => plotState.clearHovered()}
105
+ onclick={plotState.interactive ? (e) => selectPoint(d, e) : undefined}
106
+ onkeydown={plotState.interactive
107
+ ? (e) => (e.key === 'Enter' || e.key === ' ') && selectPoint(d, e)
108
+ : undefined}
92
109
  />
93
110
  {/each}
94
111
  {/if}
@@ -4,6 +4,7 @@
4
4
  import type { createCrossFilter } from '../crossfilter/createCrossFilter.svelte.js'
5
5
  import { buildGroupedBars, buildStackedBars, buildHorizontalBars } from './lib/bars.js'
6
6
  import { keyboardNav } from '../lib/keyboard-nav.js'
7
+ import { buildSelectDetail } from '../lib/select.js'
7
8
  import LabelPill from './LabelPill.svelte'
8
9
 
9
10
  type Row = Record<string, unknown>
@@ -134,6 +135,36 @@
134
135
  if (!filterable || !x || !cf) return
135
136
  cf.toggleCategorical(x, barX)
136
137
  }
138
+
139
+ function selectBar(bar: { data: Row }, event: MouseEvent | KeyboardEvent) {
140
+ if (plotState.interactive)
141
+ plotState.handleSelect(
142
+ buildSelectDetail(
143
+ bar.data,
144
+ plotState.data.indexOf(bar.data),
145
+ { x, y },
146
+ 'bar',
147
+ colorChannel ? bar.data[colorChannel] : undefined,
148
+ event
149
+ )
150
+ )
151
+ }
152
+
153
+ function handleBarActivate(bar: { data: Row }, event: MouseEvent) {
154
+ if (filterable && x) handleBarClick(bar.data[x])
155
+ onselect?.(bar.data)
156
+ selectBar(bar, event)
157
+ }
158
+
159
+ function handleBarKeyActivate(bar: { data: Row }, event: KeyboardEvent) {
160
+ if (event.key !== 'Enter' && event.key !== ' ') return
161
+ if (filterable && x) {
162
+ handleBarClick(bar.data[x])
163
+ } else {
164
+ onselect?.(bar.data)
165
+ }
166
+ selectBar(bar, event)
167
+ }
137
168
  </script>
138
169
 
139
170
  {#if bars.length > 0}
@@ -152,13 +183,15 @@
152
183
  data-plot-value={bar.data[y ?? '']}
153
184
  data-plot-category={bar.data[x ?? '']}
154
185
  data-dimmed={dimmedByKey[bar.key] ? true : undefined}
155
- style:cursor={filterable || onselect ? 'pointer' : undefined}
156
- onclick={filterable && x ? () => { handleBarClick(bar.data[x]); onselect?.(bar.data) } : onselect ? () => onselect(bar.data) : undefined}
157
- onkeydown={filterable && x
158
- ? (e) => (e.key === 'Enter' || e.key === ' ') && handleBarClick(bar.data[x])
159
- : onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(bar.data) : undefined}
160
- role={filterable || onselect || keyboard ? 'button' : 'graphics-symbol'}
161
- tabindex={filterable || onselect || keyboard ? 0 : undefined}
186
+ style:cursor={filterable || onselect || plotState.interactive ? 'pointer' : undefined}
187
+ onclick={filterable || onselect || keyboard || plotState.interactive
188
+ ? (e) => handleBarActivate(bar, e)
189
+ : undefined}
190
+ onkeydown={filterable || onselect || keyboard || plotState.interactive
191
+ ? (e) => handleBarKeyActivate(bar, e)
192
+ : undefined}
193
+ role={filterable || onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
194
+ tabindex={filterable || onselect || keyboard || plotState.interactive ? 0 : undefined}
162
195
  use:keyboardNav={keyboard}
163
196
  aria-label="{bar.data[x ?? '']}: {bar.data[y ?? '']}"
164
197
  onmouseenter={() => plotState.setHovered(bar.data)}