matterviz 0.3.1 → 0.3.3
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.
- package/dist/EmptyState.svelte +10 -2
- package/dist/FilePicker.svelte +154 -96
- package/dist/Icon.svelte +20 -14
- package/dist/MillerIndexInput.svelte +27 -21
- package/dist/api/optimade.js +6 -6
- package/dist/app.css +216 -178
- package/dist/brillouin/BrillouinZone.svelte +299 -198
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
- package/dist/brillouin/BrillouinZoneExportPane.svelte +74 -55
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
- package/dist/brillouin/BrillouinZoneScene.svelte +277 -165
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
- package/dist/brillouin/compute.js +11 -6
- package/dist/chempot-diagram/ChemPotDiagram.svelte +327 -0
- package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +847 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +3194 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte +11 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
- package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
- package/dist/chempot-diagram/async-compute.svelte.js +77 -0
- package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
- package/dist/chempot-diagram/chempot-worker.js +11 -0
- package/dist/chempot-diagram/color.d.ts +10 -0
- package/dist/chempot-diagram/color.js +32 -0
- package/dist/chempot-diagram/compute.d.ts +48 -0
- package/dist/chempot-diagram/compute.js +812 -0
- package/dist/chempot-diagram/index.d.ts +6 -0
- package/dist/chempot-diagram/index.js +6 -0
- package/dist/chempot-diagram/pointer.d.ts +16 -0
- package/dist/chempot-diagram/pointer.js +40 -0
- package/dist/chempot-diagram/temperature.d.ts +15 -0
- package/dist/chempot-diagram/temperature.js +36 -0
- package/dist/chempot-diagram/types.d.ts +86 -0
- package/dist/chempot-diagram/types.js +28 -0
- package/dist/colors/index.d.ts +3 -1
- package/dist/colors/index.js +9 -3
- package/dist/composition/BarChart.svelte +141 -77
- package/dist/composition/BubbleChart.svelte +107 -52
- package/dist/composition/Composition.svelte +100 -79
- package/dist/composition/Formula.svelte +108 -62
- package/dist/composition/FormulaFilter.svelte +973 -353
- package/dist/composition/FormulaFilter.svelte.d.ts +35 -1
- package/dist/composition/PieChart.svelte +199 -99
- package/dist/composition/PieChart.svelte.d.ts +1 -1
- package/dist/composition/format.d.ts +5 -0
- package/dist/composition/format.js +20 -3
- package/dist/composition/parse.js +14 -9
- package/dist/convex-hull/ConvexHull.svelte +93 -38
- package/dist/convex-hull/ConvexHull2D.svelte +551 -393
- package/dist/convex-hull/ConvexHull3D.svelte +1303 -825
- package/dist/convex-hull/ConvexHull4D.svelte +1012 -686
- package/dist/convex-hull/ConvexHullControls.svelte +115 -28
- package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
- package/dist/convex-hull/ConvexHullStats.svelte +821 -249
- package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
- package/dist/convex-hull/ConvexHullTooltip.svelte +41 -16
- package/dist/convex-hull/GasPressureControls.svelte +104 -61
- package/dist/convex-hull/StructurePopup.svelte +25 -4
- package/dist/convex-hull/TemperatureSlider.svelte +45 -25
- package/dist/convex-hull/barycentric-coords.js +13 -7
- package/dist/convex-hull/demo-temperature.d.ts +6 -0
- package/dist/convex-hull/demo-temperature.js +40 -0
- package/dist/convex-hull/gas-thermodynamics.js +17 -12
- package/dist/convex-hull/helpers.d.ts +10 -1
- package/dist/convex-hull/helpers.js +79 -38
- package/dist/convex-hull/index.d.ts +1 -0
- package/dist/convex-hull/index.js +1 -0
- package/dist/convex-hull/thermodynamics.d.ts +8 -21
- package/dist/convex-hull/thermodynamics.js +163 -69
- package/dist/convex-hull/types.d.ts +12 -12
- package/dist/convex-hull/types.js +0 -12
- package/dist/coordination/CoordinationBarPlot.svelte +232 -176
- package/dist/element/BohrAtom.svelte +56 -13
- package/dist/element/ElementHeading.svelte +7 -2
- package/dist/element/ElementPhoto.svelte +15 -9
- package/dist/element/ElementStats.svelte +10 -4
- package/dist/element/ElementTile.svelte +137 -73
- package/dist/element/Nucleus.svelte +39 -11
- package/dist/element/data.js +2 -14
- package/dist/element/data.json.gz +0 -0
- package/dist/element/types.d.ts +1 -0
- package/dist/feedback/ClickFeedback.svelte +16 -5
- package/dist/feedback/DragOverlay.svelte +10 -2
- package/dist/feedback/Spinner.svelte +4 -2
- package/dist/feedback/StatusMessage.svelte +8 -2
- package/dist/fermi-surface/FermiSlice.svelte +118 -88
- package/dist/fermi-surface/FermiSurface.svelte +336 -239
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
- package/dist/fermi-surface/FermiSurfaceScene.svelte +536 -343
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
- package/dist/fermi-surface/compute.js +16 -20
- package/dist/fermi-surface/parse.js +37 -33
- package/dist/fermi-surface/symmetry.js +2 -7
- package/dist/fermi-surface/types.d.ts +3 -5
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1527 -0
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +225 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +30 -0
- package/dist/heatmap-matrix/index.d.ts +53 -0
- package/dist/heatmap-matrix/index.js +100 -0
- package/dist/heatmap-matrix/shared.d.ts +2 -0
- package/dist/heatmap-matrix/shared.js +4 -0
- package/dist/icons.d.ts +111 -0
- package/dist/icons.js +158 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/io/decompress.js +1 -1
- package/dist/io/export.d.ts +3 -0
- package/dist/io/export.js +138 -140
- package/dist/io/file-drop.d.ts +7 -0
- package/dist/io/file-drop.js +43 -0
- package/dist/io/index.d.ts +2 -2
- package/dist/io/index.js +2 -112
- package/dist/io/is-binary.js +2 -3
- package/dist/io/types.d.ts +1 -0
- package/dist/io/url-drop.d.ts +2 -0
- package/dist/io/url-drop.js +117 -0
- package/dist/isosurface/Isosurface.svelte +220 -110
- package/dist/isosurface/IsosurfaceControls.svelte +65 -28
- package/dist/isosurface/parse.js +104 -56
- package/dist/isosurface/slice.d.ts +2 -1
- package/dist/isosurface/slice.js +8 -13
- package/dist/isosurface/types.d.ts +14 -1
- package/dist/isosurface/types.js +152 -5
- package/dist/labels.d.ts +2 -1
- package/dist/labels.js +12 -8
- package/dist/layout/FullscreenToggle.svelte +11 -2
- package/dist/layout/InfoCard.svelte +38 -6
- package/dist/layout/InfoTag.svelte +125 -94
- package/dist/layout/PropertyFilter.svelte +82 -37
- package/dist/layout/SettingsSection.svelte +85 -55
- package/dist/layout/SubpageGrid.svelte +82 -0
- package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.js +1 -0
- package/dist/layout/json-tree/JsonNode.svelte +266 -223
- package/dist/layout/json-tree/JsonTree.svelte +516 -429
- package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
- package/dist/layout/json-tree/JsonValue.svelte +281 -173
- package/dist/layout/json-tree/types.d.ts +10 -2
- package/dist/layout/json-tree/utils.d.ts +2 -0
- package/dist/layout/json-tree/utils.js +37 -2
- package/dist/marching-cubes.js +25 -2
- package/dist/math.d.ts +20 -17
- package/dist/math.js +474 -57
- package/dist/overlays/ContextMenu.svelte +66 -40
- package/dist/overlays/DraggablePane.svelte +331 -154
- package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
- package/dist/periodic-table/PeriodicTable.svelte +278 -145
- package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
- package/dist/periodic-table/PropertySelect.svelte +25 -7
- package/dist/periodic-table/TableInset.svelte +8 -3
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +559 -267
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
- package/dist/phase-diagram/PhaseDiagramControls.svelte +131 -51
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +126 -0
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +160 -110
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +8 -1
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +217 -86
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
- package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
- package/dist/phase-diagram/build-diagram.js +9 -9
- package/dist/phase-diagram/colors.js +1 -3
- package/dist/phase-diagram/index.d.ts +2 -0
- package/dist/phase-diagram/index.js +2 -0
- package/dist/phase-diagram/parse.js +10 -9
- package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
- package/dist/phase-diagram/svg-to-diagram.js +869 -0
- package/dist/phase-diagram/types.d.ts +10 -0
- package/dist/phase-diagram/utils.d.ts +8 -4
- package/dist/phase-diagram/utils.js +219 -74
- package/dist/plot/AxisLabel.svelte +51 -0
- package/dist/plot/AxisLabel.svelte.d.ts +16 -0
- package/dist/plot/BarPlot.svelte +1461 -768
- package/dist/plot/BarPlot.svelte.d.ts +3 -3
- package/dist/plot/BarPlotControls.svelte +33 -6
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +533 -383
- package/dist/plot/ColorBar.svelte.d.ts +1 -1
- package/dist/plot/ColorScaleSelect.svelte +28 -7
- package/dist/plot/ElementScatter.svelte +38 -16
- package/dist/plot/FillArea.svelte +152 -92
- package/dist/plot/Histogram.svelte +1162 -709
- package/dist/plot/Histogram.svelte.d.ts +1 -1
- package/dist/plot/HistogramControls.svelte +81 -18
- package/dist/plot/HistogramControls.svelte.d.ts +6 -2
- package/dist/plot/InteractiveAxisLabel.svelte +34 -11
- package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
- package/dist/plot/Line.svelte +63 -28
- package/dist/plot/PlotControls.svelte +221 -96
- package/dist/plot/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/PlotLegend.svelte +174 -91
- package/dist/plot/PlotTooltip.svelte +45 -6
- package/dist/plot/PortalSelect.svelte +175 -146
- package/dist/plot/ReferenceLine.svelte +77 -22
- package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
- package/dist/plot/ReferenceLine3D.svelte +132 -107
- package/dist/plot/ReferencePlane.svelte +146 -123
- package/dist/plot/ScatterPlot.svelte +1880 -1156
- package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
- package/dist/plot/ScatterPlot3D.svelte +256 -131
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +300 -297
- package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
- package/dist/plot/ScatterPlot3DScene.svelte +608 -406
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +150 -70
- package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ScatterPoint.svelte +98 -26
- package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
- package/dist/plot/SpacegroupBarPlot.svelte +142 -85
- package/dist/plot/Surface3D.svelte +159 -108
- package/dist/plot/ZeroLines.svelte +96 -0
- package/dist/plot/ZeroLines.svelte.d.ts +32 -0
- package/dist/plot/ZoomRect.svelte +23 -0
- package/dist/plot/ZoomRect.svelte.d.ts +8 -0
- package/dist/plot/axis-utils.d.ts +1 -1
- package/dist/plot/axis-utils.js +1 -3
- package/dist/plot/data-cleaning.js +12 -28
- package/dist/plot/data-transform.js +2 -1
- package/dist/plot/fill-utils.js +2 -0
- package/dist/plot/index.d.ts +6 -2
- package/dist/plot/index.js +6 -2
- package/dist/plot/interactions.d.ts +8 -10
- package/dist/plot/interactions.js +2 -3
- package/dist/plot/layout.d.ts +11 -2
- package/dist/plot/layout.js +44 -17
- package/dist/plot/reference-line.d.ts +5 -22
- package/dist/plot/reference-line.js +12 -84
- package/dist/plot/scales.js +24 -36
- package/dist/plot/types.d.ts +53 -40
- package/dist/plot/types.js +12 -7
- package/dist/plot/utils/label-placement.d.ts +32 -15
- package/dist/plot/utils/label-placement.js +227 -63
- package/dist/plot/utils/series-visibility.js +2 -3
- package/dist/plot/utils.d.ts +1 -0
- package/dist/plot/utils.js +14 -0
- package/dist/rdf/RdfPlot.svelte +173 -132
- package/dist/rdf/calc-rdf.js +4 -5
- package/dist/sanitize.d.ts +4 -0
- package/dist/sanitize.js +107 -0
- package/dist/settings.d.ts +21 -6
- package/dist/settings.js +63 -19
- package/dist/spectral/Bands.svelte +963 -412
- package/dist/spectral/Bands.svelte.d.ts +22 -2
- package/dist/spectral/BandsAndDos.svelte +90 -49
- package/dist/spectral/BrillouinBandsDos.svelte +151 -93
- package/dist/spectral/Dos.svelte +389 -258
- package/dist/spectral/helpers.d.ts +23 -1
- package/dist/spectral/helpers.js +119 -51
- package/dist/spectral/types.d.ts +2 -0
- package/dist/state.svelte.d.ts +1 -1
- package/dist/state.svelte.js +3 -2
- package/dist/structure/Arrow.svelte +59 -20
- package/dist/structure/AtomLegend.svelte +231 -129
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/Bond.svelte +73 -47
- package/dist/structure/CanvasTooltip.svelte +10 -2
- package/dist/structure/CellSelect.svelte +148 -51
- package/dist/structure/Cylinder.svelte +33 -17
- package/dist/structure/Lattice.svelte +88 -33
- package/dist/structure/Structure.svelte +1077 -821
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureControls.svelte +373 -139
- package/dist/structure/StructureControls.svelte.d.ts +1 -1
- package/dist/structure/StructureExportPane.svelte +124 -89
- package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +304 -231
- package/dist/structure/StructureScene.svelte +919 -445
- package/dist/structure/StructureScene.svelte.d.ts +16 -7
- package/dist/structure/atom-properties.d.ts +6 -2
- package/dist/structure/atom-properties.js +42 -29
- package/dist/structure/bonding.js +6 -7
- package/dist/structure/export.js +22 -34
- package/dist/structure/ferrox-wasm-types.d.ts +3 -2
- package/dist/structure/ferrox-wasm-types.js +0 -3
- package/dist/structure/ferrox-wasm.d.ts +3 -2
- package/dist/structure/ferrox-wasm.js +2 -3
- package/dist/structure/index.d.ts +16 -0
- package/dist/structure/index.js +88 -6
- package/dist/structure/measure.d.ts +2 -2
- package/dist/structure/measure.js +4 -44
- package/dist/structure/parse.js +130 -155
- package/dist/structure/partial-occupancy.d.ts +25 -0
- package/dist/structure/partial-occupancy.js +99 -0
- package/dist/structure/pbc.d.ts +1 -0
- package/dist/structure/pbc.js +16 -6
- package/dist/structure/supercell.d.ts +2 -2
- package/dist/structure/supercell.js +12 -22
- package/dist/structure/validation.js +5 -3
- package/dist/symmetry/SymmetryStats.svelte +94 -37
- package/dist/symmetry/WyckoffTable.svelte +42 -14
- package/dist/symmetry/cell-transform.js +5 -3
- package/dist/symmetry/index.d.ts +7 -4
- package/dist/symmetry/index.js +87 -21
- package/dist/symmetry/spacegroups.js +148 -148
- package/dist/table/HeatmapTable.svelte +1112 -516
- package/dist/table/HeatmapTable.svelte.d.ts +12 -1
- package/dist/table/ToggleMenu.svelte +125 -90
- package/dist/table/index.d.ts +2 -0
- package/dist/table/index.js +2 -4
- package/dist/theme/ThemeControl.svelte +21 -12
- package/dist/time.js +4 -1
- package/dist/tooltip/TooltipContent.svelte +33 -8
- package/dist/trajectory/Trajectory.svelte +889 -687
- package/dist/trajectory/TrajectoryError.svelte +14 -3
- package/dist/trajectory/TrajectoryExportPane.svelte +148 -90
- package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
- package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
- package/dist/trajectory/constants.d.ts +6 -0
- package/dist/trajectory/constants.js +7 -0
- package/dist/trajectory/extract.js +13 -31
- package/dist/trajectory/format-detect.d.ts +9 -0
- package/dist/trajectory/format-detect.js +76 -0
- package/dist/trajectory/frame-reader.d.ts +17 -0
- package/dist/trajectory/frame-reader.js +332 -0
- package/dist/trajectory/helpers.d.ts +14 -0
- package/dist/trajectory/helpers.js +172 -0
- package/dist/trajectory/index.d.ts +1 -0
- package/dist/trajectory/index.js +23 -14
- package/dist/trajectory/parse/ase.d.ts +2 -0
- package/dist/trajectory/parse/ase.js +77 -0
- package/dist/trajectory/parse/hdf5.d.ts +2 -0
- package/dist/trajectory/parse/hdf5.js +129 -0
- package/dist/trajectory/parse/index.d.ts +12 -0
- package/dist/trajectory/parse/index.js +299 -0
- package/dist/trajectory/parse/lammps.d.ts +5 -0
- package/dist/trajectory/parse/lammps.js +179 -0
- package/dist/trajectory/parse/vasp.d.ts +2 -0
- package/dist/trajectory/parse/vasp.js +68 -0
- package/dist/trajectory/parse/xyz.d.ts +2 -0
- package/dist/trajectory/parse/xyz.js +110 -0
- package/dist/trajectory/plotting.js +13 -8
- package/dist/trajectory/types.d.ts +11 -0
- package/dist/trajectory/types.js +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +17 -0
- package/dist/xrd/XrdPlot.svelte +337 -245
- package/dist/xrd/broadening.js +14 -9
- package/dist/xrd/calc-xrd.js +12 -19
- package/dist/xrd/parse.d.ts +1 -1
- package/dist/xrd/parse.js +17 -17
- package/package.json +103 -101
- package/readme.md +4 -4
- package/dist/trajectory/parse.d.ts +0 -42
- package/dist/trajectory/parse.js +0 -1267
- /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
- /package/dist/theme/{themes.js → themes.mjs} +0 -0
|
@@ -1,556 +1,880 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { luminance, watch_dark_mode } from '../colors'
|
|
3
|
+
import Icon from '../Icon.svelte'
|
|
4
|
+
import { format_num } from '../labels'
|
|
5
|
+
import { SettingsSection } from '../layout'
|
|
6
|
+
import ContextMenu from '../overlays/ContextMenu.svelte'
|
|
7
|
+
import DraggablePane from '../overlays/DraggablePane.svelte'
|
|
8
|
+
import type {
|
|
9
|
+
CellSnippet,
|
|
10
|
+
CellVal,
|
|
11
|
+
ExportData,
|
|
12
|
+
InitialSort,
|
|
13
|
+
Label,
|
|
14
|
+
MultiSortState,
|
|
15
|
+
Pagination,
|
|
16
|
+
RowData,
|
|
17
|
+
Search,
|
|
18
|
+
SortHint,
|
|
19
|
+
SortState,
|
|
20
|
+
SpecialCells,
|
|
21
|
+
} from './'
|
|
22
|
+
import { calc_cell_color, strip_html } from './'
|
|
23
|
+
import { sanitize_html } from '../sanitize'
|
|
24
|
+
import { normalize_unicode_minus } from '../utils'
|
|
25
|
+
import type { Snippet } from 'svelte'
|
|
26
|
+
import { tooltip } from 'svelte-multiselect/attachments'
|
|
27
|
+
import { flip } from 'svelte/animate'
|
|
28
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
29
|
+
import { SvelteMap } from 'svelte/reactivity'
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
data = $bindable([]),
|
|
33
|
+
columns = [],
|
|
34
|
+
sort_hint = undefined,
|
|
35
|
+
cell,
|
|
36
|
+
special_cells,
|
|
37
|
+
controls,
|
|
38
|
+
initial_sort = undefined,
|
|
39
|
+
sort = $bindable({ column: ``, dir: `asc` }), // allows external control/sync of sorting
|
|
40
|
+
fixed_header = false,
|
|
41
|
+
default_num_format = `.3`,
|
|
42
|
+
show_heatmap = $bindable(true),
|
|
43
|
+
heatmap_class = `heatmap`,
|
|
44
|
+
onrowclick,
|
|
45
|
+
onrowdblclick,
|
|
46
|
+
column_order = $bindable([]),
|
|
47
|
+
export_data = false,
|
|
48
|
+
show_column_toggle = false,
|
|
49
|
+
search = false,
|
|
50
|
+
show_row_select = false,
|
|
51
|
+
pagination = false,
|
|
52
|
+
selected_rows = $bindable([]),
|
|
53
|
+
hidden_columns = $bindable([]),
|
|
54
|
+
scroll_style,
|
|
55
|
+
root_style,
|
|
56
|
+
onsort = undefined,
|
|
57
|
+
onsorterror = undefined,
|
|
58
|
+
loading = $bindable(false),
|
|
59
|
+
sort_data = true,
|
|
60
|
+
heatmap_opacity = $bindable(1),
|
|
61
|
+
empty_message = `No data`,
|
|
62
|
+
show_row_numbers = false,
|
|
63
|
+
allow_better_toggle = false,
|
|
64
|
+
show_controls = $bindable(false),
|
|
65
|
+
controls_open = $bindable(false),
|
|
66
|
+
header_cell,
|
|
67
|
+
footer,
|
|
68
|
+
...rest
|
|
69
|
+
}: HTMLAttributes<HTMLDivElement> & {
|
|
70
|
+
data: RowData[]
|
|
71
|
+
columns?: Label[]
|
|
72
|
+
sort_hint?: SortHint
|
|
73
|
+
cell?: CellSnippet
|
|
74
|
+
special_cells?: SpecialCells
|
|
75
|
+
controls?: Snippet
|
|
76
|
+
initial_sort?: InitialSort
|
|
77
|
+
sort?: { column: string; dir: `asc` | `desc` }
|
|
78
|
+
fixed_header?: boolean
|
|
79
|
+
default_num_format?: string
|
|
80
|
+
show_heatmap?: boolean
|
|
81
|
+
heatmap_class?: string
|
|
82
|
+
onrowclick?: (event: MouseEvent | KeyboardEvent, row: RowData) => void
|
|
83
|
+
onrowdblclick?: (event: MouseEvent, row: RowData) => void
|
|
84
|
+
// Array of column IDs to control display order. IDs are derived as:
|
|
85
|
+
// - Ungrouped columns: col.key ?? col.label
|
|
86
|
+
// - Grouped columns: `${col.key ?? col.label} (${col.group})`
|
|
87
|
+
// This allows persisting/restoring column order across sessions.
|
|
88
|
+
column_order?: string[]
|
|
89
|
+
export_data?: ExportData
|
|
90
|
+
show_column_toggle?: boolean
|
|
91
|
+
search?: Search
|
|
92
|
+
show_row_select?: boolean
|
|
93
|
+
pagination?: Pagination
|
|
94
|
+
selected_rows?: RowData[]
|
|
95
|
+
hidden_columns?: string[]
|
|
96
|
+
scroll_style?: string
|
|
97
|
+
// Inline styles for the root table container (merged with rest.style). Use instead of global CSS overrides.
|
|
98
|
+
root_style?: string
|
|
99
|
+
// Async callback for server-side sorting. When provided, client-side sorting is skipped
|
|
100
|
+
// and the callback is called with (column_id, direction) to fetch new data from server.
|
|
101
|
+
onsort?: (column: string, dir: `asc` | `desc`) => Promise<RowData[]>
|
|
102
|
+
// Callback when onsort fails, receives the error for parent handling (e.g. toast notification)
|
|
103
|
+
onsorterror?: (error: unknown, column: string, dir: `asc` | `desc`) => void
|
|
104
|
+
// Loading state during async sort operations
|
|
105
|
+
loading?: boolean
|
|
106
|
+
// Whether to sort data client-side. Set to false when parent handles sorting externally.
|
|
107
|
+
// When onsort is provided, sort_data behavior is implicitly false.
|
|
108
|
+
sort_data?: boolean
|
|
109
|
+
// Heatmap cell background opacity (0–1). Controls both the visual fade via CSS
|
|
110
|
+
// color-mix() and the JS text contrast correction. Default 1 (fully opaque).
|
|
111
|
+
heatmap_opacity?: number
|
|
112
|
+
// Message shown when the table has no data rows. Set to empty string to hide.
|
|
113
|
+
empty_message?: string
|
|
114
|
+
// Show a row number column as the first column
|
|
115
|
+
show_row_numbers?: boolean
|
|
116
|
+
// When true, show a toggle in colored column headers to cycle gradient direction
|
|
117
|
+
allow_better_toggle?: boolean
|
|
118
|
+
// Whether the gear icon for the controls pane is visible
|
|
119
|
+
show_controls?: boolean
|
|
120
|
+
// Whether the controls pane is expanded
|
|
121
|
+
controls_open?: boolean
|
|
122
|
+
// Custom snippet for rendering header cells. Falls back to {@html col.label}.
|
|
123
|
+
header_cell?: Snippet<[{ col: Label }]>
|
|
124
|
+
// Footer snippet rendered inside <tfoot> below the table body
|
|
125
|
+
footer?: Snippet
|
|
126
|
+
} = $props()
|
|
127
|
+
|
|
128
|
+
let container_el = $state<HTMLDivElement>()
|
|
129
|
+
|
|
130
|
+
// Read --page-bg from computed style for text contrast calculation.
|
|
131
|
+
// Recalculates on mount and when the theme changes (dark/light mode toggle).
|
|
132
|
+
let page_bg_lum = $state(luminance(`white`))
|
|
133
|
+
$effect(() => {
|
|
134
|
+
if (!container_el) return
|
|
17
135
|
const read_page_bg = () => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
136
|
+
if (!container_el) return
|
|
137
|
+
const page_bg = getComputedStyle(container_el).getPropertyValue(`--page-bg`)
|
|
138
|
+
.trim()
|
|
139
|
+
page_bg_lum = luminance(page_bg || `white`)
|
|
140
|
+
}
|
|
141
|
+
read_page_bg()
|
|
142
|
+
return watch_dark_mode(read_page_bg)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Detect HTML to prevent setting raw HTML as data-sort-value. Simple string matching
|
|
146
|
+
// suffices since false positives just skip setting the attr (sorting still works by inner data-sort-value).
|
|
147
|
+
function is_html_str(val: unknown): boolean {
|
|
148
|
+
if (typeof val !== `string`) return false
|
|
149
|
+
return (
|
|
150
|
+
(val.includes(`<`) && val.includes(`>`)) || // Has angle brackets
|
|
151
|
+
val.startsWith(`<`) || // Has HTML entity for <
|
|
152
|
+
val.includes(`href=`) || // Has href attribute
|
|
153
|
+
val.includes(`class=`) // Has class attribute
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Normalize initial_sort config
|
|
158
|
+
let initial_sort_config = $derived(
|
|
159
|
+
initial_sort
|
|
160
|
+
? typeof initial_sort === `string`
|
|
161
|
+
? { column: initial_sort, direction: `asc` as const }
|
|
162
|
+
: { direction: `asc` as const, ...initial_sort }
|
|
163
|
+
: null,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
// Normalize pagination config
|
|
167
|
+
let pagination_config = $derived(
|
|
168
|
+
pagination
|
|
169
|
+
? { page_size: 25, ...(typeof pagination === `object` ? pagination : {}) }
|
|
170
|
+
: null,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
// Mutable page size — writable $derived allows user to change via dropdown
|
|
174
|
+
let effective_page_size = $derived(pagination_config?.page_size ?? 25)
|
|
175
|
+
|
|
176
|
+
// Normalize search config
|
|
177
|
+
let search_config = $derived(
|
|
178
|
+
search
|
|
179
|
+
? {
|
|
49
180
|
placeholder: `Filter...`,
|
|
50
181
|
expanded: false,
|
|
51
182
|
...(typeof search === `object` ? search : {}),
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
183
|
+
}
|
|
184
|
+
: null,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
// Normalize export_data config
|
|
188
|
+
type ExportFormat = `csv` | `json`
|
|
189
|
+
const default_formats: ExportFormat[] = [`csv`, `json`]
|
|
190
|
+
let export_config = $derived(
|
|
191
|
+
export_data
|
|
192
|
+
? {
|
|
57
193
|
formats: default_formats,
|
|
58
194
|
filename: `table-export`,
|
|
59
195
|
...(typeof export_data === `object` ? export_data : {}),
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
196
|
+
}
|
|
197
|
+
: null,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
// Derive sort_state from bindable prop, falling back to initial_sort if sort not yet set
|
|
201
|
+
// This ensures immediate sorting on first render without waiting for effects
|
|
202
|
+
let sort_state = $derived<SortState>({
|
|
65
203
|
column: sort.column || initial_sort_config?.column || ``,
|
|
66
204
|
ascending: sort.column
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
let
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let
|
|
83
|
-
let
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
205
|
+
? sort.dir !== `desc`
|
|
206
|
+
: initial_sort_config?.direction !== `desc`,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// Multi-column sort state (for Shift+click)
|
|
210
|
+
let multi_sort = $state<MultiSortState>([])
|
|
211
|
+
|
|
212
|
+
// Search/filter state
|
|
213
|
+
let search_query = $state(``)
|
|
214
|
+
let search_expanded = $derived(search_config?.expanded ?? false)
|
|
215
|
+
|
|
216
|
+
// Pagination state
|
|
217
|
+
let current_page = $state(1)
|
|
218
|
+
|
|
219
|
+
// Dropdown states
|
|
220
|
+
let show_column_dropdown = $state(false)
|
|
221
|
+
let show_export_dropdown = $state(false)
|
|
222
|
+
|
|
223
|
+
// Per-column gradient direction overrides (user-toggled via header)
|
|
224
|
+
let better_overrides = new SvelteMap<string, `higher` | `lower`>()
|
|
225
|
+
|
|
226
|
+
// Per-column color scale overrides
|
|
227
|
+
let color_scale_overrides = new SvelteMap<string, string>()
|
|
228
|
+
|
|
229
|
+
const color_scale_options = [
|
|
230
|
+
`interpolateViridis`,
|
|
231
|
+
`interpolatePlasma`,
|
|
232
|
+
`interpolateInferno`,
|
|
233
|
+
`interpolateCividis`,
|
|
234
|
+
`interpolateTurbo`,
|
|
235
|
+
`interpolateBlues`,
|
|
236
|
+
`interpolateGreens`,
|
|
237
|
+
`interpolateReds`,
|
|
238
|
+
`interpolateYlOrRd`,
|
|
239
|
+
] as const
|
|
240
|
+
|
|
241
|
+
// Columns that have a color gradient
|
|
242
|
+
let colored_columns = $derived(
|
|
243
|
+
columns.filter((col) =>
|
|
244
|
+
col.color_scale !== null && col.color_scale !== undefined
|
|
245
|
+
),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
// Column resize state
|
|
249
|
+
let resize_col_id = $state<string | null>(null)
|
|
250
|
+
let resize_start_x = $state(0)
|
|
251
|
+
let resize_start_width = $state(0)
|
|
252
|
+
let column_widths = $state<Record<string, number>>({})
|
|
253
|
+
|
|
254
|
+
// Auto-discover columns from data keys when none are provided
|
|
255
|
+
$effect.pre(() => {
|
|
256
|
+
if (columns.length > 0 || data.length === 0) return
|
|
257
|
+
const seen: Record<string, true> = {}
|
|
258
|
+
for (const row of data.slice(0, 50)) {
|
|
259
|
+
for (const key of Object.keys(row)) {
|
|
260
|
+
if (key !== `style` && key !== `class`) seen[key] = true
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
columns = Object.keys(seen).map((key) => ({ label: key }))
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// Helper to make column IDs (needed since column labels in different groups can be repeated)
|
|
267
|
+
const get_col_id = (col: Label) =>
|
|
268
|
+
col.group ? `${col.key ?? col.label} (${col.group})` : (col.key ?? col.label)
|
|
269
|
+
|
|
270
|
+
// Sync column_order with columns: initialize if empty, remove stale IDs, append new IDs
|
|
271
|
+
$effect(() => {
|
|
272
|
+
if (columns.length === 0) return
|
|
273
|
+
const col_ids = columns.map(get_col_id)
|
|
274
|
+
|
|
92
275
|
// Case 1: First render - initialize with default order
|
|
93
276
|
if (column_order.length === 0) {
|
|
94
|
-
|
|
95
|
-
|
|
277
|
+
column_order = col_ids
|
|
278
|
+
return
|
|
96
279
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
column_order
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
280
|
+
|
|
281
|
+
// Case 2: Sync needed - keep valid IDs in their order, append any new ones
|
|
282
|
+
const valid_ids = new Set(col_ids)
|
|
283
|
+
const kept = column_order.filter((id) => valid_ids.has(id))
|
|
284
|
+
const new_ids = col_ids.filter((id) => !kept.includes(id))
|
|
285
|
+
const new_order = [...kept, ...new_ids]
|
|
286
|
+
|
|
287
|
+
// Skip assignment if content is unchanged to prevent infinite effect loop.
|
|
288
|
+
// After drag reorder, column_order differs from col_ids (default order) but the
|
|
289
|
+
// computed new_order equals the current column_order — assigning a new array
|
|
290
|
+
// reference would re-trigger this effect endlessly.
|
|
291
|
+
if (new_order.length === column_order.length &&
|
|
292
|
+
new_order.every((id, idx) => id === column_order[idx])) return
|
|
293
|
+
|
|
294
|
+
column_order = new_order
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// Reorder columns based on column_order
|
|
298
|
+
let ordered_columns = $derived.by(() => {
|
|
299
|
+
if (column_order.length === 0) return columns
|
|
300
|
+
|
|
301
|
+
const col_map = new SvelteMap(columns.map((col) => [get_col_id(col), col]))
|
|
302
|
+
|
|
113
303
|
// Add columns in specified order, then any remaining columns that weren't in the order list
|
|
114
304
|
const ordered = column_order
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
305
|
+
.map((id) => col_map.get(id))
|
|
306
|
+
.filter((col): col is Label => col != null)
|
|
307
|
+
|
|
308
|
+
const ordered_ids = new Set(ordered.map(get_col_id))
|
|
309
|
+
const remaining = columns.filter((col) => !ordered_ids.has(get_col_id(col)))
|
|
310
|
+
|
|
311
|
+
return [...ordered, ...remaining]
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
let drag_col_id = $state<string | null>(null)
|
|
315
|
+
let drag_over_col_id = $state<string | null>(null)
|
|
316
|
+
|
|
317
|
+
// Merge root_style with rest.style for root div; omit style from rest to avoid duplicate
|
|
318
|
+
let rest_props = $derived.by(() => {
|
|
319
|
+
const { style: rest_style, ...other_props } = rest
|
|
320
|
+
const merged = [rest_style, root_style].filter(Boolean).join(`; `)
|
|
321
|
+
return { ...other_props, ...(merged ? { style: merged } : {}) }
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// WeakMap to assign stable unique IDs to row objects for efficient comparison and keying
|
|
325
|
+
// This avoids O(n) JSON.stringify calls and prevents unnecessary re-renders
|
|
326
|
+
const row_id_map = new WeakMap<RowData, string>()
|
|
327
|
+
let row_id_counter = 0
|
|
328
|
+
|
|
329
|
+
function get_row_id(row: RowData): string {
|
|
330
|
+
let id = row_id_map.get(row)
|
|
129
331
|
if (id === undefined) {
|
|
130
|
-
|
|
131
|
-
|
|
332
|
+
id = `row_${row_id_counter++}`
|
|
333
|
+
row_id_map.set(row, id)
|
|
132
334
|
}
|
|
133
|
-
return id
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const drag_idx = column_order.indexOf(drag_col_id)
|
|
140
|
-
const target_idx = column_order.indexOf(target_col_id)
|
|
141
|
-
if (drag_idx === -1 || target_idx === -1)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
function reset_drag_state() {
|
|
146
|
-
drag_col_id = null
|
|
147
|
-
drag_over_col_id = null
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
event.dataTransfer
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
event.
|
|
335
|
+
return id
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Returns 'left' or 'right' to indicate which side of target to insert dragged column
|
|
339
|
+
function get_drag_side(target_col_id: string): `left` | `right` | null {
|
|
340
|
+
if (!drag_col_id) return null
|
|
341
|
+
const drag_idx = column_order.indexOf(drag_col_id)
|
|
342
|
+
const target_idx = column_order.indexOf(target_col_id)
|
|
343
|
+
if (drag_idx === -1 || target_idx === -1) return null
|
|
344
|
+
return drag_idx < target_idx ? `right` : `left`
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function reset_drag_state() {
|
|
348
|
+
drag_col_id = null
|
|
349
|
+
drag_over_col_id = null
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const get_drag_col_group = () =>
|
|
353
|
+
ordered_columns.find((col) => get_col_id(col) === drag_col_id)?.group
|
|
354
|
+
|
|
355
|
+
function handle_drag_start(event: DragEvent, col: Label) {
|
|
356
|
+
if (!event.dataTransfer) return
|
|
357
|
+
drag_col_id = get_col_id(col)
|
|
358
|
+
event.dataTransfer.effectAllowed = `move`
|
|
359
|
+
event.dataTransfer.setData(`text/html`, ``)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function handle_drag_over(event: DragEvent, col: Label) {
|
|
363
|
+
event.preventDefault()
|
|
364
|
+
if (!event.dataTransfer) return
|
|
365
|
+
event.dataTransfer.dropEffect = `move`
|
|
366
|
+
|
|
162
367
|
// Prevent cross-group drag-over to keep group headers contiguous
|
|
163
368
|
if (get_drag_col_group() !== col.group) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
369
|
+
event.dataTransfer.dropEffect = `none`
|
|
370
|
+
drag_over_col_id = null
|
|
371
|
+
return
|
|
167
372
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
373
|
+
|
|
374
|
+
drag_over_col_id = get_col_id(col)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function handle_drop(event: DragEvent, target_col: Label) {
|
|
378
|
+
event.preventDefault()
|
|
379
|
+
|
|
172
380
|
// Block cross-group (or group→ungroup) reorders to preserve group contiguity
|
|
173
381
|
if (!drag_col_id || drag_col_id === get_col_id(target_col)) {
|
|
174
|
-
|
|
175
|
-
|
|
382
|
+
reset_drag_state()
|
|
383
|
+
return
|
|
176
384
|
}
|
|
385
|
+
|
|
177
386
|
// Block cross-group reorders to preserve group contiguity
|
|
178
387
|
if (get_drag_col_group() !== target_col.group) {
|
|
179
|
-
|
|
180
|
-
|
|
388
|
+
reset_drag_state()
|
|
389
|
+
return
|
|
181
390
|
}
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
const
|
|
391
|
+
|
|
392
|
+
const target_col_id = get_col_id(target_col)
|
|
393
|
+
const drag_idx = column_order.indexOf(drag_col_id)
|
|
394
|
+
const target_idx = column_order.indexOf(target_col_id)
|
|
395
|
+
|
|
185
396
|
if (drag_idx === -1 || target_idx === -1) {
|
|
186
|
-
|
|
187
|
-
|
|
397
|
+
reset_drag_state()
|
|
398
|
+
return
|
|
188
399
|
}
|
|
400
|
+
|
|
189
401
|
// Reorder: remove dragged column, then insert at target position
|
|
190
402
|
// When dragging left-to-right (drag_idx < target_idx), removing the dragged
|
|
191
403
|
// element shifts all subsequent indices down by 1, so we must adjust target_idx
|
|
192
|
-
const new_order = [...column_order]
|
|
193
|
-
new_order.splice(drag_idx, 1)
|
|
194
|
-
const adjusted_target = drag_idx < target_idx ? target_idx - 1 : target_idx
|
|
195
|
-
new_order.splice(adjusted_target, 0, drag_col_id)
|
|
196
|
-
column_order = new_order
|
|
197
|
-
reset_drag_state()
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
404
|
+
const new_order = [...column_order]
|
|
405
|
+
new_order.splice(drag_idx, 1)
|
|
406
|
+
const adjusted_target = drag_idx < target_idx ? target_idx - 1 : target_idx
|
|
407
|
+
new_order.splice(adjusted_target, 0, drag_col_id)
|
|
408
|
+
column_order = new_order
|
|
409
|
+
reset_drag_state()
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Filter data based on search query
|
|
413
|
+
let filtered_data = $derived.by(() => {
|
|
414
|
+
const base_data = data?.filter?.((row) =>
|
|
415
|
+
Object.values(row).some((val) => val !== undefined)
|
|
416
|
+
) ?? []
|
|
417
|
+
|
|
418
|
+
if (!search_query.trim()) return base_data
|
|
419
|
+
|
|
420
|
+
const query = search_query.toLowerCase().trim()
|
|
421
|
+
return base_data.filter((row) =>
|
|
422
|
+
Object.values(row).some((val) => {
|
|
423
|
+
if (val == null) return false
|
|
424
|
+
const clean_val = strip_html(String(val)).toLowerCase()
|
|
425
|
+
return clean_val.includes(query)
|
|
426
|
+
})
|
|
427
|
+
)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
let sorted_data = $derived.by(() => {
|
|
213
431
|
// Skip client-side sorting when using async onsort callback or sort_data is false
|
|
214
|
-
if (onsort || !sort_data)
|
|
215
|
-
|
|
216
|
-
if (!sort_state.column && multi_sort.length === 0)
|
|
217
|
-
|
|
432
|
+
if (onsort || !sort_data) return filtered_data
|
|
433
|
+
|
|
434
|
+
if (!sort_state.column && multi_sort.length === 0) return filtered_data
|
|
435
|
+
|
|
218
436
|
// Helper to check if value is invalid (null, undefined, NaN)
|
|
219
|
-
const is_invalid = (val) =>
|
|
437
|
+
const is_invalid = (val: unknown) =>
|
|
438
|
+
val == null || (typeof val === `number` && Number.isNaN(val))
|
|
439
|
+
|
|
220
440
|
// Get sort value from a cell (handles HTML data-sort-value and numbers with errors)
|
|
221
|
-
const get_sort_val = (val) => {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// Try parsing as a plain number (handles "1.23" strings)
|
|
239
|
-
const plain_num = Number(val);
|
|
240
|
-
if (!isNaN(plain_num) && val.trim() !== ``)
|
|
241
|
-
return plain_num;
|
|
441
|
+
const get_sort_val = (val: CellVal): string | number => {
|
|
442
|
+
if (typeof val === `string`) {
|
|
443
|
+
// Check for HTML data-sort-value attribute first
|
|
444
|
+
const sort_attr_match = val.match(/data-sort-value="([^"]*)"/)
|
|
445
|
+
if (sort_attr_match) {
|
|
446
|
+
const num = Number(sort_attr_match[1])
|
|
447
|
+
return isNaN(num) ? sort_attr_match[1] : num
|
|
448
|
+
}
|
|
449
|
+
// Handle numbers with error notation: "1.23 ± 0.05" or "1.23 +- 0.05" or "1.23(5)"
|
|
450
|
+
// Extract the primary number before the ± or +- or (
|
|
451
|
+
// Supports: ± (U+00B1), ASCII +-, Unicode minus − (U+2212), with optional whitespace
|
|
452
|
+
const error_match = val.match(
|
|
453
|
+
/^([+-−]?\d+\.?\d*(?:[eE][+-−]?\d+)?)\s*(?:[±\u00B1]|[+][−-]|\()/,
|
|
454
|
+
)
|
|
455
|
+
if (error_match) {
|
|
456
|
+
const num = Number(error_match[1])
|
|
457
|
+
if (!isNaN(num)) return num
|
|
242
458
|
}
|
|
243
|
-
|
|
244
|
-
|
|
459
|
+
// Try parsing as a plain number (handles "1.23" strings)
|
|
460
|
+
const plain_num = Number(val)
|
|
461
|
+
if (!isNaN(plain_num) && val.trim() !== ``) return plain_num
|
|
462
|
+
}
|
|
463
|
+
return val as string | number
|
|
464
|
+
}
|
|
465
|
+
|
|
245
466
|
// Build sort criteria: multi_sort takes precedence, fallback to single sort
|
|
246
467
|
const sort_criteria = multi_sort.length > 0
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
468
|
+
? multi_sort
|
|
469
|
+
: sort_state.column
|
|
470
|
+
? [sort_state]
|
|
471
|
+
: []
|
|
472
|
+
|
|
473
|
+
if (sort_criteria.length === 0) return filtered_data
|
|
474
|
+
|
|
253
475
|
return [...filtered_data].sort((row1, row2) => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const sort_val1 = get_sort_val(val1);
|
|
268
|
-
const sort_val2 = get_sort_val(val2);
|
|
269
|
-
const modifier = ascending ? 1 : -1;
|
|
270
|
-
if (typeof sort_val1 === `string` && typeof sort_val2 === `string`) {
|
|
271
|
-
const cmp = sort_val1.localeCompare(sort_val2, undefined, {
|
|
272
|
-
numeric: true,
|
|
273
|
-
sensitivity: `base`,
|
|
274
|
-
});
|
|
275
|
-
if (cmp !== 0)
|
|
276
|
-
return cmp * modifier;
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
if (sort_val1 !== sort_val2) {
|
|
280
|
-
return (sort_val1 ?? 0) < (sort_val2 ?? 0) ? -modifier : modifier;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
476
|
+
for (const { column, ascending } of sort_criteria) {
|
|
477
|
+
const matched_col = ordered_columns.find((c) => get_col_id(c) === column)
|
|
478
|
+
if (!matched_col) continue
|
|
479
|
+
|
|
480
|
+
const col_id = get_col_id(matched_col)
|
|
481
|
+
const val1 = row1[col_id]
|
|
482
|
+
const val2 = row2[col_id]
|
|
483
|
+
|
|
484
|
+
if (val1 === val2) continue
|
|
485
|
+
|
|
486
|
+
// Push invalid values to bottom
|
|
487
|
+
if (is_invalid(val1) || is_invalid(val2)) {
|
|
488
|
+
return +is_invalid(val1) - +is_invalid(val2)
|
|
283
489
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
490
|
+
|
|
491
|
+
const sort_val1 = get_sort_val(val1)
|
|
492
|
+
const sort_val2 = get_sort_val(val2)
|
|
493
|
+
const modifier = ascending ? 1 : -1
|
|
494
|
+
|
|
495
|
+
if (typeof sort_val1 === `string` && typeof sort_val2 === `string`) {
|
|
496
|
+
const cmp = sort_val1.localeCompare(sort_val2, undefined, {
|
|
497
|
+
numeric: true,
|
|
498
|
+
sensitivity: `base`,
|
|
499
|
+
})
|
|
500
|
+
if (cmp !== 0) return cmp * modifier
|
|
501
|
+
} else {
|
|
502
|
+
if (sort_val1 !== sort_val2) {
|
|
503
|
+
return (sort_val1 ?? 0) < (sort_val2 ?? 0) ? -modifier : modifier
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return 0
|
|
508
|
+
})
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
// Paginated data
|
|
512
|
+
let paginated_data = $derived.by(() => {
|
|
513
|
+
if (!pagination_config) return sorted_data
|
|
514
|
+
const start = (current_page - 1) * effective_page_size
|
|
515
|
+
return sorted_data.slice(start, start + effective_page_size)
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
let total_pages = $derived(
|
|
519
|
+
Math.ceil(sorted_data.length / effective_page_size),
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
// Track previous values to detect actual changes
|
|
523
|
+
let prev_search_query = $state(``)
|
|
524
|
+
let prev_data_length = $state(0)
|
|
525
|
+
|
|
526
|
+
// Track async sort requests to prevent race conditions
|
|
527
|
+
let sort_request_id = 0
|
|
528
|
+
|
|
529
|
+
// Reset to page 1 when search query or data length actually changes
|
|
530
|
+
$effect(() => {
|
|
531
|
+
const query_changed = search_query !== prev_search_query
|
|
532
|
+
const data_changed = sorted_data.length !== prev_data_length
|
|
533
|
+
|
|
304
534
|
if (query_changed || data_changed) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
535
|
+
current_page = 1
|
|
536
|
+
prev_search_query = search_query
|
|
537
|
+
prev_data_length = sorted_data.length
|
|
538
|
+
} else if (total_pages > 0 && current_page > total_pages) {
|
|
539
|
+
// Clamp when total pages decreases (e.g., page size increase)
|
|
540
|
+
current_page = total_pages
|
|
308
541
|
}
|
|
309
|
-
})
|
|
310
|
-
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
async function sort_rows(
|
|
545
|
+
column: string,
|
|
546
|
+
group: string | undefined,
|
|
547
|
+
event: MouseEvent | KeyboardEvent,
|
|
548
|
+
) {
|
|
311
549
|
// Find the column using both label and group if provided
|
|
312
|
-
const col = ordered_columns.find(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
550
|
+
const col = ordered_columns.find(
|
|
551
|
+
(c) => c.label === column && c.group === group,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if (!col) return // Skip if column not found
|
|
555
|
+
if (col.sortable === false) return // Skip sorting if column marked as unsortable
|
|
556
|
+
|
|
557
|
+
const col_id = get_col_id(col)
|
|
558
|
+
|
|
318
559
|
// Shift+click for multi-column sort
|
|
319
560
|
if (event.shiftKey) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
561
|
+
const existing_idx = multi_sort.findIndex((s) => s.column === col_id)
|
|
562
|
+
if (existing_idx >= 0) {
|
|
563
|
+
// Toggle direction or remove if clicked again
|
|
564
|
+
const existing = multi_sort[existing_idx]
|
|
565
|
+
if (existing.ascending === (col.better === `lower`)) {
|
|
566
|
+
// Remove from multi-sort
|
|
567
|
+
multi_sort = multi_sort.filter((_, idx) => idx !== existing_idx)
|
|
568
|
+
} else {
|
|
569
|
+
// Toggle direction
|
|
570
|
+
multi_sort = multi_sort.map((s, idx) =>
|
|
571
|
+
idx === existing_idx ? { ...s, ascending: !s.ascending } : s
|
|
572
|
+
)
|
|
332
573
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
}
|
|
574
|
+
} else {
|
|
575
|
+
// Add to multi-sort
|
|
576
|
+
multi_sort = [...multi_sort, {
|
|
577
|
+
column: col_id,
|
|
578
|
+
ascending: col.better === `lower`,
|
|
579
|
+
}]
|
|
580
|
+
}
|
|
581
|
+
// Clear single sort when using multi-sort
|
|
582
|
+
sort = { column: ``, dir: `asc` }
|
|
583
|
+
} else {
|
|
584
|
+
// Regular click - single column sort
|
|
585
|
+
multi_sort = [] // Clear multi-sort
|
|
586
|
+
// Use sort_state.column for comparison since it includes initial_sort fallback
|
|
587
|
+
const new_dir = sort_state.column !== col_id
|
|
588
|
+
? (col.better === `lower` ? `asc` : `desc`)
|
|
589
|
+
: (sort_state.ascending ? `desc` : `asc`)
|
|
590
|
+
|
|
591
|
+
// Save previous sort state in case we need to revert on error
|
|
592
|
+
const prev_sort = { ...sort }
|
|
593
|
+
sort = { column: col_id, dir: new_dir }
|
|
594
|
+
|
|
595
|
+
// If onsort callback provided, fetch new data from server
|
|
596
|
+
if (onsort) {
|
|
597
|
+
loading = true
|
|
598
|
+
const request_id = ++sort_request_id
|
|
599
|
+
try {
|
|
600
|
+
const result = await onsort(col_id, new_dir)
|
|
601
|
+
// Only update if this is still the most recent request (avoid race condition)
|
|
602
|
+
if (request_id === sort_request_id) {
|
|
603
|
+
data = result
|
|
604
|
+
}
|
|
605
|
+
} catch (err) {
|
|
606
|
+
console.error(`Sort callback failed:`, err)
|
|
607
|
+
// Revert sort state on failure so UI doesn't show wrong direction
|
|
608
|
+
if (request_id === sort_request_id) {
|
|
609
|
+
sort = prev_sort
|
|
610
|
+
onsorterror?.(err, col_id, new_dir)
|
|
611
|
+
}
|
|
612
|
+
} finally {
|
|
613
|
+
// Only clear loading if this is still the most recent request
|
|
614
|
+
if (request_id === sort_request_id) {
|
|
615
|
+
loading = false
|
|
616
|
+
}
|
|
378
617
|
}
|
|
618
|
+
}
|
|
379
619
|
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (typeof val !== `string`)
|
|
386
|
-
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Extract numeric value from strings with uncertainty notation: "1.23 ± 0.05", "1.23 +- 0.05", "1.23(5)"
|
|
623
|
+
function parse_numeric_val(val: CellVal): number | null {
|
|
624
|
+
if (typeof val === `number`) return Number.isNaN(val) ? null : val
|
|
625
|
+
if (typeof val !== `string`) return null
|
|
626
|
+
|
|
387
627
|
// Handle numbers with error notation: "1.23 ± 0.05" or "1.23 +- 0.05" or "1.23(5)"
|
|
388
628
|
// Supports: ± (U+00B1), ASCII +-, Unicode minus − (U+2212), with optional whitespace
|
|
389
629
|
// Note: [-+−] has hyphen first to avoid regex range interpretation
|
|
390
630
|
// Pattern allows leading decimals like .5 or -.5 via (?:\d+\.?\d*|\d*\.\d+)
|
|
391
|
-
const error_match = val.match(
|
|
631
|
+
const error_match = val.match(
|
|
632
|
+
/^([-+−]?(?:\d+\.?\d*|\d*\.\d+)(?:[eE][-+−]?\d+)?)\s*(?:±|\+[-−]|\()/,
|
|
633
|
+
)
|
|
392
634
|
if (error_match) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return num;
|
|
635
|
+
// Normalize unicode minus (U+2212) to ASCII hyphen for Number()
|
|
636
|
+
const normalized = normalize_unicode_minus(error_match[1])
|
|
637
|
+
const num = Number(normalized)
|
|
638
|
+
if (!isNaN(num)) return num
|
|
398
639
|
}
|
|
399
640
|
// Try parsing as a plain number (handles "1.23" strings)
|
|
400
641
|
// Also normalize unicode minus for plain numbers
|
|
401
|
-
const normalized_val = val
|
|
402
|
-
const plain_num = Number(normalized_val)
|
|
403
|
-
if (!isNaN(plain_num) && val.trim() !== ``)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
// Memoize parsed column values to avoid O(N²) re-parsing in calc_color
|
|
408
|
-
let parsed_column_values = $derived.by(() => {
|
|
409
|
-
const result = new SvelteMap()
|
|
642
|
+
const normalized_val = normalize_unicode_minus(val)
|
|
643
|
+
const plain_num = Number(normalized_val)
|
|
644
|
+
if (!isNaN(plain_num) && val.trim() !== ``) return plain_num
|
|
645
|
+
return null
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Memoize parsed column values to avoid O(N²) re-parsing in calc_color
|
|
649
|
+
let parsed_column_values = $derived.by(() => {
|
|
650
|
+
const result = new SvelteMap<string, (number | null)[]>()
|
|
410
651
|
for (const col of ordered_columns) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
result.set(col_id, sorted_data.map((row) => parse_numeric_val(row[col_id])));
|
|
652
|
+
if (col.color_scale === null) continue
|
|
653
|
+
const col_id = get_col_id(col)
|
|
654
|
+
result.set(col_id, sorted_data.map((row) => parse_numeric_val(row[col_id])))
|
|
415
655
|
}
|
|
416
|
-
return result
|
|
417
|
-
})
|
|
418
|
-
|
|
656
|
+
return result
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
function calc_color(val: CellVal, col: Label) {
|
|
419
660
|
if (!show_heatmap || col.color_scale === null) {
|
|
420
|
-
|
|
661
|
+
return { bg: null, text: null }
|
|
421
662
|
}
|
|
663
|
+
|
|
422
664
|
// Parse numeric value from strings with uncertainty notation
|
|
423
|
-
const numeric_val = parse_numeric_val(val)
|
|
424
|
-
if (numeric_val === null)
|
|
425
|
-
|
|
426
|
-
const col_id = get_col_id(col)
|
|
665
|
+
const numeric_val = parse_numeric_val(val)
|
|
666
|
+
if (numeric_val === null) return { bg: null, text: null }
|
|
667
|
+
|
|
668
|
+
const col_id = get_col_id(col)
|
|
427
669
|
// Use memoized parsed values for the column
|
|
428
|
-
const numeric_vals = parsed_column_values.get(col_id) ?? []
|
|
429
|
-
|
|
430
|
-
const
|
|
670
|
+
const numeric_vals = parsed_column_values.get(col_id) ?? []
|
|
671
|
+
|
|
672
|
+
const better = better_overrides.get(col_id) ?? col.better
|
|
673
|
+
const scale = (color_scale_overrides.get(col_id) ?? col.color_scale ??
|
|
674
|
+
`interpolateViridis`) as Parameters<typeof calc_cell_color>[3]
|
|
675
|
+
const color = calc_cell_color(
|
|
676
|
+
numeric_val,
|
|
677
|
+
numeric_vals,
|
|
678
|
+
better,
|
|
679
|
+
scale,
|
|
680
|
+
col.scale_type || `linear`,
|
|
681
|
+
)
|
|
682
|
+
|
|
431
683
|
// Recompute text contrast against effective bg (cell bg blended with page bg by opacity).
|
|
432
684
|
// Approximation: blend luminances directly; accurate enough for black/white text choice.
|
|
433
685
|
if (color.bg && heatmap_opacity < 1) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
686
|
+
const blended_lum = luminance(color.bg) * heatmap_opacity +
|
|
687
|
+
page_bg_lum * (1 - heatmap_opacity)
|
|
688
|
+
color.text = blended_lum > 0.7 ? `black` : `white`
|
|
437
689
|
}
|
|
438
|
-
return color
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
690
|
+
return color
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
let visible_columns = $derived(
|
|
694
|
+
ordered_columns.filter((col) =>
|
|
695
|
+
col.visible !== false && !hidden_columns.includes(get_col_id(col))
|
|
696
|
+
),
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
const sort_indicator = (col: Label, sort_state: SortState) => {
|
|
700
|
+
const hide_sort_indicator = col.show_sort_indicator === false ||
|
|
701
|
+
col.style?.includes(`--hide-sort-indicator`)
|
|
702
|
+
if (hide_sort_indicator) return ``
|
|
703
|
+
|
|
704
|
+
const col_id = get_col_id(col)
|
|
705
|
+
|
|
443
706
|
// Check multi-sort first
|
|
444
|
-
const multi_idx = multi_sort.findIndex((s) => s.column === col_id)
|
|
707
|
+
const multi_idx = multi_sort.findIndex((s) => s.column === col_id)
|
|
445
708
|
if (multi_idx >= 0) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
709
|
+
const arrow = multi_sort[multi_idx].ascending ? `↓` : `↑`
|
|
710
|
+
const badge = multi_sort.length > 1 ? `<sup>${multi_idx + 1}</sup>` : ``
|
|
711
|
+
return `<span style="font-size: 0.8em;">${arrow}${badge}</span>`
|
|
449
712
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
return arrow ? `<span style="font-size: 0.8em;">${arrow}</span>` :
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
713
|
+
|
|
714
|
+
const is_sorted = sort_state.column === col_id
|
|
715
|
+
if (!is_sorted) return ``
|
|
716
|
+
// Show indicator only for actively sorted columns.
|
|
717
|
+
const arrow = sort_state.ascending ? `↓` : `↑`
|
|
718
|
+
|
|
719
|
+
return arrow ? `<span style="font-size: 0.8em;">${arrow}</span>` : ``
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Context menu state for column header right-click
|
|
723
|
+
let context_menu_col = $state<string | null>(null)
|
|
724
|
+
let context_menu_pos = $state({ x: 0, y: 0 })
|
|
725
|
+
|
|
726
|
+
const better_sections = [
|
|
727
|
+
{
|
|
728
|
+
title: `Gradient direction`,
|
|
729
|
+
options: [
|
|
730
|
+
{ value: `higher`, label: `▲ Higher is better` },
|
|
731
|
+
{ value: `lower`, label: `▼ Lower is better` },
|
|
732
|
+
],
|
|
733
|
+
},
|
|
734
|
+
] as const
|
|
735
|
+
|
|
736
|
+
// Row selection using WeakMap-based ID lookup instead of O(n) JSON.stringify comparison
|
|
737
|
+
function toggle_row_select(row: RowData) {
|
|
738
|
+
const row_id = get_row_id(row)
|
|
739
|
+
const idx = selected_rows.findIndex((r) => get_row_id(r) === row_id)
|
|
462
740
|
if (idx >= 0) {
|
|
463
|
-
|
|
741
|
+
selected_rows = selected_rows.filter((_, i) => i !== idx)
|
|
742
|
+
} else {
|
|
743
|
+
selected_rows = [...selected_rows, row]
|
|
464
744
|
}
|
|
465
|
-
|
|
466
|
-
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function is_row_selected(row: RowData): boolean {
|
|
748
|
+
const row_id = get_row_id(row)
|
|
749
|
+
return selected_rows.some((r) => get_row_id(r) === row_id)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Select-all: checks if every row on the current page is selected
|
|
753
|
+
let all_page_selected = $derived(
|
|
754
|
+
paginated_data.length > 0 && paginated_data.every((row) => is_row_selected(row)),
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
function toggle_select_all() {
|
|
758
|
+
if (all_page_selected) {
|
|
759
|
+
const page_ids = new Set(paginated_data.map(get_row_id))
|
|
760
|
+
selected_rows = selected_rows.filter((row) => !page_ids.has(get_row_id(row)))
|
|
761
|
+
} else {
|
|
762
|
+
const already = new Set(selected_rows.map(get_row_id))
|
|
763
|
+
const new_rows = paginated_data.filter((row) => !already.has(get_row_id(row)))
|
|
764
|
+
selected_rows = [...selected_rows, ...new_rows]
|
|
467
765
|
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
return str_val;
|
|
486
|
-
}));
|
|
487
|
-
const csv_content = [headers.join(`,`), ...rows.map((r) => r.join(`,`))].join(`\n`);
|
|
488
|
-
download_file(csv_content, `${filename}.csv`, `text/csv`);
|
|
489
|
-
}
|
|
490
|
-
function export_json(filename = `table-export`) {
|
|
491
|
-
const rows = sorted_data.map((row) => {
|
|
492
|
-
const clean_row = {};
|
|
493
|
-
for (const col of visible_columns) {
|
|
494
|
-
const col_id = get_col_id(col);
|
|
495
|
-
const val = row[col_id];
|
|
496
|
-
clean_row[col.label] = typeof val === `string` ? strip_html(val) : val;
|
|
497
|
-
}
|
|
498
|
-
return clean_row;
|
|
499
|
-
});
|
|
500
|
-
const json_content = JSON.stringify(rows, null, 2);
|
|
501
|
-
download_file(json_content, `${filename}.json`, `application/json`);
|
|
502
|
-
}
|
|
503
|
-
function download_file(content, filename, mime_type) {
|
|
504
|
-
const blob = new Blob([content], { type: mime_type });
|
|
505
|
-
const url = URL.createObjectURL(blob);
|
|
506
|
-
const link = document.createElement(`a`);
|
|
507
|
-
link.href = url;
|
|
508
|
-
link.download = filename;
|
|
509
|
-
document.body.appendChild(link);
|
|
510
|
-
link.click();
|
|
511
|
-
document.body.removeChild(link);
|
|
512
|
-
URL.revokeObjectURL(url);
|
|
513
|
-
}
|
|
514
|
-
// Column visibility toggle
|
|
515
|
-
function toggle_column(col_id) {
|
|
516
|
-
if (hidden_columns.includes(col_id)) {
|
|
517
|
-
hidden_columns = hidden_columns.filter((id) => id !== col_id);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Data source for exports: selected rows when any are selected, otherwise all sorted data
|
|
769
|
+
let export_rows = $derived(
|
|
770
|
+
show_row_select && selected_rows.length > 0 ? selected_rows : sorted_data,
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
// Serialize table as delimited text (shared by CSV export and clipboard copy)
|
|
774
|
+
// Per RFC 4180, fields containing commas, double quotes, or newlines must be quoted
|
|
775
|
+
function serialize_table(delimiter: string, csv_quote = false): string {
|
|
776
|
+
const quote = (str: string) => {
|
|
777
|
+
if (!csv_quote) return str
|
|
778
|
+
if (str.includes(`,`) || str.includes(`"`) || str.includes(`\n`)) {
|
|
779
|
+
return `"${str.replace(/"/g, `""`)}"`
|
|
780
|
+
}
|
|
781
|
+
return str
|
|
518
782
|
}
|
|
519
|
-
|
|
520
|
-
|
|
783
|
+
const headers = visible_columns.map((col) => quote(strip_html(col.label)))
|
|
784
|
+
const rows = export_rows.map((row) =>
|
|
785
|
+
visible_columns.map((col) => {
|
|
786
|
+
const val = row[get_col_id(col)]
|
|
787
|
+
if (val == null) return ``
|
|
788
|
+
return quote(strip_html(String(val)))
|
|
789
|
+
})
|
|
790
|
+
)
|
|
791
|
+
return [headers.join(delimiter), ...rows.map((r) => r.join(delimiter))].join(`\n`)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function export_csv(filename = `table-export`) {
|
|
795
|
+
download_file(serialize_table(`,`, true), `${filename}.csv`, `text/csv`)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function export_json(filename = `table-export`) {
|
|
799
|
+
const rows = export_rows.map((row) => {
|
|
800
|
+
const clean_row: Record<string, unknown> = {}
|
|
801
|
+
for (const col of visible_columns) {
|
|
802
|
+
const col_id = get_col_id(col)
|
|
803
|
+
const val = row[col_id]
|
|
804
|
+
clean_row[strip_html(col.label)] = typeof val === `string`
|
|
805
|
+
? strip_html(val)
|
|
806
|
+
: val
|
|
807
|
+
}
|
|
808
|
+
return clean_row
|
|
809
|
+
})
|
|
810
|
+
download_file(
|
|
811
|
+
JSON.stringify(rows, null, 2),
|
|
812
|
+
`${filename}.json`,
|
|
813
|
+
`application/json`,
|
|
814
|
+
)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function download_file(content: string, filename: string, mime_type: string) {
|
|
818
|
+
const blob = new Blob([content], { type: mime_type })
|
|
819
|
+
const url = URL.createObjectURL(blob)
|
|
820
|
+
const link = document.createElement(`a`)
|
|
821
|
+
link.href = url
|
|
822
|
+
link.download = filename
|
|
823
|
+
document.body.appendChild(link)
|
|
824
|
+
link.click()
|
|
825
|
+
document.body.removeChild(link)
|
|
826
|
+
URL.revokeObjectURL(url)
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function copy_to_clipboard() {
|
|
830
|
+
navigator.clipboard.writeText(serialize_table(`\t`))
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Column visibility toggle
|
|
834
|
+
function toggle_column(col_id: string) {
|
|
835
|
+
if (hidden_columns.includes(col_id)) {
|
|
836
|
+
hidden_columns = hidden_columns.filter((id) => id !== col_id)
|
|
837
|
+
} else {
|
|
838
|
+
hidden_columns = [...hidden_columns, col_id]
|
|
521
839
|
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
event.
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Column resize handlers
|
|
843
|
+
function start_resize(event: MouseEvent, col: Label) {
|
|
844
|
+
event.preventDefault()
|
|
845
|
+
event.stopPropagation()
|
|
846
|
+
resize_col_id = get_col_id(col)
|
|
847
|
+
resize_start_x = event.clientX
|
|
848
|
+
const th = event.target instanceof Element ? event.target.parentElement : null
|
|
849
|
+
resize_start_width = th?.offsetWidth ?? 100
|
|
850
|
+
|
|
851
|
+
document.addEventListener(`mousemove`, handle_resize)
|
|
852
|
+
document.addEventListener(`mouseup`, stop_resize)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function handle_resize(event: MouseEvent) {
|
|
856
|
+
if (!resize_col_id) return
|
|
857
|
+
const delta = event.clientX - resize_start_x
|
|
858
|
+
const new_width = Math.min(500, Math.max(50, resize_start_width + delta))
|
|
859
|
+
column_widths = { ...column_widths, [resize_col_id]: new_width }
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function stop_resize() {
|
|
863
|
+
resize_col_id = null
|
|
864
|
+
document.removeEventListener(`mousemove`, handle_resize)
|
|
865
|
+
document.removeEventListener(`mouseup`, stop_resize)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Normalize sort_hint to a config object with defaults
|
|
869
|
+
let hint_config = $derived(
|
|
870
|
+
sort_hint
|
|
871
|
+
? {
|
|
872
|
+
position: `bottom` as const,
|
|
550
873
|
permanent: false,
|
|
551
874
|
...(typeof sort_hint === `string` ? { text: sort_hint } : sort_hint),
|
|
552
|
-
|
|
553
|
-
|
|
875
|
+
}
|
|
876
|
+
: null,
|
|
877
|
+
)
|
|
554
878
|
</script>
|
|
555
879
|
|
|
556
880
|
{#snippet sort_hint_element(pos: `top` | `bottom`)}
|
|
@@ -567,11 +891,15 @@ let hint_config = $derived(sort_hint
|
|
|
567
891
|
|
|
568
892
|
<div
|
|
569
893
|
{@attach tooltip()}
|
|
570
|
-
{...
|
|
894
|
+
{...rest_props}
|
|
571
895
|
bind:this={container_el}
|
|
572
|
-
class="table-container {
|
|
896
|
+
class="table-container {rest_props.class ?? ``}"
|
|
573
897
|
style:--heatmap-opacity="{heatmap_opacity * 100}%"
|
|
574
|
-
onmouseleave={() =>
|
|
898
|
+
onmouseleave={() => {
|
|
899
|
+
show_column_dropdown = false
|
|
900
|
+
show_export_dropdown = false
|
|
901
|
+
context_menu_col = null
|
|
902
|
+
}}
|
|
575
903
|
>
|
|
576
904
|
<!-- Floating control buttons -->
|
|
577
905
|
<section class="control-buttons">
|
|
@@ -592,12 +920,16 @@ let hint_config = $derived(sort_hint
|
|
|
592
920
|
search_query = ``
|
|
593
921
|
search_expanded = false
|
|
594
922
|
}}
|
|
595
|
-
|
|
923
|
+
{@attach tooltip({ content: `Clear`, placement: `top` })}
|
|
596
924
|
>
|
|
597
925
|
<Icon icon="Cross" style="width: 10px" />
|
|
598
926
|
</button>
|
|
599
927
|
{:else}
|
|
600
|
-
<button
|
|
928
|
+
<button
|
|
929
|
+
class="icon-btn"
|
|
930
|
+
onclick={() => search_expanded = true}
|
|
931
|
+
{@attach tooltip({ content: `Search`, placement: `top` })}
|
|
932
|
+
>
|
|
601
933
|
<Icon icon="Search" style="width: 14px" />
|
|
602
934
|
</button>
|
|
603
935
|
{/if}
|
|
@@ -609,7 +941,7 @@ let hint_config = $derived(sort_hint
|
|
|
609
941
|
class="icon-btn"
|
|
610
942
|
class:active={show_column_dropdown}
|
|
611
943
|
onclick={() => show_column_dropdown = !show_column_dropdown}
|
|
612
|
-
|
|
944
|
+
{@attach tooltip({ content: `Columns`, placement: `top` })}
|
|
613
945
|
>
|
|
614
946
|
<Icon icon="Columns" style="width: 14px" />
|
|
615
947
|
</button>
|
|
@@ -623,7 +955,7 @@ let hint_config = $derived(sort_hint
|
|
|
623
955
|
checked={!hidden_columns.includes(col_id)}
|
|
624
956
|
onchange={() => toggle_column(col_id)}
|
|
625
957
|
/>
|
|
626
|
-
{@html col.label}
|
|
958
|
+
{@html sanitize_html(col.label)}
|
|
627
959
|
</label>
|
|
628
960
|
{/each}
|
|
629
961
|
</div>
|
|
@@ -637,7 +969,7 @@ let hint_config = $derived(sort_hint
|
|
|
637
969
|
class="icon-btn"
|
|
638
970
|
class:active={show_export_dropdown}
|
|
639
971
|
onclick={() => show_export_dropdown = !show_export_dropdown}
|
|
640
|
-
|
|
972
|
+
{@attach tooltip({ content: `Export`, placement: `top` })}
|
|
641
973
|
>
|
|
642
974
|
<Icon icon="Export" style="width: 14px" />
|
|
643
975
|
</button>
|
|
@@ -665,6 +997,15 @@ let hint_config = $derived(sort_hint
|
|
|
665
997
|
<Icon icon="Download" style="width: 12px" /> JSON
|
|
666
998
|
</button>
|
|
667
999
|
{/if}
|
|
1000
|
+
<button
|
|
1001
|
+
class="dropdown-option"
|
|
1002
|
+
onclick={() => {
|
|
1003
|
+
copy_to_clipboard()
|
|
1004
|
+
show_export_dropdown = false
|
|
1005
|
+
}}
|
|
1006
|
+
>
|
|
1007
|
+
<Icon icon="Copy" style="width: 12px" /> Copy
|
|
1008
|
+
</button>
|
|
668
1009
|
</div>
|
|
669
1010
|
{/if}
|
|
670
1011
|
</div>
|
|
@@ -686,6 +1027,106 @@ let hint_config = $derived(sort_hint
|
|
|
686
1027
|
{/if}
|
|
687
1028
|
</section>
|
|
688
1029
|
|
|
1030
|
+
{#if show_controls}
|
|
1031
|
+
<DraggablePane
|
|
1032
|
+
bind:show={controls_open}
|
|
1033
|
+
closed_icon="Settings"
|
|
1034
|
+
open_icon="Cross"
|
|
1035
|
+
toggle_props={{
|
|
1036
|
+
title: `${controls_open ? `Close` : `Open`} table controls`,
|
|
1037
|
+
style: `position: absolute; top: 5pt; right: 1ex; z-index: 10`,
|
|
1038
|
+
}}
|
|
1039
|
+
pane_props={{ style: `max-height: 60vh; overflow-y: auto; font-size: 0.85em` }}
|
|
1040
|
+
>
|
|
1041
|
+
<SettingsSection
|
|
1042
|
+
title="Heatmap"
|
|
1043
|
+
current_values={{ show_heatmap, heatmap_opacity }}
|
|
1044
|
+
on_reset={() => {
|
|
1045
|
+
show_heatmap = true
|
|
1046
|
+
heatmap_opacity = 1
|
|
1047
|
+
}}
|
|
1048
|
+
>
|
|
1049
|
+
<label><input type="checkbox" bind:checked={show_heatmap} /> Show heatmap</label>
|
|
1050
|
+
{#if show_heatmap}
|
|
1051
|
+
<label>
|
|
1052
|
+
Opacity
|
|
1053
|
+
<input
|
|
1054
|
+
type="range"
|
|
1055
|
+
min="0"
|
|
1056
|
+
max="1"
|
|
1057
|
+
step="0.05"
|
|
1058
|
+
bind:value={heatmap_opacity}
|
|
1059
|
+
/>
|
|
1060
|
+
<input
|
|
1061
|
+
type="number"
|
|
1062
|
+
min="0"
|
|
1063
|
+
max="1"
|
|
1064
|
+
step="0.05"
|
|
1065
|
+
bind:value={heatmap_opacity}
|
|
1066
|
+
style="width: 3.5em"
|
|
1067
|
+
/>
|
|
1068
|
+
</label>
|
|
1069
|
+
{/if}
|
|
1070
|
+
</SettingsSection>
|
|
1071
|
+
|
|
1072
|
+
<SettingsSection
|
|
1073
|
+
title="Display"
|
|
1074
|
+
current_values={{ show_row_numbers }}
|
|
1075
|
+
on_reset={() => {
|
|
1076
|
+
show_row_numbers = false
|
|
1077
|
+
}}
|
|
1078
|
+
>
|
|
1079
|
+
<label><input type="checkbox" bind:checked={show_row_numbers} /> Row
|
|
1080
|
+
numbers</label>
|
|
1081
|
+
</SettingsSection>
|
|
1082
|
+
|
|
1083
|
+
{#if colored_columns.length > 0}
|
|
1084
|
+
<SettingsSection
|
|
1085
|
+
title="Column Colors"
|
|
1086
|
+
current_values={Object.fromEntries([...better_overrides, ...color_scale_overrides])}
|
|
1087
|
+
on_reset={() => {
|
|
1088
|
+
better_overrides.clear()
|
|
1089
|
+
color_scale_overrides.clear()
|
|
1090
|
+
}}
|
|
1091
|
+
>
|
|
1092
|
+
{#each colored_columns as col (get_col_id(col))}
|
|
1093
|
+
{@const col_id = get_col_id(col)}
|
|
1094
|
+
<div class="col-color-row">
|
|
1095
|
+
<span class="col-color-label">{@html sanitize_html(col.label)}</span>
|
|
1096
|
+
<select
|
|
1097
|
+
value={color_scale_overrides.get(col_id) ?? col.color_scale ??
|
|
1098
|
+
`interpolateViridis`}
|
|
1099
|
+
onchange={(event) => {
|
|
1100
|
+
const val = event.currentTarget.value
|
|
1101
|
+
if (
|
|
1102
|
+
val === (col.color_scale ?? `interpolateViridis`)
|
|
1103
|
+
) color_scale_overrides.delete(col_id)
|
|
1104
|
+
else color_scale_overrides.set(col_id, val)
|
|
1105
|
+
}}
|
|
1106
|
+
>
|
|
1107
|
+
{#each color_scale_options as scale (scale)}
|
|
1108
|
+
<option value={scale}>{scale.replace(`interpolate`, ``)}</option>
|
|
1109
|
+
{/each}
|
|
1110
|
+
</select>
|
|
1111
|
+
<select
|
|
1112
|
+
value={better_overrides.get(col_id) ?? col.better ?? ``}
|
|
1113
|
+
onchange={(event) => {
|
|
1114
|
+
const val = event.currentTarget.value
|
|
1115
|
+
if (!val) better_overrides.delete(col_id)
|
|
1116
|
+
else better_overrides.set(col_id, val as `higher` | `lower`)
|
|
1117
|
+
}}
|
|
1118
|
+
>
|
|
1119
|
+
<option value="">Default</option>
|
|
1120
|
+
<option value="higher">▲ High</option>
|
|
1121
|
+
<option value="lower">▼ Low</option>
|
|
1122
|
+
</select>
|
|
1123
|
+
</div>
|
|
1124
|
+
{/each}
|
|
1125
|
+
</SettingsSection>
|
|
1126
|
+
{/if}
|
|
1127
|
+
</DraggablePane>
|
|
1128
|
+
{/if}
|
|
1129
|
+
|
|
689
1130
|
{@render sort_hint_element(`top`)}
|
|
690
1131
|
|
|
691
1132
|
<div
|
|
@@ -707,20 +1148,24 @@ let hint_config = $derived(sort_hint
|
|
|
707
1148
|
{#if show_row_select}
|
|
708
1149
|
<th class="select-col"></th>
|
|
709
1150
|
{/if}
|
|
710
|
-
{#
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
{#if !group}
|
|
715
|
-
<th class:sticky-col={sticky}></th>
|
|
1151
|
+
{#if show_row_numbers}
|
|
1152
|
+
<th class="row-num-col"></th>
|
|
1153
|
+
{/if}
|
|
1154
|
+
{#each visible_columns as col (get_col_id(col))}
|
|
1155
|
+
{#if !col.group}
|
|
1156
|
+
<th class:sticky-col={col.sticky}></th>
|
|
716
1157
|
{:else}
|
|
717
|
-
{@const group_cols = visible_columns.filter((c) =>
|
|
1158
|
+
{@const group_cols = visible_columns.filter((c) =>
|
|
1159
|
+
c.group === col.group
|
|
1160
|
+
)}
|
|
718
1161
|
<!-- Only render the group header once for each group by checking if this is the first column of this group -->
|
|
719
|
-
{#if visible_columns.findIndex((c) => c.group === group) ===
|
|
1162
|
+
{#if visible_columns.findIndex((c) => c.group === col.group) ===
|
|
720
1163
|
visible_columns.findIndex((c) =>
|
|
721
|
-
c.group === group && c.label === label
|
|
1164
|
+
c.group === col.group && c.label === col.label
|
|
722
1165
|
)}
|
|
723
|
-
<th title={description} colspan={group_cols.length}>
|
|
1166
|
+
<th title={col.description} colspan={group_cols.length}>
|
|
1167
|
+
{@html sanitize_html(col.group)}
|
|
1168
|
+
</th>
|
|
724
1169
|
{/if}
|
|
725
1170
|
{/if}
|
|
726
1171
|
{/each}
|
|
@@ -729,11 +1174,21 @@ let hint_config = $derived(sort_hint
|
|
|
729
1174
|
<!-- Second level headers -->
|
|
730
1175
|
<tr>
|
|
731
1176
|
{#if show_row_select}
|
|
732
|
-
<th
|
|
733
|
-
|
|
1177
|
+
<th
|
|
1178
|
+
class="select-col"
|
|
1179
|
+
title={all_page_selected ? `Deselect all` : `Select all on this page`}
|
|
1180
|
+
>
|
|
1181
|
+
<input
|
|
1182
|
+
type="checkbox"
|
|
1183
|
+
checked={all_page_selected}
|
|
1184
|
+
onchange={toggle_select_all}
|
|
1185
|
+
/>
|
|
734
1186
|
</th>
|
|
735
1187
|
{/if}
|
|
736
|
-
{#
|
|
1188
|
+
{#if show_row_numbers}
|
|
1189
|
+
<th class="row-num-col">#</th>
|
|
1190
|
+
{/if}
|
|
1191
|
+
{#each visible_columns as col (get_col_id(col))}
|
|
737
1192
|
{@const col_id = get_col_id(col)}
|
|
738
1193
|
{@const drag_side = drag_over_col_id === col_id
|
|
739
1194
|
? get_drag_side(col_id)
|
|
@@ -743,6 +1198,20 @@ let hint_config = $derived(sort_hint
|
|
|
743
1198
|
title={col.description}
|
|
744
1199
|
tabindex={col.sortable === false ? undefined : 0}
|
|
745
1200
|
role={col.sortable === false ? undefined : `button`}
|
|
1201
|
+
oncontextmenu={(event) => {
|
|
1202
|
+
if (
|
|
1203
|
+
!allow_better_toggle || col.color_scale === null ||
|
|
1204
|
+
col.color_scale === undefined
|
|
1205
|
+
) return
|
|
1206
|
+
event.preventDefault()
|
|
1207
|
+
event.stopPropagation()
|
|
1208
|
+
context_menu_col = col_id
|
|
1209
|
+
const rect = container_el?.getBoundingClientRect()
|
|
1210
|
+
context_menu_pos = {
|
|
1211
|
+
x: event.clientX - (rect?.left ?? 0),
|
|
1212
|
+
y: event.clientY - (rect?.top ?? 0),
|
|
1213
|
+
}
|
|
1214
|
+
}}
|
|
746
1215
|
onclick={(event) => {
|
|
747
1216
|
if (!drag_col_id && !resize_col_id) {
|
|
748
1217
|
sort_rows(
|
|
@@ -788,8 +1257,12 @@ let hint_config = $derived(sort_hint
|
|
|
788
1257
|
event.currentTarget.removeAttribute(`aria-grabbed`)
|
|
789
1258
|
}}
|
|
790
1259
|
>
|
|
791
|
-
{
|
|
792
|
-
|
|
1260
|
+
{#if header_cell}
|
|
1261
|
+
{@render header_cell({ col })}
|
|
1262
|
+
{:else}
|
|
1263
|
+
{@html sanitize_html(col.label)}
|
|
1264
|
+
{/if}
|
|
1265
|
+
{@html sanitize_html(sort_indicator(col, sort_state))}
|
|
793
1266
|
<!-- Column resize handle -->
|
|
794
1267
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
795
1268
|
<span
|
|
@@ -806,14 +1279,32 @@ let hint_config = $derived(sort_hint
|
|
|
806
1279
|
</tr>
|
|
807
1280
|
</thead>
|
|
808
1281
|
<tbody>
|
|
809
|
-
{#each paginated_data as row (get_row_id(row))}
|
|
1282
|
+
{#each paginated_data as row, row_idx (get_row_id(row))}
|
|
810
1283
|
{@const row_selected = show_row_select && is_row_selected(row)}
|
|
811
1284
|
<tr
|
|
812
1285
|
animate:flip={{ duration: 500 }}
|
|
813
1286
|
style={row.style}
|
|
814
1287
|
class={row.class ?? ``}
|
|
815
1288
|
class:selected={row_selected}
|
|
1289
|
+
tabindex={onrowclick ? 0 : undefined}
|
|
1290
|
+
onclick={onrowclick ? (event) => onrowclick(event, row) : undefined}
|
|
816
1291
|
ondblclick={onrowdblclick ? (event) => onrowdblclick(event, row) : undefined}
|
|
1292
|
+
onkeydown={onrowclick
|
|
1293
|
+
? (event) => {
|
|
1294
|
+
if (event.key === `Enter` || event.key === ` `) {
|
|
1295
|
+
event.preventDefault()
|
|
1296
|
+
onrowclick(event, row)
|
|
1297
|
+
} else if (event.key === `ArrowDown`) {
|
|
1298
|
+
event.preventDefault()
|
|
1299
|
+
const next = event.currentTarget.nextElementSibling
|
|
1300
|
+
if (next instanceof HTMLElement) next.focus()
|
|
1301
|
+
} else if (event.key === `ArrowUp`) {
|
|
1302
|
+
event.preventDefault()
|
|
1303
|
+
const prev = event.currentTarget.previousElementSibling
|
|
1304
|
+
if (prev instanceof HTMLElement) prev.focus()
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
: undefined}
|
|
817
1308
|
>
|
|
818
1309
|
{#if show_row_select}
|
|
819
1310
|
<td class="select-col">
|
|
@@ -824,7 +1315,12 @@ let hint_config = $derived(sort_hint
|
|
|
824
1315
|
/>
|
|
825
1316
|
</td>
|
|
826
1317
|
{/if}
|
|
827
|
-
{#
|
|
1318
|
+
{#if show_row_numbers}
|
|
1319
|
+
<td class="row-num-col">
|
|
1320
|
+
{(current_page - 1) * effective_page_size + row_idx + 1}
|
|
1321
|
+
</td>
|
|
1322
|
+
{/if}
|
|
1323
|
+
{#each visible_columns as col (get_col_id(col))}
|
|
828
1324
|
{@const val = row[get_col_id(col)]}
|
|
829
1325
|
{@const color = calc_color(val, col)}
|
|
830
1326
|
{@const col_width = column_widths[get_col_id(col)]}
|
|
@@ -851,13 +1347,29 @@ let hint_config = $derived(sort_hint
|
|
|
851
1347
|
n/a
|
|
852
1348
|
</span>
|
|
853
1349
|
{:else}
|
|
854
|
-
{@html val}
|
|
1350
|
+
{@html sanitize_html(val)}
|
|
855
1351
|
{/if}
|
|
856
1352
|
</td>
|
|
857
1353
|
{/each}
|
|
858
1354
|
</tr>
|
|
1355
|
+
{:else}
|
|
1356
|
+
{#if empty_message}
|
|
1357
|
+
<tr class="empty-row">
|
|
1358
|
+
<td
|
|
1359
|
+
colspan={visible_columns.length + (show_row_select ? 1 : 0) +
|
|
1360
|
+
(show_row_numbers ? 1 : 0)}
|
|
1361
|
+
>
|
|
1362
|
+
{empty_message}
|
|
1363
|
+
</td>
|
|
1364
|
+
</tr>
|
|
1365
|
+
{/if}
|
|
859
1366
|
{/each}
|
|
860
1367
|
</tbody>
|
|
1368
|
+
{#if footer}
|
|
1369
|
+
<tfoot>
|
|
1370
|
+
{@render footer()}
|
|
1371
|
+
</tfoot>
|
|
1372
|
+
{/if}
|
|
861
1373
|
</table>
|
|
862
1374
|
</div>
|
|
863
1375
|
|
|
@@ -914,8 +1426,47 @@ let hint_config = $derived(sort_hint
|
|
|
914
1426
|
>
|
|
915
1427
|
»
|
|
916
1428
|
</button>
|
|
1429
|
+
{#if pagination_config.page_sizes}
|
|
1430
|
+
<select
|
|
1431
|
+
class="page-size-select"
|
|
1432
|
+
onchange={(event) => {
|
|
1433
|
+
effective_page_size = parseInt(event.currentTarget.value, 10)
|
|
1434
|
+
current_page = 1
|
|
1435
|
+
}}
|
|
1436
|
+
>
|
|
1437
|
+
{#each pagination_config.page_sizes as size (size)}
|
|
1438
|
+
<option value={size} selected={size === effective_page_size}>
|
|
1439
|
+
{size} / page
|
|
1440
|
+
</option>
|
|
1441
|
+
{/each}
|
|
1442
|
+
</select>
|
|
1443
|
+
{/if}
|
|
917
1444
|
</div>
|
|
918
1445
|
{/if}
|
|
1446
|
+
|
|
1447
|
+
<ContextMenu
|
|
1448
|
+
sections={better_sections}
|
|
1449
|
+
selected_values={{ 'Gradient direction': better_overrides.get(context_menu_col ?? ``) ?? `` }}
|
|
1450
|
+
position={context_menu_pos}
|
|
1451
|
+
visible={context_menu_col !== null}
|
|
1452
|
+
on_close={() => context_menu_col = null}
|
|
1453
|
+
style={[
|
|
1454
|
+
`--surface-bg: light-dark(#fff, #1e1e1e)`,
|
|
1455
|
+
`--border-color: light-dark(rgba(0,0,0,0.15), rgba(255,255,255,0.15))`,
|
|
1456
|
+
`--text-color: light-dark(#333, #eee)`,
|
|
1457
|
+
`--text-color-muted: light-dark(#888, #999)`,
|
|
1458
|
+
`--surface-bg-hover: light-dark(rgba(0,0,0,0.06), rgba(255,255,255,0.1))`,
|
|
1459
|
+
`--accent-color: light-dark(rgba(0,0,0,0.1), rgba(255,255,255,0.15))`,
|
|
1460
|
+
`z-index: 200`,
|
|
1461
|
+
].join(`; `)}
|
|
1462
|
+
on_select={(_, option) => {
|
|
1463
|
+
if (!context_menu_col) return
|
|
1464
|
+
const current = better_overrides.get(context_menu_col)
|
|
1465
|
+
if (current === option.value) better_overrides.delete(context_menu_col)
|
|
1466
|
+
else better_overrides.set(context_menu_col, option.value as `higher` | `lower`)
|
|
1467
|
+
context_menu_col = null
|
|
1468
|
+
}}
|
|
1469
|
+
/>
|
|
919
1470
|
</div>
|
|
920
1471
|
|
|
921
1472
|
<style>
|
|
@@ -1001,6 +1552,13 @@ let hint_config = $derived(sort_hint
|
|
|
1001
1552
|
tbody tr:hover {
|
|
1002
1553
|
filter: var(--heatmap-row-hover-filter, brightness(1.1));
|
|
1003
1554
|
}
|
|
1555
|
+
tbody tr[tabindex] {
|
|
1556
|
+
cursor: pointer;
|
|
1557
|
+
}
|
|
1558
|
+
tbody tr:focus-visible {
|
|
1559
|
+
outline: 2px solid var(--highlight, #4a9eff);
|
|
1560
|
+
outline-offset: -2px;
|
|
1561
|
+
}
|
|
1004
1562
|
td[data-sort-value] {
|
|
1005
1563
|
cursor: default;
|
|
1006
1564
|
}
|
|
@@ -1018,7 +1576,7 @@ let hint_config = $derived(sort_hint
|
|
|
1018
1576
|
justify-content: flex-end;
|
|
1019
1577
|
align-items: center;
|
|
1020
1578
|
gap: 2px;
|
|
1021
|
-
margin-bottom:
|
|
1579
|
+
margin-bottom: 1px;
|
|
1022
1580
|
opacity: 0;
|
|
1023
1581
|
pointer-events: none;
|
|
1024
1582
|
transition: opacity 0.15s;
|
|
@@ -1029,21 +1587,21 @@ let hint_config = $derived(sort_hint
|
|
|
1029
1587
|
pointer-events: auto;
|
|
1030
1588
|
}
|
|
1031
1589
|
.icon-btn {
|
|
1032
|
-
padding:
|
|
1590
|
+
padding: 2px 4px;
|
|
1033
1591
|
border: none;
|
|
1034
|
-
border-radius:
|
|
1592
|
+
border-radius: 3px;
|
|
1035
1593
|
background: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.1));
|
|
1036
1594
|
color: light-dark(#333, #ddd);
|
|
1037
1595
|
cursor: pointer;
|
|
1038
1596
|
display: flex;
|
|
1039
1597
|
align-items: center;
|
|
1040
1598
|
justify-content: center;
|
|
1041
|
-
gap:
|
|
1042
|
-
font-size: 0.
|
|
1599
|
+
gap: 2px;
|
|
1600
|
+
font-size: 0.8em;
|
|
1043
1601
|
}
|
|
1044
1602
|
.icon-btn :global(svg) {
|
|
1045
|
-
width:
|
|
1046
|
-
height:
|
|
1603
|
+
width: 12px;
|
|
1604
|
+
height: 12px;
|
|
1047
1605
|
}
|
|
1048
1606
|
.icon-btn:hover {
|
|
1049
1607
|
background: light-dark(rgba(0, 0, 0, 0.12), rgba(255, 255, 255, 0.2));
|
|
@@ -1105,13 +1663,13 @@ let hint_config = $derived(sort_hint
|
|
|
1105
1663
|
gap: 6px;
|
|
1106
1664
|
}
|
|
1107
1665
|
.search-input {
|
|
1108
|
-
padding:
|
|
1666
|
+
padding: 2px 4px;
|
|
1109
1667
|
border: 1px solid light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.2));
|
|
1110
|
-
border-radius:
|
|
1668
|
+
border-radius: 3px;
|
|
1111
1669
|
background: light-dark(rgba(255, 255, 255, 0.9), rgba(0, 0, 0, 0.3));
|
|
1112
1670
|
color: light-dark(#333, #eee);
|
|
1113
|
-
font-size: 0.
|
|
1114
|
-
width:
|
|
1671
|
+
font-size: 0.8em;
|
|
1672
|
+
width: 110px;
|
|
1115
1673
|
box-sizing: border-box;
|
|
1116
1674
|
}
|
|
1117
1675
|
.search-input:focus {
|
|
@@ -1218,6 +1776,23 @@ let hint_config = $derived(sort_hint
|
|
|
1218
1776
|
font-size: 0.85em;
|
|
1219
1777
|
}
|
|
1220
1778
|
|
|
1779
|
+
.col-color-row {
|
|
1780
|
+
display: flex;
|
|
1781
|
+
align-items: center;
|
|
1782
|
+
gap: 4px;
|
|
1783
|
+
padding: 2px 0;
|
|
1784
|
+
select {
|
|
1785
|
+
font-size: 0.85em;
|
|
1786
|
+
padding: 1px 2px;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
.col-color-label {
|
|
1790
|
+
flex: 1;
|
|
1791
|
+
overflow: hidden;
|
|
1792
|
+
text-overflow: ellipsis;
|
|
1793
|
+
white-space: nowrap;
|
|
1794
|
+
min-width: 0;
|
|
1795
|
+
}
|
|
1221
1796
|
/* Column resize */
|
|
1222
1797
|
.resize-handle {
|
|
1223
1798
|
position: absolute;
|
|
@@ -1255,4 +1830,25 @@ let hint_config = $derived(sort_hint
|
|
|
1255
1830
|
transform: rotate(360deg);
|
|
1256
1831
|
}
|
|
1257
1832
|
}
|
|
1833
|
+
.empty-row td {
|
|
1834
|
+
text-align: center;
|
|
1835
|
+
padding: 2em !important;
|
|
1836
|
+
color: var(--text-muted, #888);
|
|
1837
|
+
font-style: italic;
|
|
1838
|
+
}
|
|
1839
|
+
.row-num-col {
|
|
1840
|
+
text-align: right;
|
|
1841
|
+
color: var(--text-muted, #888);
|
|
1842
|
+
font-size: 0.85em;
|
|
1843
|
+
width: 2em;
|
|
1844
|
+
padding-right: 8px !important;
|
|
1845
|
+
}
|
|
1846
|
+
.page-size-select {
|
|
1847
|
+
padding: 2px 4px;
|
|
1848
|
+
border: 1px solid light-dark(rgba(0, 0, 0, 0.2), rgba(255, 255, 255, 0.2));
|
|
1849
|
+
border-radius: 3px;
|
|
1850
|
+
background: light-dark(#fff, #333);
|
|
1851
|
+
color: inherit;
|
|
1852
|
+
font-size: 0.9em;
|
|
1853
|
+
}
|
|
1258
1854
|
</style>
|