matterviz 0.1.12 → 0.1.13

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 (195) hide show
  1. package/dist/ContextMenu.svelte +1 -1
  2. package/dist/ContextMenu.svelte.d.ts +3 -3
  3. package/dist/DraggablePane.svelte +10 -5
  4. package/dist/DraggablePane.svelte.d.ts +3 -3
  5. package/dist/FilePicker.svelte.d.ts +3 -3
  6. package/dist/Icon.svelte.d.ts +3 -3
  7. package/dist/Nav.svelte +76 -31
  8. package/dist/SettingsSection.svelte.d.ts +3 -3
  9. package/dist/Spinner.svelte +18 -29
  10. package/dist/Spinner.svelte.d.ts +3 -3
  11. package/dist/app.css +4 -0
  12. package/dist/bands/Bands.svelte +216 -0
  13. package/dist/bands/Bands.svelte.d.ts +16 -0
  14. package/dist/bands/BandsAndDos.svelte +26 -0
  15. package/dist/bands/BandsAndDos.svelte.d.ts +16 -0
  16. package/dist/bands/Dos.svelte +124 -0
  17. package/dist/bands/Dos.svelte.d.ts +18 -0
  18. package/dist/bands/helpers.d.ts +11 -0
  19. package/dist/bands/helpers.js +195 -0
  20. package/dist/bands/index.d.ts +5 -0
  21. package/dist/bands/index.js +6 -0
  22. package/dist/bands/types.d.ts +54 -0
  23. package/dist/bands/types.js +2 -0
  24. package/dist/brillouin/BrillouinZone.svelte +373 -0
  25. package/dist/brillouin/BrillouinZone.svelte.d.ts +57 -0
  26. package/dist/brillouin/BrillouinZoneControls.svelte +88 -0
  27. package/dist/brillouin/BrillouinZoneControls.svelte.d.ts +14 -0
  28. package/dist/brillouin/BrillouinZoneExportPane.svelte +129 -0
  29. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +15 -0
  30. package/dist/brillouin/BrillouinZoneInfoPane.svelte +115 -0
  31. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +13 -0
  32. package/dist/brillouin/BrillouinZoneScene.svelte +180 -0
  33. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +34 -0
  34. package/dist/brillouin/compute.d.ts +7 -0
  35. package/dist/brillouin/compute.js +215 -0
  36. package/dist/brillouin/index.d.ts +7 -0
  37. package/dist/brillouin/index.js +7 -0
  38. package/dist/brillouin/types.d.ts +23 -0
  39. package/dist/brillouin/types.js +1 -0
  40. package/dist/composition/BarChart.svelte.d.ts +3 -3
  41. package/dist/composition/BubbleChart.svelte +7 -6
  42. package/dist/composition/BubbleChart.svelte.d.ts +3 -3
  43. package/dist/composition/Composition.svelte.d.ts +3 -3
  44. package/dist/composition/PieChart.svelte +1 -1
  45. package/dist/composition/PieChart.svelte.d.ts +3 -3
  46. package/dist/composition/parse.d.ts +3 -3
  47. package/dist/coordination/CoordinationBarPlot.svelte +241 -0
  48. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +29 -0
  49. package/dist/coordination/calc-coordination.d.ts +15 -0
  50. package/dist/coordination/calc-coordination.js +66 -0
  51. package/dist/coordination/index.d.ts +8 -0
  52. package/dist/coordination/index.js +7 -0
  53. package/dist/element/BohrAtom.svelte.d.ts +3 -3
  54. package/dist/element/ElementHeading.svelte.d.ts +3 -3
  55. package/dist/element/ElementPhoto.svelte.d.ts +3 -3
  56. package/dist/element/ElementStats.svelte.d.ts +3 -3
  57. package/dist/element/ElementTile.svelte +1 -1
  58. package/dist/element/ElementTile.svelte.d.ts +4 -4
  59. package/dist/element/Nucleus.svelte.d.ts +3 -3
  60. package/dist/icons.d.ts +4 -0
  61. package/dist/icons.js +4 -0
  62. package/dist/index.d.ts +5 -1
  63. package/dist/index.js +5 -1
  64. package/dist/io/export.d.ts +1 -6
  65. package/dist/io/export.js +12 -12
  66. package/dist/labels.d.ts +19 -0
  67. package/dist/labels.js +59 -0
  68. package/dist/math.d.ts +7 -2
  69. package/dist/math.js +24 -12
  70. package/dist/periodic-table/PeriodicTable.svelte.d.ts +3 -3
  71. package/dist/periodic-table/PeriodicTableControls.svelte.d.ts +3 -3
  72. package/dist/periodic-table/PropertySelect.svelte +1 -1
  73. package/dist/periodic-table/PropertySelect.svelte.d.ts +5 -4
  74. package/dist/periodic-table/TableInset.svelte.d.ts +3 -3
  75. package/dist/phase-diagram/PhaseDiagram2D.svelte +44 -107
  76. package/dist/phase-diagram/PhaseDiagram2D.svelte.d.ts +7 -27
  77. package/dist/phase-diagram/PhaseDiagram3D.svelte +67 -170
  78. package/dist/phase-diagram/PhaseDiagram3D.svelte.d.ts +7 -31
  79. package/dist/phase-diagram/PhaseDiagram4D.svelte +87 -145
  80. package/dist/phase-diagram/PhaseDiagram4D.svelte.d.ts +5 -28
  81. package/dist/phase-diagram/PhaseDiagramControls.svelte +9 -9
  82. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +6 -6
  83. package/dist/phase-diagram/PhaseDiagramInfoPane.svelte +4 -4
  84. package/dist/phase-diagram/PhaseDiagramInfoPane.svelte.d.ts +5 -5
  85. package/dist/phase-diagram/PhaseDiagramStats.svelte +9 -16
  86. package/dist/phase-diagram/PhaseDiagramStats.svelte.d.ts +3 -3
  87. package/dist/phase-diagram/StructurePopup.svelte.d.ts +3 -3
  88. package/dist/phase-diagram/barycentric-coords.js +13 -24
  89. package/dist/phase-diagram/helpers.d.ts +12 -14
  90. package/dist/phase-diagram/helpers.js +70 -23
  91. package/dist/phase-diagram/index.d.ts +58 -0
  92. package/dist/phase-diagram/index.js +44 -0
  93. package/dist/phase-diagram/thermodynamics.js +2 -3
  94. package/dist/phase-diagram/types.d.ts +3 -8
  95. package/dist/plot/BarPlot.svelte +474 -240
  96. package/dist/plot/BarPlot.svelte.d.ts +16 -37
  97. package/dist/plot/BarPlotControls.svelte +33 -201
  98. package/dist/plot/BarPlotControls.svelte.d.ts +11 -22
  99. package/dist/plot/ColorBar.svelte.d.ts +3 -3
  100. package/dist/plot/ColorScaleSelect.svelte +1 -1
  101. package/dist/plot/ColorScaleSelect.svelte.d.ts +3 -3
  102. package/dist/plot/ElementScatter.svelte +5 -5
  103. package/dist/plot/ElementScatter.svelte.d.ts +8 -8
  104. package/dist/plot/Histogram.svelte +318 -170
  105. package/dist/plot/Histogram.svelte.d.ts +21 -41
  106. package/dist/plot/HistogramControls.svelte +115 -347
  107. package/dist/plot/HistogramControls.svelte.d.ts +13 -28
  108. package/dist/plot/Line.svelte +1 -1
  109. package/dist/plot/Line.svelte.d.ts +4 -4
  110. package/dist/plot/PlotControls.svelte +280 -0
  111. package/dist/plot/PlotControls.svelte.d.ts +4 -0
  112. package/dist/plot/PlotLegend.svelte +9 -24
  113. package/dist/plot/PlotLegend.svelte.d.ts +3 -3
  114. package/dist/plot/ScatterPlot.svelte +536 -571
  115. package/dist/plot/ScatterPlot.svelte.d.ts +18 -90
  116. package/dist/plot/ScatterPlotControls.svelte +108 -316
  117. package/dist/plot/ScatterPlotControls.svelte.d.ts +11 -38
  118. package/dist/plot/ScatterPoint.svelte +1 -1
  119. package/dist/plot/ScatterPoint.svelte.d.ts +5 -5
  120. package/dist/plot/SpacegroupBarPlot.svelte +235 -0
  121. package/dist/plot/SpacegroupBarPlot.svelte.d.ts +9 -0
  122. package/dist/plot/data-transform.d.ts +2 -2
  123. package/dist/plot/data-transform.js +1 -5
  124. package/dist/plot/index.d.ts +12 -200
  125. package/dist/plot/index.js +14 -6
  126. package/dist/plot/layout.d.ts +10 -16
  127. package/dist/plot/layout.js +40 -25
  128. package/dist/plot/scales.d.ts +7 -8
  129. package/dist/plot/scales.js +22 -7
  130. package/dist/plot/types.d.ts +303 -0
  131. package/dist/plot/types.js +1 -0
  132. package/dist/rdf/RdfPlot.svelte +156 -0
  133. package/dist/rdf/RdfPlot.svelte.d.ts +20 -0
  134. package/dist/rdf/calc-rdf.d.ts +4 -0
  135. package/dist/rdf/calc-rdf.js +107 -0
  136. package/dist/rdf/index.d.ts +22 -0
  137. package/dist/rdf/index.js +2 -0
  138. package/dist/settings.d.ts +86 -75
  139. package/dist/settings.js +257 -155
  140. package/dist/structure/Bond.svelte +126 -110
  141. package/dist/structure/Bond.svelte.d.ts +7 -20
  142. package/dist/structure/CanvasTooltip.svelte.d.ts +4 -4
  143. package/dist/structure/Cylinder.svelte +29 -0
  144. package/dist/structure/Cylinder.svelte.d.ts +10 -0
  145. package/dist/structure/Lattice.svelte +1 -2
  146. package/dist/structure/Lattice.svelte.d.ts +3 -3
  147. package/dist/structure/Structure.svelte +23 -8
  148. package/dist/structure/Structure.svelte.d.ts +10 -7
  149. package/dist/structure/StructureControls.svelte +25 -134
  150. package/dist/structure/StructureControls.svelte.d.ts +4 -9
  151. package/dist/structure/StructureExportPane.svelte +188 -0
  152. package/dist/structure/StructureExportPane.svelte.d.ts +17 -0
  153. package/dist/structure/StructureInfoPane.svelte.d.ts +3 -3
  154. package/dist/structure/StructureLegend.svelte +91 -27
  155. package/dist/structure/StructureLegend.svelte.d.ts +5 -4
  156. package/dist/structure/StructureScene.svelte +93 -100
  157. package/dist/structure/StructureScene.svelte.d.ts +15 -12
  158. package/dist/structure/Vector.svelte +19 -25
  159. package/dist/structure/Vector.svelte.d.ts +3 -3
  160. package/dist/structure/bonding.d.ts +16 -14
  161. package/dist/structure/bonding.js +207 -216
  162. package/dist/structure/export.d.ts +3 -0
  163. package/dist/structure/export.js +120 -0
  164. package/dist/structure/index.d.ts +15 -1
  165. package/dist/structure/index.js +2 -0
  166. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -3
  167. package/dist/symmetry/index.d.ts +1 -0
  168. package/dist/symmetry/index.js +1 -0
  169. package/dist/symmetry/spacegroups.d.ts +9 -0
  170. package/dist/symmetry/spacegroups.js +397 -0
  171. package/dist/theme/ThemeControl.svelte.d.ts +3 -3
  172. package/dist/theme/themes.js +2 -16
  173. package/dist/trajectory/Trajectory.svelte +52 -78
  174. package/dist/trajectory/Trajectory.svelte.d.ts +5 -5
  175. package/dist/trajectory/TrajectoryError.svelte.d.ts +3 -3
  176. package/dist/trajectory/TrajectoryExportPane.svelte +1 -1
  177. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +5 -5
  178. package/dist/trajectory/TrajectoryInfoPane.svelte +1 -1
  179. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -3
  180. package/dist/trajectory/extract.js +2 -1
  181. package/dist/trajectory/index.d.ts +1 -1
  182. package/dist/trajectory/index.js +1 -1
  183. package/dist/trajectory/parse.js +21 -14
  184. package/dist/utils.d.ts +1 -0
  185. package/dist/utils.js +17 -0
  186. package/dist/xrd/XrdPlot.svelte +14 -11
  187. package/dist/xrd/XrdPlot.svelte.d.ts +6 -5
  188. package/package.json +20 -12
  189. package/readme.md +1 -1
  190. package/dist/material/MaterialCard.svelte +0 -123
  191. package/dist/material/MaterialCard.svelte.d.ts +0 -9
  192. package/dist/material/index.d.ts +0 -1
  193. package/dist/material/index.js +0 -1
  194. package/dist/plot/formatting.d.ts +0 -7
  195. package/dist/plot/formatting.js +0 -53
@@ -1,12 +1,23 @@
1
- <script lang="ts">import { DraggablePane } from '..';
1
+ <script lang="ts">import { format_value } from '../labels';
2
2
  import { find_best_plot_area, HistogramControls, PlotLegend } from './';
3
+ import { DEFAULTS } from '../settings';
3
4
  import { bin, max } from 'd3-array';
4
- import { extract_series_color, filter_visible_series, prepare_legend_data, } from './data-transform';
5
- import { format_value } from './formatting';
5
+ import { untrack } from 'svelte';
6
+ import { extract_series_color, prepare_legend_data } from './data-transform';
6
7
  import { get_relative_coords } from './interactions';
7
8
  import { calc_auto_padding, constrain_tooltip_position } from './layout';
8
- import { create_scale, generate_ticks, get_nice_data_range } from './scales';
9
- let { series = $bindable([]), x_lim = [null, null], y_lim = [null, null], x_range, y_range, range_padding = 0.05, bins = $bindable(100), x_label = `Value`, y_label = `Count`, x_format = $bindable(`.2~s`), y_format = $bindable(`d`), x_scale_type = $bindable(`linear`), y_scale_type = $bindable(`linear`), padding = { t: 20, b: 60, l: 60, r: 20 }, show_legend = $bindable(true), legend = { series_data: [] }, bar_opacity = $bindable(0.7), bar_stroke_width = $bindable(1), bar_color = $bindable(`cornflowerblue`), selected_property = $bindable(``), mode = $bindable(`single`), show_zero_lines = $bindable(true), x_grid = $bindable(true), y_grid = $bindable(true), x_ticks = $bindable(8), y_ticks = $bindable(6), tooltip, hovered = $bindable(false), change = () => { }, on_bar_click, on_bar_hover, show_controls = $bindable(true), controls_open = $bindable(false), plot_controls, on_series_toggle = () => { }, controls_toggle_props, ...rest } = $props();
9
+ import { create_scale, generate_ticks, get_nice_data_range, get_tick_label, } from './scales';
10
+ let { series = $bindable([]), x_axis = $bindable({ label: `Value`, format: `.2~s`, scale_type: `linear` }), y_axis = $bindable({ label: `Count`, format: `d`, scale_type: `linear` }), y2_axis = $bindable({ label: `Count`, format: `d`, scale_type: `linear` }), display = $bindable(DEFAULTS.histogram.display), x_range = [null, null], y_range = [null, null], y2_range = [null, null], range_padding = 0.05, padding = { t: 20, b: 60, l: 60, r: 20 }, bins = $bindable(100), show_legend = $bindable(true), legend = { series_data: [] }, bar = $bindable({}), selected_property = $bindable(``), mode = $bindable(`single`), tooltip, hovered = $bindable(false), change = () => { }, on_bar_click, on_bar_hover, show_controls = $bindable(true), controls_open = $bindable(false), on_series_toggle = () => { }, controls_toggle_props, controls_pane_props, children, ...rest } = $props();
11
+ bar = { ...DEFAULTS.histogram.bar, ...bar }; // Initialize bar style defaults
12
+ y2_axis = {
13
+ format: `d`,
14
+ scale_type: `linear`,
15
+ ticks: 5,
16
+ label_shift: { y: 60 },
17
+ tick_label_shift: { x: 8, y: 0 },
18
+ range: [null, null],
19
+ ...y2_axis,
20
+ };
10
21
  // Core state
11
22
  let [width, height] = $state([0, 0]);
12
23
  let svg_element = $state(null);
@@ -14,63 +25,93 @@ let hover_info = $state(null);
14
25
  let drag_state = $state({ start: null, current: null, bounds: null });
15
26
  // Derived data
16
27
  let selected_series = $derived(mode === `single` && selected_property
17
- ? filter_visible_series(series).filter((s) => s.label === selected_property)
18
- : filter_visible_series(series));
28
+ ? series.filter((srs) => (srs.visible ?? true) && srs.label === selected_property)
29
+ : series.filter((srs) => srs.visible ?? true));
30
+ // Separate series by y-axis
31
+ let y1_series = $derived(selected_series.filter((srs) => (srs.y_axis ?? `y1`) === `y1`));
32
+ let y2_series = $derived(selected_series.filter((srs) => srs.y_axis === `y2`));
19
33
  let auto_ranges = $derived.by(() => {
20
34
  const all_values = selected_series.flatMap((s) => s.y);
21
- const auto_x = get_nice_data_range(all_values.map((val) => ({ x: val, y: 0 })), (p) => p.x, x_lim, x_scale_type, range_padding, false);
22
- if (!selected_series.length) {
23
- return {
24
- x: auto_x,
25
- y: [y_scale_type === `log` ? 1 : 0, 1],
26
- };
27
- }
28
- const hist = bin().domain([auto_x[0], auto_x[1]]).thresholds(bins);
29
- const max_count = Math.max(0, ...selected_series.map((s) => max(hist(s.y), (d) => d.length) || 0));
30
- const [y0, y1] = get_nice_data_range([{ x: 0, y: 0 }, { x: max_count, y: 0 }], (p) => p.x, y_lim, y_scale_type, range_padding, false);
31
- const y_min = y_scale_type === `log` ? Math.max(1, y0) : Math.max(0, y0);
32
- return { x: auto_x, y: [y_min, y1] };
35
+ const auto_x = get_nice_data_range(all_values.map((val) => ({ x: val, y: 0 })), (p) => p.x, x_range, x_axis.scale_type ?? `linear`, range_padding, false);
36
+ // Calculate y-range for a specific set of series
37
+ const calc_y_range = (series_list, y_limit, scale_type) => {
38
+ if (!series_list.length) {
39
+ const fallback = scale_type === `log` ? 1 : 0;
40
+ return [fallback, 1];
41
+ }
42
+ const hist = bin().domain([auto_x[0], auto_x[1]]).thresholds(bins);
43
+ const max_count = Math.max(0, ...series_list.map((s) => max(hist(s.y), (d) => d.length) || 0));
44
+ const [y0, y1] = get_nice_data_range([{ x: 0, y: 0 }, { x: max_count, y: 0 }], (p) => p.x, y_limit, scale_type, range_padding, false);
45
+ const y_min = scale_type === `log` ? Math.max(1, y0) : Math.max(0, y0);
46
+ return [y_min, y1];
47
+ };
48
+ const y1_range = calc_y_range(y1_series, y_range, y_axis.scale_type ?? `linear`);
49
+ const y2_auto_range = calc_y_range(y2_series, y2_range, y2_axis.scale_type ?? `linear`);
50
+ return { x: auto_x, y: y1_range, y2: y2_auto_range };
33
51
  });
34
52
  // Initialize ranges
35
53
  let ranges = $state({
36
- initial: { x: [0, 1], y: [0, 1] },
37
- current: { x: [0, 1], y: [0, 1] },
54
+ initial: {
55
+ x: [0, 1],
56
+ y: [0, 1],
57
+ y2: [0, 1],
58
+ },
59
+ current: {
60
+ x: [0, 1],
61
+ y: [0, 1],
62
+ y2: [0, 1],
63
+ },
38
64
  });
39
65
  $effect(() => {
40
66
  // Support one-sided range pinning: merge user range with auto range for null values
41
- const new_x = x_range
42
- ? [x_range[0] ?? auto_ranges.x[0], x_range[1] ?? auto_ranges.x[1]]
67
+ const new_x = x_axis.range
68
+ ? [x_axis.range[0] ?? auto_ranges.x[0], x_axis.range[1] ?? auto_ranges.x[1]]
43
69
  : auto_ranges.x;
44
- const new_y = y_range
45
- ? [y_range[0] ?? auto_ranges.y[0], y_range[1] ?? auto_ranges.y[1]]
70
+ const new_y = y_axis.range
71
+ ? [y_axis.range[0] ?? auto_ranges.y[0], y_axis.range[1] ?? auto_ranges.y[1]]
46
72
  : auto_ranges.y;
47
- const x_changed = (x_range !== undefined) !== (ranges.initial.x === auto_ranges.x) ||
48
- new_x[0] !== ranges.initial.x[0] || new_x[1] !== ranges.initial.x[1];
49
- const y_changed = (y_range !== undefined) !== (ranges.initial.y === auto_ranges.y) ||
50
- new_y[0] !== ranges.initial.y[0] || new_y[1] !== ranges.initial.y[1];
73
+ const new_y2 = y2_axis.range
74
+ ? [y2_axis.range[0] ?? auto_ranges.y2[0], y2_axis.range[1] ?? auto_ranges.y2[1]]
75
+ : auto_ranges.y2;
76
+ // Only update if values changed (prevent infinite loop)
77
+ const x_changed = new_x[0] !== ranges.current.x[0] ||
78
+ new_x[1] !== ranges.current.x[1];
79
+ const y_changed = new_y[0] !== ranges.current.y[0] ||
80
+ new_y[1] !== ranges.current.y[1];
81
+ const y2_changed = new_y2[0] !== ranges.current.y2[0] ||
82
+ new_y2[1] !== ranges.current.y2[1];
51
83
  if (x_changed)
52
84
  [ranges.initial.x, ranges.current.x] = [new_x, new_x];
53
85
  if (y_changed)
54
86
  [ranges.initial.y, ranges.current.y] = [new_y, new_y];
87
+ if (y2_changed)
88
+ [ranges.initial.y2, ranges.current.y2] = [new_y2, new_y2];
55
89
  });
56
90
  // Layout: dynamic padding based on tick label widths
57
91
  const default_padding = { t: 20, b: 60, l: 60, r: 20 };
58
92
  let pad = $state({ ...default_padding, ...padding });
59
- // Update padding when format or ticks change, but prevent infinite loop
93
+ // Update padding based on tick label widths (untrack breaks circular dependency)
60
94
  $effect(() => {
61
- const base_pad = { ...default_padding, ...padding };
62
- const new_pad = width && height && ticks.y.length
63
- ? calc_auto_padding({ base_padding: base_pad, y_ticks: ticks.y, y_format })
64
- : base_pad;
65
- // Only update if padding actually changed (prevents infinite loop)
95
+ const current_ticks_y = untrack(() => ticks.y);
96
+ const current_ticks_y2 = untrack(() => ticks.y2);
97
+ const new_pad = width && height && current_ticks_y.length
98
+ ? calc_auto_padding({
99
+ padding,
100
+ default_padding,
101
+ y_axis: { ...y_axis, tick_values: current_ticks_y },
102
+ y2_axis: { ...y2_axis, tick_values: current_ticks_y2 },
103
+ })
104
+ : { ...default_padding, ...padding };
105
+ // Only update if padding actually changed
66
106
  if (pad.t !== new_pad.t || pad.b !== new_pad.b || pad.l !== new_pad.l ||
67
107
  pad.r !== new_pad.r)
68
108
  pad = new_pad;
69
109
  });
70
110
  // Scales and data
71
111
  let scales = $derived({
72
- x: create_scale(x_scale_type, ranges.current.x, [pad.l, width - pad.r]),
73
- y: create_scale(y_scale_type, ranges.current.y, [height - pad.b, pad.t]),
112
+ x: create_scale(x_axis.scale_type ?? `linear`, ranges.current.x, [pad.l, width - pad.r]),
113
+ y: create_scale(y_axis.scale_type ?? `linear`, ranges.current.y, [height - pad.b, pad.t]),
114
+ y2: create_scale(y2_axis.scale_type ?? `linear`, ranges.current.y2, [height - pad.b, pad.t]),
74
115
  });
75
116
  let histogram_data = $derived.by(() => {
76
117
  if (!selected_series.length || !width || !height)
@@ -80,27 +121,30 @@ let histogram_data = $derived.by(() => {
80
121
  .thresholds(bins);
81
122
  return selected_series.map((series_data, series_idx) => {
82
123
  const bins_arr = hist_generator(series_data.y);
124
+ const use_y2 = series_data.y_axis === `y2`;
83
125
  return {
126
+ id: series_data.id ?? series_idx,
84
127
  series_idx,
85
128
  label: series_data.label || `Series ${series_idx + 1}`,
86
129
  color: selected_series.length === 1
87
- ? bar_color
130
+ ? bar.color
88
131
  : extract_series_color(series_data),
89
132
  bins: bins_arr,
90
133
  max_count: max(bins_arr, (d) => d.length) || 0,
134
+ y_axis: series_data.y_axis,
135
+ y_scale: use_y2 ? scales.y2 : scales.y,
91
136
  };
92
137
  });
93
138
  });
94
139
  let ticks = $derived({
95
140
  x: width && height
96
- ? generate_ticks(ranges.current.x, x_scale_type, x_ticks, scales.x, {
97
- default_count: 8,
98
- })
141
+ ? generate_ticks(ranges.current.x, x_axis.scale_type ?? `linear`, x_axis.ticks, scales.x, { default_count: 8 })
99
142
  : [],
100
143
  y: width && height
101
- ? generate_ticks(ranges.current.y, y_scale_type, y_ticks, scales.y, {
102
- default_count: 6,
103
- })
144
+ ? generate_ticks(ranges.current.y, y_axis.scale_type ?? `linear`, y_axis.ticks, scales.y, { default_count: 6 })
145
+ : [],
146
+ y2: width && height && y2_series.length > 0
147
+ ? generate_ticks(ranges.current.y2, y2_axis.scale_type ?? `linear`, y2_axis.ticks, scales.y2, { default_count: 6 })
104
148
  : [],
105
149
  });
106
150
  let legend_data = $derived(prepare_legend_data(series));
@@ -109,11 +153,11 @@ let hist_points_for_placement = $derived.by(() => {
109
153
  if (!width || !height || !histogram_data.length)
110
154
  return [];
111
155
  const points = [];
112
- for (const { bins } of histogram_data) {
156
+ for (const { bins, y_scale } of histogram_data) {
113
157
  for (const bin of bins) {
114
158
  if (bin.length > 0) {
115
159
  const bar_x = scales.x((bin.x0 + bin.x1) / 2);
116
- const bar_y = scales.y(bin.length);
160
+ const bar_y = y_scale(bin.length);
117
161
  if (isFinite(bar_x) && isFinite(bar_y)) {
118
162
  // Add multiple points for taller bars to increase their weight
119
163
  const weight = Math.ceil(bin.length / 10); // More points for taller bars
@@ -146,12 +190,25 @@ const handle_zoom = () => {
146
190
  const end_x = scales.x.invert(drag_state.current.x);
147
191
  const start_y = scales.y.invert(drag_state.start.y);
148
192
  const end_y = scales.y.invert(drag_state.current.y);
193
+ const start_y2 = scales.y2.invert(drag_state.start.y);
194
+ const end_y2 = scales.y2.invert(drag_state.current.y);
149
195
  if (typeof start_x === `number` && typeof end_x === `number`) {
150
196
  const dx = Math.abs(drag_state.start.x - drag_state.current.x);
151
197
  const dy = Math.abs(drag_state.start.y - drag_state.current.y);
152
198
  if (dx > 5 && dy > 5) {
153
- ranges.current.x = [Math.min(start_x, end_x), Math.max(start_x, end_x)];
154
- ranges.current.y = [Math.min(start_y, end_y), Math.max(start_y, end_y)];
199
+ // Update axis ranges to trigger reactivity and prevent effect from overriding
200
+ x_axis = {
201
+ ...x_axis,
202
+ range: [Math.min(start_x, end_x), Math.max(start_x, end_x)],
203
+ };
204
+ y_axis = {
205
+ ...y_axis,
206
+ range: [Math.min(start_y, end_y), Math.max(start_y, end_y)],
207
+ };
208
+ y2_axis = {
209
+ ...y2_axis,
210
+ range: [Math.min(start_y2, end_y2), Math.max(start_y2, end_y2)],
211
+ };
155
212
  }
156
213
  }
157
214
  };
@@ -184,11 +241,14 @@ function handle_mouse_down(evt) {
184
241
  evt.preventDefault();
185
242
  }
186
243
  function handle_double_click() {
187
- ranges.current = { x: [...ranges.initial.x], y: [...ranges.initial.y] };
244
+ // Clear axis ranges to reset to auto ranges
245
+ x_axis = { ...x_axis, range: undefined };
246
+ y_axis = { ...y_axis, range: undefined };
247
+ y2_axis = { ...y2_axis, range: undefined };
188
248
  }
189
- function handle_mouse_move(evt, value, count, property) {
249
+ function handle_mouse_move(evt, value, count, property, axis = `y1`) {
190
250
  hovered = true;
191
- hover_info = { value, count, property };
251
+ hover_info = { value, count, property, axis };
192
252
  change({ value, count, property });
193
253
  on_bar_hover?.({ value, count, property, event: evt });
194
254
  }
@@ -231,83 +291,13 @@ function toggle_series_visibility(series_idx) {
231
291
  }
232
292
  }}
233
293
  >
234
- <!-- Zero lines -->
235
- {#if show_zero_lines}
236
- {#if ranges.current.x[0] <= 0 && ranges.current.x[1] >= 0}
237
- {@const zero_x = scales.x(0)}
238
- {#if isFinite(zero_x)}
239
- <line
240
- y1={pad.t}
241
- y2={height - pad.b}
242
- x1={zero_x}
243
- x2={zero_x}
244
- stroke="gray"
245
- stroke-width="0.5"
246
- />
247
- {/if}
248
- {/if}
249
- {#if y_scale_type === `linear` && ranges.current.y[0] < 0 &&
250
- ranges.current.y[1] > 0}
251
- {@const zero_y = scales.y(0)}
252
- {#if isFinite(zero_y)}
253
- <line
254
- x1={pad.l}
255
- x2={width - pad.r}
256
- y1={zero_y}
257
- y2={zero_y}
258
- stroke="gray"
259
- stroke-width="0.5"
260
- />
261
- {/if}
262
- {/if}
263
- {/if}
264
-
265
- <!-- Histogram bars -->
266
- {#each histogram_data as { bins, color, label }, series_idx (series_idx)}
267
- <g class="histogram-series" data-series-idx={series_idx}>
268
- {#each bins as bin, bin_idx (bin_idx)}
269
- {@const bar_x = scales.x(bin.x0!)}
270
- {@const bar_width = Math.max(1, Math.abs(scales.x(bin.x1!) - bar_x))}
271
- {@const bar_height = Math.max(0, (height - pad.b) - scales.y(bin.length))}
272
- {@const bar_y = scales.y(bin.length)}
273
- {@const value = (bin.x0! + bin.x1!) / 2}
274
- {#if bar_height > 0}
275
- <rect
276
- x={bar_x}
277
- y={bar_y}
278
- width={bar_width}
279
- height={bar_height}
280
- fill={color}
281
- opacity={bar_opacity}
282
- stroke={mode === `overlay` ? color : `none`}
283
- stroke-width={mode === `overlay` ? bar_stroke_width : 0}
284
- role="button"
285
- tabindex="0"
286
- onmousemove={(evt) => handle_mouse_move(evt, value, bin.length, label)}
287
- onmouseleave={() => {
288
- hover_info = null
289
- change(null)
290
- on_bar_hover?.(null)
291
- }}
292
- onclick={(event) =>
293
- on_bar_click?.({ value, count: bin.length, property: label, event })}
294
- onkeydown={(event: KeyboardEvent) => {
295
- if ([`Enter`, ` `].includes(event.key)) {
296
- event.preventDefault()
297
- on_bar_click?.({ value, count: bin.length, property: label, event })
298
- }
299
- }}
300
- style:cursor={on_bar_click ? `pointer` : undefined}
301
- />
302
- {/if}
303
- {/each}
304
- </g>
305
- {/each}
306
-
307
294
  <!-- Tooltip -->
308
295
  {#if hover_info}
309
296
  {@const tooltip_x = scales.x(hover_info.value)}
310
- {@const tooltip_y = scales.y(hover_info.count)}
297
+ {@const y_cfg = (hover_info.axis === `y2`) ? y2_axis : y_axis}
298
+ {@const tooltip_y = (hover_info.axis === `y2` ? scales.y2 : scales.y)(
299
+ hover_info.count,
300
+ )}
311
301
  {@const tooltip_size = { width: 120, height: mode === `overlay` ? 60 : 40 }}
312
302
  {@const tooltip_pos = constrain_tooltip_position(
313
303
  tooltip_x,
@@ -327,8 +317,8 @@ function toggle_series_visibility(series_idx) {
327
317
  {#if tooltip}
328
318
  {@render tooltip(hover_info)}
329
319
  {:else}
330
- <div>Value: {format_value(hover_info.value, x_format)}</div>
331
- <div>Count: {hover_info.count}</div>
320
+ <div>Value: {format_value(hover_info.value, x_axis.format || `.3~s`)}</div>
321
+ <div>Count: {format_value(hover_info.count, y_cfg.format || `.3~s`)}</div>
332
322
  {#if mode === `overlay`}<div>{hover_info.property}</div>{/if}
333
323
  {/if}
334
324
  </div>
@@ -351,35 +341,41 @@ function toggle_series_visibility(series_idx) {
351
341
  x2={width - pad.r}
352
342
  y1={height - pad.b}
353
343
  y2={height - pad.b}
354
- stroke="var(--border-color, gray)"
344
+ stroke={x_axis.color || `var(--border-color, gray)`}
355
345
  stroke-width="1"
356
346
  />
357
347
  {#each ticks.x as tick (tick)}
358
348
  {@const tick_x = scales.x(tick as number)}
349
+ {@const custom_label = get_tick_label(tick as number, x_axis.ticks)}
359
350
  <g class="tick" transform="translate({tick_x}, {height - pad.b})">
360
- {#if x_grid}
351
+ {#if display.x_grid}
361
352
  <line
362
353
  y1={-(height - pad.b - pad.t)}
363
354
  y2="0"
364
355
  stroke="var(--border-color, gray)"
365
356
  stroke-dasharray="4"
366
- stroke-width="0.4"
367
- {...typeof x_grid === `object` ? x_grid : {}}
357
+ stroke-width="1"
358
+ {...x_axis.grid_style ?? {}}
368
359
  />
369
360
  {/if}
370
- <line y1="0" y2="5" stroke="var(--border-color, gray)" stroke-width="1" />
371
- <text y="18" text-anchor="middle" fill="var(--text-color)">
372
- {format_value(tick, x_format)}
361
+ <line
362
+ y1="0"
363
+ y2="5"
364
+ stroke={x_axis.color || `var(--border-color, gray)`}
365
+ stroke-width="1"
366
+ />
367
+ <text y="18" text-anchor="middle" fill={x_axis.color || `var(--text-color)`}>
368
+ {custom_label ?? format_value(tick, x_axis.format)}
373
369
  </text>
374
370
  </g>
375
371
  {/each}
376
372
  <text
377
- x={(pad.l + width - pad.r) / 2}
378
- y={height - 10}
373
+ x={(pad.l + width - pad.r) / 2 + (x_axis.label_shift?.x ?? 0)}
374
+ y={height - 10 + (x_axis.label_shift?.y ?? 0)}
379
375
  text-anchor="middle"
380
- fill="var(--text-color)"
376
+ fill={x_axis.color || `var(--text-color)`}
381
377
  >
382
- {x_label}
378
+ {@html x_axis.label}
383
379
  </text>
384
380
  </g>
385
381
 
@@ -390,73 +386,217 @@ function toggle_series_visibility(series_idx) {
390
386
  x2={pad.l}
391
387
  y1={pad.t}
392
388
  y2={height - pad.b}
393
- stroke="var(--border-color, gray)"
389
+ stroke={y_axis.color || `var(--border-color, gray)`}
394
390
  stroke-width="1"
395
391
  />
396
392
  {#each ticks.y as tick (tick)}
397
393
  {@const tick_y = scales.y(tick as number)}
394
+ {@const custom_label = get_tick_label(tick as number, y_axis.ticks)}
398
395
  <g class="tick" transform="translate({pad.l}, {tick_y})">
399
- {#if y_grid}
396
+ {#if display.y_grid}
400
397
  <line
401
398
  x1="0"
402
399
  x2={width - pad.l - pad.r}
403
400
  stroke="var(--border-color, gray)"
404
401
  stroke-dasharray="4"
405
- stroke-width="0.4"
406
- {...typeof y_grid === `object` ? y_grid : {}}
402
+ stroke-width="1"
403
+ {...y_axis.grid_style ?? {}}
407
404
  />
408
405
  {/if}
409
- <line x1="-5" x2="0" stroke="var(--border-color, gray)" stroke-width="1" />
406
+ <line
407
+ x1="-5"
408
+ x2="0"
409
+ stroke={y_axis.color || `var(--border-color, gray)`}
410
+ stroke-width="1"
411
+ />
410
412
  <text
411
413
  x="-10"
412
414
  text-anchor="end"
413
415
  dominant-baseline="central"
414
- fill="var(--text-color)"
416
+ fill={y_axis.color || `var(--text-color)`}
415
417
  >
416
- {format_value(tick, y_format)}
418
+ {custom_label ?? format_value(tick, y_axis.format)}
417
419
  </text>
418
420
  </g>
419
421
  {/each}
420
- <text
421
- x={15}
422
- y={(pad.t + height - pad.b) / 2}
423
- text-anchor="middle"
424
- fill="var(--text-color)"
425
- transform="rotate(-90, 15, {(pad.t + height - pad.b) / 2})"
426
- >
427
- {y_label}
428
- </text>
422
+ {#if y_axis.label}
423
+ {@const y_label_x = 15 + (y_axis.label_shift?.x ?? 0)}
424
+ {@const y_label_y = (pad.t + height - pad.b) / 2 + (y_axis.label_shift?.y ?? 0)}
425
+ <text
426
+ x={y_label_x}
427
+ y={y_label_y}
428
+ text-anchor="middle"
429
+ fill={y_axis.color || `var(--text-color)`}
430
+ transform="rotate(-90, {y_label_x}, {y_label_y})"
431
+ >
432
+ {@html y_axis.label}
433
+ </text>
434
+ {/if}
429
435
  </g>
436
+
437
+ <!-- Y2-axis (Right) -->
438
+ {#if y2_series.length > 0}
439
+ <g class="y2-axis">
440
+ <line
441
+ x1={width - pad.r}
442
+ x2={width - pad.r}
443
+ y1={pad.t}
444
+ y2={height - pad.b}
445
+ stroke={y2_axis.color || `var(--border-color, gray)`}
446
+ stroke-width="1"
447
+ />
448
+ {#each ticks.y2 as tick (tick)}
449
+ {@const tick_y = scales.y2(tick as number)}
450
+ {@const custom_label = get_tick_label(tick as number, y2_axis.ticks)}
451
+ <g class="tick" transform="translate({width - pad.r}, {tick_y})">
452
+ {#if display.y2_grid}
453
+ <line
454
+ x1={-(width - pad.l - pad.r)}
455
+ x2="0"
456
+ stroke="var(--border-color, gray)"
457
+ stroke-dasharray="4"
458
+ stroke-width="1"
459
+ {...y2_axis.grid_style ?? {}}
460
+ />
461
+ {/if}
462
+ <line
463
+ x1="0"
464
+ x2="5"
465
+ stroke={y2_axis.color || `var(--border-color, gray)`}
466
+ stroke-width="1"
467
+ />
468
+ <text
469
+ x={y2_axis.tick_label_shift?.x ?? 8}
470
+ y={y2_axis.tick_label_shift?.y ?? 0}
471
+ text-anchor="start"
472
+ dominant-baseline="central"
473
+ fill={y2_axis.color || `var(--text-color)`}
474
+ >
475
+ {custom_label ?? format_value(tick, y2_axis.format)}
476
+ </text>
477
+ </g>
478
+ {/each}
479
+ {#if y2_axis.label}
480
+ {@const y2_label_x = width - pad.r + 50 + (y2_axis.label_shift?.x ?? 0)}
481
+ {@const y2_label_y = (pad.t + height - pad.b) / 2 +
482
+ (y2_axis.label_shift?.y ?? 0)}
483
+ <text
484
+ x={y2_label_x}
485
+ y={y2_label_y}
486
+ text-anchor="middle"
487
+ fill={y2_axis.color || `var(--text-color)`}
488
+ transform="rotate(-90, {y2_label_x}, {y2_label_y})"
489
+ >
490
+ {@html y2_axis.label}
491
+ </text>
492
+ {/if}
493
+ </g>
494
+ {/if}
495
+
496
+ <!-- Zero lines -->
497
+ {#if display.x_zero_line && ranges.current.x[0] <= 0 && ranges.current.x[1] >= 0}
498
+ {@const x0 = scales.x(0)}
499
+ {#if isFinite(x0)}
500
+ <line class="zero-line" x1={x0} x2={x0} y1={pad.t} y2={height - pad.b} />
501
+ {/if}
502
+ {/if}
503
+ {#if display.y_zero_line && (y_axis.scale_type ?? `linear`) === `linear` &&
504
+ ranges.current.y[0] <= 0 && ranges.current.y[1] >= 0}
505
+ {@const zero_y = scales.y(0)}
506
+ {#if isFinite(zero_y)}
507
+ <line class="zero-line" x1={pad.l} x2={width - pad.r} y1={zero_y} y2={zero_y} />
508
+ {/if}
509
+ {/if}
510
+ {#if display.y_zero_line && y2_series.length > 0 &&
511
+ (y2_axis.scale_type ?? `linear`) === `linear` &&
512
+ ranges.current.y2[0] <= 0 && ranges.current.y2[1] >= 0}
513
+ {@const zero_y2 = scales.y2(0)}
514
+ {#if isFinite(zero_y2)}
515
+ <line
516
+ class="zero-line"
517
+ x1={pad.l}
518
+ x2={width - pad.r}
519
+ y1={zero_y2}
520
+ y2={zero_y2}
521
+ />
522
+ {/if}
523
+ {/if}
524
+
525
+ <!-- Histogram bars -->
526
+ {#each histogram_data as
527
+ { id, bins, color, label, y_scale, y_axis },
528
+ series_idx
529
+ (id ?? series_idx)
530
+ }
531
+ <g class="histogram-series" data-series-idx={series_idx}>
532
+ {#each bins as bin, bin_idx (bin_idx)}
533
+ {@const bar_x = scales.x(bin.x0!)}
534
+ {@const bar_width = Math.max(1, Math.abs(scales.x(bin.x1!) - bar_x))}
535
+ {@const bar_height = Math.max(0, (height - pad.b) - y_scale(bin.length))}
536
+ {@const bar_y = y_scale(bin.length)}
537
+ {@const value = (bin.x0! + bin.x1!) / 2}
538
+ {#if bar_height > 0}
539
+ <rect
540
+ x={bar_x}
541
+ y={bar_y}
542
+ width={bar_width}
543
+ height={bar_height}
544
+ fill={color}
545
+ opacity={bar.opacity}
546
+ stroke={bar.stroke_color}
547
+ stroke-opacity={bar.stroke_opacity}
548
+ stroke-width={bar.stroke_width}
549
+ role="button"
550
+ tabindex="0"
551
+ onmousemove={(evt) =>
552
+ handle_mouse_move(
553
+ evt,
554
+ value,
555
+ bin.length,
556
+ label,
557
+ (y_axis ?? `y1`) as `y1` | `y2`,
558
+ )}
559
+ onmouseleave={() => {
560
+ hover_info = null
561
+ change(null)
562
+ on_bar_hover?.(null)
563
+ }}
564
+ onclick={(event) =>
565
+ on_bar_click?.({ value, count: bin.length, property: label, event })}
566
+ onkeydown={(event: KeyboardEvent) => {
567
+ if ([`Enter`, ` `].includes(event.key)) {
568
+ event.preventDefault()
569
+ on_bar_click?.({ value, count: bin.length, property: label, event })
570
+ }
571
+ }}
572
+ style:cursor={on_bar_click ? `pointer` : undefined}
573
+ />
574
+ {/if}
575
+ {/each}
576
+ </g>
577
+ {/each}
430
578
  </svg>
431
579
  {/if}
432
580
 
433
581
  {#if show_controls}
434
582
  <HistogramControls
435
583
  toggle_props={controls_toggle_props}
584
+ pane_props={controls_pane_props}
436
585
  bind:show_controls
437
586
  bind:controls_open
438
587
  bind:bins
439
588
  bind:mode
440
- bind:bar_opacity
441
- bind:bar_stroke_width
442
- bind:bar_color
589
+ bind:bar
443
590
  bind:show_legend
444
- bind:x_grid
445
- bind:y_grid
446
- bind:x_scale_type
447
- bind:y_scale_type
448
- bind:x_ticks
449
- bind:y_ticks
450
- bind:x_format
451
- bind:y_format
452
591
  bind:selected_property
453
- bind:show_zero_lines
454
- bind:x_range
455
- bind:y_range
592
+ bind:x_axis
593
+ bind:y_axis
594
+ bind:y2_axis
595
+ bind:display
456
596
  auto_x_range={auto_ranges.x}
457
597
  auto_y_range={auto_ranges.y}
598
+ auto_y2_range={auto_ranges.y2}
458
599
  {series}
459
- {plot_controls}
460
600
  />
461
601
  {/if}
462
602
 
@@ -468,6 +608,9 @@ function toggle_series_visibility(series_idx) {
468
608
  wrapper_style="position: absolute; left: {legend_placement.x}px; top: {legend_placement.y}px; transform: {legend_placement.transform}; {legend?.wrapper_style || ``}"
469
609
  />
470
610
  {/if}
611
+
612
+ <!-- User-provided children (e.g. for custom absolutely-positioned overlays) -->
613
+ {@render children?.()}
471
614
  </div>
472
615
 
473
616
  <style>
@@ -483,7 +626,7 @@ function toggle_series_visibility(series_idx) {
483
626
  width: 100%;
484
627
  height: 100%;
485
628
  }
486
- g:is(.x-axis, .y-axis) .tick text {
629
+ g:is(.x-axis, .y-axis, .y2-axis) .tick text {
487
630
  font-size: var(--tick-font-size, 0.8em); /* shrink tick labels */
488
631
  }
489
632
  .tooltip {
@@ -508,4 +651,9 @@ function toggle_series_visibility(series_idx) {
508
651
  stroke-width: var(--histogram-zoom-rect-stroke-width, 1);
509
652
  pointer-events: none;
510
653
  }
654
+ .zero-line {
655
+ stroke: var(--histogram-zero-line-color, light-dark(black, white));
656
+ stroke-width: var(--histogram-zero-line-width, 1);
657
+ opacity: var(--histogram-zero-line-opacity);
658
+ }
511
659
  </style>