matterviz 0.3.3 → 0.3.5

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.
Files changed (126) hide show
  1. package/dist/FilePicker.svelte +1 -1
  2. package/dist/app.css +7 -0
  3. package/dist/brillouin/BrillouinZone.svelte +5 -2
  4. package/dist/brillouin/compute.js +8 -4
  5. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +6 -6
  6. package/dist/chempot-diagram/async-compute.svelte.js +6 -5
  7. package/dist/chempot-diagram/chempot-worker.js +2 -2
  8. package/dist/chempot-diagram/compute.js +16 -16
  9. package/dist/composition/FormulaFilter.svelte +3 -3
  10. package/dist/constants.js +2 -8
  11. package/dist/convex-hull/ConvexHull.svelte +2 -2
  12. package/dist/convex-hull/ConvexHull2D.svelte +11 -10
  13. package/dist/convex-hull/ConvexHull3D.svelte +16 -14
  14. package/dist/convex-hull/ConvexHull4D.svelte +26 -14
  15. package/dist/convex-hull/ConvexHullControls.svelte +1 -1
  16. package/dist/convex-hull/ConvexHullInfoPane.svelte +68 -61
  17. package/dist/convex-hull/ConvexHullStats.svelte +23 -6
  18. package/dist/convex-hull/GasPressureControls.svelte +3 -3
  19. package/dist/convex-hull/TemperatureSlider.svelte +1 -1
  20. package/dist/convex-hull/barycentric-coords.js +2 -2
  21. package/dist/convex-hull/helpers.js +45 -27
  22. package/dist/convex-hull/thermodynamics.js +2 -2
  23. package/dist/element/BohrAtom.svelte +25 -27
  24. package/dist/element/BohrAtom.svelte.d.ts +2 -2
  25. package/dist/element/data.d.ts +2 -3
  26. package/dist/element/data.js +1 -1
  27. package/dist/fermi-surface/FermiSurface.svelte +5 -2
  28. package/dist/fermi-surface/compute.js +3 -3
  29. package/dist/fermi-surface/parse.js +2 -2
  30. package/dist/fermi-surface/symmetry.js +1 -1
  31. package/dist/heatmap-matrix/HeatmapMatrix.svelte +8 -8
  32. package/dist/icons.d.ts +6 -6
  33. package/dist/icons.js +6 -6
  34. package/dist/io/decompress.js +12 -7
  35. package/dist/io/export.js +20 -16
  36. package/dist/io/is-binary.js +19 -4
  37. package/dist/isosurface/parse.js +8 -8
  38. package/dist/isosurface/types.js +9 -9
  39. package/dist/layout/InfoTag.svelte +1 -1
  40. package/dist/layout/json-tree/JsonNode.svelte +1 -0
  41. package/dist/layout/json-tree/utils.js +2 -1
  42. package/dist/marching-cubes.js +1 -1
  43. package/dist/math.js +1 -1
  44. package/dist/overlays/CopyButton.svelte +45 -0
  45. package/dist/overlays/CopyButton.svelte.d.ts +8 -0
  46. package/dist/overlays/InfoPaneCards.svelte +149 -0
  47. package/dist/overlays/InfoPaneCards.svelte.d.ts +22 -0
  48. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +33 -35
  49. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +2 -2
  50. package/dist/phase-diagram/PhaseDiagramControls.svelte +27 -29
  51. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +2 -2
  52. package/dist/phase-diagram/parse.js +3 -3
  53. package/dist/phase-diagram/svg-to-diagram.js +10 -12
  54. package/dist/plot/BarPlot.svelte +24 -15
  55. package/dist/plot/BarPlot.svelte.d.ts +3 -2
  56. package/dist/plot/FillArea.svelte +2 -3
  57. package/dist/plot/FillArea.svelte.d.ts +3 -2
  58. package/dist/plot/Histogram.svelte +37 -19
  59. package/dist/plot/Line.svelte +2 -3
  60. package/dist/plot/Line.svelte.d.ts +2 -2
  61. package/dist/plot/PlotLegend.svelte +79 -8
  62. package/dist/plot/PlotLegend.svelte.d.ts +4 -0
  63. package/dist/plot/PortalSelect.svelte +5 -5
  64. package/dist/plot/ScatterPlot.svelte +47 -33
  65. package/dist/plot/ScatterPlot.svelte.d.ts +5 -4
  66. package/dist/plot/ScatterPlot3D.svelte +6 -3
  67. package/dist/plot/ScatterPoint.svelte +10 -4
  68. package/dist/plot/ScatterPoint.svelte.d.ts +4 -2
  69. package/dist/plot/SpacegroupBarPlot.svelte +5 -4
  70. package/dist/plot/data-cleaning.js +9 -9
  71. package/dist/plot/index.d.ts +0 -6
  72. package/dist/plot/scales.d.ts +3 -3
  73. package/dist/plot/scales.js +29 -29
  74. package/dist/plot/types.d.ts +5 -9
  75. package/dist/rdf/calc-rdf.js +1 -1
  76. package/dist/sanitize.js +22 -15
  77. package/dist/settings.d.ts +2 -0
  78. package/dist/settings.js +12 -3
  79. package/dist/spectral/Bands.svelte +6 -6
  80. package/dist/spectral/BandsAndDos.svelte +4 -4
  81. package/dist/spectral/BrillouinBandsDos.svelte +3 -3
  82. package/dist/spectral/Dos.svelte +2 -2
  83. package/dist/spectral/helpers.js +1 -1
  84. package/dist/structure/AtomLegend.svelte +4 -4
  85. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  86. package/dist/structure/Cylinder.svelte +7 -7
  87. package/dist/structure/Structure.svelte +169 -27
  88. package/dist/structure/Structure.svelte.d.ts +6 -2
  89. package/dist/structure/StructureControls.svelte +130 -16
  90. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  91. package/dist/structure/StructureInfoPane.svelte +519 -218
  92. package/dist/structure/StructureInfoPane.svelte.d.ts +2 -1
  93. package/dist/structure/StructureScene.svelte +399 -68
  94. package/dist/structure/StructureScene.svelte.d.ts +8 -4
  95. package/dist/structure/atom-properties.js +3 -1
  96. package/dist/structure/bond-order-perception.d.ts +13 -0
  97. package/dist/structure/bond-order-perception.js +367 -0
  98. package/dist/structure/bonding.d.ts +10 -1
  99. package/dist/structure/bonding.js +232 -11
  100. package/dist/structure/export.js +6 -4
  101. package/dist/structure/index.d.ts +19 -4
  102. package/dist/structure/index.js +3 -0
  103. package/dist/structure/label-placement.d.ts +14 -0
  104. package/dist/structure/label-placement.js +72 -0
  105. package/dist/structure/parse.d.ts +2 -1
  106. package/dist/structure/parse.js +25 -36
  107. package/dist/structure/supercell.js +35 -2
  108. package/dist/symmetry/SymmetryStats.svelte +1 -1
  109. package/dist/symmetry/cell-transform.js +15 -1
  110. package/dist/symmetry/index.js +3 -3
  111. package/dist/table/HeatmapTable.svelte +3 -3
  112. package/dist/table/ToggleMenu.svelte +1 -1
  113. package/dist/trajectory/Trajectory.svelte +2 -2
  114. package/dist/trajectory/TrajectoryInfoPane.svelte +14 -88
  115. package/dist/trajectory/extract.js +4 -4
  116. package/dist/trajectory/frame-reader.js +2 -2
  117. package/dist/trajectory/parse/ase.js +2 -6
  118. package/dist/trajectory/parse/hdf5.js +1 -3
  119. package/dist/trajectory/plotting.js +1 -1
  120. package/dist/utils.js +1 -1
  121. package/dist/xrd/calc-xrd.js +1 -1
  122. package/package.json +23 -38
  123. package/dist/structure/ferrox-wasm-types.d.ts +0 -46
  124. package/dist/structure/ferrox-wasm-types.js +0 -18
  125. package/dist/structure/ferrox-wasm.d.ts +0 -94
  126. package/dist/structure/ferrox-wasm.js +0 -249
@@ -218,6 +218,7 @@
218
218
 
219
219
  // Legend placement stability state
220
220
  let legend_element = $state<HTMLDivElement | undefined>()
221
+ let hovered_legend_series_idx = $state<number | null>(null)
221
222
  const legend_hover = create_hover_lock()
222
223
  const dim_tracker = create_dimension_tracker()
223
224
  let has_initial_legend_placement = $state(false)
@@ -226,12 +227,17 @@
226
227
  $effect(() => () => legend_hover.cleanup())
227
228
 
228
229
  // Derived data
230
+ type IndexedSeries = { series_data: DataSeries; series_idx: number }
231
+ let selected_series_entries = $derived<IndexedSeries[]>(
232
+ series
233
+ .map((series_data: DataSeries, series_idx: number) => ({ series_data, series_idx }))
234
+ .filter(({ series_data }) =>
235
+ (series_data.visible ?? true) &&
236
+ (mode !== `single` || !selected_property || series_data.label === selected_property)
237
+ ),
238
+ )
229
239
  let selected_series = $derived(
230
- mode === `single` && selected_property
231
- ? series.filter((srs: DataSeries) =>
232
- (srs.visible ?? true) && srs.label === selected_property
233
- )
234
- : series.filter((srs: DataSeries) => srs.visible ?? true),
240
+ selected_series_entries.map(({ series_data }) => series_data),
235
241
  )
236
242
 
237
243
  // Separate series by y-axis
@@ -472,7 +478,7 @@
472
478
  const x2_hist_generator = x2_series.length > 0
473
479
  ? bin().domain([ranges.current.x2[0], ranges.current.x2[1]]).thresholds(bins)
474
480
  : null
475
- return selected_series.map((series_data, series_idx) => {
481
+ return selected_series_entries.map(({ series_data, series_idx }) => {
476
482
  const use_x2 = series_data.x_axis === `x2`
477
483
  const active_hist = use_x2 && x2_hist_generator
478
484
  ? x2_hist_generator
@@ -551,15 +557,15 @@
551
557
 
552
558
  const points: { x: number; y: number }[] = []
553
559
 
554
- for (const { bins, x_scale, y_scale } of histogram_data) {
555
- for (const bin of bins) {
556
- if (bin.length > 0) {
557
- const bar_x = x_scale(((bin.x0 ?? 0) + (bin.x1 ?? 0)) / 2)
558
- const bar_y = y_scale(bin.length)
560
+ for (const { bins: series_bins, x_scale, y_scale } of histogram_data) {
561
+ for (const series_bin of series_bins) {
562
+ if (series_bin.length > 0) {
563
+ const bar_x = x_scale(((series_bin.x0 ?? 0) + (series_bin.x1 ?? 0)) / 2)
564
+ const bar_y = y_scale(series_bin.length)
559
565
  if (isFinite(bar_x) && isFinite(bar_y)) {
560
566
  // Add multiple points for taller bars to increase their weight
561
567
  // Cap to prevent O(N·count/10) blow-ups for large counts
562
- const weight = Math.min(20, Math.ceil(bin.length / 10))
568
+ const weight = Math.min(20, Math.ceil(series_bin.length / 10))
563
569
  for (let idx = 0; idx < weight; idx++) points.push({ x: bar_x, y: bar_y })
564
570
  }
565
571
  }
@@ -596,7 +602,7 @@
596
602
  // untrack() explicitly captures initial tween config (intentional - config set once at mount)
597
603
  const tweened_legend_coords = new Tween(
598
604
  { x: 0, y: 0 },
599
- untrack(() => ({ duration: 400, ...(legend?.tween ?? {}) })),
605
+ untrack(() => ({ duration: 400, ...legend?.tween })),
600
606
  )
601
607
 
602
608
  // Update legend position with stability checks
@@ -1431,11 +1437,18 @@
1431
1437
 
1432
1438
  <!-- Histogram bars (rendered after axes so bars appear above grid lines) -->
1433
1439
  {#each histogram_data as
1434
- { id, bins, color, label, x_scale, y_scale, x_axis: srs_x_axis, y_axis },
1435
- series_idx
1436
- (id ?? series_idx)
1440
+ { id, bins, color, label, x_scale, y_scale, x_axis: srs_x_axis, y_axis, series_idx },
1441
+ idx
1442
+ (id ?? idx)
1437
1443
  }
1438
- <g class="histogram-series" data-series-idx={series_idx}>
1444
+ <g
1445
+ class="histogram-series"
1446
+ data-series-idx={series_idx}
1447
+ opacity={hovered_legend_series_idx !== null &&
1448
+ hovered_legend_series_idx !== series_idx
1449
+ ? 0.25
1450
+ : 1}
1451
+ >
1439
1452
  {#each bins as bin, bin_idx (bin_idx)}
1440
1453
  {@const bar_x = x_scale(bin.x0!)}
1441
1454
  {@const bar_width = Math.max(1, Math.abs(x_scale(bin.x1!) - bar_x))}
@@ -1561,6 +1574,11 @@
1561
1574
  series_data={legend_data}
1562
1575
  on_toggle={legend?.on_toggle || toggle_series_visibility}
1563
1576
  on_hover_change={legend_hover.set_locked}
1577
+ on_item_hover={(series_idx) =>
1578
+ (hovered_legend_series_idx = series_idx != null && series_idx >= 0
1579
+ ? series_idx
1580
+ : null)}
1581
+ active_series_idx={hover_info?.series_idx ?? hovered_legend_series_idx}
1564
1582
  style={`
1565
1583
  position: absolute;
1566
1584
  left: ${legend_placement ? tweened_legend_coords.current.x : pad.l + 10}px;
@@ -1595,8 +1613,8 @@
1595
1613
  left: 0;
1596
1614
  width: 100vw !important;
1597
1615
  height: 100vh !important;
1598
- /* Must be higher than Structure.svelte's --struct-buttons-z-index (100000000) */
1599
- z-index: var(--histogram-fullscreen-z-index, 100000001);
1616
+ /* Must be higher than Structure.svelte's --struct-buttons-z-index. */
1617
+ z-index: var(--histogram-fullscreen-z-index, var(--z-index-overlay-nav, 100000001));
1600
1618
  margin: 0;
1601
1619
  border-radius: 0;
1602
1620
  background: var(--histogram-fullscreen-bg, var(--histogram-bg, var(--plot-bg)));
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- import type { TweenedOptions } from './'
3
2
  import { DEFAULTS } from '../settings'
4
3
  import { extent, min } from 'd3-array'
5
4
  import { interpolatePath } from 'd3-interpolate-path'
@@ -7,7 +6,7 @@
7
6
  import { untrack } from 'svelte'
8
7
  import { linear } from 'svelte/easing'
9
8
  import type { SVGAttributes } from 'svelte/elements'
10
- import { Tween } from 'svelte/motion'
9
+ import { Tween, type TweenOptions } from 'svelte/motion'
11
10
 
12
11
  let {
13
12
  points,
@@ -26,7 +25,7 @@
26
25
  line_width?: number
27
26
  area_color?: string
28
27
  area_stroke?: string | null
29
- line_tween?: TweenedOptions<string>
28
+ line_tween?: TweenOptions<string>
30
29
  line_dash?: string
31
30
  } = $props()
32
31
 
@@ -1,5 +1,5 @@
1
- import type { TweenedOptions } from './';
2
1
  import type { SVGAttributes } from 'svelte/elements';
2
+ import { type TweenOptions } from 'svelte/motion';
3
3
  type $$ComponentProps = Omit<SVGAttributes<SVGPathElement>, `origin` | `points`> & {
4
4
  points: readonly [number, number][];
5
5
  origin: [number, number];
@@ -7,7 +7,7 @@ type $$ComponentProps = Omit<SVGAttributes<SVGPathElement>, `origin` | `points`>
7
7
  line_width?: number;
8
8
  area_color?: string;
9
9
  area_stroke?: string | null;
10
- line_tween?: TweenedOptions<string>;
10
+ line_tween?: TweenOptions<string>;
11
11
  line_dash?: string;
12
12
  };
13
13
  declare const Line: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -26,6 +26,10 @@
26
26
  on_drag = () => {},
27
27
  on_drag_end = () => {},
28
28
  on_hover_change,
29
+ on_item_hover,
30
+ active_series_idx = null,
31
+ filterable = true,
32
+ filter_threshold = 12,
29
33
  draggable = true,
30
34
  root_element = $bindable<HTMLDivElement | undefined>(undefined),
31
35
  ...rest
@@ -54,6 +58,10 @@
54
58
  on_drag_end?: (event: MouseEvent) => void
55
59
  // Callback when hover state changes (for placement stability)
56
60
  on_hover_change?: (is_hovered: boolean) => void
61
+ on_item_hover?: (series_idx: number | null) => void
62
+ active_series_idx?: number | null
63
+ filterable?: boolean
64
+ filter_threshold?: number
57
65
  draggable?: boolean
58
66
  // Bindable reference to the root DOM element for size measurements
59
67
  root_element?: HTMLDivElement
@@ -61,9 +69,14 @@
61
69
 
62
70
  let is_dragging = $state(false)
63
71
  let drag_start_coords = $state<{ x: number; y: number } | null>(null)
72
+ let legend_filter = $state(``)
64
73
 
65
74
  // Group series by legend_group, preserving order
66
- type GroupedData = { group_name: string | null; items: LegendItem[] }
75
+ type GroupedData = {
76
+ group_name: string | null
77
+ items: LegendItem[]
78
+ all_items?: LegendItem[]
79
+ }
67
80
  let grouped_series = $derived.by<GroupedData[]>(() => {
68
81
  const groups: GroupedData[] = []
69
82
  const group_map = new SvelteMap<string | null, LegendItem[]>()
@@ -88,6 +101,22 @@
88
101
  ),
89
102
  )
90
103
 
104
+ let show_filter = $derived(filterable && series_data.length >= filter_threshold)
105
+
106
+ let filtered_grouped_series = $derived.by<GroupedData[]>(() => {
107
+ const filter = show_filter ? legend_filter.trim().toLowerCase() : ``
108
+ if (!filter) return grouped_series
109
+ return grouped_series
110
+ .map(({ group_name, items }) => ({
111
+ group_name,
112
+ all_items: items,
113
+ items: items.filter((item) =>
114
+ `${group_name ?? ``} ${strip_html(item.label)}`.toLowerCase().includes(filter)
115
+ ),
116
+ }))
117
+ .filter(({ items }) => items.length > 0)
118
+ })
119
+
91
120
  function toggle_group_collapse(group_name: string) {
92
121
  // Normalize to SvelteSet if a plain Set was passed (ensures reactivity)
93
122
  if (!(collapsed_groups instanceof SvelteSet)) {
@@ -111,7 +140,10 @@
111
140
  document.body.style.userSelect = `auto`
112
141
  }
113
142
  }
114
- onDestroy(cleanup_drag_listeners)
143
+ onDestroy(() => {
144
+ cleanup_drag_listeners()
145
+ on_item_hover?.(null)
146
+ })
115
147
 
116
148
  function handle_legend_mouse_down(event: MouseEvent) {
117
149
  if (!draggable) return
@@ -185,6 +217,7 @@
185
217
  <div
186
218
  class="legend-item"
187
219
  class:hidden={!series.visible}
220
+ class:active={active_series_idx === series.series_idx}
188
221
  class:indented={indent}
189
222
  class:fill-item={is_fill_item}
190
223
  style={item_style}
@@ -204,6 +237,10 @@
204
237
  toggle_item(series)
205
238
  }
206
239
  }}
240
+ onmouseenter={() => on_item_hover?.(series.series_idx)}
241
+ onmouseleave={() => on_item_hover?.(null)}
242
+ onfocus={() => on_item_hover?.(series.series_idx)}
243
+ onblur={() => on_item_hover?.(null)}
207
244
  role="button"
208
245
  tabindex="0"
209
246
  aria-pressed={series.visible}
@@ -312,28 +349,43 @@
312
349
  class:is-dragging={is_dragging}
313
350
  class:grouped={has_groups}
314
351
  >
315
- {#each grouped_series as { group_name, items } (group_name ?? `__ungrouped__`)}
352
+ {#if show_filter}
353
+ <input
354
+ class="legend-filter"
355
+ type="search"
356
+ bind:value={legend_filter}
357
+ placeholder="Filter legend"
358
+ aria-label="Filter legend items"
359
+ onclick={(event) => event.stopPropagation()}
360
+ onmousedown={(event) => event.stopPropagation()}
361
+ />
362
+ {/if}
363
+ {#if show_filter && legend_filter && filtered_grouped_series.length === 0}
364
+ <span class="legend-empty">No legend items</span>
365
+ {/if}
366
+ {#each filtered_grouped_series as { group_name, items, all_items } (group_name ?? `__ungrouped__`)}
316
367
  {#if group_name !== null && has_groups}
317
368
  <!-- Group header -->
369
+ {@const group_items = all_items ?? items}
318
370
  {@const is_collapsed = collapsed_groups.has(group_name)}
319
- {@const group_visible = items.some((item) => item.visible)}
371
+ {@const group_visible = group_items.some((item) => item.visible)}
320
372
  <div
321
373
  class="legend-group-header"
322
374
  class:hidden={!group_visible}
323
375
  onclick={(event: MouseEvent) => {
324
376
  event.preventDefault()
325
377
  event.stopPropagation()
326
- handle_group_click(group_name, items)
378
+ handle_group_click(group_name, group_items)
327
379
  }}
328
380
  ondblclick={(event: MouseEvent) => {
329
381
  event.preventDefault()
330
382
  event.stopPropagation()
331
- on_group_double_click?.(group_name, items.map((item) => item.series_idx))
383
+ on_group_double_click?.(group_name, group_items.map((item) => item.series_idx))
332
384
  }}
333
385
  onkeydown={(event) => {
334
386
  if ([`Enter`, ` `].includes(event.key)) {
335
387
  event.preventDefault()
336
- handle_group_click(group_name, items)
388
+ handle_group_click(group_name, group_items)
337
389
  }
338
390
  }}
339
391
  role="button"
@@ -425,6 +477,22 @@
425
477
  transition: var(--plot-legend-item-transition, opacity 0.3s ease);
426
478
  color: var(--plot-legend-item-color);
427
479
  }
480
+ .legend-filter {
481
+ box-sizing: border-box;
482
+ width: calc(100% - 6px);
483
+ min-width: 10em;
484
+ margin: 3px;
485
+ padding: 2px 5px;
486
+ border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
487
+ border-radius: var(--border-radius, 3pt);
488
+ background: color-mix(in srgb, var(--plot-legend-bg-color, Canvas) 88%, currentColor);
489
+ color: inherit;
490
+ font: inherit;
491
+ }
492
+ .legend-empty {
493
+ padding: var(--plot-legend-item-padding, 1px 8px 1px 3px);
494
+ opacity: 0.7;
495
+ }
428
496
  .legend-item.indented {
429
497
  padding: var(--plot-legend-item-padding, 0 8px 1px 3px);
430
498
  padding-left: var(--plot-legend-group-indent, 16px);
@@ -432,9 +500,12 @@
432
500
  .legend-item.hidden {
433
501
  opacity: var(--plot-legend-item-hidden-opacity, 0.5);
434
502
  }
435
- .legend-item:hover, .legend-item:focus {
503
+ .legend-item:hover, .legend-item:focus, .legend-item.active {
436
504
  background-color: var(--plot-legend-item-hover-bg-color);
437
505
  }
506
+ .legend-item.active {
507
+ box-shadow: inset 2px 0 0 var(--accent-color, currentColor);
508
+ }
438
509
  .legend-marker {
439
510
  display: inline-flex; /* Use flex to align items */
440
511
  align-items: center; /* Vertically center items */
@@ -17,6 +17,10 @@ type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, `style`> & {
17
17
  on_drag?: (event: MouseEvent) => void;
18
18
  on_drag_end?: (event: MouseEvent) => void;
19
19
  on_hover_change?: (is_hovered: boolean) => void;
20
+ on_item_hover?: (series_idx: number | null) => void;
21
+ active_series_idx?: number | null;
22
+ filterable?: boolean;
23
+ filter_threshold?: number;
20
24
  draggable?: boolean;
21
25
  root_element?: HTMLDivElement;
22
26
  };
@@ -100,15 +100,15 @@
100
100
  btn.setAttribute(`role`, `option`)
101
101
  btn.innerHTML = sanitize_html(format_option(opt))
102
102
  style_sub_sup(btn)
103
- btn.onclick = () => select(opt.key)
104
- btn.onmouseenter = () => {
103
+ btn.addEventListener(`click`, () => select(opt.key))
104
+ btn.addEventListener(`mouseenter`, () => {
105
105
  if (!btn.classList.contains(`selected`)) {
106
106
  btn.style.background = `rgba(128, 128, 128, 0.15)`
107
107
  }
108
- }
109
- btn.onmouseleave = () => {
108
+ })
109
+ btn.addEventListener(`mouseleave`, () => {
110
110
  if (!btn.classList.contains(`selected`)) btn.style.background = `transparent`
111
- }
111
+ })
112
112
  if (opt.key === selected_key) {
113
113
  btn.classList.add(`selected`)
114
114
  btn.style.cssText = portal_styles.btn + portal_styles.btn_selected
@@ -32,7 +32,6 @@
32
32
  ScatterHandlerProps,
33
33
  Sides,
34
34
  StyleOverrides,
35
- TweenedOptions,
36
35
  UserContentProps,
37
36
  XyObj,
38
37
  } from './'
@@ -81,7 +80,7 @@
81
80
  import type { ComponentProps, Snippet } from 'svelte'
82
81
  import { untrack } from 'svelte'
83
82
  import type { HTMLAttributes } from 'svelte/elements'
84
- import { Tween } from 'svelte/motion'
83
+ import { Tween, type TweenOptions } from 'svelte/motion'
85
84
  import { SvelteSet } from 'svelte/reactivity'
86
85
  import type { FillPathPoint } from './fill-utils'
87
86
  import {
@@ -206,15 +205,15 @@
206
205
  color_bar?:
207
206
  | (ComponentProps<typeof ColorBar> & {
208
207
  margin?: number | Sides
209
- tween?: TweenedOptions<XyObj>
208
+ tween?: TweenOptions<XyObj>
210
209
  responsive?: boolean // Allow colorbar to reposition if density changes (default: false)
211
210
  })
212
211
  | null
213
212
  label_placement_config?: Partial<LabelPlacementConfig>
214
213
  hover_config?: Partial<HoverConfig>
215
214
  legend?: LegendConfig | null
216
- point_tween?: TweenedOptions<XyObj>
217
- line_tween?: TweenedOptions<string>
215
+ point_tween?: TweenOptions<XyObj>
216
+ line_tween?: TweenOptions<string>
218
217
  point_events?: Record<
219
218
  string,
220
219
  (payload: { point: InternalPoint<Metadata>; event: Event }) => void
@@ -245,15 +244,15 @@
245
244
  const final_x_axis = $derived({
246
245
  ...AXIS_DEFAULTS,
247
246
  label_shift: { x: 0, y: -40 }, // x-axis needs different label position
248
- ...(x_axis ?? {}),
247
+ ...x_axis,
249
248
  })
250
- const final_y_axis = $derived({ ...AXIS_DEFAULTS, ...(y_axis ?? {}) })
249
+ const final_y_axis = $derived({ ...AXIS_DEFAULTS, ...y_axis })
251
250
  const final_x2_axis = $derived({
252
251
  ...AXIS_DEFAULTS,
253
252
  label_shift: { x: 0, y: 40 }, // x2-axis label above top edge
254
- ...(x2_axis ?? {}),
253
+ ...x2_axis,
255
254
  })
256
- const final_y2_axis = $derived({ ...AXIS_DEFAULTS, ...(y2_axis ?? {}) })
255
+ const final_y2_axis = $derived({ ...AXIS_DEFAULTS, ...y2_axis })
257
256
  // Cache time-axis check — used in ~10 places for scale/tick/tooltip logic
258
257
  let is_time_x = $derived(
259
258
  is_time_scale(final_x_axis.scale_type, final_x_axis.format),
@@ -261,16 +260,16 @@
261
260
  let is_time_x2 = $derived(
262
261
  is_time_scale(final_x2_axis.scale_type, final_x2_axis.format),
263
262
  )
264
- const final_display = $derived({ ...DEFAULTS.scatter.display, ...(display ?? {}) })
263
+ const final_display = $derived({ ...DEFAULTS.scatter.display, ...display })
265
264
  // Local state for styles (initialized from prop, owned by this component for controls)
266
265
  // Using $state because styles has bindings in ScatterPlotControls
267
266
  // untrack() explicitly captures initial prop value (intentional - props provide initial config)
268
267
  let styles = $state(untrack(() => ({
269
268
  show_points: DEFAULTS.scatter.show_points,
270
269
  show_lines: DEFAULTS.scatter.show_lines,
271
- point: { ...DEFAULTS.scatter.point, ...(styles_init?.point ?? {}) },
272
- line: { ...DEFAULTS.scatter.line, ...(styles_init?.line ?? {}) },
273
- ...(styles_init ?? {}),
270
+ point: { ...DEFAULTS.scatter.point, ...styles_init?.point },
271
+ line: { ...DEFAULTS.scatter.line, ...styles_init?.line },
272
+ ...styles_init,
274
273
  })))
275
274
  let controls = $derived({ show: true, open: false, ...controls_init })
276
275
 
@@ -364,6 +363,7 @@
364
363
  let legend_is_dragging = $state(false)
365
364
  let legend_drag_offset = $state<{ x: number; y: number }>({ x: 0, y: 0 })
366
365
  let legend_manual_position = $state<{ x: number; y: number } | null>(null)
366
+ let hovered_legend_series_idx = $state<number | null>(null)
367
367
 
368
368
  // State for legend/colorbar placement stability
369
369
  let legend_element = $state<HTMLDivElement | undefined>()
@@ -394,13 +394,13 @@
394
394
 
395
395
  for (const srs of series_with_ids) {
396
396
  if (!srs) continue
397
- const { x: xs, y: ys, visible = true, y_axis = `y1`, x_axis: x_ax = `x1` } =
397
+ const { x: xs, y: ys, visible = true, y_axis: series_y_axis = `y1`, x_axis: x_ax = `x1` } =
398
398
  srs as DataSeries
399
399
  for (let idx = 0; idx < xs.length; idx++) {
400
400
  const point = { x: xs[idx], y: ys[idx] }
401
401
  all.push(point)
402
402
  if (visible) {
403
- if (y_axis === `y2`) y2.push(point)
403
+ if (series_y_axis === `y2`) y2.push(point)
404
404
  else y1.push(point)
405
405
  if (x_ax === `x2`) x2.push(point)
406
406
  }
@@ -666,7 +666,7 @@
666
666
  }
667
667
  }
668
668
 
669
- const { x: xs, y: ys, color_values, size_values, ...rest } = data_series
669
+ const { x: xs, y: ys, color_values, size_values, ...series_rest } = data_series
670
670
 
671
671
  // Process points internally, adding properties beyond the base Point type
672
672
  const processed_points: InternalPoint<Metadata>[] = xs.map(
@@ -674,11 +674,11 @@
674
674
  x: x_val,
675
675
  y: ys[point_idx],
676
676
  color_value: color_values?.[point_idx],
677
- metadata: process_prop(rest.metadata, point_idx) as Metadata | undefined,
678
- point_style: process_prop(rest.point_style, point_idx),
679
- point_hover: process_prop(rest.point_hover, point_idx),
680
- point_label: process_prop(rest.point_label, point_idx),
681
- point_offset: process_prop(rest.point_offset, point_idx),
677
+ metadata: process_prop(series_rest.metadata, point_idx) as Metadata | undefined,
678
+ point_style: process_prop(series_rest.point_style, point_idx),
679
+ point_hover: process_prop(series_rest.point_hover, point_idx),
680
+ point_label: process_prop(series_rest.point_label, point_idx),
681
+ point_offset: process_prop(series_rest.point_offset, point_idx),
682
682
  series_idx,
683
683
  point_idx,
684
684
  size_value: size_values?.[point_idx],
@@ -1141,12 +1141,12 @@
1141
1141
  // untrack() explicitly captures initial tween config (intentional - config set once at mount)
1142
1142
  const tweened_colorbar_coords = new Tween(
1143
1143
  { x: 0, y: 0 },
1144
- untrack(() => ({ duration: 400, ...(color_bar?.tween ?? {}) })),
1144
+ untrack(() => ({ duration: 400, ...color_bar?.tween })),
1145
1145
  )
1146
1146
  // Initialize tweened values for legend position - create once, update target via effect
1147
1147
  const tweened_legend_coords = new Tween(
1148
1148
  { x: 0, y: 0 },
1149
- untrack(() => ({ duration: 400, ...(legend?.tween ?? {}) })),
1149
+ untrack(() => ({ duration: 400, ...legend?.tween })),
1150
1150
  )
1151
1151
 
1152
1152
  // Update placement positions (with animation and stability checks)
@@ -1727,9 +1727,9 @@
1727
1727
  legend_manual_position = { x: constrained_x, y: constrained_y }
1728
1728
  }
1729
1729
 
1730
- function get_screen_coords(point: Point, series?: DataSeries): [number, number] {
1730
+ function get_screen_coords(point: Point, data_series?: DataSeries): [number, number] {
1731
1731
  // convert data coordinates to potentially non-finite screen coordinates
1732
- const use_x2 = series?.x_axis === `x2`
1732
+ const use_x2 = data_series?.x_axis === `x2`
1733
1733
  const active_x_scale = use_x2 ? x2_scale_fn : x_scale_fn
1734
1734
  const active_is_time_x = use_x2 ? is_time_x2 : is_time_x
1735
1735
  const screen_x = active_is_time_x
@@ -1738,7 +1738,7 @@
1738
1738
 
1739
1739
  const y_val = point.y
1740
1740
  // Determine which y-scale to use based on series y_axis property
1741
- const use_y2 = series?.y_axis === `y2`
1741
+ const use_y2 = data_series?.y_axis === `y2`
1742
1742
  const y_scale = use_y2 ? y2_scale_fn : y_scale_fn
1743
1743
  const y_scale_type = use_y2
1744
1744
  ? get_scale_type_name(final_y2_axis.scale_type)
@@ -2035,7 +2035,7 @@
2035
2035
  y1={-(height - pad.b - pad.t)}
2036
2036
  y2="0"
2037
2037
  {...DEFAULT_GRID_STYLE}
2038
- {...(final_x_axis.grid_style ?? {})}
2038
+ {...final_x_axis.grid_style}
2039
2039
  />
2040
2040
  {/if}
2041
2041
  <line y1="0" y2={inside ? -5 : 5} stroke="var(--border-color, gray)" />
@@ -2116,7 +2116,7 @@
2116
2116
  x1="0"
2117
2117
  x2={width - pad.l - pad.r}
2118
2118
  {...DEFAULT_GRID_STYLE}
2119
- {...(final_y_axis.grid_style ?? {})}
2119
+ {...final_y_axis.grid_style}
2120
2120
  />
2121
2121
  {/if}
2122
2122
  <line
@@ -2186,7 +2186,7 @@
2186
2186
  x1={-(width - pad.l - pad.r)}
2187
2187
  x2="0"
2188
2188
  {...DEFAULT_GRID_STYLE}
2189
- {...(final_y2_axis.grid_style ?? {})}
2189
+ {...final_y2_axis.grid_style}
2190
2190
  />
2191
2191
  {/if}
2192
2192
  <line
@@ -2256,7 +2256,7 @@
2256
2256
  y1="0"
2257
2257
  y2={height - pad.b - pad.t}
2258
2258
  {...DEFAULT_GRID_STYLE}
2259
- {...(final_x2_axis.grid_style ?? {})}
2259
+ {...final_x2_axis.grid_style}
2260
2260
  />
2261
2261
  {/if}
2262
2262
  <line
@@ -2353,7 +2353,14 @@
2353
2353
  {#each filtered_series ?? [] as series_data (series_data._id)}
2354
2354
  {@const series_markers = series_data.markers ?? DEFAULT_MARKERS}
2355
2355
  {@const series_default_color = get_series_color(series_data.orig_series_idx ?? 0)}
2356
- <g data-series-id={series_data._id} clip-path="url(#{clip_path_id})">
2356
+ <g
2357
+ data-series-id={series_data._id}
2358
+ clip-path="url(#{clip_path_id})"
2359
+ opacity={hovered_legend_series_idx !== null &&
2360
+ hovered_legend_series_idx !== series_data.orig_series_idx
2361
+ ? 0.25
2362
+ : 1}
2363
+ >
2357
2364
  {#if series_markers?.includes(`line`)}
2358
2365
  {@const all_line_points = series_data.x.map((x, idx) => ({
2359
2366
  x,
@@ -2439,6 +2446,8 @@
2439
2446
  <ScatterPoint
2440
2447
  x={screen_x}
2441
2448
  y={screen_y}
2449
+ is_dimmed={hovered_legend_series_idx !== null &&
2450
+ hovered_legend_series_idx !== point.series_idx}
2442
2451
  is_hovered={tooltip_point?.series_idx === point.series_idx &&
2443
2452
  tooltip_point?.point_idx === point.point_idx}
2444
2453
  is_selected={selected_point?.series_idx === point.series_idx &&
@@ -2662,6 +2671,11 @@
2662
2671
  on_drag={handle_legend_drag}
2663
2672
  on_drag_end={() => (legend_is_dragging = false)}
2664
2673
  on_hover_change={legend_hover.set_locked}
2674
+ on_item_hover={(series_idx) =>
2675
+ (hovered_legend_series_idx = series_idx != null && series_idx >= 0
2676
+ ? series_idx
2677
+ : null)}
2678
+ active_series_idx={tooltip_point?.series_idx ?? hovered_legend_series_idx}
2665
2679
  draggable={legend?.draggable ?? true}
2666
2680
  {...legend}
2667
2681
  on_toggle={legend?.on_toggle ??
@@ -2745,8 +2759,8 @@
2745
2759
  left: 0;
2746
2760
  width: 100vw !important;
2747
2761
  height: 100vh !important;
2748
- /* Must be higher than Structure.svelte's --struct-buttons-z-index (100000000) */
2749
- z-index: var(--scatter-fullscreen-z-index, 100000001);
2762
+ /* Must be higher than Structure.svelte's --struct-buttons-z-index. */
2763
+ z-index: var(--scatter-fullscreen-z-index, var(--z-index-overlay-nav, 100000001));
2750
2764
  margin: 0;
2751
2765
  border-radius: 0;
2752
2766
  background: var(--scatter-fullscreen-bg, var(--scatter-bg, var(--plot-bg)));
@@ -1,8 +1,9 @@
1
1
  import type { D3ColorSchemeName, D3InterpolateName } from '../colors';
2
- import type { AxisLoadError, BasePlotProps, ControlsConfig, DataLoaderFn, DataSeries, ErrorBand, FillHandlerEvent, FillRegion, HoverConfig, InternalPoint, LabelPlacementConfig, LegendConfig, PanConfig, PlotConfig, Point, RefLine, RefLineEvent, ScaleType, ScatterHandlerEvent, ScatterHandlerProps, Sides, StyleOverrides, TweenedOptions, UserContentProps, XyObj } from './';
2
+ import type { AxisLoadError, BasePlotProps, ControlsConfig, DataLoaderFn, DataSeries, ErrorBand, FillHandlerEvent, FillRegion, HoverConfig, InternalPoint, LabelPlacementConfig, LegendConfig, PanConfig, PlotConfig, Point, RefLine, RefLineEvent, ScaleType, ScatterHandlerEvent, ScatterHandlerProps, Sides, StyleOverrides, UserContentProps, XyObj } from './';
3
3
  import { ColorBar } from './';
4
4
  import type { ComponentProps, Snippet } from 'svelte';
5
5
  import type { HTMLAttributes } from 'svelte/elements';
6
+ import { type TweenOptions } from 'svelte/motion';
6
7
  declare function $$render<Metadata extends Record<string, unknown> = Record<string, unknown>>(): {
7
8
  props: HTMLAttributes<HTMLDivElement> & Omit<BasePlotProps, "change"> & PlotConfig & {
8
9
  series?: DataSeries<Metadata>[];
@@ -40,14 +41,14 @@ declare function $$render<Metadata extends Record<string, unknown> = Record<stri
40
41
  };
41
42
  color_bar?: (ComponentProps<typeof ColorBar> & {
42
43
  margin?: number | Sides;
43
- tween?: TweenedOptions<XyObj>;
44
+ tween?: TweenOptions<XyObj>;
44
45
  responsive?: boolean;
45
46
  }) | null;
46
47
  label_placement_config?: Partial<LabelPlacementConfig>;
47
48
  hover_config?: Partial<HoverConfig>;
48
49
  legend?: LegendConfig | null;
49
- point_tween?: TweenedOptions<XyObj>;
50
- line_tween?: TweenedOptions<string>;
50
+ point_tween?: TweenOptions<XyObj>;
51
+ line_tween?: TweenOptions<string>;
51
52
  point_events?: Record<string, (payload: {
52
53
  point: InternalPoint<Metadata>;
53
54
  event: Event;
@@ -355,8 +355,10 @@
355
355
  }}
356
356
  pane_props={{
357
357
  ...controls.pane_props,
358
- // z-index must exceed fullscreen z-index (100000001) to remain clickable in fullscreen mode
359
- style: `--pane-z-index: 100000002; ${controls.pane_props?.style ?? ``}`,
358
+ // z-index must exceed fullscreen z-index to remain clickable in fullscreen mode
359
+ style: `--pane-z-index: var(--z-index-overlay-dialog, 100000002); ${
360
+ controls.pane_props?.style ?? ``
361
+ }`,
360
362
  }}
361
363
  bind:x_axis
362
364
  bind:y_axis
@@ -394,6 +396,7 @@
394
396
  <PlotLegend
395
397
  series_data={legend_data}
396
398
  on_toggle={toggle_series_visibility}
399
+ active_series_idx={tooltip_point?.series_idx ?? null}
397
400
  draggable={legend?.draggable ?? true}
398
401
  {...legend}
399
402
  style={`position: absolute; top: 2.5em; right: 1em; ${legend?.style ?? ``}`}
@@ -427,7 +430,7 @@
427
430
  left: 0;
428
431
  width: 100vw !important;
429
432
  height: 100vh !important;
430
- z-index: var(--scatter3d-fullscreen-z-index, 100000001);
433
+ z-index: var(--scatter3d-fullscreen-z-index, var(--z-index-overlay-nav, 100000001));
431
434
  margin: 0;
432
435
  border-radius: 0;
433
436
  max-height: none !important;