matterviz 0.3.0 → 0.3.2

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 (286) hide show
  1. package/dist/FilePicker.svelte +37 -20
  2. package/dist/Icon.svelte +2 -2
  3. package/dist/MillerIndexInput.svelte +60 -0
  4. package/dist/MillerIndexInput.svelte.d.ts +7 -0
  5. package/dist/app.css +38 -2
  6. package/dist/brillouin/BrillouinZone.svelte +20 -62
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  8. package/dist/brillouin/BrillouinZoneExportPane.svelte +12 -20
  9. package/dist/brillouin/BrillouinZoneScene.svelte +2 -2
  10. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  11. package/dist/chempot-diagram/ChemPotDiagram.svelte +192 -0
  12. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
  13. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +677 -0
  14. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
  15. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2688 -0
  16. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
  17. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -0
  18. package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
  19. package/dist/chempot-diagram/color.d.ts +10 -0
  20. package/dist/chempot-diagram/color.js +33 -0
  21. package/dist/chempot-diagram/compute.d.ts +38 -0
  22. package/dist/chempot-diagram/compute.js +650 -0
  23. package/dist/chempot-diagram/index.d.ts +5 -0
  24. package/dist/chempot-diagram/index.js +5 -0
  25. package/dist/chempot-diagram/pointer.d.ts +16 -0
  26. package/dist/chempot-diagram/pointer.js +40 -0
  27. package/dist/chempot-diagram/temperature.d.ts +15 -0
  28. package/dist/chempot-diagram/temperature.js +37 -0
  29. package/dist/chempot-diagram/types.d.ts +83 -0
  30. package/dist/chempot-diagram/types.js +27 -0
  31. package/dist/colors/index.d.ts +3 -1
  32. package/dist/colors/index.js +4 -0
  33. package/dist/composition/BarChart.svelte +13 -22
  34. package/dist/composition/BubbleChart.svelte +5 -3
  35. package/dist/composition/FormulaFilter.svelte +770 -90
  36. package/dist/composition/FormulaFilter.svelte.d.ts +37 -1
  37. package/dist/composition/PieChart.svelte +43 -18
  38. package/dist/composition/PieChart.svelte.d.ts +1 -1
  39. package/dist/constants.d.ts +1 -0
  40. package/dist/constants.js +2 -0
  41. package/dist/convex-hull/ConvexHull.svelte +14 -1
  42. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  43. package/dist/convex-hull/ConvexHull2D.svelte +14 -45
  44. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  45. package/dist/convex-hull/ConvexHull3D.svelte +396 -134
  46. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  47. package/dist/convex-hull/ConvexHull4D.svelte +93 -42
  48. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  49. package/dist/convex-hull/ConvexHullControls.svelte +94 -31
  50. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +4 -2
  51. package/dist/convex-hull/ConvexHullStats.svelte +697 -128
  52. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
  53. package/dist/convex-hull/ConvexHullTooltip.svelte +1 -0
  54. package/dist/convex-hull/GasPressureControls.svelte +72 -38
  55. package/dist/convex-hull/GasPressureControls.svelte.d.ts +2 -1
  56. package/dist/convex-hull/TemperatureSlider.svelte +46 -19
  57. package/dist/convex-hull/TemperatureSlider.svelte.d.ts +2 -1
  58. package/dist/convex-hull/demo-temperature.d.ts +6 -0
  59. package/dist/convex-hull/demo-temperature.js +36 -0
  60. package/dist/convex-hull/gas-thermodynamics.js +16 -5
  61. package/dist/convex-hull/helpers.d.ts +7 -1
  62. package/dist/convex-hull/helpers.js +45 -15
  63. package/dist/convex-hull/index.d.ts +15 -1
  64. package/dist/convex-hull/index.js +1 -0
  65. package/dist/convex-hull/thermodynamics.d.ts +8 -21
  66. package/dist/convex-hull/thermodynamics.js +106 -17
  67. package/dist/convex-hull/types.d.ts +7 -0
  68. package/dist/convex-hull/types.js +11 -0
  69. package/dist/coordination/CoordinationBarPlot.svelte +29 -46
  70. package/dist/element/BohrAtom.svelte +1 -1
  71. package/dist/element/data.js +2 -14
  72. package/dist/element/data.json.gz +0 -0
  73. package/dist/element/index.d.ts +1 -1
  74. package/dist/element/index.js +1 -0
  75. package/dist/element/types.d.ts +1 -0
  76. package/dist/fermi-surface/FermiSurface.svelte +21 -65
  77. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  78. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  79. package/dist/fermi-surface/FermiSurfaceScene.svelte +1 -1
  80. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  81. package/dist/fermi-surface/compute.js +1 -21
  82. package/dist/fermi-surface/marching-cubes.d.ts +2 -13
  83. package/dist/fermi-surface/marching-cubes.js +2 -519
  84. package/dist/fermi-surface/parse.js +17 -23
  85. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1273 -0
  86. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
  87. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +171 -0
  88. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +31 -0
  89. package/dist/heatmap-matrix/index.d.ts +53 -0
  90. package/dist/heatmap-matrix/index.js +100 -0
  91. package/dist/heatmap-matrix/shared.d.ts +2 -0
  92. package/dist/heatmap-matrix/shared.js +4 -0
  93. package/dist/icons.d.ts +119 -0
  94. package/dist/icons.js +119 -0
  95. package/dist/index.d.ts +6 -1
  96. package/dist/index.js +6 -1
  97. package/dist/io/export.js +15 -3
  98. package/dist/io/file-drop.d.ts +7 -0
  99. package/dist/io/file-drop.js +43 -0
  100. package/dist/io/index.d.ts +2 -2
  101. package/dist/io/index.js +2 -112
  102. package/dist/io/types.d.ts +1 -0
  103. package/dist/io/url-drop.d.ts +2 -0
  104. package/dist/io/url-drop.js +118 -0
  105. package/dist/isosurface/Isosurface.svelte +231 -0
  106. package/dist/isosurface/Isosurface.svelte.d.ts +8 -0
  107. package/dist/isosurface/IsosurfaceControls.svelte +273 -0
  108. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +9 -0
  109. package/dist/isosurface/index.d.ts +5 -0
  110. package/dist/isosurface/index.js +6 -0
  111. package/dist/isosurface/parse.d.ts +6 -0
  112. package/dist/isosurface/parse.js +548 -0
  113. package/dist/isosurface/slice.d.ts +11 -0
  114. package/dist/isosurface/slice.js +145 -0
  115. package/dist/isosurface/types.d.ts +55 -0
  116. package/dist/isosurface/types.js +178 -0
  117. package/dist/labels.d.ts +2 -1
  118. package/dist/labels.js +1 -0
  119. package/dist/layout/InfoTag.svelte +62 -62
  120. package/dist/layout/SubpageGrid.svelte +74 -0
  121. package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
  122. package/dist/layout/index.d.ts +1 -0
  123. package/dist/layout/index.js +1 -0
  124. package/dist/layout/json-tree/JsonNode.svelte +226 -53
  125. package/dist/layout/json-tree/JsonTree.svelte +425 -51
  126. package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
  127. package/dist/layout/json-tree/JsonValue.svelte +218 -97
  128. package/dist/layout/json-tree/types.d.ts +27 -2
  129. package/dist/layout/json-tree/utils.d.ts +14 -1
  130. package/dist/layout/json-tree/utils.js +254 -0
  131. package/dist/marching-cubes.d.ts +14 -0
  132. package/dist/marching-cubes.js +519 -0
  133. package/dist/math.d.ts +8 -0
  134. package/dist/math.js +374 -7
  135. package/dist/overlays/ContextMenu.svelte +3 -2
  136. package/dist/overlays/DraggablePane.svelte +163 -58
  137. package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
  138. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +232 -77
  139. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
  140. package/dist/phase-diagram/PhaseDiagramControls.svelte +32 -11
  141. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
  142. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +103 -0
  143. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
  144. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +102 -95
  145. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +7 -0
  146. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +100 -26
  147. package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
  148. package/dist/phase-diagram/index.d.ts +2 -0
  149. package/dist/phase-diagram/index.js +2 -0
  150. package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
  151. package/dist/phase-diagram/svg-to-diagram.js +865 -0
  152. package/dist/phase-diagram/types.d.ts +10 -0
  153. package/dist/phase-diagram/utils.d.ts +7 -4
  154. package/dist/phase-diagram/utils.js +149 -59
  155. package/dist/plot/AxisLabel.svelte +26 -0
  156. package/dist/plot/AxisLabel.svelte.d.ts +16 -0
  157. package/dist/plot/BarPlot.svelte +473 -228
  158. package/dist/plot/BarPlot.svelte.d.ts +3 -3
  159. package/dist/plot/BarPlotControls.svelte +3 -2
  160. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  161. package/dist/plot/ColorBar.svelte +54 -54
  162. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  163. package/dist/plot/ElementScatter.svelte +4 -3
  164. package/dist/plot/FillArea.svelte +4 -1
  165. package/dist/plot/Histogram.svelte +320 -230
  166. package/dist/plot/Histogram.svelte.d.ts +2 -2
  167. package/dist/plot/HistogramControls.svelte +29 -10
  168. package/dist/plot/HistogramControls.svelte.d.ts +6 -2
  169. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +2 -2
  170. package/dist/plot/PlotControls.svelte +109 -27
  171. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  172. package/dist/plot/PlotLegend.svelte +1 -1
  173. package/dist/plot/PortalSelect.svelte +2 -1
  174. package/dist/plot/ReferenceLine.svelte +2 -1
  175. package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
  176. package/dist/plot/ReferencePlane.svelte +1 -3
  177. package/dist/plot/ScatterPlot.svelte +343 -209
  178. package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
  179. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  180. package/dist/plot/ScatterPlot3DControls.svelte +203 -250
  181. package/dist/plot/ScatterPlot3DScene.svelte +4 -7
  182. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  183. package/dist/plot/ScatterPlotControls.svelte +95 -55
  184. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  185. package/dist/plot/ZeroLines.svelte +44 -0
  186. package/dist/plot/ZeroLines.svelte.d.ts +32 -0
  187. package/dist/plot/ZoomRect.svelte +21 -0
  188. package/dist/plot/ZoomRect.svelte.d.ts +8 -0
  189. package/dist/plot/axis-utils.d.ts +1 -1
  190. package/dist/plot/data-cleaning.js +1 -5
  191. package/dist/plot/index.d.ts +6 -2
  192. package/dist/plot/index.js +6 -2
  193. package/dist/plot/interactions.d.ts +8 -10
  194. package/dist/plot/interactions.js +10 -19
  195. package/dist/plot/layout.d.ts +7 -1
  196. package/dist/plot/layout.js +12 -4
  197. package/dist/plot/reference-line.d.ts +4 -21
  198. package/dist/plot/reference-line.js +7 -81
  199. package/dist/plot/types.d.ts +42 -17
  200. package/dist/plot/types.js +10 -0
  201. package/dist/plot/utils/label-placement.js +14 -11
  202. package/dist/plot/utils.d.ts +1 -0
  203. package/dist/plot/utils.js +14 -0
  204. package/dist/rdf/RdfPlot.svelte +55 -66
  205. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  206. package/dist/rdf/index.d.ts +1 -1
  207. package/dist/rdf/index.js +1 -1
  208. package/dist/settings.d.ts +5 -0
  209. package/dist/settings.js +37 -3
  210. package/dist/spectral/Bands.svelte +515 -143
  211. package/dist/spectral/Bands.svelte.d.ts +22 -2
  212. package/dist/spectral/helpers.d.ts +23 -1
  213. package/dist/spectral/helpers.js +65 -9
  214. package/dist/spectral/types.d.ts +2 -0
  215. package/dist/structure/AtomLegend.svelte +31 -10
  216. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  217. package/dist/structure/CellSelect.svelte +92 -22
  218. package/dist/structure/Lattice.svelte +2 -0
  219. package/dist/structure/Structure.svelte +716 -173
  220. package/dist/structure/Structure.svelte.d.ts +7 -2
  221. package/dist/structure/StructureControls.svelte +26 -14
  222. package/dist/structure/StructureControls.svelte.d.ts +5 -1
  223. package/dist/structure/StructureInfoPane.svelte +7 -1
  224. package/dist/structure/StructureScene.svelte +386 -95
  225. package/dist/structure/StructureScene.svelte.d.ts +15 -4
  226. package/dist/structure/atom-properties.d.ts +6 -2
  227. package/dist/structure/atom-properties.js +38 -25
  228. package/dist/structure/export.js +10 -7
  229. package/dist/structure/ferrox-wasm-types.d.ts +3 -2
  230. package/dist/structure/ferrox-wasm-types.js +0 -3
  231. package/dist/structure/ferrox-wasm.d.ts +3 -2
  232. package/dist/structure/ferrox-wasm.js +1 -2
  233. package/dist/structure/index.d.ts +7 -0
  234. package/dist/structure/index.js +22 -0
  235. package/dist/structure/parse.js +19 -16
  236. package/dist/structure/partial-occupancy.d.ts +25 -0
  237. package/dist/structure/partial-occupancy.js +102 -0
  238. package/dist/structure/validation.js +6 -3
  239. package/dist/symmetry/SymmetryStats.svelte +18 -4
  240. package/dist/symmetry/WyckoffTable.svelte +18 -10
  241. package/dist/symmetry/index.d.ts +7 -4
  242. package/dist/symmetry/index.js +83 -18
  243. package/dist/table/HeatmapTable.svelte +468 -69
  244. package/dist/table/HeatmapTable.svelte.d.ts +13 -1
  245. package/dist/table/ToggleMenu.svelte +291 -44
  246. package/dist/table/ToggleMenu.svelte.d.ts +4 -1
  247. package/dist/table/index.d.ts +3 -0
  248. package/dist/tooltip/index.d.ts +1 -1
  249. package/dist/tooltip/index.js +1 -0
  250. package/dist/trajectory/Trajectory.svelte +147 -145
  251. package/dist/trajectory/TrajectoryExportPane.svelte +13 -9
  252. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
  253. package/dist/trajectory/constants.d.ts +6 -0
  254. package/dist/trajectory/constants.js +7 -0
  255. package/dist/trajectory/extract.js +3 -5
  256. package/dist/trajectory/format-detect.d.ts +9 -0
  257. package/dist/trajectory/format-detect.js +76 -0
  258. package/dist/trajectory/frame-reader.d.ts +17 -0
  259. package/dist/trajectory/frame-reader.js +339 -0
  260. package/dist/trajectory/helpers.d.ts +15 -0
  261. package/dist/trajectory/helpers.js +187 -0
  262. package/dist/trajectory/index.d.ts +1 -0
  263. package/dist/trajectory/index.js +11 -4
  264. package/dist/trajectory/parse/ase.d.ts +2 -0
  265. package/dist/trajectory/parse/ase.js +76 -0
  266. package/dist/trajectory/parse/hdf5.d.ts +2 -0
  267. package/dist/trajectory/parse/hdf5.js +121 -0
  268. package/dist/trajectory/parse/index.d.ts +12 -0
  269. package/dist/trajectory/parse/index.js +304 -0
  270. package/dist/trajectory/parse/lammps.d.ts +5 -0
  271. package/dist/trajectory/parse/lammps.js +169 -0
  272. package/dist/trajectory/parse/vasp.d.ts +2 -0
  273. package/dist/trajectory/parse/vasp.js +65 -0
  274. package/dist/trajectory/parse/xyz.d.ts +2 -0
  275. package/dist/trajectory/parse/xyz.js +109 -0
  276. package/dist/trajectory/types.d.ts +11 -0
  277. package/dist/trajectory/types.js +1 -0
  278. package/dist/utils.d.ts +2 -0
  279. package/dist/utils.js +4 -0
  280. package/dist/xrd/XrdPlot.svelte +6 -4
  281. package/dist/xrd/calc-xrd.js +0 -1
  282. package/package.json +33 -23
  283. package/readme.md +4 -4
  284. package/dist/trajectory/parse.d.ts +0 -42
  285. package/dist/trajectory/parse.js +0 -1267
  286. /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
@@ -3,26 +3,26 @@
3
3
  generics="Metadata extends Record<string, unknown> = Record<string, unknown>"
4
4
  >import { format_value, symbol_names } from '../labels';
5
5
  import { FullscreenToggle, set_fullscreen_bg } from '../layout';
6
- import { ColorBar, compute_element_placement, FillArea, get_tick_label, InteractiveAxisLabel, Line, PlotLegend, PlotTooltip, ReferenceLine, ScatterPlotControls, ScatterPoint, } from './';
7
- import { AXIS_LABEL_CONTAINER, create_axis_change_handler, } from './axis-utils';
6
+ import { AxisLabel, ColorBar, compute_element_placement, FillArea, get_tick_label, Line, PlotLegend, PlotTooltip, ReferenceLine, ScatterPlotControls, ScatterPoint, ZeroLines, ZoomRect, } from './';
7
+ import { create_axis_change_handler } from './axis-utils';
8
8
  import { get_series_color, get_series_symbol, process_prop, } from './data-transform';
9
9
  import { AXIS_DEFAULTS } from './defaults';
10
- import { untrack } from 'svelte';
11
10
  import { create_dimension_tracker, create_hover_lock, } from './hover-lock.svelte';
12
- import { DEFAULT_GRID_STYLE, DEFAULT_MARKERS, get_scale_type_name, } from './types';
11
+ import { DEFAULT_GRID_STYLE, DEFAULT_MARKERS, get_scale_type_name, is_time_scale, } from './types';
13
12
  import { compute_label_positions } from './utils/label-placement';
14
13
  import { handle_legend_double_click, toggle_group_visibility, toggle_series_visibility, } from './utils/series-visibility';
15
14
  import { DEFAULTS } from '../settings';
16
15
  import { extent } from 'd3-array';
17
16
  import { scaleTime } from 'd3-scale';
17
+ import { untrack } from 'svelte';
18
18
  import { Tween } from 'svelte/motion';
19
19
  import { SvelteSet } from 'svelte/reactivity';
20
20
  import { apply_range_constraints, apply_where_condition, clamp_for_log_scale, convert_error_band_to_fill_region, generate_fill_path, is_fill_gradient, resolve_boundary, } from './fill-utils';
21
21
  import { expand_range_if_needed, get_relative_coords, normalize_y2_sync, pan_range, PINCH_ZOOM_THRESHOLD, pixels_to_data_delta, sync_y2_range, } from './interactions';
22
- import { calc_auto_padding, constrain_tooltip_position, filter_padding, } from './layout';
22
+ import { calc_auto_padding, constrain_tooltip_position, filter_padding, LABEL_GAP_DEFAULT, measure_max_tick_width, } from './layout';
23
23
  import { group_ref_lines_by_z, index_ref_lines } from './reference-line';
24
24
  import { create_color_scale, create_scale, create_size_scale, generate_ticks, get_nice_data_range, } from './scales';
25
- let { series = $bindable([]), x_axis = $bindable({}), y_axis = $bindable({}), y2_axis = $bindable({}), display = $bindable(DEFAULTS.scatter.display), styles: styles_init = {}, controls: controls_init = {}, padding = {}, range_padding = 0.05, current_x_value = null, tooltip_point = $bindable(null), selected_point = null, hovered = $bindable(false), tooltip, user_content, change = () => { }, color_scale = {
25
+ let { series = $bindable([]), x_axis = $bindable({}), x2_axis = $bindable({}), y_axis = $bindable({}), y2_axis = $bindable({}), display = $bindable(DEFAULTS.scatter.display), styles: styles_init = {}, controls: controls_init = {}, padding = {}, range_padding = 0.05, current_x_value = null, tooltip_point = $bindable(null), selected_point = null, hovered = $bindable(false), tooltip, user_content, change = () => { }, color_scale = {
26
26
  type: `linear`,
27
27
  scheme: `interpolateViridis`,
28
28
  value_range: undefined,
@@ -34,7 +34,15 @@ const final_x_axis = $derived({
34
34
  ...(x_axis ?? {}),
35
35
  });
36
36
  const final_y_axis = $derived({ ...AXIS_DEFAULTS, ...(y_axis ?? {}) });
37
+ const final_x2_axis = $derived({
38
+ ...AXIS_DEFAULTS,
39
+ label_shift: { x: 0, y: 40 }, // x2-axis label above top edge
40
+ ...(x2_axis ?? {}),
41
+ });
37
42
  const final_y2_axis = $derived({ ...AXIS_DEFAULTS, ...(y2_axis ?? {}) });
43
+ // Cache time-axis check — used in ~10 places for scale/tick/tooltip logic
44
+ let is_time_x = $derived(is_time_scale(final_x_axis.scale_type, final_x_axis.format));
45
+ let is_time_x2 = $derived(is_time_scale(final_x2_axis.scale_type, final_x2_axis.format));
38
46
  const final_display = $derived({ ...DEFAULTS.scatter.display, ...(display ?? {}) });
39
47
  // Local state for styles (initialized from prop, owned by this component for controls)
40
48
  // Using $state because styles has bindings in ScatterPlotControls
@@ -68,9 +76,11 @@ let drag_start_coords = $state(null);
68
76
  let drag_current_coords = $state(null);
69
77
  // Zoom/pan state - track both initial (data-driven) and current (after pan/zoom) ranges
70
78
  let initial_x_range = $state([0, 1]);
79
+ let initial_x2_range = $state([0, 1]);
71
80
  let initial_y_range = $state([0, 1]);
72
81
  let initial_y2_range = $state([0, 1]);
73
82
  let zoom_x_range = $state([0, 1]);
83
+ let zoom_x2_range = $state([0, 1]);
74
84
  let zoom_y_range = $state([0, 1]);
75
85
  let zoom_y2_range = $state([0, 1]);
76
86
  let previous_series_visibility = $state(null);
@@ -135,10 +145,11 @@ let points_by_axis = $derived.by(() => {
135
145
  const all = [];
136
146
  const y1 = [];
137
147
  const y2 = [];
148
+ const x2 = [];
138
149
  for (const srs of series_with_ids) {
139
150
  if (!srs)
140
151
  continue;
141
- const { x: xs, y: ys, visible = true, y_axis = `y1` } = srs;
152
+ const { x: xs, y: ys, visible = true, y_axis = `y1`, x_axis: x_ax = `x1` } = srs;
142
153
  for (let idx = 0; idx < xs.length; idx++) {
143
154
  const point = { x: xs[idx], y: ys[idx] };
144
155
  all.push(point);
@@ -147,28 +158,36 @@ let points_by_axis = $derived.by(() => {
147
158
  y2.push(point);
148
159
  else
149
160
  y1.push(point);
161
+ if (x_ax === `x2`)
162
+ x2.push(point);
150
163
  }
151
164
  }
152
165
  }
153
- return { all, y1, y2 };
166
+ return { all, y1, y2, x2 };
154
167
  });
155
168
  let all_points = $derived(points_by_axis.all);
156
169
  let y1_points = $derived(points_by_axis.y1);
157
170
  let y2_points = $derived(points_by_axis.y2);
171
+ let x2_points = $derived(points_by_axis.x2);
158
172
  // Layout: dynamic padding based on tick label widths
159
173
  const default_padding = { t: 5, b: 50, l: 50, r: 20 };
160
- let pad = $derived(filter_padding(padding, default_padding));
174
+ let pad = $state(untrack(() => filter_padding(padding, default_padding)));
161
175
  // Update padding when format or ticks change
162
176
  $effect(() => {
163
- const new_pad = width && height && (y_tick_values.length || y2_tick_values.length)
177
+ const new_pad = width && height &&
178
+ (y_tick_values.length || y2_tick_values.length || x2_tick_values.length)
164
179
  ? calc_auto_padding({
165
180
  padding,
166
181
  default_padding,
182
+ x2_axis: { ...final_x2_axis, tick_values: x2_tick_values },
167
183
  y_axis: { ...final_y_axis, tick_values: y_tick_values },
168
184
  y2_axis: { ...final_y2_axis, tick_values: y2_tick_values },
169
185
  })
170
186
  : filter_padding(padding, default_padding);
171
- if (JSON.stringify(pad) !== JSON.stringify(new_pad))
187
+ if (pad.t !== new_pad.t ||
188
+ pad.b !== new_pad.b ||
189
+ pad.l !== new_pad.l ||
190
+ pad.r !== new_pad.r)
172
191
  pad = new_pad;
173
192
  });
174
193
  // Reactive clip area dimensions to ensure proper responsiveness
@@ -206,9 +225,10 @@ let series_value_arrays = $derived.by(() => {
206
225
  });
207
226
  let all_color_values = $derived(series_value_arrays.color_values);
208
227
  // Compute auto ranges based on data and limits
209
- let auto_x_range = $derived(get_nice_data_range(all_points, ({ x }) => x, (final_x_axis.range ?? [null, null]), final_x_axis.scale_type, range_padding, final_x_axis.format?.startsWith(`%`) || false));
210
- let auto_y_range = $derived(get_nice_data_range(y1_points, ({ y }) => y, (final_y_axis.range ?? [null, null]), final_y_axis.scale_type, range_padding, false));
211
- let auto_y2_range = $derived(get_nice_data_range(y2_points, ({ y }) => y, (final_y2_axis.range ?? [null, null]), final_y2_axis.scale_type, range_padding, false));
228
+ let auto_x_range = $derived(get_nice_data_range(all_points, ({ x }) => x, final_x_axis.range ?? [null, null], final_x_axis.scale_type ?? `linear`, range_padding, is_time_x));
229
+ let auto_y_range = $derived(get_nice_data_range(y1_points, ({ y }) => y, final_y_axis.range ?? [null, null], final_y_axis.scale_type ?? `linear`, range_padding, false));
230
+ let auto_x2_range = $derived(get_nice_data_range(x2_points, ({ x }) => x, final_x2_axis.range ?? [null, null], final_x2_axis.scale_type ?? `linear`, range_padding, is_time_x2));
231
+ let auto_y2_range = $derived(get_nice_data_range(y2_points, ({ y }) => y, final_y2_axis.range ?? [null, null], final_y2_axis.scale_type ?? `linear`, range_padding, false));
212
232
  // Update zoom ranges when auto ranges or explicit ranges change
213
233
  // - Explicit ranges (from zoom/pan): apply directly
214
234
  // - Auto ranges (from data changes): use lazy expansion to preserve view context
@@ -222,6 +242,7 @@ $effect(() => {
222
242
  };
223
243
  };
224
244
  const x = get_range(final_x_axis, auto_x_range);
245
+ const x2 = get_range(final_x2_axis, auto_x2_range);
225
246
  const y = get_range(final_y_axis, auto_y_range);
226
247
  const y2 = get_range(final_y2_axis, auto_y2_range);
227
248
  // X axis: explicit → direct, auto → lazy expand
@@ -235,6 +256,17 @@ $effect(() => {
235
256
  [initial_x_range, zoom_x_range] = [result.range, result.range];
236
257
  }
237
258
  }
259
+ // X2 axis: explicit → direct, auto → lazy expand
260
+ if (x2.explicit) {
261
+ zoom_x2_range = x2.range;
262
+ }
263
+ else {
264
+ const result = expand_range_if_needed(initial_x2_range, x2.range);
265
+ if (result.changed) {
266
+ ;
267
+ [initial_x2_range, zoom_x2_range] = [result.range, result.range];
268
+ }
269
+ }
238
270
  // Y axis: explicit → direct, auto → lazy expand
239
271
  if (y.explicit) {
240
272
  zoom_y_range = y.range;
@@ -264,6 +296,7 @@ $effect(() => {
264
296
  }
265
297
  });
266
298
  let [x_min, x_max] = $derived(zoom_x_range);
299
+ let [x2_min, x2_max] = $derived(zoom_x2_range);
267
300
  let [y_min, y_max] = $derived(zoom_y_range);
268
301
  let [y2_min, y2_max] = $derived(zoom_y2_range);
269
302
  // Create auto color range
@@ -274,7 +307,7 @@ all_color_values.length > 0
274
307
  : [0, 1]);
275
308
  // Create scale functions
276
309
  // For time scales, use scaleTime directly; otherwise use create_scale (supports linear/log/arcsinh)
277
- let x_scale_fn = $derived(final_x_axis.format?.startsWith(`%`)
310
+ let x_scale_fn = $derived(is_time_x
278
311
  ? scaleTime()
279
312
  .domain([new Date(x_min), new Date(x_max)])
280
313
  .range([pad.l, width - pad.r])
@@ -282,6 +315,14 @@ let x_scale_fn = $derived(final_x_axis.format?.startsWith(`%`)
282
315
  pad.l,
283
316
  width - pad.r,
284
317
  ]));
318
+ let x2_scale_fn = $derived(is_time_x2
319
+ ? scaleTime()
320
+ .domain([new Date(x2_min), new Date(x2_max)])
321
+ .range([pad.l, width - pad.r])
322
+ : create_scale(final_x2_axis.scale_type ?? `linear`, [x2_min, x2_max], [
323
+ pad.l,
324
+ width - pad.r,
325
+ ]));
285
326
  let y_scale_fn = $derived(create_scale(final_y_axis.scale_type ?? `linear`, [y_min, y_max], [
286
327
  height - pad.b,
287
328
  pad.t,
@@ -334,14 +375,18 @@ let filtered_series = $derived(series_with_ids
334
375
  point_idx,
335
376
  size_value: size_values?.[point_idx],
336
377
  }));
337
- // Filter to points within the plot bounds
338
- const is_valid_dim = (val, min, max) => val !== null && val !== undefined && !isNaN(val) && val >= min && val <= max;
339
- // Determine which y-range to use based on series y_axis property
378
+ // Filter to points within the plot bounds (handles inverted ranges like [3.5, 1.4])
379
+ const in_range = (val, lo, hi) => val != null && !isNaN(val) && val >= Math.min(lo, hi) &&
380
+ val <= Math.max(lo, hi);
381
+ // Determine which ranges to use based on series axis properties
382
+ const [series_x_min, series_x_max] = (data_series.x_axis ?? `x1`) === `x2`
383
+ ? [x2_min, x2_max]
384
+ : [x_min, x_max];
340
385
  const [series_y_min, series_y_max] = (data_series.y_axis ?? `y1`) === `y2`
341
386
  ? [y2_min, y2_max]
342
387
  : [y_min, y_max];
343
- const filtered_data_with_extras = processed_points.filter(({ x, y }) => is_valid_dim(x, x_min, x_max) &&
344
- is_valid_dim(y, series_y_min, series_y_max));
388
+ const filtered_data_with_extras = processed_points.filter(({ x, y }) => in_range(x, series_x_min, series_x_max) &&
389
+ in_range(y, series_y_min, series_y_max));
345
390
  // Return structure consistent with DataSeries but acknowledge internal data structure (filtered_data)
346
391
  return {
347
392
  ...data_series,
@@ -360,10 +405,13 @@ let plot_points_for_placement = $derived.by(() => {
360
405
  for (const series_data of filtered_series) {
361
406
  if (!series_data?.filtered_data)
362
407
  continue;
408
+ const use_x2_scale = series_data.x_axis === `x2`;
363
409
  for (const point of series_data.filtered_data) {
364
- const point_x_coord = final_x_axis.format?.startsWith(`%`)
365
- ? x_scale_fn(new Date(point.x))
366
- : x_scale_fn(point.x);
410
+ const active_x_scale = use_x2_scale ? x2_scale_fn : x_scale_fn;
411
+ const active_is_time_x = use_x2_scale ? is_time_x2 : is_time_x;
412
+ const point_x_coord = active_is_time_x
413
+ ? active_x_scale(new Date(point.x))
414
+ : active_x_scale(point.x);
367
415
  const point_y_coord = (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(point.y);
368
416
  if (isFinite(point_x_coord) && isFinite(point_y_coord)) {
369
417
  points.push({ x: point_x_coord, y: point_y_coord });
@@ -704,14 +752,20 @@ $effect(() => {
704
752
  // Generate axis ticks - consolidated into single derived for efficiency
705
753
  let axis_ticks = $derived.by(() => {
706
754
  if (!width || !height)
707
- return { x: [], y: [], y2: [] };
755
+ return { x: [], x2: [], y: [], y2: [] };
708
756
  // X-axis ticks: choose appropriate scale for tick generation
709
757
  // Time scales (format starts with %) use scaleTime for better tick placement
710
- const x_scale_for_ticks = final_x_axis.format?.startsWith(`%`)
758
+ const x_scale_for_ticks = is_time_x
711
759
  ? scaleTime().domain([new Date(x_min), new Date(x_max)])
712
760
  : create_scale(final_x_axis.scale_type ?? `linear`, [x_min, x_max], [0, 1]);
761
+ const x2_scale_for_ticks = is_time_x2
762
+ ? scaleTime().domain([new Date(x2_min), new Date(x2_max)])
763
+ : create_scale(final_x2_axis.scale_type ?? `linear`, [x2_min, x2_max], [0, 1]);
713
764
  return {
714
765
  x: generate_ticks([x_min, x_max], final_x_axis.scale_type ?? `linear`, final_x_axis.ticks, x_scale_for_ticks, { format: final_x_axis.format }),
766
+ x2: x2_points.length > 0
767
+ ? generate_ticks([x2_min, x2_max], final_x2_axis.scale_type ?? `linear`, final_x2_axis.ticks, x2_scale_for_ticks, { format: final_x2_axis.format })
768
+ : [],
715
769
  y: generate_ticks([y_min, y_max], final_y_axis.scale_type ?? `linear`, final_y_axis.ticks, y_scale_fn, { default_count: 5 }),
716
770
  y2: y2_points.length > 0
717
771
  ? generate_ticks([y2_min, y2_max], final_y2_axis.scale_type ?? `linear`, final_y2_axis.ticks, y2_scale_fn, { default_count: 5 })
@@ -719,8 +773,16 @@ let axis_ticks = $derived.by(() => {
719
773
  };
720
774
  });
721
775
  let x_tick_values = $derived(axis_ticks.x);
776
+ let x2_tick_values = $derived(axis_ticks.x2);
722
777
  let y_tick_values = $derived(axis_ticks.y);
723
778
  let y2_tick_values = $derived(axis_ticks.y2);
779
+ // Cache measured tick-label widths so expensive text measurement only runs
780
+ // when tick values/format change, not on every template rerender.
781
+ let tick_label_widths = $derived({
782
+ x2_max: measure_max_tick_width(x2_tick_values, final_x2_axis.format ?? ``),
783
+ y_max: measure_max_tick_width(y_tick_values, final_y_axis.format ?? ``),
784
+ y2_max: measure_max_tick_width(y2_tick_values, final_y2_axis.format ?? ``),
785
+ });
724
786
  // Define global handlers reference for adding/removing listeners
725
787
  const on_window_mouse_move = (evt) => {
726
788
  if (!drag_start_coords || !svg_bounding_box)
@@ -786,6 +848,21 @@ const on_window_mouse_up = (_evt) => {
786
848
  // Y2 sync is handled by the effect that reacts to y_axis changes
787
849
  x_axis = { ...x_axis, range: next_x_range };
788
850
  y_axis = { ...y_axis, range: next_y_range };
851
+ // X2 axis: invert screen coords using x2 scale
852
+ if (x2_points.length > 0) {
853
+ const start_x2_val = x2_scale_fn.invert(drag_start_coords.x);
854
+ const end_x2_val = x2_scale_fn.invert(drag_current_coords.x);
855
+ const x2_a = start_x2_val instanceof Date
856
+ ? start_x2_val.getTime()
857
+ : start_x2_val;
858
+ const x2_b = end_x2_val instanceof Date
859
+ ? end_x2_val.getTime()
860
+ : end_x2_val;
861
+ x2_axis = {
862
+ ...x2_axis,
863
+ range: [Math.min(x2_a, x2_b), Math.max(x2_a, x2_b)],
864
+ };
865
+ }
789
866
  }
790
867
  }
791
868
  // Reset states and remove listeners
@@ -808,9 +885,11 @@ const on_pan_move = (evt) => {
808
885
  const plot_height = Math.max(1, height - pad.t - pad.b);
809
886
  const sensitivity = pan?.drag_sensitivity ?? 1;
810
887
  const x_delta = pixels_to_data_delta(-dx * sensitivity, pan_drag_state.initial_x_range, plot_width);
888
+ const x2_delta = pixels_to_data_delta(-dx * sensitivity, pan_drag_state.initial_x2_range, plot_width);
811
889
  const y_delta = pixels_to_data_delta(dy * sensitivity, pan_drag_state.initial_y_range, plot_height);
812
890
  const y2_delta = pixels_to_data_delta(dy * sensitivity, pan_drag_state.initial_y2_range, plot_height);
813
891
  zoom_x_range = pan_range(pan_drag_state.initial_x_range, x_delta);
892
+ zoom_x2_range = pan_range(pan_drag_state.initial_x2_range, x2_delta);
814
893
  zoom_y_range = pan_range(pan_drag_state.initial_y_range, y_delta);
815
894
  zoom_y2_range = get_synced_y2(zoom_y_range, pan_range(pan_drag_state.initial_y2_range, y2_delta));
816
895
  };
@@ -830,6 +909,7 @@ function handle_mouse_down(evt) {
830
909
  pan_drag_state = {
831
910
  start: { x: evt.clientX, y: evt.clientY },
832
911
  initial_x_range: [...zoom_x_range],
912
+ initial_x2_range: [...zoom_x2_range],
833
913
  initial_y_range: [...zoom_y_range],
834
914
  initial_y2_range: [...zoom_y2_range],
835
915
  };
@@ -866,10 +946,12 @@ function handle_wheel(evt) {
866
946
  // Determine pan direction based on wheel delta
867
947
  // deltaX for horizontal scroll (trackpad), deltaY for vertical
868
948
  const x_delta = pixels_to_data_delta(evt.deltaX * sensitivity, zoom_x_range, plot_width);
949
+ const x2_delta = pixels_to_data_delta(evt.deltaX * sensitivity, zoom_x2_range, plot_width);
869
950
  const y_delta = pixels_to_data_delta(evt.deltaY * sensitivity, zoom_y_range, plot_height);
870
951
  const y2_delta = pixels_to_data_delta(evt.deltaY * sensitivity, zoom_y2_range, plot_height);
871
952
  if (Math.abs(evt.deltaX) > Math.abs(evt.deltaY)) {
872
953
  zoom_x_range = pan_range(zoom_x_range, x_delta);
954
+ zoom_x2_range = pan_range(zoom_x2_range, x2_delta);
873
955
  }
874
956
  else {
875
957
  zoom_y_range = pan_range(zoom_y_range, y_delta);
@@ -886,6 +968,7 @@ function handle_touch_start(evt) {
886
968
  touch_state = {
887
969
  start_touches: touches.map((touch) => ({ x: touch.clientX, y: touch.clientY })),
888
970
  initial_x_range: [...zoom_x_range],
971
+ initial_x2_range: [...zoom_x2_range],
889
972
  initial_y_range: [...zoom_y_range],
890
973
  initial_y2_range: [...zoom_y2_range],
891
974
  };
@@ -920,13 +1003,20 @@ function handle_touch_move(evt) {
920
1003
  // Pinch zoom centered on gesture center
921
1004
  // Divide by scale so spread (scale > 1) = smaller span (zoom in)
922
1005
  const x_span = touch_state.initial_x_range[1] - touch_state.initial_x_range[0];
1006
+ const x2_span = touch_state.initial_x2_range[1] -
1007
+ touch_state.initial_x2_range[0];
923
1008
  const y_span = touch_state.initial_y_range[1] - touch_state.initial_y_range[0];
924
1009
  const y2_span = touch_state.initial_y2_range[1] -
925
1010
  touch_state.initial_y2_range[0];
926
1011
  const x_center = (touch_state.initial_x_range[0] + touch_state.initial_x_range[1]) / 2;
1012
+ const x2_center = (touch_state.initial_x2_range[0] + touch_state.initial_x2_range[1]) / 2;
927
1013
  const y_center = (touch_state.initial_y_range[0] + touch_state.initial_y_range[1]) / 2;
928
1014
  const y2_center = (touch_state.initial_y2_range[0] + touch_state.initial_y2_range[1]) / 2;
929
1015
  zoom_x_range = [x_center - x_span / scale / 2, x_center + x_span / scale / 2];
1016
+ zoom_x2_range = [
1017
+ x2_center - x2_span / scale / 2,
1018
+ x2_center + x2_span / scale / 2,
1019
+ ];
930
1020
  zoom_y_range = [y_center - y_span / scale / 2, y_center + y_span / scale / 2];
931
1021
  zoom_y2_range = get_synced_y2(zoom_y_range, [
932
1022
  y2_center - y2_span / scale / 2,
@@ -936,9 +1026,11 @@ function handle_touch_move(evt) {
936
1026
  else {
937
1027
  // Pan
938
1028
  const x_delta = pixels_to_data_delta(-dx, touch_state.initial_x_range, plot_width);
1029
+ const x2_delta = pixels_to_data_delta(-dx, touch_state.initial_x2_range, plot_width);
939
1030
  const y_delta = pixels_to_data_delta(dy, touch_state.initial_y_range, plot_height);
940
1031
  const y2_delta = pixels_to_data_delta(dy, touch_state.initial_y2_range, plot_height);
941
1032
  zoom_x_range = pan_range(touch_state.initial_x_range, x_delta);
1033
+ zoom_x2_range = pan_range(touch_state.initial_x2_range, x2_delta);
942
1034
  zoom_y_range = pan_range(touch_state.initial_y_range, y_delta);
943
1035
  zoom_y2_range = get_synced_y2(zoom_y_range, pan_range(touch_state.initial_y2_range, y2_delta));
944
1036
  }
@@ -959,11 +1051,14 @@ function update_tooltip_point(x_rel, y_rel, evt) {
959
1051
  for (const series_data of filtered_series) {
960
1052
  if (!series_data?.filtered_data)
961
1053
  continue;
1054
+ const tooltip_use_x2 = series_data.x_axis === `x2`;
1055
+ const tooltip_x_scale = tooltip_use_x2 ? x2_scale_fn : x_scale_fn;
1056
+ const tooltip_is_time_x = tooltip_use_x2 ? is_time_x2 : is_time_x;
962
1057
  for (const point of series_data.filtered_data) {
963
1058
  // Calculate screen coordinates of the point
964
- const point_cx = final_x_axis.format?.startsWith(`%`)
965
- ? x_scale_fn(new Date(point.x))
966
- : x_scale_fn(point.x);
1059
+ const point_cx = tooltip_is_time_x
1060
+ ? tooltip_x_scale(new Date(point.x))
1061
+ : tooltip_x_scale(point.x);
967
1062
  const point_cy = (series_data.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(point.y);
968
1063
  // Calculate squared screen distance between mouse and point
969
1064
  const screen_dx = x_rel - point_cx;
@@ -1031,6 +1126,8 @@ function handle_legend_drag_start(event) {
1031
1126
  legend_is_dragging = true;
1032
1127
  // Get the actual rendered position of the legend element (accounts for transforms)
1033
1128
  const legend_el = event.currentTarget;
1129
+ if (!(legend_el instanceof HTMLElement))
1130
+ return;
1034
1131
  const legend_rect = legend_el.getBoundingClientRect();
1035
1132
  // Calculate offset from mouse to legend's actual rendered position relative to SVG
1036
1133
  const [x, y] = [event.clientX - legend_rect.left, event.clientY - legend_rect.top];
@@ -1053,9 +1150,12 @@ function handle_legend_drag(event) {
1053
1150
  }
1054
1151
  function get_screen_coords(point, series) {
1055
1152
  // convert data coordinates to potentially non-finite screen coordinates
1056
- const screen_x = final_x_axis.format?.startsWith(`%`)
1057
- ? x_scale_fn(new Date(point.x))
1058
- : x_scale_fn(point.x);
1153
+ const use_x2 = series?.x_axis === `x2`;
1154
+ const active_x_scale = use_x2 ? x2_scale_fn : x_scale_fn;
1155
+ const active_is_time_x = use_x2 ? is_time_x2 : is_time_x;
1156
+ const screen_x = active_is_time_x
1157
+ ? active_x_scale(new Date(point.x))
1158
+ : active_x_scale(point.x);
1059
1159
  const y_val = point.y;
1060
1160
  // Determine which y-scale to use based on series y_axis property
1061
1161
  const use_y2 = series?.y_axis === `y2`;
@@ -1075,17 +1175,23 @@ function construct_handler_props(point) {
1075
1175
  if (!hovered_series)
1076
1176
  return null;
1077
1177
  const { x, y, color_value, metadata, series_idx } = point;
1078
- const cx = final_x_axis.format?.startsWith(`%`)
1079
- ? x_scale_fn(new Date(x))
1080
- : x_scale_fn(x);
1178
+ const handler_use_x2 = hovered_series.x_axis === `x2`;
1179
+ const handler_x_scale = handler_use_x2 ? x2_scale_fn : x_scale_fn;
1180
+ const handler_is_time_x = handler_use_x2 ? is_time_x2 : is_time_x;
1181
+ const cx = handler_is_time_x ? handler_x_scale(new Date(x)) : handler_x_scale(x);
1081
1182
  const cy = (hovered_series.y_axis === `y2` ? y2_scale_fn : y_scale_fn)(y);
1183
+ const active_x_config = handler_use_x2 ? final_x2_axis : final_x_axis;
1184
+ const active_y_config = hovered_series.y_axis === `y2`
1185
+ ? final_y2_axis
1186
+ : final_y_axis;
1082
1187
  const coords = {
1083
1188
  x,
1084
1189
  y,
1085
1190
  cx,
1086
1191
  cy,
1087
- x_axis: final_x_axis,
1088
- y_axis: final_y_axis,
1192
+ x_axis: active_x_config,
1193
+ x2_axis: final_x2_axis,
1194
+ y_axis: active_y_config,
1089
1195
  y2_axis: final_y2_axis,
1090
1196
  };
1091
1197
  return {
@@ -1094,10 +1200,8 @@ function construct_handler_props(point) {
1094
1200
  metadata,
1095
1201
  label: hovered_series.label ?? null,
1096
1202
  series_idx,
1097
- x_formatted: format_value(x, final_x_axis.format || `.3~s`),
1098
- y_formatted: format_value(y, (hovered_series.y_axis === `y2`
1099
- ? final_y2_axis.format
1100
- : final_y_axis.format) || `.3~s`),
1203
+ x_formatted: format_value(x, active_x_config.format || `.3~s`),
1204
+ y_formatted: format_value(y, active_y_config.format || `.3~s`),
1101
1205
  color_value: color_value ?? null,
1102
1206
  colorbar: {
1103
1207
  value: color_value ?? null,
@@ -1115,17 +1219,32 @@ let handler_props = $derived.by(() => {
1115
1219
  });
1116
1220
  let using_controls = $derived(controls.show);
1117
1221
  let has_multiple_series = $derived(series_with_ids.filter(Boolean).length > 1);
1222
+ // Precompute non-click event names from point_events so we don't rebuild
1223
+ // the entries array on every point render.
1224
+ let point_event_names = $derived(point_events
1225
+ ? Object.keys(point_events).filter((name) => name !== `onclick`)
1226
+ : []);
1118
1227
  // Set theme-aware background when entering fullscreen
1119
1228
  $effect(() => {
1120
1229
  set_fullscreen_bg(wrapper, fullscreen, `--scatter-fullscreen-bg`);
1121
1230
  });
1122
1231
  // State accessors for shared axis change handler
1123
1232
  const axis_state = {
1124
- get_axis: (axis) => (axis === `x` ? x_axis : axis === `y` ? y_axis : y2_axis),
1233
+ get_axis: (axis) => {
1234
+ if (axis === `x`)
1235
+ return x_axis;
1236
+ if (axis === `x2`)
1237
+ return x2_axis;
1238
+ if (axis === `y`)
1239
+ return y_axis;
1240
+ return y2_axis;
1241
+ },
1125
1242
  set_axis: (axis, config) => {
1126
1243
  // Spread into existing state to preserve merged type structure
1127
1244
  if (axis === `x`)
1128
1245
  x_axis = { ...x_axis, ...config };
1246
+ else if (axis === `x2`)
1247
+ x2_axis = { ...x2_axis, ...config };
1129
1248
  else if (axis === `y`)
1130
1249
  y_axis = { ...y_axis, ...config };
1131
1250
  else
@@ -1192,11 +1311,12 @@ $effect(() => {
1192
1311
  <ReferenceLine
1193
1312
  ref_line={line}
1194
1313
  line_idx={line.idx}
1195
- {x_min}
1196
- {x_max}
1314
+ x_min={line.x_axis === `x2` ? x2_min : x_min}
1315
+ x_max={line.x_axis === `x2` ? x2_max : x_max}
1197
1316
  y_min={line.y_axis === `y2` ? y2_min : y_min}
1198
1317
  y_max={line.y_axis === `y2` ? y2_max : y_max}
1199
1318
  x_scale={x_scale_fn}
1319
+ x2_scale={x2_scale_fn}
1200
1320
  y_scale={y_scale_fn}
1201
1321
  y2_scale={y2_scale_fn}
1202
1322
  {clip_path_id}
@@ -1247,6 +1367,9 @@ $effect(() => {
1247
1367
  <svg
1248
1368
  bind:this={svg_element}
1249
1369
  role="application"
1370
+ aria-label={rest[`aria-label`] ??
1371
+ ([final_x_axis.label, final_y_axis.label].filter(Boolean).join(` vs `) ||
1372
+ `Scatter plot`)}
1250
1373
  tabindex="0"
1251
1374
  onfocusin={() => (is_focused = true)}
1252
1375
  onfocusout={() => (is_focused = false)}
@@ -1265,13 +1388,16 @@ $effect(() => {
1265
1388
  // Reset to current auto ranges (not stale initial_*_range which may have expanded)
1266
1389
  // This ensures lazy expansion restarts fresh from current data bounds
1267
1390
  initial_x_range = [...auto_x_range] as [number, number]
1391
+ initial_x2_range = [...auto_x2_range] as [number, number]
1268
1392
  initial_y_range = [...auto_y_range] as [number, number]
1269
1393
  initial_y2_range = [...auto_y2_range] as [number, number]
1270
1394
  zoom_x_range = [...auto_x_range] as [number, number]
1395
+ zoom_x2_range = [...auto_x2_range] as [number, number]
1271
1396
  zoom_y_range = [...auto_y_range] as [number, number]
1272
1397
  zoom_y2_range = get_synced_y2(auto_y_range, [...auto_y2_range] as Vec2)
1273
1398
  // Also reset axis props so future data changes recalculate auto ranges
1274
1399
  x_axis = { ...x_axis, range: [null, null] }
1400
+ x2_axis = { ...x2_axis, range: [null, null] }
1275
1401
  y_axis = { ...y_axis, range: [null, null] }
1276
1402
  y2_axis = { ...y2_axis, range: [null, null] }
1277
1403
  }}
@@ -1289,10 +1415,12 @@ $effect(() => {
1289
1415
  height,
1290
1416
  width,
1291
1417
  x_scale_fn,
1418
+ x2_scale_fn,
1292
1419
  y_scale_fn,
1293
1420
  y2_scale_fn,
1294
1421
  pad,
1295
1422
  x_range: [x_min, x_max],
1423
+ x2_range: [x2_min, x2_max],
1296
1424
  y_range: [y_min, y_max],
1297
1425
  y2_range: [y2_min, y2_max],
1298
1426
  fullscreen,
@@ -1306,9 +1434,7 @@ $effect(() => {
1306
1434
  <g class="x-axis">
1307
1435
  {#if width > 0 && height > 0}
1308
1436
  {#each x_tick_values as tick (tick)}
1309
- {@const tick_pos_raw = final_x_axis.format?.startsWith(`%`)
1310
- ? x_scale_fn(new Date(tick))
1311
- : x_scale_fn(tick)}
1437
+ {@const tick_pos_raw = is_time_x ? x_scale_fn(new Date(tick)) : x_scale_fn(tick)}
1312
1438
  {#if isFinite(tick_pos_raw)}
1313
1439
  // Check if tick position is finite
1314
1440
  {@const tick_pos = tick_pos_raw}
@@ -1325,14 +1451,19 @@ $effect(() => {
1325
1451
  {/if}
1326
1452
  <line y1="0" y2={inside ? -5 : 5} stroke="var(--border-color, gray)" />
1327
1453
 
1328
- {#if tick >= x_min && tick <= x_max}
1454
+ {#if tick >= Math.min(x_min, x_max) && tick <= Math.max(x_min, x_max)}
1329
1455
  {@const base_y = inside ? -8 : 20}
1330
1456
  {@const shift = final_x_axis.tick?.label?.shift ?? { x: 0, y: 0 }}
1331
1457
  {@const x = shift.x ?? 0}
1332
1458
  {@const y = base_y + (shift.y ?? 0)}
1333
1459
  {@const custom_label = get_tick_label(tick, final_x_axis.ticks)}
1334
1460
  {@const dominant_baseline = inside ? `auto` : `hanging`}
1335
- <text {x} {y} dominant-baseline={dominant_baseline}>
1461
+ <text
1462
+ {x}
1463
+ {y}
1464
+ dominant-baseline={dominant_baseline}
1465
+ fill={final_x_axis.color}
1466
+ >
1336
1467
  {custom_label ?? format_value(tick, final_x_axis.format ?? ``)}
1337
1468
  </text>
1338
1469
  {/if}
@@ -1344,7 +1475,7 @@ $effect(() => {
1344
1475
 
1345
1476
  <!-- Current frame indicator -->
1346
1477
  {#if current_x_value !== null && current_x_value !== undefined}
1347
- {@const current_pos_raw = final_x_axis.format?.startsWith(`%`)
1478
+ {@const current_pos_raw = is_time_x
1348
1479
  ? x_scale_fn(new Date(current_x_value))
1349
1480
  : x_scale_fn(current_x_value)}
1350
1481
  {#if isFinite(current_pos_raw)}
@@ -1366,28 +1497,18 @@ $effect(() => {
1366
1497
  {/if}
1367
1498
 
1368
1499
  {#if final_x_axis.label || final_x_axis.options?.length}
1369
- <foreignObject
1370
- x={width / 2 + (final_x_axis.label_shift?.x ?? 0) -
1371
- AXIS_LABEL_CONTAINER.x_offset}
1372
- y={height - pad.b - (final_x_axis.label_shift?.y ?? -40) -
1373
- AXIS_LABEL_CONTAINER.y_offset}
1374
- width={AXIS_LABEL_CONTAINER.width}
1375
- height={AXIS_LABEL_CONTAINER.height}
1376
- style="overflow: visible; pointer-events: none"
1377
- >
1378
- <div xmlns="http://www.w3.org/1999/xhtml" style="pointer-events: auto">
1379
- <InteractiveAxisLabel
1380
- label={final_x_axis.label ?? ``}
1381
- options={final_x_axis.options}
1382
- selected_key={final_x_axis.selected_key}
1383
- loading={axis_loading === `x`}
1384
- axis_type="x"
1385
- color={final_x_axis.color}
1386
- on_select={(key) => handle_axis_change(`x`, key)}
1387
- class="axis-label x-label"
1388
- />
1389
- </div>
1390
- </foreignObject>
1500
+ {@const { label_shift, label = ``, options, selected_key, color } = final_x_axis}
1501
+ <AxisLabel
1502
+ x={width / 2 + (label_shift?.x ?? 0)}
1503
+ y={height - pad.b - (label_shift?.y ?? -40)}
1504
+ {label}
1505
+ {options}
1506
+ {selected_key}
1507
+ loading={axis_loading === `x`}
1508
+ axis_type="x"
1509
+ {color}
1510
+ on_select={(key) => handle_axis_change(`x`, key)}
1511
+ />
1391
1512
  {/if}
1392
1513
  </g>
1393
1514
 
@@ -1415,7 +1536,7 @@ $effect(() => {
1415
1536
  stroke="var(--border-color, gray)"
1416
1537
  />
1417
1538
 
1418
- {#if tick >= y_min && tick <= y_max}
1539
+ {#if tick >= Math.min(y_min, y_max) && tick <= Math.max(y_min, y_max)}
1419
1540
  {@const base_x = inside ? 8 : -8}
1420
1541
  {@const shift = final_y_axis.tick?.label?.shift ?? { x: 0, y: 0 }}
1421
1542
  {@const x = base_x + (shift.x ?? 0)}
@@ -1436,31 +1557,26 @@ $effect(() => {
1436
1557
  {/if}
1437
1558
 
1438
1559
  {#if height > 0 && (final_y_axis.label || final_y_axis.options?.length)}
1439
- <foreignObject
1440
- x={-AXIS_LABEL_CONTAINER.x_offset}
1441
- y={-AXIS_LABEL_CONTAINER.y_offset}
1442
- width={AXIS_LABEL_CONTAINER.width}
1443
- height={AXIS_LABEL_CONTAINER.height}
1444
- style="overflow: visible; pointer-events: none"
1445
- transform="rotate(-90, {(final_y_axis.label_shift?.y ?? 12)}, {pad.t +
1446
- (height - pad.t - pad.b) / 2 +
1447
- ((final_y_axis.label_shift?.x ?? 0))}) translate({(final_y_axis.label_shift?.y ?? 12)}, {pad.t +
1448
- (height - pad.t - pad.b) / 2 +
1449
- ((final_y_axis.label_shift?.x ?? 0))})"
1450
- >
1451
- <div xmlns="http://www.w3.org/1999/xhtml" style="pointer-events: auto">
1452
- <InteractiveAxisLabel
1453
- label={final_y_axis.label ?? ``}
1454
- options={final_y_axis.options}
1455
- selected_key={final_y_axis.selected_key}
1456
- loading={axis_loading === `y`}
1457
- axis_type="y"
1458
- color={final_y_axis.color}
1459
- on_select={(key) => handle_axis_change(`y`, key)}
1460
- class="axis-label y-label"
1461
- />
1462
- </div>
1463
- </foreignObject>
1560
+ {@const { label_shift, label = ``, options, selected_key, color, tick } =
1561
+ final_y_axis}
1562
+ {@const y_inside = tick?.label?.inside ?? false}
1563
+ {@const y_label_x = Math.max(
1564
+ 12,
1565
+ pad.l - (y_inside ? 0 : tick_label_widths.y_max) - LABEL_GAP_DEFAULT,
1566
+ ) +
1567
+ (label_shift?.x ?? 0)}
1568
+ <AxisLabel
1569
+ x={y_label_x}
1570
+ y={pad.t + (height - pad.t - pad.b) / 2 + (label_shift?.y ?? 0)}
1571
+ rotate
1572
+ {label}
1573
+ {options}
1574
+ {selected_key}
1575
+ loading={axis_loading === `y`}
1576
+ axis_type="y"
1577
+ {color}
1578
+ on_select={(key) => handle_axis_change(`y`, key)}
1579
+ />
1464
1580
  {/if}
1465
1581
  </g>
1466
1582
 
@@ -1490,7 +1606,7 @@ $effect(() => {
1490
1606
  stroke="var(--border-color, gray)"
1491
1607
  />
1492
1608
 
1493
- {#if tick >= y2_min && tick <= y2_max}
1609
+ {#if tick >= Math.min(y2_min, y2_max) && tick <= Math.max(y2_min, y2_max)}
1494
1610
  {@const base_x = inside ? -8 : 8}
1495
1611
  {@const shift = final_y2_axis.tick?.label?.shift ?? { x: 0, y: 0 }}
1496
1612
  {@const x = base_x + (shift.x ?? 0)}
@@ -1511,94 +1627,122 @@ $effect(() => {
1511
1627
  {/if}
1512
1628
 
1513
1629
  {#if height > 0 && (final_y2_axis.label || final_y2_axis.options?.length)}
1514
- <foreignObject
1515
- x={-AXIS_LABEL_CONTAINER.x_offset}
1516
- y={-AXIS_LABEL_CONTAINER.y_offset}
1517
- width={AXIS_LABEL_CONTAINER.width}
1518
- height={AXIS_LABEL_CONTAINER.height}
1519
- style="overflow: visible; pointer-events: none"
1520
- transform="rotate(-90, {width - pad.r + ((final_y2_axis.label_shift?.y ?? 0))}, {pad.t +
1521
- (height - pad.t - pad.b) / 2 +
1522
- ((final_y2_axis.label_shift?.x ?? 0))}) translate({width -
1523
- pad.r +
1524
- ((final_y2_axis.label_shift?.y ?? 0))}, {pad.t +
1525
- (height - pad.t - pad.b) / 2 +
1526
- ((final_y2_axis.label_shift?.x ?? 0))})"
1527
- >
1528
- <div xmlns="http://www.w3.org/1999/xhtml" style="pointer-events: auto">
1529
- <InteractiveAxisLabel
1530
- label={final_y2_axis.label ?? ``}
1531
- options={final_y2_axis.options}
1532
- selected_key={final_y2_axis.selected_key}
1533
- loading={axis_loading === `y2`}
1534
- axis_type="y2"
1535
- color={final_y2_axis.color}
1536
- on_select={(key) => handle_axis_change(`y2`, key)}
1537
- class="axis-label y2-label"
1538
- />
1539
- </div>
1540
- </foreignObject>
1630
+ {@const { label_shift, label = ``, options, selected_key, color, tick } =
1631
+ final_y2_axis}
1632
+ {@const inside = tick?.label?.inside ?? false}
1633
+ {@const tick_shift = inside ? 0 : (tick?.label?.shift?.x ?? 0) + 8}
1634
+ {@const tick_width_contribution = inside ? 0 : tick_label_widths.y2_max}
1635
+ <AxisLabel
1636
+ x={width - pad.r + tick_shift + tick_width_contribution +
1637
+ LABEL_GAP_DEFAULT + (label_shift?.x ?? 0)}
1638
+ y={pad.t + (height - pad.t - pad.b) / 2 + (label_shift?.y ?? 0)}
1639
+ rotate
1640
+ {label}
1641
+ {options}
1642
+ {selected_key}
1643
+ loading={axis_loading === `y2`}
1644
+ axis_type="y2"
1645
+ {color}
1646
+ on_select={(key) => handle_axis_change(`y2`, key)}
1647
+ />
1541
1648
  {/if}
1542
1649
  </g>
1543
1650
  {/if}
1544
1651
 
1545
- <!-- Tooltip rendered inside overlay (moved outside SVG for stacking above colorbar) -->
1652
+ <!-- X2-axis (Top) -->
1653
+ {#if x2_points.length > 0}
1654
+ <g class="x2-axis">
1655
+ {#if width > 0 && height > 0}
1656
+ {#each x2_tick_values as tick (tick)}
1657
+ {@const tick_pos_raw = is_time_x2
1658
+ ? x2_scale_fn(new Date(tick))
1659
+ : x2_scale_fn(tick)}
1660
+ {#if isFinite(tick_pos_raw)}
1661
+ {@const tick_pos = tick_pos_raw}
1662
+ {#if tick_pos >= pad.l && tick_pos <= width - pad.r}
1663
+ {@const inside = final_x2_axis.tick?.label?.inside ?? false}
1664
+ <g class="tick" transform="translate({tick_pos}, {pad.t})">
1665
+ {#if final_display.x2_grid}
1666
+ <line
1667
+ y1="0"
1668
+ y2={height - pad.b - pad.t}
1669
+ {...DEFAULT_GRID_STYLE}
1670
+ {...(final_x2_axis.grid_style ?? {})}
1671
+ />
1672
+ {/if}
1673
+ <line
1674
+ y1="0"
1675
+ y2={inside ? 5 : -5}
1676
+ stroke={final_x2_axis.color || `var(--border-color, gray)`}
1677
+ />
1546
1678
 
1547
- <!-- Zoom Selection Rectangle -->
1548
- {#if drag_start_coords && drag_current_coords && isFinite(drag_start_coords.x) &&
1549
- isFinite(drag_start_coords.y) && isFinite(drag_current_coords.x) &&
1550
- isFinite(drag_current_coords.y)}
1551
- {@const x = Math.min(drag_start_coords.x, drag_current_coords.x)}
1552
- {@const y = Math.min(drag_start_coords.y, drag_current_coords.y)}
1553
- {@const rect_width = Math.abs(drag_start_coords.x - drag_current_coords.x)}
1554
- {@const rect_height = Math.abs(drag_start_coords.y - drag_current_coords.y)}
1555
- <rect class="zoom-rect" {x} {y} width={rect_width} height={rect_height} />
1556
- {/if}
1679
+ {#if tick >= Math.min(x2_min, x2_max) && tick <= Math.max(x2_min, x2_max)}
1680
+ {@const base_y = inside ? 8 : -20}
1681
+ {@const shift = final_x2_axis.tick?.label?.shift ?? { x: 0, y: 0 }}
1682
+ {@const x = shift.x ?? 0}
1683
+ {@const y = base_y + (shift.y ?? 0)}
1684
+ {@const custom_label = get_tick_label(tick, final_x2_axis.ticks)}
1685
+ {@const dominant_baseline = inside ? `hanging` : `auto`}
1686
+ <text
1687
+ {x}
1688
+ {y}
1689
+ dominant-baseline={dominant_baseline}
1690
+ fill={final_x2_axis.color}
1691
+ >
1692
+ {custom_label ?? format_value(tick, final_x2_axis.format ?? ``)}
1693
+ </text>
1694
+ {/if}
1695
+ </g>
1696
+ {/if}
1697
+ {/if}
1698
+ {/each}
1699
+ {/if}
1557
1700
 
1558
- <!-- Zero lines (shown for linear and arcsinh scales, not log) -->
1559
- {#if final_display.x_zero_line &&
1560
- get_scale_type_name(final_x_axis.scale_type) !== `log` &&
1561
- !final_x_axis.format?.startsWith(`%`) && x_min <= 0 && x_max >= 0}
1562
- {@const zero_x_pos = x_scale_fn(0)}
1563
- {#if isFinite(zero_x_pos)}
1564
- <line
1565
- class="zero-line"
1566
- x1={zero_x_pos}
1567
- x2={zero_x_pos}
1568
- y1={pad.t}
1569
- y2={height - pad.b}
1570
- />
1571
- {/if}
1572
- {/if}
1573
- {#if final_display.y_zero_line &&
1574
- get_scale_type_name(final_y_axis.scale_type) !== `log` &&
1575
- y_min <= 0 && y_max >= 0}
1576
- {@const zero_y_pos = y_scale_fn(0)}
1577
- {#if isFinite(zero_y_pos)}
1578
- <line
1579
- class="zero-line"
1580
- x1={pad.l}
1581
- x2={width - pad.r}
1582
- y1={zero_y_pos}
1583
- y2={zero_y_pos}
1584
- />
1585
- {/if}
1586
- {/if}
1587
- {#if final_display.y_zero_line && y2_points.length > 0 &&
1588
- get_scale_type_name(final_y2_axis.scale_type) !== `log` && y2_min <= 0 &&
1589
- y2_max >= 0}
1590
- {@const zero_y2_pos = y2_scale_fn(0)}
1591
- {#if isFinite(zero_y2_pos)}
1592
- <line
1593
- class="zero-line"
1594
- x1={pad.l}
1595
- x2={width - pad.r}
1596
- y1={zero_y2_pos}
1597
- y2={zero_y2_pos}
1598
- />
1599
- {/if}
1701
+ {#if final_x2_axis.label || final_x2_axis.options?.length}
1702
+ {@const { label_shift, label = ``, options, selected_key, color } =
1703
+ final_x2_axis}
1704
+ <AxisLabel
1705
+ x={width / 2 + (label_shift?.x ?? 0)}
1706
+ y={Math.max(12, pad.t - (label_shift?.y ?? 40))}
1707
+ {label}
1708
+ {options}
1709
+ {selected_key}
1710
+ loading={axis_loading === `x2`}
1711
+ axis_type="x2"
1712
+ {color}
1713
+ on_select={(key) => handle_axis_change(`x2`, key)}
1714
+ />
1715
+ {/if}
1716
+ </g>
1600
1717
  {/if}
1601
1718
 
1719
+ <!-- Tooltip rendered inside overlay (moved outside SVG for stacking above colorbar) -->
1720
+
1721
+ <ZoomRect start={drag_start_coords} current={drag_current_coords} />
1722
+
1723
+ <ZeroLines
1724
+ display={final_display}
1725
+ {x_scale_fn}
1726
+ {x2_scale_fn}
1727
+ {y_scale_fn}
1728
+ {y2_scale_fn}
1729
+ x_range={zoom_x_range}
1730
+ x2_range={zoom_x2_range}
1731
+ y_range={zoom_y_range}
1732
+ y2_range={zoom_y2_range}
1733
+ x_scale_type={final_x_axis.scale_type}
1734
+ x2_scale_type={final_x2_axis.scale_type}
1735
+ y_scale_type={final_y_axis.scale_type}
1736
+ y2_scale_type={final_y2_axis.scale_type}
1737
+ x_is_time={is_time_x}
1738
+ x2_is_time={is_time_x2}
1739
+ has_x2={x2_points.length > 0}
1740
+ has_y2={y2_points.length > 0}
1741
+ {width}
1742
+ {height}
1743
+ {pad}
1744
+ />
1745
+
1602
1746
  <defs>
1603
1747
  <clipPath id={clip_path_id}>
1604
1748
  <rect
@@ -1644,9 +1788,7 @@ $effect(() => {
1644
1788
  <Line
1645
1789
  points={finite_screen_points}
1646
1790
  origin={[
1647
- final_x_axis.format?.startsWith(`%`)
1648
- ? x_scale_fn(new Date(x_min))
1649
- : x_scale_fn(x_min),
1791
+ is_time_x ? x_scale_fn(new Date(x_min)) : x_scale_fn(x_min),
1650
1792
  series_data.y_axis === `y2` ? y2_scale_fn(y2_min) : y_scale_fn(y_min),
1651
1793
  ]}
1652
1794
  line_color={(tc(`line.color`) ? styles.line?.color : null) ?? color_fallback}
@@ -1685,9 +1827,7 @@ $effect(() => {
1685
1827
  ...label_style,
1686
1828
  offset: {
1687
1829
  x: calculated_label_pos.x -
1688
- (final_x_axis.format?.startsWith(`%`)
1689
- ? x_scale_fn(new Date(point.x))
1690
- : x_scale_fn(point.x)),
1830
+ (is_time_x ? x_scale_fn(new Date(point.x)) : x_scale_fn(point.x)),
1691
1831
  y: calculated_label_pos.y - (series_data.y_axis === `y2`
1692
1832
  ? y2_scale_fn(point.y)
1693
1833
  : y_scale_fn(point.y)),
@@ -1744,10 +1884,9 @@ $effect(() => {
1744
1884
  series_default_color}
1745
1885
  {...point_events &&
1746
1886
  Object.fromEntries(
1747
- Object.entries(point_events)
1748
- .filter(([event_name]) => event_name !== `onclick`).map((
1749
- [event_name, handler],
1750
- ) => [event_name, (event: Event) => handler({ point, event })]),
1887
+ point_event_names.map((name) => [name, (event: Event) =>
1888
+ point_events?.[name]?.({ point, event })]
1889
+ ),
1751
1890
  )}
1752
1891
  onclick={(event: MouseEvent) => {
1753
1892
  // Call user-provided onclick handler first if it exists
@@ -1830,9 +1969,16 @@ $effect(() => {
1830
1969
  {#if tooltip}
1831
1970
  {@render tooltip(handler_props)}
1832
1971
  {:else}
1833
- {@html point_label?.text ? `${point_label.text}<br />` : ``}x: {
1834
- handler_props.x_formatted
1835
- }<br />y: {handler_props.y_formatted}
1972
+ {@const hp = handler_props}
1973
+ {#if has_multiple_series && hp.label}<strong>{hp.label}</strong><br />{/if}
1974
+ {@html point_label?.text ? `${point_label.text}<br />` : ``}
1975
+ {@html hp.x_axis.label || `x`}: {hp.x_formatted}<br />
1976
+ {@html hp.y_axis.label || `y`}: {hp.y_formatted}
1977
+ {#if hp.colorbar?.value != null}
1978
+ <br />{@html hp.colorbar.title || `Color`}: {
1979
+ format_value(hp.colorbar.value, hp.colorbar.tick_format || `.3~g`)
1980
+ }
1981
+ {/if}
1836
1982
  {/if}
1837
1983
  </PlotTooltip>
1838
1984
  {/if}
@@ -1843,21 +1989,24 @@ $effect(() => {
1843
1989
  toggle_props={{
1844
1990
  ...controls.toggle_props,
1845
1991
  style:
1846
- `--ctrl-btn-right: var(--fullscreen-btn-offset, 36px); top: var(--ctrl-btn-top, 5pt); ${
1992
+ `--ctrl-btn-right: var(--fullscreen-btn-offset, 30px); top: var(--ctrl-btn-top, 5pt); ${
1847
1993
  controls.toggle_props?.style ?? ``
1848
1994
  }`,
1849
1995
  }}
1850
1996
  pane_props={controls.pane_props}
1851
1997
  bind:x_axis
1998
+ bind:x2_axis
1852
1999
  bind:y_axis
1853
2000
  bind:y2_axis
1854
2001
  bind:display
1855
2002
  bind:styles
1856
2003
  {auto_x_range}
2004
+ {auto_x2_range}
1857
2005
  {auto_y_range}
1858
2006
  {auto_y2_range}
1859
2007
  bind:selected_series_idx
1860
2008
  series={series_with_ids}
2009
+ has_x2_points={x2_points.length > 0}
1861
2010
  has_y2_points={y2_points.length > 0}
1862
2011
  children={controls_extra}
1863
2012
  on_touch={(key) => touched.add(key)}
@@ -2057,19 +2206,16 @@ $effect(() => {
2057
2206
  stroke-dasharray: var(--scatter-grid-dash, 4);
2058
2207
  stroke-width: var(--scatter-grid-width, 0.4);
2059
2208
  }
2060
- g.x-axis text {
2209
+ g:is(.x-axis, .x2-axis) text {
2061
2210
  text-anchor: middle;
2062
2211
  dominant-baseline: top;
2063
2212
  }
2064
2213
  g:is(.y-axis, .y2-axis) text {
2065
2214
  dominant-baseline: central;
2066
2215
  }
2067
- g:is(.x-axis, .y-axis, .y2-axis) .tick text {
2216
+ g:is(.x-axis, .x2-axis, .y-axis, .y2-axis) .tick text {
2068
2217
  font-size: var(--tick-font-size, 0.8em); /* shrink tick labels */
2069
2218
  }
2070
- foreignobject {
2071
- overflow: visible;
2072
- }
2073
2219
  .scatter :global(.axis-label) {
2074
2220
  text-align: center;
2075
2221
  width: 100%;
@@ -2092,16 +2238,4 @@ $effect(() => {
2092
2238
  .current-frame-indicator:hover {
2093
2239
  opacity: 0.8;
2094
2240
  }
2095
- .zoom-rect {
2096
- fill: var(--scatter-zoom-rect-fill, rgba(100, 100, 255, 0.2));
2097
- stroke: var(--scatter-zoom-rect-stroke, rgba(100, 100, 255, 0.8));
2098
- stroke-width: var(--scatter-zoom-rect-stroke-width, 1);
2099
- pointer-events: none; /* Prevent rect from interfering with mouse events */
2100
- }
2101
- .zero-line {
2102
- stroke: var(--scatter-zero-line-color, light-dark(black, white));
2103
- stroke-width: var(--scatter-zero-line-width, 1);
2104
- stroke-dasharray: none;
2105
- opacity: var(--scatter-zero-line-opacity, 0.3);
2106
- }
2107
2241
  </style>