matterviz 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/dist/MillerIndexInput.svelte +5 -5
  2. package/dist/api/optimade.js +3 -3
  3. package/dist/brillouin/BrillouinZone.svelte +5 -2
  4. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  5. package/dist/brillouin/BrillouinZoneExportPane.svelte +1 -3
  6. package/dist/brillouin/BrillouinZoneInfoPane.svelte +1 -1
  7. package/dist/brillouin/BrillouinZoneScene.svelte +5 -5
  8. package/dist/brillouin/compute.js +21 -21
  9. package/dist/brillouin/index.d.ts +1 -1
  10. package/dist/brillouin/index.js +0 -1
  11. package/dist/brillouin/types.d.ts +8 -13
  12. package/dist/chempot-diagram/ChemPotDiagram.svelte +3 -3
  13. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +3 -4
  14. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +33 -34
  15. package/dist/chempot-diagram/compute.js +1 -7
  16. package/dist/chempot-diagram/temperature.d.ts +1 -1
  17. package/dist/chempot-diagram/temperature.js +1 -3
  18. package/dist/chempot-diagram/types.d.ts +4 -9
  19. package/dist/colors/index.js +5 -5
  20. package/dist/composition/Composition.svelte +2 -1
  21. package/dist/composition/Formula.svelte +7 -4
  22. package/dist/composition/FormulaFilter.svelte +1 -3
  23. package/dist/composition/format.js +4 -4
  24. package/dist/composition/parse.d.ts +2 -1
  25. package/dist/composition/parse.js +61 -46
  26. package/dist/convex-hull/ConvexHull2D.svelte +62 -51
  27. package/dist/convex-hull/ConvexHull3D.svelte +101 -90
  28. package/dist/convex-hull/ConvexHull4D.svelte +70 -58
  29. package/dist/convex-hull/ConvexHullControls.svelte +24 -35
  30. package/dist/convex-hull/ConvexHullInfoPane.svelte +8 -5
  31. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +2 -0
  32. package/dist/convex-hull/ConvexHullStats.svelte +9 -2
  33. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +2 -0
  34. package/dist/convex-hull/GasPressureControls.svelte +7 -7
  35. package/dist/convex-hull/StructurePopup.svelte +65 -30
  36. package/dist/convex-hull/StructurePopup.svelte.d.ts +6 -6
  37. package/dist/convex-hull/TemperatureSlider.svelte +8 -5
  38. package/dist/convex-hull/barycentric-coords.d.ts +2 -2
  39. package/dist/convex-hull/barycentric-coords.js +2 -2
  40. package/dist/convex-hull/gas-thermodynamics.js +2 -4
  41. package/dist/convex-hull/helpers.d.ts +13 -2
  42. package/dist/convex-hull/helpers.js +37 -16
  43. package/dist/convex-hull/index.d.ts +1 -0
  44. package/dist/convex-hull/index.js +1 -0
  45. package/dist/convex-hull/thermodynamics.d.ts +2 -1
  46. package/dist/convex-hull/thermodynamics.js +7 -7
  47. package/dist/convex-hull/types.d.ts +15 -15
  48. package/dist/effects.svelte.d.ts +12 -0
  49. package/dist/effects.svelte.js +37 -0
  50. package/dist/element/BohrAtom.svelte +4 -4
  51. package/dist/element/data.json.gz.d.ts +3 -1
  52. package/dist/element/index.d.ts +1 -1
  53. package/dist/element/index.js +0 -1
  54. package/dist/fermi-surface/FermiSurface.svelte +4 -4
  55. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  56. package/dist/fermi-surface/FermiSurfaceControls.svelte +15 -19
  57. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  58. package/dist/fermi-surface/FermiSurfaceScene.svelte +8 -6
  59. package/dist/fermi-surface/compute.js +2 -2
  60. package/dist/fermi-surface/export.js +13 -26
  61. package/dist/fermi-surface/parse.js +8 -12
  62. package/dist/fermi-surface/types.d.ts +2 -5
  63. package/dist/heatmap-matrix/HeatmapMatrix.svelte +21 -3
  64. package/dist/heatmap-matrix/index.js +6 -6
  65. package/dist/io/decompress.d.ts +2 -1
  66. package/dist/io/decompress.js +1 -1
  67. package/dist/io/export.js +1 -1
  68. package/dist/io/index.d.ts +1 -1
  69. package/dist/io/index.js +0 -1
  70. package/dist/io/url-drop.js +7 -1
  71. package/dist/isosurface/IsosurfaceControls.svelte +11 -25
  72. package/dist/isosurface/slice.js +1 -1
  73. package/dist/isosurface/types.js +12 -12
  74. package/dist/labels.d.ts +1 -1
  75. package/dist/labels.js +14 -11
  76. package/dist/layout/InfoTag.svelte +6 -4
  77. package/dist/layout/PropertyFilter.svelte +4 -2
  78. package/dist/layout/json-tree/JsonTree.svelte +22 -14
  79. package/dist/layout/json-tree/JsonValue.svelte +2 -2
  80. package/dist/layout/json-tree/types.d.ts +3 -2
  81. package/dist/layout/json-tree/types.js +0 -1
  82. package/dist/layout/json-tree/utils.d.ts +4 -4
  83. package/dist/layout/json-tree/utils.js +12 -20
  84. package/dist/marching-cubes.js +13 -15
  85. package/dist/math.d.ts +11 -1
  86. package/dist/math.js +15 -6
  87. package/dist/overlays/DragControlTab.svelte +98 -0
  88. package/dist/overlays/DragControlTab.svelte.d.ts +8 -0
  89. package/dist/overlays/DraggablePane.svelte +7 -84
  90. package/dist/overlays/index.d.ts +1 -0
  91. package/dist/overlays/index.js +1 -0
  92. package/dist/periodic-table/PeriodicTable.svelte +11 -11
  93. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +4 -2
  94. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  95. package/dist/phase-diagram/PhaseDiagramControls.svelte +4 -9
  96. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  97. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +2 -10
  98. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +2 -3
  99. package/dist/phase-diagram/TdbInfoPanel.svelte +3 -3
  100. package/dist/phase-diagram/build-diagram.js +11 -18
  101. package/dist/phase-diagram/diagram-input.d.ts +5 -9
  102. package/dist/phase-diagram/index.d.ts +2 -2
  103. package/dist/phase-diagram/index.js +0 -2
  104. package/dist/phase-diagram/parse.d.ts +2 -2
  105. package/dist/phase-diagram/parse.js +6 -10
  106. package/dist/phase-diagram/svg-to-diagram.js +15 -15
  107. package/dist/phase-diagram/types.d.ts +5 -11
  108. package/dist/phase-diagram/utils.d.ts +2 -2
  109. package/dist/phase-diagram/utils.js +9 -11
  110. package/dist/plot/BarPlot.svelte +162 -314
  111. package/dist/plot/BarPlot.svelte.d.ts +5 -4
  112. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  113. package/dist/plot/BinnedScatterPlot.svelte +1114 -0
  114. package/dist/plot/BinnedScatterPlot.svelte.d.ts +66 -0
  115. package/dist/plot/ColorBar.svelte +19 -17
  116. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  117. package/dist/plot/FillArea.svelte +2 -4
  118. package/dist/plot/FillArea.svelte.d.ts +1 -1
  119. package/dist/plot/Histogram.svelte +167 -281
  120. package/dist/plot/Histogram.svelte.d.ts +1 -1
  121. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  122. package/dist/plot/InteractiveAxisLabel.svelte +5 -3
  123. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  124. package/dist/plot/PlotAxis.svelte +169 -0
  125. package/dist/plot/PlotAxis.svelte.d.ts +24 -0
  126. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  127. package/dist/plot/ReferenceLine3D.svelte +53 -51
  128. package/dist/plot/ReferencePlane.svelte +39 -42
  129. package/dist/plot/ScatterPlot.svelte +300 -367
  130. package/dist/plot/ScatterPlot.svelte.d.ts +8 -5
  131. package/dist/plot/ScatterPlot3D.svelte +33 -6
  132. package/dist/plot/ScatterPlot3D.svelte.d.ts +3 -2
  133. package/dist/plot/ScatterPlot3DControls.svelte +9 -9
  134. package/dist/plot/ScatterPlotControls.svelte +3 -4
  135. package/dist/plot/ScatterPoint.svelte +18 -27
  136. package/dist/plot/ScatterPoint.svelte.d.ts +4 -3
  137. package/dist/plot/Surface3D.svelte +4 -7
  138. package/dist/plot/ZeroLines.svelte +2 -1
  139. package/dist/plot/ZeroLines.svelte.d.ts +2 -1
  140. package/dist/plot/ZoomRect.svelte +2 -2
  141. package/dist/plot/ZoomRect.svelte.d.ts +3 -3
  142. package/dist/plot/adaptive-density.d.ts +69 -0
  143. package/dist/plot/adaptive-density.js +191 -0
  144. package/dist/plot/auto-place.d.ts +43 -0
  145. package/dist/plot/auto-place.js +122 -0
  146. package/dist/plot/axis-utils.js +3 -5
  147. package/dist/plot/binned-scatter-types.d.ts +59 -0
  148. package/dist/plot/binned-scatter-types.js +1 -0
  149. package/dist/plot/data-cleaning.js +1 -1
  150. package/dist/plot/data-transform.js +1 -1
  151. package/dist/plot/fill-utils.d.ts +4 -9
  152. package/dist/plot/fill-utils.js +29 -44
  153. package/dist/plot/index.d.ts +4 -0
  154. package/dist/plot/index.js +2 -0
  155. package/dist/plot/interactions.d.ts +4 -4
  156. package/dist/plot/interactions.js +4 -3
  157. package/dist/plot/layout.d.ts +20 -2
  158. package/dist/plot/layout.js +59 -16
  159. package/dist/plot/reference-line.d.ts +1 -1
  160. package/dist/plot/reference-line.js +9 -11
  161. package/dist/plot/scales.d.ts +1 -1
  162. package/dist/plot/scales.js +20 -23
  163. package/dist/plot/types.d.ts +30 -58
  164. package/dist/plot/types.js +2 -6
  165. package/dist/plot/utils/label-placement.d.ts +24 -3
  166. package/dist/plot/utils/label-placement.js +82 -12
  167. package/dist/plot/utils/series-visibility.d.ts +8 -2
  168. package/dist/plot/utils/series-visibility.js +23 -5
  169. package/dist/rdf/RdfPlot.svelte +5 -5
  170. package/dist/rdf/calc-rdf.js +3 -3
  171. package/dist/sanitize.d.ts +2 -0
  172. package/dist/sanitize.js +2 -0
  173. package/dist/spectral/Bands.svelte +1 -1
  174. package/dist/spectral/BandsAndDos.svelte +22 -16
  175. package/dist/spectral/BrillouinBandsDos.svelte +20 -16
  176. package/dist/spectral/Dos.svelte +1 -1
  177. package/dist/spectral/helpers.d.ts +4 -2
  178. package/dist/spectral/helpers.js +44 -35
  179. package/dist/spectral/index.d.ts +1 -1
  180. package/dist/spectral/index.js +0 -1
  181. package/dist/structure/AtomLegend.svelte +23 -6
  182. package/dist/structure/AtomLegend.svelte.d.ts +1 -0
  183. package/dist/structure/CanvasTooltip.svelte +9 -9
  184. package/dist/structure/CanvasTooltip.svelte.d.ts +1 -1
  185. package/dist/structure/CellSelect.svelte +14 -16
  186. package/dist/structure/Structure.svelte +317 -68
  187. package/dist/structure/Structure.svelte.d.ts +4 -2
  188. package/dist/structure/StructureControls.svelte +20 -45
  189. package/dist/structure/StructureExportPane.svelte +2 -1
  190. package/dist/structure/StructureInfoPane.svelte +10 -8
  191. package/dist/structure/StructureScene.svelte +527 -177
  192. package/dist/structure/StructureScene.svelte.d.ts +5 -2
  193. package/dist/structure/atom-properties.js +4 -4
  194. package/dist/structure/bond-order-perception.js +115 -98
  195. package/dist/structure/bonding.d.ts +27 -1
  196. package/dist/structure/bonding.js +187 -16
  197. package/dist/structure/export.js +1 -1
  198. package/dist/structure/index.d.ts +3 -2
  199. package/dist/structure/index.js +0 -2
  200. package/dist/structure/parse.js +88 -59
  201. package/dist/symmetry/WyckoffTable.svelte +7 -0
  202. package/dist/symmetry/index.js +13 -14
  203. package/dist/table/HeatmapTable.svelte +45 -66
  204. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  205. package/dist/table/ToggleMenu.svelte +19 -10
  206. package/dist/theme/themes.mjs +12 -0
  207. package/dist/tooltip/index.d.ts +1 -1
  208. package/dist/tooltip/index.js +0 -1
  209. package/dist/trajectory/Trajectory.svelte +43 -15
  210. package/dist/trajectory/TrajectoryInfoPane.svelte +2 -2
  211. package/dist/trajectory/extract.js +1 -1
  212. package/dist/trajectory/frame-reader.js +4 -4
  213. package/dist/trajectory/helpers.d.ts +5 -4
  214. package/dist/trajectory/helpers.js +9 -17
  215. package/dist/trajectory/index.d.ts +2 -2
  216. package/dist/trajectory/index.js +2 -2
  217. package/dist/trajectory/parse/ase.js +4 -4
  218. package/dist/trajectory/parse/hdf5.js +1 -1
  219. package/dist/trajectory/parse/index.js +2 -3
  220. package/dist/trajectory/parse/lammps.js +1 -1
  221. package/dist/trajectory/parse/vasp.js +1 -1
  222. package/dist/trajectory/plotting.d.ts +1 -1
  223. package/dist/trajectory/plotting.js +38 -38
  224. package/dist/trajectory/types.d.ts +1 -1
  225. package/dist/utils.d.ts +1 -0
  226. package/dist/utils.js +9 -0
  227. package/dist/xrd/calc-xrd.js +3 -4
  228. package/dist/xrd/parse.js +1 -1
  229. package/package.json +42 -22
@@ -0,0 +1,1114 @@
1
+ <script module lang="ts">
2
+ let next_clip_id = 0
3
+ </script>
4
+
5
+ <script
6
+ lang="ts"
7
+ generics="Metadata extends Record<string, unknown> = Record<string, unknown>, PointData extends Record<string, unknown> = Record<string, unknown>"
8
+ >
9
+ import Icon from '../Icon.svelte'
10
+ import { format_value } from '../labels'
11
+ import { FullscreenToggle, set_fullscreen_bg } from '../layout'
12
+ import type { Point2D, Vec2 } from '../math'
13
+ import { create_pulse_animation } from '../effects.svelte'
14
+ import ColorBar from './ColorBar.svelte'
15
+ import PlotAxis from './PlotAxis.svelte'
16
+ import PlotTooltip from './PlotTooltip.svelte'
17
+ import ZoomRect from './ZoomRect.svelte'
18
+ import { compute_element_placement, filter_padding } from './layout'
19
+ import type { Sides } from './layout'
20
+ import {
21
+ build_pick_index,
22
+ bin_points,
23
+ density_bin_at_point,
24
+ first_point_in_bin,
25
+ get_metadata_at,
26
+ pick_from_index,
27
+ range_bounds,
28
+ series_extents,
29
+ should_render_points,
30
+ type DensityBin,
31
+ type DenseInternalPoint,
32
+ type DensePointSeries,
33
+ } from './adaptive-density'
34
+ import { create_color_scale, create_scale, create_size_scale, generate_ticks } from './scales'
35
+ import type { AxisConfig, DataSeries, InternalPoint, ScatterHandlerProps } from './types'
36
+ import {
37
+ compute_label_positions,
38
+ estimate_label_size,
39
+ label_leader_segment,
40
+ type LabelSize,
41
+ } from './utils/label-placement'
42
+ import type { ComponentProps, Snippet } from 'svelte'
43
+ import { onMount, tick } from 'svelte'
44
+ import type { HTMLAttributes } from 'svelte/elements'
45
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity'
46
+ import type {
47
+ BinnedColorScaleConfig,
48
+ BinnedDensityConfig,
49
+ BinnedOverlaysConfig,
50
+ BinnedPointDataFn,
51
+ BinnedPointLabelsConfig,
52
+ BinnedPointPayload,
53
+ BinnedPointTooltipPayload,
54
+ BinnedSizeScaleConfig,
55
+ } from './binned-scatter-types'
56
+
57
+ type RenderMode = `density` | `points`
58
+ type DensePointEvent = {
59
+ point: DenseInternalPoint<Metadata>
60
+ event: MouseEvent
61
+ color?: string
62
+ point_data?: PointData
63
+ }
64
+ type DensityZoomEvent = {
65
+ bin: DensityBin
66
+ event: MouseEvent
67
+ }
68
+ type OverlayContext = { height: number; width: number; fullscreen: boolean }
69
+ const default_color_bar_size = { width: 220, height: 10 }
70
+ const default_density_auto_point_mode = { max_points: 25_000, max_points_per_px: 0.12 }
71
+ const default_density_color_bar: ComponentProps<typeof ColorBar> = { title: `Density` }
72
+ const default_density_color_scale = {
73
+ type: `linear`,
74
+ scheme: `interpolateViridis`,
75
+ } satisfies Exclude<BinnedColorScaleConfig, string>
76
+ const default_pad = { l: 64, r: 24, t: 24, b: 56 }
77
+ const default_point_radius_range: [number, number] = [4, 12]
78
+ const default_pick_radius = default_point_radius_range[1]
79
+ const default_size_scale = {
80
+ type: `linear`,
81
+ radius_range: default_point_radius_range,
82
+ pick_radius: default_pick_radius,
83
+ } satisfies BinnedSizeScaleConfig
84
+ const max_placement_bins = 500
85
+ const default_point_color = `#4dabf7`
86
+
87
+ let {
88
+ series,
89
+ x_axis = {},
90
+ y_axis = {},
91
+ size_scale = default_size_scale,
92
+ density: density_config = {},
93
+ overlays: overlays_config = {},
94
+ padding: padding_config = {},
95
+ tooltip,
96
+ point_data,
97
+ point_labels = {},
98
+ selected_point_id = null,
99
+ on_point_click,
100
+ on_density_zoom,
101
+ render_mode = $bindable<RenderMode>(`density`),
102
+ wrapper = $bindable(),
103
+ fullscreen = $bindable(false),
104
+ fullscreen_toggle = true,
105
+ children,
106
+ header_controls,
107
+ ...rest
108
+ }: Omit<HTMLAttributes<HTMLDivElement>, `children`> & {
109
+ series: DensePointSeries<Metadata>[]
110
+ x_axis?: AxisConfig
111
+ y_axis?: AxisConfig
112
+ size_scale?: BinnedSizeScaleConfig
113
+ density?: BinnedDensityConfig
114
+ overlays?: BinnedOverlaysConfig
115
+ padding?: Sides
116
+ tooltip?: Snippet<[BinnedPointTooltipPayload<Metadata, PointData>]>
117
+ point_data?: BinnedPointDataFn<Metadata, PointData>
118
+ point_labels?: BinnedPointLabelsConfig<Metadata, PointData>
119
+ selected_point_id?: string | number | null
120
+ on_point_click?: (payload: ScatterHandlerProps<Metadata> & DensePointEvent) => void
121
+ on_density_zoom?: (payload: DensityZoomEvent) => void
122
+ render_mode?: RenderMode
123
+ wrapper?: HTMLDivElement
124
+ fullscreen?: boolean
125
+ fullscreen_toggle?: boolean
126
+ children?: Snippet<[OverlayContext]>
127
+ header_controls?: Snippet<[OverlayContext]>
128
+ } = $props()
129
+
130
+ let canvas = $state<HTMLCanvasElement>()
131
+ let width = $state(0)
132
+ let height = $state(0)
133
+ let x_range = $state<Vec2>([0, 1])
134
+ let y_range = $state<Vec2>([0, 1])
135
+ let has_user_range = $state(false)
136
+ let drag_start = $state<Point2D | null>(null)
137
+ let drag_current = $state<Point2D | null>(null)
138
+ let suppress_next_click = false
139
+ let hovered_bin = $state<DensityBin | null>(null)
140
+ let hovered_point = $state<DenseInternalPoint<Metadata> | null>(null)
141
+ let tooltip_pos = $state<Point2D>({ x: 0, y: 0 })
142
+ let colorbar_element = $state<HTMLDivElement>()
143
+ let label_measure_root = $state<HTMLDivElement>()
144
+ let label_sizes = new SvelteMap<string, LabelSize>()
145
+ const clip_path_id = `binned-scatter-plot-area-${next_clip_id++}`
146
+
147
+ let pad = $derived(filter_padding(padding_config, default_pad))
148
+ let density_settings = $derived({
149
+ bin_px: density_config.bin_px ?? 2.8,
150
+ color_scale: density_config.color_scale ?? default_density_color_scale,
151
+ color_bar: density_config.color_bar === undefined ? default_density_color_bar : density_config.color_bar,
152
+ auto_point_mode: density_config.auto_point_mode === undefined
153
+ ? default_density_auto_point_mode
154
+ : density_config.auto_point_mode,
155
+ bin_click: density_config.bin_click ?? `zoom`,
156
+ })
157
+ let ref_lines = $derived(overlays_config.ref_lines ?? [])
158
+ let point_labels_settings = $derived({
159
+ font_size: point_labels.font_size ?? `11px`,
160
+ max_count: point_labels.max_count ?? 50,
161
+ gap_px: point_labels.gap_px ?? 3,
162
+ placement: point_labels.placement ?? {},
163
+ leaders: {
164
+ min_length_px: point_labels.leaders?.min_length_px ?? 6,
165
+ },
166
+ render: point_labels.render,
167
+ measure_text: point_labels.measure_text,
168
+ })
169
+
170
+ $effect(() => {
171
+ set_fullscreen_bg(wrapper, fullscreen, `--binned-scatter-fullscreen-bg`)
172
+ })
173
+
174
+ const selected_pulse = create_pulse_animation(
175
+ () => selected_point_id != null && render_mode === `points`,
176
+ { step: 0.035 },
177
+ )
178
+
179
+ const needs_data_range = (range: AxisConfig[`range`] | undefined): boolean =>
180
+ range?.[0] == null || range?.[1] == null
181
+
182
+ let needs_auto_range = $derived(
183
+ needs_data_range(x_axis.range) || needs_data_range(y_axis.range),
184
+ )
185
+ let auto_ranges = $derived(
186
+ needs_auto_range ? series_extents(series) : { x: [0, 1] as Vec2, y: [0, 1] as Vec2 },
187
+ )
188
+ let x_scale_type = $derived(x_axis.scale_type ?? `linear`)
189
+ let y_scale_type = $derived(y_axis.scale_type ?? `linear`)
190
+ let has_plot_size = $derived(width > 0 && height > 0)
191
+
192
+ const axis_range = (axis: AxisConfig, fallback: Vec2): Vec2 => [
193
+ axis.range?.[0] ?? fallback[0],
194
+ axis.range?.[1] ?? fallback[1],
195
+ ]
196
+ const same_range = (a: Vec2, b: Vec2): boolean => a[0] === b[0] && a[1] === b[1]
197
+
198
+ function set_auto_range() {
199
+ const next_x_range = axis_range(x_axis, auto_ranges.x)
200
+ const next_y_range = axis_range(y_axis, auto_ranges.y)
201
+ if (!same_range(x_range, next_x_range)) x_range = next_x_range
202
+ if (!same_range(y_range, next_y_range)) y_range = next_y_range
203
+ }
204
+
205
+ $effect(() => {
206
+ if (has_user_range) return
207
+ set_auto_range()
208
+ })
209
+
210
+ let plot_width = $derived(Math.max(1, width - pad.l - pad.r))
211
+ let plot_height = $derived(Math.max(1, height - pad.t - pad.b))
212
+ let plot_rect = $derived({
213
+ x: pad.l,
214
+ y: pad.t,
215
+ width: plot_width,
216
+ height: plot_height,
217
+ })
218
+ let x_scale_fn = $derived(create_scale(x_scale_type, x_range, [
219
+ pad.l,
220
+ width - pad.r,
221
+ ]))
222
+ let y_scale_fn = $derived(create_scale(y_scale_type, y_range, [
223
+ height - pad.b,
224
+ pad.t,
225
+ ]))
226
+ let x_ticks = $derived(
227
+ generate_ticks(x_range, x_scale_type, x_axis.ticks, x_scale_fn, {
228
+ default_count: 7,
229
+ }),
230
+ )
231
+ let y_ticks = $derived(
232
+ generate_ticks(y_range, y_scale_type, y_axis.ticks, y_scale_fn, {
233
+ default_count: 6,
234
+ }),
235
+ )
236
+ let density_bins = $derived({
237
+ x: Math.max(8, Math.ceil(plot_width / density_settings.bin_px)),
238
+ y: Math.max(8, Math.ceil(plot_height / density_settings.bin_px)),
239
+ })
240
+ let density_result = $derived(
241
+ bin_points(has_plot_size ? series : [], x_range, y_range, density_bins.x, density_bins.y),
242
+ )
243
+ let auto_color_range = $derived<Vec2>([1, Math.max(1, density_result.max_count)])
244
+ let color_scale_fn = $derived(create_color_scale(density_settings.color_scale, auto_color_range))
245
+ let hovered_bin_color = $derived(hovered_bin ? color_scale_fn(hovered_bin.count) : undefined)
246
+ let color_scale_type = $derived(
247
+ typeof density_settings.color_scale === `string` ? undefined : density_settings.color_scale.type,
248
+ )
249
+ let color_bar_props = $derived.by((): ComponentProps<typeof ColorBar> | null => {
250
+ const color_bar = density_settings.color_bar
251
+ if (!color_bar) return null
252
+ return {
253
+ ...color_bar,
254
+ scale_type: color_bar.scale_type ?? color_scale_type,
255
+ title: `${color_bar.title ?? `Density`} (${density_result.visible_count.toLocaleString()} points)`,
256
+ tick_format: color_bar.tick_format ?? `.2~s`,
257
+ tick_labels: color_bar.tick_labels ?? 4,
258
+ tick_side: color_bar.tick_side ?? `primary`,
259
+ bar_style: color_bar.bar_style ??
260
+ `width: ${default_color_bar_size.width}px; height: ${default_color_bar_size.height}px; ${color_bar.style ?? ``}`,
261
+ }
262
+ })
263
+ let density_placement_points = $derived.by(() => {
264
+ const points: Point2D[] = []
265
+ const bin_w = plot_width / density_result.x_bins
266
+ const bin_h = plot_height / density_result.y_bins
267
+ let occupied_count = 0
268
+ for (let idx = 0; idx < density_result.counts.length; idx++) {
269
+ if (density_result.counts[idx]) occupied_count++
270
+ }
271
+ const stride = Math.max(1, Math.ceil(occupied_count / max_placement_bins))
272
+ let occupied_idx = 0
273
+ for (let idx = 0; idx < density_result.counts.length; idx++) {
274
+ if (!density_result.counts[idx]) continue
275
+ if (occupied_idx++ % stride) continue
276
+ const x_bin = idx % density_result.x_bins
277
+ const y_bin = Math.floor(idx / density_result.x_bins)
278
+ points.push({
279
+ x: pad.l + (x_bin + 0.5) * bin_w,
280
+ y: pad.t + (density_result.y_bins - y_bin - 0.5) * bin_h,
281
+ })
282
+ }
283
+ return points
284
+ })
285
+ let color_bar_placement = $derived.by(() => {
286
+ if (
287
+ !color_bar_props ||
288
+ render_mode !== `density` ||
289
+ density_result.max_count <= 0 ||
290
+ !width ||
291
+ !height
292
+ ) {
293
+ return null
294
+ }
295
+
296
+ const is_vertical = color_bar_props?.orientation === `vertical`
297
+ // Fallback sizes (incl. room for tick labels) used before the colorbar first
298
+ // renders; compute_element_placement measures the real footprint once laid out
299
+ const fallback_size = is_vertical
300
+ ? { width: 56, height: 120 }
301
+ : { width: default_color_bar_size.width, height: 50 }
302
+
303
+ return compute_element_placement({
304
+ plot_bounds: plot_rect,
305
+ element: colorbar_element,
306
+ element_size: fallback_size,
307
+ // Small gap from the corner; the full-footprint measurement reserves the tick
308
+ // labels, so this alone keeps the colorbar off the axes
309
+ axis_clearance: 12,
310
+ points: density_placement_points,
311
+ grid_resolution: 12,
312
+ })
313
+ })
314
+
315
+ let auto_render_mode = $derived.by((): RenderMode => {
316
+ const auto_point_mode = density_settings.auto_point_mode
317
+ if (auto_point_mode === false) return render_mode
318
+ return should_render_points(
319
+ density_result.visible_count,
320
+ plot_width * plot_height,
321
+ auto_point_mode.max_points ?? default_density_auto_point_mode.max_points,
322
+ auto_point_mode.max_points_per_px ?? default_density_auto_point_mode.max_points_per_px,
323
+ )
324
+ ? `points`
325
+ : `density`
326
+ })
327
+ let all_size_values = $derived.by(() => {
328
+ const values: number[] = []
329
+ for (const srs of series) {
330
+ if (!srs.size_values) continue
331
+ for (let idx = 0; idx < srs.size_values.length; idx++) {
332
+ const size_value = srs.size_values[idx]
333
+ if (size_value == null || !Number.isFinite(size_value)) continue
334
+ values.push(size_value)
335
+ }
336
+ }
337
+ return values
338
+ })
339
+ let size_scale_fn = $derived(create_size_scale(size_scale, all_size_values))
340
+ let min_point_radius = $derived(size_scale.radius_range?.[0] ?? default_point_radius_range[0])
341
+ let max_point_radius = $derived(size_scale.radius_range?.[1] ?? default_point_radius_range[1])
342
+ let pick_radius_px = $derived(
343
+ size_scale.pick_radius === `auto`
344
+ ? max_point_radius
345
+ : size_scale.pick_radius ?? default_pick_radius,
346
+ )
347
+
348
+ $effect(() => {
349
+ if (!has_plot_size) return
350
+ if (density_settings.auto_point_mode !== false) render_mode = auto_render_mode
351
+ })
352
+ let pick_index = $derived(
353
+ render_mode === `points`
354
+ ? build_pick_index(series, {
355
+ x_range,
356
+ y_range,
357
+ x_scale: x_scale_fn,
358
+ y_scale: y_scale_fn,
359
+ radius_px: pick_radius_px,
360
+ })
361
+ : null,
362
+ )
363
+ let actual_label_placement_config = $derived({
364
+ sa_iterations: 2000,
365
+ max_labels: 300,
366
+ leader_line_threshold: 15,
367
+ candidate_gap: 0,
368
+ ...point_labels_settings.placement,
369
+ })
370
+
371
+ const reset_view = () => {
372
+ has_user_range = false
373
+ set_auto_range()
374
+ }
375
+
376
+ function point_radius_for_value(size_value: number | null | undefined): number {
377
+ if (size_value == null || !Number.isFinite(size_value)) {
378
+ return min_point_radius
379
+ }
380
+ return size_scale_fn(size_value)
381
+ }
382
+
383
+ function resize_canvas() {
384
+ if (!canvas) return
385
+ const dpr = globalThis.devicePixelRatio || 1
386
+ canvas.width = Math.max(1, Math.round(width * dpr))
387
+ canvas.height = Math.max(1, Math.round(height * dpr))
388
+ canvas.style.width = `${width}px`
389
+ canvas.style.height = `${height}px`
390
+ }
391
+
392
+ function draw_density(ctx: CanvasRenderingContext2D) {
393
+ const bin_w = plot_width / density_result.x_bins
394
+ const bin_h = plot_height / density_result.y_bins
395
+ const style_cache = new Map<number, { fill: string; alpha: number }>()
396
+ for (let y_bin = 0; y_bin < density_result.y_bins; y_bin++) {
397
+ for (let x_bin = 0; x_bin < density_result.x_bins; x_bin++) {
398
+ const count = density_result.counts[y_bin * density_result.x_bins + x_bin]
399
+ if (!count) continue
400
+ let style = style_cache.get(count)
401
+ if (!style) {
402
+ style = {
403
+ fill: color_scale_fn(count),
404
+ alpha: Math.min(0.95, 0.2 + Math.log1p(count) / Math.log1p(density_result.max_count)),
405
+ }
406
+ style_cache.set(count, style)
407
+ }
408
+ ctx.fillStyle = style.fill
409
+ ctx.globalAlpha = style.alpha
410
+ ctx.fillRect(
411
+ pad.l + x_bin * bin_w,
412
+ pad.t + (density_result.y_bins - y_bin - 1) * bin_h,
413
+ Math.ceil(bin_w) + 0.5,
414
+ Math.ceil(bin_h) + 0.5,
415
+ )
416
+ }
417
+ }
418
+ ctx.globalAlpha = 1
419
+ }
420
+
421
+ function draw_points(ctx: CanvasRenderingContext2D) {
422
+ const [x_min, x_max] = range_bounds(x_range)
423
+ const [y_min, y_max] = range_bounds(y_range)
424
+ const pulse = selected_pulse.unit
425
+ for (const [series_idx, srs] of series.entries()) {
426
+ ctx.fillStyle = srs.color ?? default_point_color
427
+ const n_points = Math.min(srs.x.length, srs.y.length)
428
+ for (let point_idx = 0; point_idx < n_points; point_idx++) {
429
+ const x = srs.x[point_idx]
430
+ const y = srs.y[point_idx]
431
+ if (!Number.isFinite(x) || !Number.isFinite(y)) continue
432
+ if (x < x_min || x > x_max || y < y_min || y > y_max) continue
433
+ const cx = x_scale_fn(x)
434
+ const cy = y_scale_fn(y)
435
+ const point_id = srs.point_ids?.[point_idx]
436
+ const is_selected = selected_point_id != null && point_id === selected_point_id
437
+ const radius = point_radius_for_value(srs.size_values?.[point_idx])
438
+ const is_hovered =
439
+ hovered_point?.series_idx === series_idx && hovered_point?.point_idx === point_idx
440
+ ctx.globalAlpha = is_selected || is_hovered ? 1 : 0.65
441
+ ctx.beginPath()
442
+ ctx.arc(cx, cy, radius * (is_selected ? 1.08 + 0.08 * pulse : 1), 0, 2 * Math.PI)
443
+ ctx.fill()
444
+ if (is_selected) {
445
+ ctx.globalAlpha = 0.35 + 0.25 * pulse
446
+ ctx.strokeStyle = srs.color ?? default_point_color
447
+ ctx.lineWidth = 1.5 + pulse
448
+ ctx.beginPath()
449
+ ctx.arc(cx, cy, radius * (1.45 + 0.25 * pulse), 0, 2 * Math.PI)
450
+ ctx.stroke()
451
+ }
452
+ }
453
+ }
454
+ ctx.globalAlpha = 1
455
+ }
456
+
457
+ $effect(() => {
458
+ if (!canvas || width <= 0 || height <= 0) return
459
+ resize_canvas()
460
+ const ctx = canvas.getContext(`2d`)
461
+ if (!ctx) return
462
+ const dpr = globalThis.devicePixelRatio || 1
463
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
464
+ ctx.clearRect(0, 0, width, height)
465
+ ctx.save()
466
+ ctx.beginPath()
467
+ ctx.rect(pad.l, pad.t, plot_width, plot_height)
468
+ ctx.clip()
469
+ if (render_mode === `points`) draw_points(ctx)
470
+ else draw_density(ctx)
471
+ ctx.restore()
472
+ })
473
+
474
+ function pointer_coords(event: PointerEvent | MouseEvent): Point2D | null {
475
+ if (!wrapper) return null
476
+ const rect = wrapper.getBoundingClientRect()
477
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top }
478
+ }
479
+
480
+ function pick_at(coords: Point2D | null): DenseInternalPoint<Metadata> | null {
481
+ if (!coords || !pick_index) return null
482
+ return pick_from_index(pick_index, coords)
483
+ }
484
+
485
+ function clear_hover() {
486
+ hovered_bin = null
487
+ hovered_point = null
488
+ }
489
+
490
+ function handler_props(
491
+ point: DenseInternalPoint<Metadata>,
492
+ ): ScatterHandlerProps<Metadata> {
493
+ return {
494
+ x: point.x,
495
+ y: point.y,
496
+ cx: point.cx,
497
+ cy: point.cy,
498
+ metadata: point.metadata,
499
+ label: series[point.series_idx]?.label ?? null,
500
+ series_idx: point.series_idx,
501
+ x_axis,
502
+ y_axis,
503
+ x_formatted: format_value(point.x, x_axis.format ?? `.3~g`),
504
+ y_formatted: format_value(point.y, y_axis.format ?? `.3~g`),
505
+ }
506
+ }
507
+
508
+ function point_color(point: DenseInternalPoint<Metadata>): string {
509
+ return series[point.series_idx]?.color ?? default_point_color
510
+ }
511
+
512
+ function point_label_key(point: DenseInternalPoint<Metadata>): string {
513
+ return `${point.series_idx}-${point.point_idx}`
514
+ }
515
+
516
+ function make_point(series_idx: number, point_idx: number): DenseInternalPoint<Metadata> {
517
+ const srs = series[series_idx]
518
+ const x = srs.x[point_idx]
519
+ const y = srs.y[point_idx]
520
+ return {
521
+ x,
522
+ y,
523
+ cx: x_scale_fn(x),
524
+ cy: y_scale_fn(y),
525
+ series_idx,
526
+ point_idx,
527
+ metadata: get_metadata_at(srs.metadata, point_idx),
528
+ point_id: srs.point_ids?.[point_idx],
529
+ size_value: srs.size_values?.[point_idx],
530
+ }
531
+ }
532
+
533
+ function fallback_label_text(point: DenseInternalPoint<Metadata>): string {
534
+ return String(point.point_id ?? point_label_key(point))
535
+ }
536
+
537
+ function point_payload(
538
+ point: DenseInternalPoint<Metadata>,
539
+ color = point_color(point),
540
+ ): BinnedPointPayload<Metadata, PointData> {
541
+ const base_payload = { ...handler_props(point), point, color }
542
+ return { ...base_payload, point_data: point_data?.(base_payload) ?? undefined }
543
+ }
544
+
545
+ function label_measure_text(payload: BinnedPointPayload<Metadata, PointData>): string {
546
+ return point_labels_settings.measure_text?.(payload) ?? fallback_label_text(payload.point)
547
+ }
548
+
549
+ function label_size_for_payload(
550
+ payload: BinnedPointPayload<Metadata, PointData>,
551
+ ): LabelSize {
552
+ return label_sizes.get(point_label_key(payload.point)) ??
553
+ estimate_label_size(label_measure_text(payload), point_labels_settings.font_size)
554
+ }
555
+
556
+ let point_label_payloads = $derived.by(() => {
557
+ if (!point_labels_settings.render || render_mode !== `points`) return []
558
+
559
+ const [x_min, x_max] = range_bounds(x_range)
560
+ const [y_min, y_max] = range_bounds(y_range)
561
+ const payloads: BinnedPointPayload<Metadata, PointData>[] = []
562
+ for (let series_idx = 0; series_idx < series.length; series_idx++) {
563
+ const srs = series[series_idx]
564
+ const n_points = Math.min(srs.x.length, srs.y.length)
565
+ for (let point_idx = 0; point_idx < n_points; point_idx++) {
566
+ const x = srs.x[point_idx]
567
+ const y = srs.y[point_idx]
568
+ if (
569
+ !Number.isFinite(x) ||
570
+ !Number.isFinite(y) ||
571
+ x < x_min ||
572
+ x > x_max ||
573
+ y < y_min ||
574
+ y > y_max
575
+ ) continue
576
+ payloads.push(point_payload(make_point(series_idx, point_idx)))
577
+ if (payloads.length > point_labels_settings.max_count) return []
578
+ }
579
+ }
580
+ return payloads
581
+ })
582
+
583
+ let point_label_positions = $derived.by(() => {
584
+ if (!point_label_payloads.length) return {}
585
+
586
+ const filtered_data: InternalPoint<Metadata>[] = point_label_payloads.map(
587
+ (payload) => ({
588
+ ...payload.point,
589
+ point_label: {
590
+ text: label_measure_text(payload),
591
+ auto_placement: true,
592
+ font_size: point_labels_settings.font_size,
593
+ size: label_sizes.get(point_label_key(payload.point)),
594
+ },
595
+ point_style: {
596
+ radius: point_radius_for_value(payload.point.size_value) + point_labels_settings.gap_px,
597
+ },
598
+ }),
599
+ )
600
+ const label_series: DataSeries<Metadata>[] = [{ x: [], y: [], filtered_data }]
601
+
602
+ return compute_label_positions(
603
+ label_series,
604
+ actual_label_placement_config,
605
+ { x_scale_fn, y_scale_fn, y2_scale_fn: y_scale_fn, x_axis },
606
+ { width, height, pad },
607
+ )
608
+ })
609
+
610
+ async function measure_point_labels() {
611
+ await tick()
612
+ if (!label_measure_root) return
613
+
614
+ const active_keys = new SvelteSet<string>()
615
+ const measured_elements = label_measure_root.querySelectorAll<HTMLElement>(`[data-label-key]`)
616
+ for (const element of measured_elements) {
617
+ const label_key = element.dataset.labelKey
618
+ if (!label_key) continue
619
+ const { width: label_width, height: label_height } = element.getBoundingClientRect()
620
+ if (label_width <= 0 || label_height <= 0) continue
621
+ active_keys.add(label_key)
622
+ const current_size = label_sizes.get(label_key)
623
+ if (current_size?.width === label_width && current_size.height === label_height) continue
624
+ label_sizes.set(label_key, { width: label_width, height: label_height })
625
+ }
626
+
627
+ for (const label_key of label_sizes.keys()) {
628
+ if (!active_keys.has(label_key)) label_sizes.delete(label_key)
629
+ }
630
+ }
631
+
632
+ $effect(() => {
633
+ if (!label_measure_root || !point_label_payloads.length) return
634
+ void measure_point_labels()
635
+ })
636
+
637
+ function label_leader_line(
638
+ payload: BinnedPointPayload<Metadata, PointData>,
639
+ label_position: Point2D,
640
+ ): { x1: number; y1: number; x2: number; y2: number } | null {
641
+ const displacement = Math.hypot(label_position.x - payload.cx, label_position.y - payload.cy)
642
+ if (displacement <= (actual_label_placement_config.leader_line_threshold ?? 15)) {
643
+ return null
644
+ }
645
+ return label_leader_segment({
646
+ point: { x: payload.cx, y: payload.cy },
647
+ point_radius: point_radius_for_value(payload.point.size_value),
648
+ label_center: label_position,
649
+ label_size: label_size_for_payload(payload),
650
+ min_length: point_labels_settings.leaders.min_length_px,
651
+ })
652
+ }
653
+
654
+ function point_tooltip_props(
655
+ point: DenseInternalPoint<Metadata>,
656
+ ): BinnedPointTooltipPayload<Metadata, PointData> {
657
+ return point_payload(point)
658
+ }
659
+
660
+ function on_pointer_move(event: PointerEvent) {
661
+ const coords = pointer_coords(event)
662
+ if (coords) tooltip_pos = { x: coords.x + 12, y: coords.y + 8 }
663
+
664
+ if (!coords) {
665
+ clear_hover()
666
+ return
667
+ }
668
+
669
+ if (render_mode === `density`) {
670
+ hovered_point = null
671
+ const bin = density_bin_at_point(density_result, coords, plot_rect, x_range, y_range)
672
+ if (
673
+ hovered_bin?.x_bin !== bin?.x_bin ||
674
+ hovered_bin?.y_bin !== bin?.y_bin ||
675
+ hovered_bin?.count !== bin?.count
676
+ ) hovered_bin = bin
677
+ return
678
+ }
679
+
680
+ hovered_bin = null
681
+ const point = pick_at(coords)
682
+ if (
683
+ hovered_point?.series_idx !== point?.series_idx ||
684
+ hovered_point?.point_idx !== point?.point_idx
685
+ ) hovered_point = point
686
+ }
687
+
688
+ function emit_point_click(
689
+ point: DenseInternalPoint<Metadata>,
690
+ event: MouseEvent,
691
+ color = series[point.series_idx]?.color,
692
+ ) {
693
+ on_point_click?.({
694
+ ...point_payload(point, color),
695
+ event,
696
+ })
697
+ }
698
+
699
+ function zoom_to_bin(bin: DensityBin, event: MouseEvent) {
700
+ x_range = bin.x_range
701
+ y_range = bin.y_range
702
+ has_user_range = true
703
+ hovered_bin = null
704
+ on_density_zoom?.({ bin, event })
705
+ }
706
+
707
+ function on_click(event: MouseEvent) {
708
+ if (suppress_next_click) {
709
+ suppress_next_click = false
710
+ return
711
+ }
712
+
713
+ const coords = pointer_coords(event)
714
+ if (!coords) return
715
+
716
+ if (render_mode === `density`) {
717
+ const bin = density_bin_at_point(density_result, coords, plot_rect, x_range, y_range)
718
+ if (!bin) return
719
+ if (density_settings.bin_click === `none`) return
720
+ if (bin.count > 1 && density_settings.bin_click === `zoom`) {
721
+ zoom_to_bin(bin, event)
722
+ return
723
+ }
724
+ if (bin.count > 1 && density_settings.bin_click !== `point`) return
725
+
726
+ const point = first_point_in_bin(
727
+ series,
728
+ density_result,
729
+ bin,
730
+ x_scale_fn,
731
+ y_scale_fn,
732
+ )
733
+ if (point) emit_point_click(point, event, color_scale_fn(bin.count))
734
+ return
735
+ }
736
+
737
+ const point = pick_at(coords)
738
+ if (!point) return
739
+ emit_point_click(point, event)
740
+ }
741
+
742
+ function on_pointer_down(event: PointerEvent) {
743
+ if (event.button !== 0) return
744
+ const coords = pointer_coords(event)
745
+ if (!coords) return
746
+ drag_start = coords
747
+ drag_current = coords
748
+ if (event.currentTarget instanceof HTMLElement) {
749
+ event.currentTarget.setPointerCapture?.(event.pointerId)
750
+ }
751
+ }
752
+
753
+ function on_pointer_drag(event: PointerEvent) {
754
+ if (!drag_start) {
755
+ on_pointer_move(event)
756
+ return
757
+ }
758
+ const coords = pointer_coords(event)
759
+ if (coords) drag_current = coords
760
+ }
761
+
762
+ function on_pointer_up(event: PointerEvent) {
763
+ const start = drag_start
764
+ const end = drag_current
765
+ drag_start = null
766
+ drag_current = null
767
+
768
+ if (start && end && Math.abs(end.x - start.x) > 5 && Math.abs(end.y - start.y) > 5) {
769
+ const x0 = x_scale_fn.invert(start.x)
770
+ const x1 = x_scale_fn.invert(end.x)
771
+ const y0 = y_scale_fn.invert(start.y)
772
+ const y1 = y_scale_fn.invert(end.y)
773
+ x_range = [Math.min(x0, x1), Math.max(x0, x1)]
774
+ y_range = [Math.min(y0, y1), Math.max(y0, y1)]
775
+ has_user_range = true
776
+ suppress_next_click = true
777
+ }
778
+
779
+ if (event.currentTarget instanceof HTMLElement) {
780
+ event.currentTarget.releasePointerCapture?.(event.pointerId)
781
+ }
782
+ }
783
+
784
+ onMount(() => {
785
+ if (!wrapper) return
786
+ const observer = new ResizeObserver(([entry]) => {
787
+ width = Math.round(entry.contentRect.width)
788
+ height = Math.round(entry.contentRect.height)
789
+ })
790
+ observer.observe(wrapper)
791
+ return () => observer.disconnect()
792
+ })
793
+ </script>
794
+
795
+ <svelte:window
796
+ onkeydown={(event) => {
797
+ if (event.key === `Escape` && fullscreen) {
798
+ event.preventDefault()
799
+ fullscreen = false
800
+ }
801
+ }}
802
+ />
803
+
804
+ <div
805
+ {...rest}
806
+ bind:this={wrapper}
807
+ class="binned-scatter {rest.class ?? ``}"
808
+ class:fullscreen
809
+ data-render-mode={render_mode}
810
+ style:--binned-scatter-label-font-size={point_labels_settings.font_size}
811
+ onpointermove={on_pointer_drag}
812
+ onpointerdown={on_pointer_down}
813
+ onpointerup={on_pointer_up}
814
+ onmouseleave={clear_hover}
815
+ onclick={on_click}
816
+ ondblclick={reset_view}
817
+ >
818
+ {#if width && height}
819
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
820
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
821
+ <div
822
+ class="header-controls"
823
+ onpointerdown={(event) => event.stopPropagation()}
824
+ onclick={(event) => event.stopPropagation()}
825
+ ondblclick={(event) => event.stopPropagation()}
826
+ >
827
+ {@render header_controls?.({ height, width, fullscreen })}
828
+ {#if has_user_range}
829
+ <button
830
+ type="button"
831
+ class="reset-view"
832
+ aria-label="Reset view"
833
+ title="Reset view"
834
+ onclick={reset_view}
835
+ >
836
+ <Icon icon="Reset" width="18" height="18" />
837
+ </button>
838
+ {/if}
839
+ {#if fullscreen_toggle}
840
+ <FullscreenToggle bind:fullscreen />
841
+ {/if}
842
+ </div>
843
+ {/if}
844
+
845
+ <canvas bind:this={canvas}></canvas>
846
+
847
+ <svg width={width} height={height} aria-hidden="true">
848
+ <defs>
849
+ <clipPath id={clip_path_id}>
850
+ <rect x={pad.l} y={pad.t} width={plot_width} height={plot_height} />
851
+ </clipPath>
852
+ </defs>
853
+
854
+ <g class="reference-lines" clip-path="url(#{clip_path_id})">
855
+ {#each ref_lines as line}
856
+ <line
857
+ x1={x_scale_fn(line.x1)}
858
+ x2={x_scale_fn(line.x2)}
859
+ y1={y_scale_fn(line.y1)}
860
+ y2={y_scale_fn(line.y2)}
861
+ stroke={line.color ?? `currentColor`}
862
+ stroke-width={line.width ?? 1.5}
863
+ stroke-dasharray={line.dash ?? `5 4`}
864
+ />
865
+ {/each}
866
+ </g>
867
+ <PlotAxis
868
+ side="x"
869
+ ticks={x_ticks}
870
+ place={(tick) => x_scale_fn(tick)}
871
+ axis={x_axis}
872
+ {pad}
873
+ {width}
874
+ {height}
875
+ show_grid
876
+ tick_label={(tick) => format_value(tick, x_axis.format ?? `.2~g`)}
877
+ label_x={pad.l + plot_width / 2}
878
+ label_y={height - 12}
879
+ />
880
+ <PlotAxis
881
+ side="y"
882
+ ticks={y_ticks}
883
+ place={(tick) => y_scale_fn(tick)}
884
+ axis={y_axis}
885
+ {pad}
886
+ {width}
887
+ {height}
888
+ show_grid
889
+ tick_label={(tick) => format_value(tick, y_axis.format ?? `.2~g`)}
890
+ label_x={22}
891
+ label_y={pad.t + plot_height / 2}
892
+ />
893
+
894
+ <ZoomRect start={drag_start} current={drag_current} />
895
+
896
+ {#if point_label_payloads.length}
897
+ <g class="point-label-leaders" clip-path="url(#{clip_path_id})">
898
+ {#each point_label_payloads as payload (point_label_key(payload.point))}
899
+ {@const label_position = point_label_positions[point_label_key(payload.point)]}
900
+ {@const leader_line = label_position ? label_leader_line(payload, label_position) : null}
901
+ {#if leader_line}
902
+ <line
903
+ x1={leader_line.x1}
904
+ y1={leader_line.y1}
905
+ x2={leader_line.x2}
906
+ y2={leader_line.y2}
907
+ />
908
+ {/if}
909
+ {/each}
910
+ </g>
911
+ {/if}
912
+ </svg>
913
+
914
+ {#if point_labels_settings.render && point_label_payloads.length}
915
+ <div bind:this={label_measure_root} class="point-label-measurements" aria-hidden="true">
916
+ {#each point_label_payloads as payload (point_label_key(payload.point))}
917
+ <div
918
+ class="point-label point-label-measure"
919
+ data-label-key={point_label_key(payload.point)}
920
+ >
921
+ {@render point_labels_settings.render(payload)}
922
+ </div>
923
+ {/each}
924
+ </div>
925
+ {/if}
926
+
927
+ {#if point_labels_settings.render && point_label_payloads.length}
928
+ <div class="point-labels">
929
+ {#each point_label_payloads as payload (point_label_key(payload.point))}
930
+ {@const label_position = point_label_positions[point_label_key(payload.point)]}
931
+ {#if label_position}
932
+ <div
933
+ class="point-label"
934
+ style:left={`${label_position.x}px`}
935
+ style:top={`${label_position.y}px`}
936
+ >
937
+ {@render point_labels_settings.render(payload)}
938
+ </div>
939
+ {/if}
940
+ {/each}
941
+ </div>
942
+ {/if}
943
+
944
+ {#if color_bar_props && render_mode === `density` && density_result.max_count > 0 && color_bar_placement}
945
+ <div
946
+ bind:this={colorbar_element}
947
+ class="color-bar"
948
+ style:left={`${color_bar_placement.x}px`}
949
+ style:top={`${color_bar_placement.y}px`}
950
+ >
951
+ <ColorBar
952
+ {...color_bar_props}
953
+ color_scale_fn={color_scale_fn}
954
+ color_scale_domain={auto_color_range}
955
+ range={auto_color_range}
956
+ />
957
+ </div>
958
+ {/if}
959
+
960
+ {#if hovered_bin}
961
+ <PlotTooltip
962
+ x={tooltip_pos.x}
963
+ y={tooltip_pos.y}
964
+ offset={{ x: 0, y: 0 }}
965
+ bg_color={hovered_bin_color}
966
+ >
967
+ {hovered_bin.count.toLocaleString()} samples<br>
968
+ x: {format_value(hovered_bin.x_range[0], x_axis.format ?? `.3~g`)}
969
+ - {format_value(hovered_bin.x_range[1], x_axis.format ?? `.3~g`)}<br>
970
+ y: {format_value(hovered_bin.y_range[0], y_axis.format ?? `.3~g`)}
971
+ - {format_value(hovered_bin.y_range[1], y_axis.format ?? `.3~g`)}
972
+ </PlotTooltip>
973
+ {:else if hovered_point}
974
+ {@const props = point_tooltip_props(hovered_point)}
975
+ <PlotTooltip x={tooltip_pos.x} y={tooltip_pos.y} offset={{ x: 0, y: 0 }}>
976
+ {#if tooltip}
977
+ {@render tooltip(props)}
978
+ {:else}
979
+ {x_axis.label ?? `x`}: {props.x_formatted}<br>
980
+ {y_axis.label ?? `y`}: {props.y_formatted}
981
+ {/if}
982
+ </PlotTooltip>
983
+ {/if}
984
+
985
+ {@render children?.({ height, width, fullscreen })}
986
+ </div>
987
+
988
+ <style>
989
+ .binned-scatter {
990
+ position: relative;
991
+ min-height: 300px;
992
+ color: var(--text-color, CanvasText);
993
+ touch-action: none;
994
+ user-select: none;
995
+ }
996
+ .binned-scatter :global(.axis-label) {
997
+ color: currentColor;
998
+ font-size: 13px;
999
+ font-weight: 600;
1000
+ height: 100%;
1001
+ line-height: 24px;
1002
+ text-align: center;
1003
+ white-space: nowrap;
1004
+ width: 100%;
1005
+ }
1006
+ .binned-scatter.fullscreen {
1007
+ background: var(
1008
+ --binned-scatter-fullscreen-bg,
1009
+ var(--binned-scatter-bg, var(--plot-bg, Canvas))
1010
+ );
1011
+ border-radius: 0;
1012
+ box-sizing: border-box;
1013
+ height: 100vh !important;
1014
+ left: 0;
1015
+ margin: 0;
1016
+ max-height: none !important;
1017
+ overflow: hidden;
1018
+ padding-top: var(--plot-fullscreen-padding-top, 2em);
1019
+ position: fixed;
1020
+ top: 0;
1021
+ width: 100vw !important;
1022
+ z-index: var(--scatter-fullscreen-z-index, var(--z-index-overlay-nav, 100000001));
1023
+ }
1024
+ .header-controls {
1025
+ align-items: center;
1026
+ display: flex;
1027
+ gap: 8px;
1028
+ opacity: 0;
1029
+ position: absolute;
1030
+ right: var(--fullscreen-btn-right, 4px);
1031
+ top: var(--ctrl-btn-top, 5pt);
1032
+ transition: opacity 0.2s, background-color 0.2s;
1033
+ z-index: var(--fullscreen-btn-z-index, 10);
1034
+ }
1035
+ .header-controls :global(.fullscreen-toggle) {
1036
+ opacity: 1;
1037
+ position: static;
1038
+ }
1039
+ .reset-view {
1040
+ align-items: center;
1041
+ background-color: transparent;
1042
+ border-radius: var(--fullscreen-btn-border-radius, var(--border-radius, 3pt));
1043
+ cursor: pointer;
1044
+ display: flex;
1045
+ justify-content: center;
1046
+ padding: var(--fullscreen-btn-padding, 2pt);
1047
+ }
1048
+ .reset-view:hover,
1049
+ .reset-view:focus {
1050
+ background-color: color-mix(in srgb, currentColor 8%, transparent);
1051
+ }
1052
+ .binned-scatter:hover .header-controls,
1053
+ .binned-scatter .header-controls:focus-within {
1054
+ opacity: 1;
1055
+ }
1056
+ canvas,
1057
+ svg {
1058
+ inset: 0;
1059
+ position: absolute;
1060
+ }
1061
+ canvas {
1062
+ background: transparent;
1063
+ }
1064
+ svg {
1065
+ overflow: visible;
1066
+ pointer-events: none;
1067
+ }
1068
+ .reference-lines line {
1069
+ opacity: 0.75;
1070
+ }
1071
+ .point-label-leaders line {
1072
+ stroke: var(--binned-scatter-label-leader-color, color-mix(in srgb, currentColor 60%, transparent));
1073
+ stroke-dasharray: var(--binned-scatter-label-leader-dash, 2 2);
1074
+ stroke-width: var(--binned-scatter-label-leader-width, 0.8);
1075
+ }
1076
+ .point-labels {
1077
+ inset: 0;
1078
+ pointer-events: none;
1079
+ position: absolute;
1080
+ z-index: 1;
1081
+ }
1082
+ .point-label-measurements {
1083
+ contain: layout style;
1084
+ inset: 0;
1085
+ pointer-events: none;
1086
+ position: absolute;
1087
+ visibility: hidden;
1088
+ z-index: -1;
1089
+ }
1090
+ .point-label {
1091
+ background: var(--binned-scatter-label-bg, color-mix(in srgb, Canvas 84%, transparent));
1092
+ border: 0 !important;
1093
+ border-radius: var(--binned-scatter-label-radius, 3px);
1094
+ box-shadow: none;
1095
+ color: var(--binned-scatter-label-color, currentColor);
1096
+ font-size: var(--binned-scatter-label-font-size, 11px);
1097
+ line-height: 1.2;
1098
+ outline: 0;
1099
+ padding: var(--binned-scatter-label-padding, 1px 3px);
1100
+ position: absolute;
1101
+ text-align: center;
1102
+ transform: translate(-50%, -50%);
1103
+ white-space: nowrap;
1104
+ }
1105
+ .point-label-measure {
1106
+ left: 0;
1107
+ top: 0;
1108
+ transform: none;
1109
+ }
1110
+ .color-bar {
1111
+ pointer-events: auto;
1112
+ position: absolute;
1113
+ }
1114
+ </style>