matterviz 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EmptyState.svelte +10 -4
- package/dist/FilePicker.svelte +12 -14
- package/dist/Icon.svelte +7 -4
- package/dist/MillerIndexInput.svelte +2 -2
- package/dist/app.css +53 -0
- package/dist/brillouin/BrillouinZone.svelte +75 -85
- package/dist/brillouin/BrillouinZone.svelte.d.ts +5 -7
- package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
- package/dist/brillouin/BrillouinZoneExportPane.svelte +18 -14
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +6 -10
- package/dist/brillouin/BrillouinZoneScene.svelte +26 -39
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/chempot-diagram/ChemPotDiagram.svelte +33 -38
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +62 -74
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +1 -1
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +511 -547
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +1 -1
- package/dist/chempot-diagram/async-compute.svelte.js +1 -1
- package/dist/chempot-diagram/controls-state.svelte.js +1 -1
- package/dist/colors/index.d.ts +0 -4
- package/dist/colors/index.js +19 -20
- package/dist/composition/BarChart.svelte +37 -31
- package/dist/composition/BubbleChart.svelte +17 -17
- package/dist/composition/Composition.svelte +7 -11
- package/dist/composition/Formula.svelte +18 -25
- package/dist/composition/FormulaFilter.svelte +92 -95
- package/dist/composition/PieChart.svelte +55 -41
- package/dist/composition/format.js +1 -1
- package/dist/composition/index.d.ts +2 -0
- package/dist/composition/index.js +14 -0
- package/dist/composition/parse.d.ts +2 -2
- package/dist/composition/parse.js +11 -10
- package/dist/constants.js +11 -9
- package/dist/convex-hull/ConvexHull.svelte +12 -15
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +93 -92
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +109 -140
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +75 -94
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullChrome.svelte +36 -40
- package/dist/convex-hull/ConvexHullControls.svelte +46 -48
- package/dist/convex-hull/ConvexHullStats.svelte +193 -195
- package/dist/convex-hull/ConvexHullTooltip.svelte +5 -12
- package/dist/convex-hull/GasPressureControls.svelte +5 -12
- package/dist/convex-hull/StructurePopup.svelte +17 -21
- package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
- package/dist/convex-hull/TemperatureSlider.svelte +4 -10
- package/dist/convex-hull/canvas-interactions.svelte.js +2 -1
- package/dist/convex-hull/helpers.d.ts +8 -10
- package/dist/convex-hull/helpers.js +36 -36
- package/dist/convex-hull/hull-state.svelte.d.ts +1 -1
- package/dist/convex-hull/thermodynamics.js +18 -17
- package/dist/coordination/CoordinationBarPlot.svelte +38 -33
- package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
- package/dist/coordination/calc-coordination.d.ts +1 -1
- package/dist/coordination/calc-coordination.js +28 -25
- package/dist/element/BohrAtom.svelte +5 -8
- package/dist/element/ElementHeading.svelte +4 -1
- package/dist/element/ElementPhoto.svelte +11 -9
- package/dist/element/ElementStats.svelte +12 -10
- package/dist/element/ElementTile.svelte +24 -29
- package/dist/element/Nucleus.svelte +4 -2
- package/dist/element/data.d.ts +1 -2
- package/dist/element/helpers.d.ts +4 -0
- package/dist/element/helpers.js +18 -0
- package/dist/element/index.d.ts +1 -4
- package/dist/element/index.js +3 -18
- package/dist/feedback/DragOverlay.svelte +5 -5
- package/dist/feedback/DragOverlay.svelte.d.ts +2 -2
- package/dist/feedback/StatusMessage.svelte +7 -4
- package/dist/fermi-surface/FermiSlice.svelte +21 -18
- package/dist/fermi-surface/FermiSurface.svelte +14 -22
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +5 -7
- package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +37 -64
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
- package/dist/fermi-surface/compute.js +3 -3
- package/dist/fermi-surface/parse.js +22 -24
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +109 -148
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +2 -2
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +9 -7
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
- package/dist/heatmap-matrix/index.d.ts +5 -0
- package/dist/icons.d.ts +4 -0
- package/dist/icons.js +4 -0
- package/dist/io/ExportPane.svelte +10 -6
- package/dist/io/decompress.d.ts +2 -1
- package/dist/io/decompress.js +22 -35
- package/dist/io/export.d.ts +4 -0
- package/dist/io/export.js +5 -1
- package/dist/io/fetch.d.ts +0 -3
- package/dist/io/fetch.js +0 -11
- package/dist/io/index.d.ts +0 -1
- package/dist/io/index.js +2 -9
- package/dist/io/is-binary.d.ts +9 -0
- package/dist/io/is-binary.js +45 -1
- package/dist/io/url-drop.js +13 -25
- package/dist/isosurface/Isosurface.svelte +17 -23
- package/dist/isosurface/IsosurfaceControls.svelte +37 -42
- package/dist/isosurface/parse-vaspwave.d.ts +3 -0
- package/dist/isosurface/parse-vaspwave.js +138 -0
- package/dist/isosurface/parse.js +6 -6
- package/dist/labels.d.ts +6 -2
- package/dist/labels.js +43 -13
- package/dist/layout/FullscreenButton.svelte +2 -1
- package/dist/layout/FullscreenToggle.svelte +10 -4
- package/dist/layout/InfoCard.svelte +7 -18
- package/dist/layout/InfoTag.svelte +1 -3
- package/dist/layout/NumberRangeInput.svelte +46 -0
- package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
- package/dist/layout/PropertyFilter.svelte +2 -6
- package/dist/layout/SettingsSection.svelte +21 -17
- package/dist/layout/SubpageGrid.svelte +3 -1
- package/dist/layout/ViewerChrome.svelte +9 -10
- package/dist/layout/ViewerChrome.svelte.d.ts +2 -1
- package/dist/layout/fullscreen.js +3 -5
- package/dist/layout/fullscreen.svelte.js +1 -2
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.js +1 -0
- package/dist/layout/json-tree/JsonNode.svelte +8 -11
- package/dist/layout/json-tree/JsonTree.svelte +53 -77
- package/dist/layout/json-tree/JsonValue.svelte +7 -15
- package/dist/layout/json-tree/utils.js +14 -23
- package/dist/math.d.ts +2 -0
- package/dist/math.js +19 -0
- package/dist/overlays/ContextMenu.svelte +4 -2
- package/dist/overlays/CopyButton.svelte +11 -5
- package/dist/overlays/DragControlTab.svelte +6 -1
- package/dist/overlays/DraggablePane.svelte +81 -26
- package/dist/overlays/DraggablePane.svelte.d.ts +1 -0
- package/dist/overlays/GlassChip.svelte +30 -0
- package/dist/overlays/GlassChip.svelte.d.ts +8 -0
- package/dist/overlays/InfoPaneCards.svelte +11 -23
- package/dist/overlays/index.d.ts +7 -0
- package/dist/overlays/index.js +26 -0
- package/dist/overlays/portal.d.ts +2 -0
- package/dist/overlays/portal.js +14 -0
- package/dist/periodic-table/PeriodicTable.svelte +113 -145
- package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
- package/dist/periodic-table/PeriodicTableControls.svelte +11 -63
- package/dist/periodic-table/TableInset.svelte +9 -4
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +50 -71
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
- package/dist/phase-diagram/PhaseDiagramControls.svelte +70 -94
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +12 -12
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
- package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
- package/dist/phase-diagram/build-diagram.js +3 -3
- package/dist/phase-diagram/diagram-input.d.ts +1 -2
- package/dist/phase-diagram/parse.js +15 -15
- package/dist/phase-diagram/svg-to-diagram.js +21 -15
- package/dist/phase-diagram/utils.js +2 -3
- package/dist/plot/bar/BarPlot.svelte +430 -391
- package/dist/plot/bar/BarPlot.svelte.d.ts +2 -0
- package/dist/plot/bar/BarPlotControls.svelte +1 -3
- package/dist/plot/bar/BarPlotControls.svelte.d.ts +4 -6
- package/dist/plot/bar/SpacegroupBarPlot.svelte +29 -36
- package/dist/plot/bar/data.d.ts +5 -4
- package/dist/plot/bar/data.js +2 -1
- package/dist/plot/box/BoxPlot.svelte +301 -209
- package/dist/plot/box/BoxPlot.svelte.d.ts +2 -0
- package/dist/plot/box/Violin.svelte.d.ts +1 -0
- package/dist/plot/box/box-plot.js +1 -1
- package/dist/plot/core/auto-place.js +1 -1
- package/dist/plot/core/axis-utils.d.ts +12 -7
- package/dist/plot/core/axis-utils.js +48 -48
- package/dist/plot/core/components/AxisLabel.svelte +82 -20
- package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
- package/dist/plot/core/components/ColorBar.svelte +101 -106
- package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
- package/dist/plot/core/components/FillArea.svelte +10 -17
- package/dist/plot/core/components/HierarchyControls.svelte +249 -0
- package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
- package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
- package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
- package/dist/plot/core/components/Line.svelte +18 -9
- package/dist/plot/core/components/Line.svelte.d.ts +2 -0
- package/dist/plot/core/components/PlotAxis.svelte +40 -17
- package/dist/plot/core/components/PlotControls.svelte +65 -41
- package/dist/plot/core/components/PlotLegend.svelte +31 -35
- package/dist/plot/core/components/PlotMarginals.svelte +616 -0
- package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
- package/dist/plot/core/components/PlotTooltip.svelte +1 -1
- package/dist/plot/core/components/PortalSelect.svelte +4 -8
- package/dist/plot/core/components/ReferenceLine.svelte +27 -24
- package/dist/plot/core/components/ReferenceLine3D.svelte +54 -57
- package/dist/plot/core/components/ReferencePlane.svelte +7 -8
- package/dist/plot/core/components/ZeroLines.svelte +4 -8
- package/dist/plot/core/components/ZoomRect.svelte +1 -2
- package/dist/plot/core/components/index.d.ts +1 -0
- package/dist/plot/core/components/index.js +1 -0
- package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
- package/dist/plot/core/data-cleaning-signal.js +467 -0
- package/dist/plot/core/data-cleaning.d.ts +2 -42
- package/dist/plot/core/data-cleaning.js +27 -474
- package/dist/plot/core/data-transform.js +9 -5
- package/dist/plot/core/fill-utils.d.ts +3 -1
- package/dist/plot/core/fill-utils.js +28 -6
- package/dist/plot/core/index.d.ts +1 -0
- package/dist/plot/core/index.js +1 -0
- package/dist/plot/core/interactions.js +0 -2
- package/dist/plot/core/layout.d.ts +3 -1
- package/dist/plot/core/layout.js +22 -13
- package/dist/plot/core/marginals.d.ts +170 -0
- package/dist/plot/core/marginals.js +436 -0
- package/dist/plot/core/reference-line.js +1 -1
- package/dist/plot/core/scales.d.ts +23 -0
- package/dist/plot/core/scales.js +39 -13
- package/dist/plot/core/types/fills.d.ts +101 -0
- package/dist/plot/core/types/fills.js +15 -0
- package/dist/plot/core/types/plot-3d.d.ts +87 -0
- package/dist/plot/core/types/plot-3d.js +4 -0
- package/dist/plot/core/types/reference-lines.d.ts +140 -0
- package/dist/plot/core/types/reference-lines.js +9 -0
- package/dist/plot/core/types.d.ts +29 -324
- package/dist/plot/core/types.js +23 -20
- package/dist/plot/core/utils/hierarchy-chart.d.ts +51 -0
- package/dist/plot/core/utils/hierarchy-chart.js +179 -0
- package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
- package/dist/plot/core/utils/hierarchy-labels.js +99 -0
- package/dist/plot/core/utils/label-placement.js +2 -2
- package/dist/plot/histogram/Histogram.svelte +304 -325
- package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
- package/dist/plot/histogram/HistogramControls.svelte +30 -28
- package/dist/plot/histogram/histogram.d.ts +31 -0
- package/dist/plot/histogram/histogram.js +70 -0
- package/dist/plot/histogram/index.d.ts +1 -0
- package/dist/plot/histogram/index.js +1 -0
- package/dist/plot/index.d.ts +1 -0
- package/dist/plot/index.js +1 -0
- package/dist/plot/sankey/Sankey.svelte +78 -69
- package/dist/plot/sankey/Sankey.svelte.d.ts +2 -2
- package/dist/plot/sankey/SankeyControls.svelte +22 -16
- package/dist/plot/scatter/BinnedScatterPlot.svelte +263 -109
- package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +7 -0
- package/dist/plot/scatter/ElementScatter.svelte +4 -1
- package/dist/plot/scatter/ScatterPlot.svelte +477 -385
- package/dist/plot/scatter/ScatterPlot.svelte.d.ts +2 -0
- package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
- package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/scatter/ScatterPoint.svelte +16 -9
- package/dist/plot/scatter/binned-scatter-types.d.ts +3 -2
- package/dist/plot/scatter/binned-scatter-types.js +6 -1
- package/dist/plot/scatter/scatter-data.d.ts +1 -1
- package/dist/plot/scatter/scatter-data.js +8 -7
- package/dist/plot/scatter-3d/ScatterPlot3D.svelte +25 -34
- package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +8 -5
- package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +113 -116
- package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/scatter-3d/Surface3D.svelte +4 -13
- package/dist/plot/sunburst/Sunburst.svelte +251 -329
- package/dist/plot/sunburst/Sunburst.svelte.d.ts +2 -2
- package/dist/plot/sunburst/index.d.ts +1 -1
- package/dist/plot/sunburst/index.js +2 -1
- package/dist/plot/sunburst/render.d.ts +2 -2
- package/dist/plot/sunburst/render.js +47 -22
- package/dist/plot/sunburst/sunburst.d.ts +5 -1
- package/dist/plot/sunburst/sunburst.js +10 -3
- package/dist/plot/treemap/Treemap.svelte +907 -0
- package/dist/plot/treemap/Treemap.svelte.d.ts +84 -0
- package/dist/plot/treemap/index.d.ts +3 -0
- package/dist/plot/treemap/index.js +4 -0
- package/dist/plot/treemap/treemap.d.ts +26 -0
- package/dist/plot/treemap/treemap.js +99 -0
- package/dist/rdf/RdfPlot.svelte +22 -26
- package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
- package/dist/rdf/calc-rdf.js +4 -8
- package/dist/sanitize.js +1 -1
- package/dist/scene/bind-renderer.svelte.js +14 -6
- package/dist/settings.d.ts +19 -9
- package/dist/settings.js +77 -50
- package/dist/spectral/Bands.svelte +223 -230
- package/dist/spectral/BandsAndDos.svelte +11 -9
- package/dist/spectral/BrillouinBandsDos.svelte +19 -27
- package/dist/spectral/Dos.svelte +70 -73
- package/dist/spectral/helpers.d.ts +3 -3
- package/dist/spectral/helpers.js +25 -35
- package/dist/state.svelte.js +5 -18
- package/dist/structure/Arrow.svelte +5 -18
- package/dist/structure/ArrowInstances.svelte +160 -0
- package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
- package/dist/structure/AtomLegend.svelte +115 -75
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/Bond.svelte +31 -23
- package/dist/structure/CanvasTooltip.svelte +15 -3
- package/dist/structure/CellSelect.svelte +11 -26
- package/dist/structure/Cylinder.svelte +24 -40
- package/dist/structure/InstancedAtoms.svelte +114 -0
- package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
- package/dist/structure/Lattice.svelte +85 -149
- package/dist/structure/SiteLabels.svelte +122 -0
- package/dist/structure/SiteLabels.svelte.d.ts +18 -0
- package/dist/structure/Structure.svelte +473 -419
- package/dist/structure/Structure.svelte.d.ts +4 -2
- package/dist/structure/StructureCarousel.svelte +630 -0
- package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
- package/dist/structure/StructureControls.svelte +157 -399
- package/dist/structure/StructureControls.svelte.d.ts +1 -1
- package/dist/structure/StructureExportPane.svelte +17 -21
- package/dist/structure/StructureInfoPane.svelte +72 -66
- package/dist/structure/StructureInfoPane.svelte.d.ts +1 -1
- package/dist/structure/StructureScene.svelte +467 -476
- package/dist/structure/StructureScene.svelte.d.ts +3 -1
- package/dist/structure/StructureViewport.svelte +386 -0
- package/dist/structure/StructureViewport.svelte.d.ts +63 -0
- package/dist/structure/atom-label-placement.d.ts +16 -12
- package/dist/structure/atom-label-placement.js +70 -37
- package/dist/structure/atom-properties.d.ts +2 -0
- package/dist/structure/atom-properties.js +104 -38
- package/dist/structure/bonding.d.ts +10 -3
- package/dist/structure/bonding.js +84 -28
- package/dist/structure/edit-history.d.ts +6 -0
- package/dist/structure/edit-history.js +27 -0
- package/dist/structure/export.d.ts +1 -0
- package/dist/structure/export.js +39 -31
- package/dist/structure/format-detect.d.ts +1 -0
- package/dist/structure/format-detect.js +22 -0
- package/dist/structure/geometry.d.ts +9 -0
- package/dist/structure/geometry.js +27 -0
- package/dist/structure/index.d.ts +16 -1
- package/dist/structure/index.js +13 -2
- package/dist/structure/measure.d.ts +1 -1
- package/dist/structure/measure.js +6 -12
- package/dist/structure/parse.d.ts +2 -1
- package/dist/structure/parse.js +243 -222
- package/dist/structure/partial-occupancy.js +4 -2
- package/dist/structure/pbc.js +78 -98
- package/dist/structure/polyhedra.d.ts +0 -1
- package/dist/structure/polyhedra.js +3 -18
- package/dist/structure/supercell.d.ts +1 -2
- package/dist/structure/supercell.js +7 -13
- package/dist/symmetry/SymmetryElements.svelte +11 -15
- package/dist/symmetry/SymmetryStats.svelte +56 -68
- package/dist/symmetry/WyckoffTable.svelte +5 -24
- package/dist/symmetry/cell-transform.js +1 -0
- package/dist/symmetry/index.d.ts +1 -0
- package/dist/symmetry/index.js +8 -2
- package/dist/symmetry/spacegroups.js +3 -2
- package/dist/table/HeatmapTable.svelte +1102 -365
- package/dist/table/HeatmapTable.svelte.d.ts +12 -2
- package/dist/table/ToggleMenu.svelte +5 -12
- package/dist/table/index.d.ts +21 -6
- package/dist/table/index.js +34 -21
- package/dist/theme/ThemeControl.svelte +16 -8
- package/dist/theme/index.d.ts +1 -5
- package/dist/theme/index.js +4 -10
- package/dist/theme/themes.mjs +14 -7
- package/dist/tooltip/KCoords.svelte +4 -1
- package/dist/tooltip/TooltipContent.svelte +2 -5
- package/dist/trajectory/Trajectory.svelte +515 -308
- package/dist/trajectory/Trajectory.svelte.d.ts +1 -1
- package/dist/trajectory/TrajectoryError.svelte +11 -8
- package/dist/trajectory/TrajectoryExportPane.svelte +14 -31
- package/dist/trajectory/TrajectoryInfoPane.svelte +85 -97
- package/dist/trajectory/extract.js +56 -34
- package/dist/trajectory/format-detect.js +11 -9
- package/dist/trajectory/frame-reader.d.ts +2 -0
- package/dist/trajectory/frame-reader.js +33 -29
- package/dist/trajectory/helpers.d.ts +2 -0
- package/dist/trajectory/helpers.js +18 -7
- package/dist/trajectory/index.d.ts +1 -0
- package/dist/trajectory/index.js +3 -0
- package/dist/trajectory/parse/h5-utils.d.ts +13 -0
- package/dist/trajectory/parse/h5-utils.js +109 -0
- package/dist/trajectory/parse/hdf5.d.ts +1 -1
- package/dist/trajectory/parse/hdf5.js +116 -106
- package/dist/trajectory/parse/index.d.ts +2 -3
- package/dist/trajectory/parse/index.js +17 -20
- package/dist/trajectory/parse/lammps.js +8 -12
- package/dist/trajectory/parse/vasp.js +10 -7
- package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
- package/dist/trajectory/parse/vaspout-electronic.js +207 -0
- package/dist/trajectory/parse/vaspout-h5.d.ts +5 -0
- package/dist/trajectory/parse/vaspout-h5.js +284 -0
- package/dist/trajectory/parse/xyz.js +11 -7
- package/dist/trajectory/plotting.d.ts +8 -9
- package/dist/trajectory/plotting.js +99 -27
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +7 -0
- package/dist/xrd/XrdPlot.svelte +47 -61
- package/dist/xrd/broadening.js +2 -1
- package/dist/xrd/calc-xrd.js +2 -4
- package/dist/xrd/parse.d.ts +0 -1
- package/dist/xrd/parse.js +56 -73
- package/package.json +61 -33
- package/dist/api/mp.d.ts +0 -6
- package/dist/api/mp.js +0 -22
- package/dist/plot/sunburst/SunburstControls.svelte +0 -200
|
@@ -6,21 +6,25 @@
|
|
|
6
6
|
import { SettingsSection } from '../layout'
|
|
7
7
|
import ContextMenu from '../overlays/ContextMenu.svelte'
|
|
8
8
|
import DraggablePane from '../overlays/DraggablePane.svelte'
|
|
9
|
+
import { portal } from '../overlays/portal'
|
|
9
10
|
import type {
|
|
11
|
+
CellColor,
|
|
10
12
|
CellSnippet,
|
|
11
13
|
CellVal,
|
|
14
|
+
DateTimeFormatMode,
|
|
12
15
|
ExportData,
|
|
13
16
|
InitialSort,
|
|
14
17
|
Label,
|
|
15
18
|
MultiSortState,
|
|
16
19
|
Pagination,
|
|
20
|
+
VirtualScroll,
|
|
17
21
|
RowData,
|
|
18
22
|
Search,
|
|
19
23
|
SortHint,
|
|
20
24
|
SortState,
|
|
21
25
|
SpecialCells,
|
|
22
26
|
} from './'
|
|
23
|
-
import {
|
|
27
|
+
import { make_cell_color_scale, strip_html } from './'
|
|
24
28
|
import { sanitize_html } from '../sanitize'
|
|
25
29
|
import { normalize_unicode_minus } from '../utils'
|
|
26
30
|
import type { Snippet } from 'svelte'
|
|
@@ -84,8 +88,32 @@
|
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
const close_datetime_select_on_outside_pointerdown = (event: PointerEvent) => {
|
|
92
|
+
if (datetime_select_open_col_id === null) return
|
|
93
|
+
if (event.target instanceof Element && event.target.closest(`.datetime-format-control`))
|
|
94
|
+
return
|
|
95
|
+
datetime_select_open_col_id = null
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
const NUMERIC_WITH_ERROR_RE =
|
|
88
|
-
/^([-+−]?(?:\d+\.?\d*|\d*\.\d+)(?:[eE][-+−]?\d+)?)\s*(?:±|\+[-−]|\()/
|
|
99
|
+
/^(?<numeric>[-+−]?(?:\d+\.?\d*|\d*\.\d+)(?:[eE][-+−]?\d+)?)\s*(?:±|\+[-−]|\()/
|
|
100
|
+
const DATA_SORT_VALUE_RE = /data-sort-value="(?<value>[^"]*)"/
|
|
101
|
+
const DATE_ONLY_RE = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/
|
|
102
|
+
const DATE_TIME_RE =
|
|
103
|
+
/^\d{4}-\d{2}-\d{2}(?:[T\s]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/
|
|
104
|
+
type DateTimeColumnKind = `date` | `time` | `datetime`
|
|
105
|
+
const datetime_format_modes_by_kind: Record<DateTimeColumnKind, DateTimeFormatMode[]> = {
|
|
106
|
+
date: [`date`, `relative`],
|
|
107
|
+
time: [`time`],
|
|
108
|
+
datetime: [`date`, `time`, `datetime`, `iso`, `relative`],
|
|
109
|
+
}
|
|
110
|
+
const datetime_format_labels: Record<DateTimeFormatMode, string> = {
|
|
111
|
+
date: `Date`,
|
|
112
|
+
time: `Time`,
|
|
113
|
+
datetime: `Date + time`,
|
|
114
|
+
iso: `ISO`,
|
|
115
|
+
relative: `Since now`,
|
|
116
|
+
}
|
|
89
117
|
|
|
90
118
|
const parse_numeric_string = (val: string): number | null => {
|
|
91
119
|
const numeric_str = val.match(NUMERIC_WITH_ERROR_RE)?.[1] ?? val
|
|
@@ -94,14 +122,18 @@
|
|
|
94
122
|
return isNaN(num) ? null : num
|
|
95
123
|
}
|
|
96
124
|
|
|
125
|
+
const get_data_sort_value = (val: string): string | null =>
|
|
126
|
+
val.match(DATA_SORT_VALUE_RE)?.groups?.value ?? null
|
|
127
|
+
|
|
97
128
|
// Get sort value from a cell (handles HTML data-sort-value and numbers with errors)
|
|
98
129
|
const get_sort_val = (val: CellVal): string | number => {
|
|
130
|
+
if (val instanceof Date) return val.getTime()
|
|
99
131
|
if (typeof val === `string`) {
|
|
100
132
|
// Check for HTML data-sort-value attribute first
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
const num = Number(
|
|
104
|
-
return isNaN(num) ?
|
|
133
|
+
const sort_attr = get_data_sort_value(val)
|
|
134
|
+
if (sort_attr != null) {
|
|
135
|
+
const num = Number(sort_attr)
|
|
136
|
+
return isNaN(num) ? sort_attr : num
|
|
105
137
|
}
|
|
106
138
|
const num = parse_numeric_string(val)
|
|
107
139
|
if (num !== null) return num
|
|
@@ -109,6 +141,9 @@
|
|
|
109
141
|
return val as string | number
|
|
110
142
|
}
|
|
111
143
|
|
|
144
|
+
const get_cell_sort_attr = (val: CellVal): CellVal | number | null =>
|
|
145
|
+
is_html_str(val) ? null : val instanceof Date ? val.getTime() : val
|
|
146
|
+
|
|
112
147
|
let {
|
|
113
148
|
data = $bindable([]),
|
|
114
149
|
columns = [],
|
|
@@ -122,14 +157,20 @@
|
|
|
122
157
|
default_num_format = `.3`,
|
|
123
158
|
show_heatmap = $bindable(true),
|
|
124
159
|
heatmap_class = `heatmap`,
|
|
160
|
+
onrowpointerdown,
|
|
125
161
|
onrowclick,
|
|
126
162
|
onrowdblclick,
|
|
163
|
+
row_title,
|
|
127
164
|
column_order = $bindable([]),
|
|
128
165
|
export_data = false,
|
|
129
166
|
show_column_toggle = false,
|
|
130
167
|
search = false,
|
|
168
|
+
search_query = $bindable(``),
|
|
131
169
|
show_row_select = false,
|
|
132
170
|
pagination = false,
|
|
171
|
+
virtual = false,
|
|
172
|
+
on_visible_range,
|
|
173
|
+
controls_target = undefined,
|
|
133
174
|
selected_rows = $bindable([]),
|
|
134
175
|
hidden_columns = $bindable([]),
|
|
135
176
|
scroll_style,
|
|
@@ -160,8 +201,12 @@
|
|
|
160
201
|
default_num_format?: string
|
|
161
202
|
show_heatmap?: boolean
|
|
162
203
|
heatmap_class?: string
|
|
204
|
+
onrowpointerdown?: (event: PointerEvent, row: RowData) => void
|
|
163
205
|
onrowclick?: (event: MouseEvent | KeyboardEvent, row: RowData) => void
|
|
164
206
|
onrowdblclick?: (event: MouseEvent, row: RowData) => void
|
|
207
|
+
// Per-row hover tooltip content (rendered via the table tooltip
|
|
208
|
+
// attachment; `\r` breaks lines, HTML must be pre-escaped by the caller)
|
|
209
|
+
row_title?: (row: RowData) => string | null | undefined
|
|
165
210
|
// Array of column IDs to control display order. IDs are derived as:
|
|
166
211
|
// - Ungrouped columns: col.key ?? col.label
|
|
167
212
|
// - Grouped columns: `${col.key ?? col.label} (${col.group})`
|
|
@@ -170,8 +215,22 @@
|
|
|
170
215
|
export_data?: ExportData
|
|
171
216
|
show_column_toggle?: boolean
|
|
172
217
|
search?: Search
|
|
218
|
+
// Current search/filter query. Bindable so parents can control or persist it.
|
|
219
|
+
search_query?: string
|
|
173
220
|
show_row_select?: boolean
|
|
174
221
|
pagination?: Pagination
|
|
222
|
+
// Opt-in infinite-scroll row virtualization. Renders only the rows near the
|
|
223
|
+
// viewport plus spacer rows, so DOM size stays bounded for any data length.
|
|
224
|
+
// Inactive when pagination is enabled. Off by default (every row renders);
|
|
225
|
+
// pass true (or a config object) to enable.
|
|
226
|
+
virtual?: VirtualScroll
|
|
227
|
+
// Notifies the parent which slice of the sorted+filtered rows is rendered
|
|
228
|
+
// (e.g. to progressively fetch more data as the user scrolls near the end).
|
|
229
|
+
on_visible_range?: (range: { start: number; end: number; total: number }) => void
|
|
230
|
+
// Host element to render the search/export/settings buttons into (e.g. an
|
|
231
|
+
// embedding panel's own header) instead of a row above the table. When set,
|
|
232
|
+
// the buttons are always visible; when unset they render inline as usual.
|
|
233
|
+
controls_target?: HTMLElement | null
|
|
175
234
|
selected_rows?: RowData[]
|
|
176
235
|
hidden_columns?: string[]
|
|
177
236
|
scroll_style?: string
|
|
@@ -215,8 +274,7 @@
|
|
|
215
274
|
if (!container_el) return
|
|
216
275
|
const read_page_bg = () => {
|
|
217
276
|
if (!container_el) return
|
|
218
|
-
const page_bg = getComputedStyle(container_el).getPropertyValue(`--page-bg`)
|
|
219
|
-
.trim()
|
|
277
|
+
const page_bg = getComputedStyle(container_el).getPropertyValue(`--page-bg`).trim()
|
|
220
278
|
page_bg_lum = luminance(page_bg || `white`)
|
|
221
279
|
}
|
|
222
280
|
read_page_bg()
|
|
@@ -251,17 +309,19 @@
|
|
|
251
309
|
: null,
|
|
252
310
|
)
|
|
253
311
|
|
|
254
|
-
// Mutable page size
|
|
312
|
+
// Mutable page size: user can change it, but parent pagination.page_size changes still resync.
|
|
255
313
|
let effective_page_size = $derived(pagination_config?.page_size ?? 25)
|
|
256
314
|
|
|
257
315
|
// Normalize search config
|
|
258
316
|
let search_config = $derived(
|
|
259
317
|
search
|
|
260
318
|
? {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
319
|
+
placeholder: `Filter...`,
|
|
320
|
+
expanded: false,
|
|
321
|
+
keys: undefined as string[] | undefined,
|
|
322
|
+
fuzzy: false,
|
|
323
|
+
...(typeof search === `object` ? search : {}),
|
|
324
|
+
}
|
|
265
325
|
: null,
|
|
266
326
|
)
|
|
267
327
|
|
|
@@ -271,10 +331,10 @@
|
|
|
271
331
|
let export_config = $derived(
|
|
272
332
|
export_data
|
|
273
333
|
? {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
334
|
+
formats: default_formats,
|
|
335
|
+
filename: `table-export`,
|
|
336
|
+
...(typeof export_data === `object` ? export_data : {}),
|
|
337
|
+
}
|
|
278
338
|
: null,
|
|
279
339
|
)
|
|
280
340
|
|
|
@@ -282,16 +342,13 @@
|
|
|
282
342
|
// This ensures immediate sorting on first render without waiting for effects
|
|
283
343
|
let sort_state = $derived<SortState>({
|
|
284
344
|
column: sort.column || initial_sort_config?.column || ``,
|
|
285
|
-
ascending: sort.column
|
|
286
|
-
? sort.dir !== `desc`
|
|
287
|
-
: initial_sort_config?.direction !== `desc`,
|
|
345
|
+
ascending: sort.column ? sort.dir !== `desc` : initial_sort_config?.direction !== `desc`,
|
|
288
346
|
})
|
|
289
347
|
|
|
290
348
|
// Multi-column sort state (for Shift+click)
|
|
291
349
|
let multi_sort = $state<MultiSortState>([])
|
|
292
350
|
|
|
293
|
-
// Search/filter state
|
|
294
|
-
let search_query = $state(``)
|
|
351
|
+
// Search/filter state (query itself is the bindable search_query prop)
|
|
295
352
|
let search_expanded = $derived(search_config?.expanded ?? false)
|
|
296
353
|
|
|
297
354
|
// Pagination state
|
|
@@ -307,6 +364,10 @@
|
|
|
307
364
|
// Per-column color scale overrides
|
|
308
365
|
let color_scale_overrides = new SvelteMap<string, string>()
|
|
309
366
|
|
|
367
|
+
// Per-column date/time display overrides (user-toggled via header)
|
|
368
|
+
let datetime_format_overrides = new SvelteMap<string, DateTimeFormatMode>()
|
|
369
|
+
let datetime_select_open_col_id = $state<string | null>(null)
|
|
370
|
+
|
|
310
371
|
const color_scale_options = [
|
|
311
372
|
`interpolateViridis`,
|
|
312
373
|
`interpolatePlasma`,
|
|
@@ -321,9 +382,7 @@
|
|
|
321
382
|
|
|
322
383
|
// Columns that have a color gradient
|
|
323
384
|
let colored_columns = $derived(
|
|
324
|
-
columns.filter((col) =>
|
|
325
|
-
col.color_scale !== null && col.color_scale !== undefined
|
|
326
|
-
),
|
|
385
|
+
columns.filter((col) => col.color_scale !== null && col.color_scale !== undefined),
|
|
327
386
|
)
|
|
328
387
|
|
|
329
388
|
// Column resize state
|
|
@@ -347,6 +406,169 @@
|
|
|
347
406
|
// Helper to make column IDs (needed since column labels in different groups can be repeated)
|
|
348
407
|
const get_col_id = (col: Label) =>
|
|
349
408
|
col.group ? `${col.key ?? col.label} (${col.group})` : (col.key ?? col.label)
|
|
409
|
+
const get_datetime_label_id = (col_id: string) =>
|
|
410
|
+
`datetime-format-label-${encodeURIComponent(col_id)}`
|
|
411
|
+
|
|
412
|
+
const has_explicit_datetime_format = (col: Label): boolean =>
|
|
413
|
+
col.format_type === `datetime` || Boolean(col.datetime_format)
|
|
414
|
+
|
|
415
|
+
const normalize_timestamp = (val: number): number | null => {
|
|
416
|
+
if (!Number.isFinite(val)) return null
|
|
417
|
+
const abs = Math.abs(val)
|
|
418
|
+
if (abs >= 1_000_000_000_000 && abs < 100_000_000_000_000) return val
|
|
419
|
+
if (abs >= 1_000_000_000 && abs < 1_000_000_000_000) return val * 1000
|
|
420
|
+
return null
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const parse_datetime_string = (val: string): number | null => {
|
|
424
|
+
const clean = strip_html(val).trim()
|
|
425
|
+
const date_only = clean.match(DATE_ONLY_RE)
|
|
426
|
+
if (date_only?.groups) {
|
|
427
|
+
const year = Number(date_only.groups.year)
|
|
428
|
+
const month = Number(date_only.groups.month)
|
|
429
|
+
const day = Number(date_only.groups.day)
|
|
430
|
+
return new Date(year, month - 1, day).getTime()
|
|
431
|
+
}
|
|
432
|
+
if (!DATE_TIME_RE.test(clean)) return null
|
|
433
|
+
const parsed = Date.parse(
|
|
434
|
+
clean.replace(` `, `T`).replace(/\.(?<millis>\d{3})\d+/, `.$<millis>`),
|
|
435
|
+
)
|
|
436
|
+
return Number.isNaN(parsed) ? null : parsed
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const value_datetime_kind = (val: CellVal, col: Label): DateTimeColumnKind | null => {
|
|
440
|
+
if (typeof val === `string` && DATE_ONLY_RE.test(strip_html(val).trim())) {
|
|
441
|
+
return `date`
|
|
442
|
+
}
|
|
443
|
+
return parse_datetime_val(val, col) === null ? null : `datetime`
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const parse_datetime_val = (val: CellVal, col: Label): number | null => {
|
|
447
|
+
if (val instanceof Date) return Number.isNaN(val.getTime()) ? null : val.getTime()
|
|
448
|
+
if (typeof val === `number`) {
|
|
449
|
+
return has_explicit_datetime_format(col) ? normalize_timestamp(val) : null
|
|
450
|
+
}
|
|
451
|
+
if (typeof val !== `string`) return null
|
|
452
|
+
|
|
453
|
+
const parsed_text = parse_datetime_string(val)
|
|
454
|
+
if (parsed_text !== null) return parsed_text
|
|
455
|
+
if (!has_explicit_datetime_format(col)) return null
|
|
456
|
+
|
|
457
|
+
const sort_attr = get_data_sort_value(val)
|
|
458
|
+
return normalize_timestamp(Number(sort_attr ?? strip_html(val).trim()))
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const infer_datetime_column_kind = (col: Label): DateTimeColumnKind | null => {
|
|
462
|
+
if (col.datetime_format === `date`) return `date`
|
|
463
|
+
if (col.datetime_format === `time`) return `time`
|
|
464
|
+
if (col.datetime_format || col.format_type === `datetime`) return `datetime`
|
|
465
|
+
|
|
466
|
+
const col_id = get_col_id(col)
|
|
467
|
+
let has_date_value = false
|
|
468
|
+
for (const row of data.slice(0, 25)) {
|
|
469
|
+
const kind = value_datetime_kind(row[col_id], col)
|
|
470
|
+
if (kind === `datetime`) return `datetime`
|
|
471
|
+
if (kind === `date`) has_date_value = true
|
|
472
|
+
}
|
|
473
|
+
if (has_date_value) return `date`
|
|
474
|
+
return null
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
let datetime_column_kinds = $derived.by(() => {
|
|
478
|
+
const kinds = new SvelteMap<string, DateTimeColumnKind>()
|
|
479
|
+
for (const col of columns) {
|
|
480
|
+
const kind = infer_datetime_column_kind(col)
|
|
481
|
+
if (kind) kinds.set(get_col_id(col), kind)
|
|
482
|
+
}
|
|
483
|
+
return kinds
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
const is_datetime_column = (col: Label): boolean =>
|
|
487
|
+
datetime_column_kinds.has(get_col_id(col))
|
|
488
|
+
|
|
489
|
+
const datetime_column_kind = (col: Label): DateTimeColumnKind =>
|
|
490
|
+
datetime_column_kinds.get(get_col_id(col)) ?? `datetime`
|
|
491
|
+
|
|
492
|
+
const datetime_format_options = (col: Label): DateTimeFormatMode[] =>
|
|
493
|
+
datetime_format_modes_by_kind[datetime_column_kind(col)]
|
|
494
|
+
|
|
495
|
+
const datetime_mode = (col: Label): DateTimeFormatMode => {
|
|
496
|
+
const options = datetime_format_options(col)
|
|
497
|
+
const selected =
|
|
498
|
+
datetime_format_overrides.get(get_col_id(col)) ??
|
|
499
|
+
col.datetime_format ??
|
|
500
|
+
datetime_column_kind(col)
|
|
501
|
+
return options.includes(selected) ? selected : options[0]
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function set_datetime_format(col: Label, mode: DateTimeFormatMode) {
|
|
505
|
+
if (datetime_format_options(col).includes(mode)) {
|
|
506
|
+
datetime_format_overrides.set(get_col_id(col), mode)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const pad2 = (val: number): string => String(val).padStart(2, `0`)
|
|
511
|
+
|
|
512
|
+
function format_date(date: Date): string {
|
|
513
|
+
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function format_time(date: Date): string {
|
|
517
|
+
return `${pad2(date.getHours())}:${pad2(date.getMinutes())}`
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function format_datetime(date: Date): string {
|
|
521
|
+
return `${format_date(date)} ${format_time(date)}`
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Ticks once a minute while any column shows relative times, so "Xm ago"
|
|
525
|
+
// cells don't go stale (format granularity is minutes).
|
|
526
|
+
let relative_now_ms = $state(Date.now())
|
|
527
|
+
$effect(() => {
|
|
528
|
+
const shows_relative = columns.some(
|
|
529
|
+
(col) => is_datetime_column(col) && datetime_mode(col) === `relative`,
|
|
530
|
+
)
|
|
531
|
+
if (!shows_relative) return
|
|
532
|
+
relative_now_ms = Date.now() // refresh immediately when relative mode turns on
|
|
533
|
+
const interval = setInterval(() => (relative_now_ms = Date.now()), 60_000)
|
|
534
|
+
return () => clearInterval(interval)
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
function format_since_now(timestamp: number): string {
|
|
538
|
+
const diff = relative_now_ms - timestamp
|
|
539
|
+
let remaining_minutes = Math.max(0, Math.floor(Math.abs(diff) / 60_000))
|
|
540
|
+
const parts: string[] = []
|
|
541
|
+
const units = [
|
|
542
|
+
[`y`, 365 * 24 * 60],
|
|
543
|
+
[`mo`, 30 * 24 * 60],
|
|
544
|
+
[`w`, 7 * 24 * 60],
|
|
545
|
+
[`d`, 24 * 60],
|
|
546
|
+
[`h`, 60],
|
|
547
|
+
[`m`, 1],
|
|
548
|
+
] as const
|
|
549
|
+
|
|
550
|
+
for (const [suffix, minutes_per_unit] of units) {
|
|
551
|
+
const value = Math.floor(remaining_minutes / minutes_per_unit)
|
|
552
|
+
if (value === 0 && parts.length === 0 && suffix !== `m`) continue
|
|
553
|
+
if (value > 0 || suffix === `m`) parts.push(`${value}${suffix}`)
|
|
554
|
+
remaining_minutes -= value * minutes_per_unit
|
|
555
|
+
if (parts.length >= 3) break
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return `${parts.join(` `)} ${diff >= 0 ? `ago` : `from now`}`
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function format_datetime_cell(val: CellVal, col: Label): string | null {
|
|
562
|
+
const timestamp = parse_datetime_val(val, col)
|
|
563
|
+
if (timestamp === null) return null
|
|
564
|
+
const date = new Date(timestamp)
|
|
565
|
+
const mode = datetime_mode(col)
|
|
566
|
+
if (mode === `date`) return format_date(date)
|
|
567
|
+
if (mode === `time`) return format_time(date)
|
|
568
|
+
if (mode === `datetime`) return format_datetime(date)
|
|
569
|
+
if (mode === `iso`) return date.toISOString()
|
|
570
|
+
return format_since_now(timestamp)
|
|
571
|
+
}
|
|
350
572
|
|
|
351
573
|
// Sync column_order with columns: initialize if empty, remove stale IDs, append new IDs
|
|
352
574
|
$effect(() => {
|
|
@@ -369,8 +591,11 @@
|
|
|
369
591
|
// After drag reorder, column_order differs from col_ids (default order) but the
|
|
370
592
|
// computed new_order equals the current column_order — assigning a new array
|
|
371
593
|
// reference would re-trigger this effect endlessly.
|
|
372
|
-
if (
|
|
373
|
-
new_order.
|
|
594
|
+
if (
|
|
595
|
+
new_order.length === column_order.length &&
|
|
596
|
+
new_order.every((id, idx) => id === column_order[idx])
|
|
597
|
+
)
|
|
598
|
+
return
|
|
374
599
|
|
|
375
600
|
column_order = new_order
|
|
376
601
|
})
|
|
@@ -458,23 +683,19 @@
|
|
|
458
683
|
function handle_drop(event: DragEvent, target_col: Label) {
|
|
459
684
|
event.preventDefault()
|
|
460
685
|
|
|
461
|
-
// Block cross-group (or group→ungroup) reorders to preserve group contiguity
|
|
462
|
-
if (!drag_col_id || drag_col_id === get_col_id(target_col)) {
|
|
463
|
-
reset_drag_state()
|
|
464
|
-
return
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Block cross-group reorders to preserve group contiguity
|
|
468
|
-
if (get_drag_col_group() !== target_col.group) {
|
|
469
|
-
reset_drag_state()
|
|
470
|
-
return
|
|
471
|
-
}
|
|
472
|
-
|
|
473
686
|
const target_col_id = get_col_id(target_col)
|
|
474
|
-
const drag_idx = column_order.indexOf(drag_col_id)
|
|
687
|
+
const drag_idx = drag_col_id ? column_order.indexOf(drag_col_id) : -1
|
|
475
688
|
const target_idx = column_order.indexOf(target_col_id)
|
|
476
689
|
|
|
477
|
-
|
|
690
|
+
// Block no-op drops and cross-group (or group→ungroup) reorders to
|
|
691
|
+
// preserve group contiguity
|
|
692
|
+
if (
|
|
693
|
+
!drag_col_id ||
|
|
694
|
+
drag_col_id === target_col_id ||
|
|
695
|
+
get_drag_col_group() !== target_col.group ||
|
|
696
|
+
drag_idx === -1 ||
|
|
697
|
+
target_idx === -1
|
|
698
|
+
) {
|
|
478
699
|
reset_drag_state()
|
|
479
700
|
return
|
|
480
701
|
}
|
|
@@ -490,22 +711,37 @@
|
|
|
490
711
|
reset_drag_state()
|
|
491
712
|
}
|
|
492
713
|
|
|
714
|
+
// True when every char of query appears in text in order (subsequence match),
|
|
715
|
+
// e.g. "mdla" matches "Model A". Cheap fuzzy matching for short filter queries.
|
|
716
|
+
const fuzzy_match = (text: string, query: string): boolean => {
|
|
717
|
+
let query_idx = 0
|
|
718
|
+
for (const char of text) {
|
|
719
|
+
if (char === query[query_idx] && ++query_idx === query.length) return true
|
|
720
|
+
}
|
|
721
|
+
return query.length === 0
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const row_matches_query = (row: RowData, query: string): boolean => {
|
|
725
|
+
const values = search_config?.keys
|
|
726
|
+
? search_config.keys.map((key) => row[key])
|
|
727
|
+
: Object.values(row)
|
|
728
|
+
return values.some((val) => {
|
|
729
|
+
if (val == null) return false
|
|
730
|
+
const clean_val = strip_html(String(val)).toLowerCase()
|
|
731
|
+
if (clean_val.includes(query)) return true
|
|
732
|
+
return (search_config?.fuzzy ?? false) && fuzzy_match(clean_val, query)
|
|
733
|
+
})
|
|
734
|
+
}
|
|
735
|
+
|
|
493
736
|
// Filter data based on search query
|
|
494
737
|
let filtered_data = $derived.by(() => {
|
|
495
|
-
const base_data =
|
|
496
|
-
Object.values(row).some((val) => val !== undefined)
|
|
497
|
-
) ?? []
|
|
498
|
-
|
|
499
|
-
if (!search_query.trim()) return base_data
|
|
738
|
+
const base_data =
|
|
739
|
+
data?.filter?.((row) => Object.values(row).some((val) => val !== undefined)) ?? []
|
|
500
740
|
|
|
501
741
|
const query = search_query.toLowerCase().trim()
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const clean_val = strip_html(String(val)).toLowerCase()
|
|
506
|
-
return clean_val.includes(query)
|
|
507
|
-
})
|
|
508
|
-
)
|
|
742
|
+
if (!query) return base_data
|
|
743
|
+
|
|
744
|
+
return base_data.filter((row) => row_matches_query(row, query))
|
|
509
745
|
})
|
|
510
746
|
|
|
511
747
|
let sorted_data = $derived.by(() => {
|
|
@@ -515,11 +751,8 @@
|
|
|
515
751
|
if (!sort_state.column && multi_sort.length === 0) return filtered_data
|
|
516
752
|
|
|
517
753
|
// Build sort criteria: multi_sort takes precedence, fallback to single sort
|
|
518
|
-
const sort_criteria =
|
|
519
|
-
? multi_sort
|
|
520
|
-
: sort_state.column
|
|
521
|
-
? [sort_state]
|
|
522
|
-
: []
|
|
754
|
+
const sort_criteria =
|
|
755
|
+
multi_sort.length > 0 ? multi_sort : sort_state.column ? [sort_state] : []
|
|
523
756
|
|
|
524
757
|
if (sort_criteria.length === 0) return filtered_data
|
|
525
758
|
|
|
@@ -560,17 +793,91 @@
|
|
|
560
793
|
})
|
|
561
794
|
})
|
|
562
795
|
|
|
563
|
-
//
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
796
|
+
// --- Infinite scroll (virtualized rows). Opt-in via the `virtual` prop (and
|
|
797
|
+
// inactive under pagination): only rows near the viewport render; spacer rows
|
|
798
|
+
// preserve scroll geometry so the DOM stays bounded for any row count.
|
|
799
|
+
let scroll_el = $state<HTMLDivElement>()
|
|
800
|
+
let scroll_top = $state(0)
|
|
801
|
+
let viewport_height = $state(0)
|
|
802
|
+
let avg_row_height = $state(33) // refined from rendered rows after mount
|
|
803
|
+
|
|
804
|
+
let virtual_config = $derived(
|
|
805
|
+
pagination_config || !virtual
|
|
806
|
+
? null
|
|
807
|
+
: { overscan: 10, min_window: 60, ...(typeof virtual === `object` ? virtual : {}) },
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
let virtual_range = $derived.by(() => {
|
|
811
|
+
const total = sorted_data.length
|
|
812
|
+
if (!virtual_config) return { start: 0, end: total }
|
|
813
|
+
const { overscan, min_window } = virtual_config
|
|
814
|
+
// Shrinking data can leave scroll_top past the new content height (the
|
|
815
|
+
// browser only clamps the real scrollTop after a re-render); clamp here so
|
|
816
|
+
// the window and spacers never index past the data.
|
|
817
|
+
const max_scroll = Math.max(0, total * avg_row_height - viewport_height)
|
|
818
|
+
const first_visible = Math.floor(Math.min(scroll_top, max_scroll) / avg_row_height)
|
|
819
|
+
const visible_count = Math.ceil(viewport_height / avg_row_height)
|
|
820
|
+
const start = Math.max(0, first_visible - overscan)
|
|
821
|
+
const end = Math.min(
|
|
822
|
+
total,
|
|
823
|
+
Math.max(first_visible + visible_count + overscan, start + min_window),
|
|
824
|
+
)
|
|
825
|
+
return { start, end }
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
const sync_viewport = () => {
|
|
829
|
+
if (!scroll_el) return
|
|
830
|
+
scroll_top = scroll_el.scrollTop
|
|
831
|
+
viewport_height = scroll_el.clientHeight
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Refine the row-height estimate from actually rendered rows (needed for
|
|
835
|
+
// accurate spacer heights).
|
|
836
|
+
$effect(() => {
|
|
837
|
+
if (!virtual_config || !scroll_el) return
|
|
838
|
+
void virtual_range // re-measure whenever the rendered window changes
|
|
839
|
+
const rows = scroll_el.querySelectorAll<HTMLTableRowElement>(
|
|
840
|
+
`tbody tr:not(.virtual-spacer):not(.empty-row)`,
|
|
841
|
+
)
|
|
842
|
+
let height_sum = 0
|
|
843
|
+
for (const row of rows) height_sum += row.offsetHeight
|
|
844
|
+
const measured = rows.length ? height_sum / rows.length : 0
|
|
845
|
+
// threshold stops measure→window→measure feedback loops from tiny jitters
|
|
846
|
+
if (measured > 0 && Math.abs(measured - avg_row_height) > 0.5) {
|
|
847
|
+
avg_row_height = Math.min(400, Math.max(8, measured))
|
|
848
|
+
}
|
|
849
|
+
sync_viewport()
|
|
568
850
|
})
|
|
569
851
|
|
|
570
|
-
|
|
571
|
-
|
|
852
|
+
// Track scroll-container resizes (e.g. dashboard card resizing)
|
|
853
|
+
$effect(() => {
|
|
854
|
+
if (!virtual_config || !scroll_el || typeof ResizeObserver === `undefined`) return
|
|
855
|
+
const observer = new ResizeObserver(sync_viewport)
|
|
856
|
+
observer.observe(scroll_el)
|
|
857
|
+
return () => observer.disconnect()
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
// Window of sorted_data rendered in the DOM (one page, or the virtual window).
|
|
861
|
+
// start doubles as the absolute index of the first rendered row (for row
|
|
862
|
+
// numbering and stable cell-selection coordinates).
|
|
863
|
+
let display_range = $derived.by(() => {
|
|
864
|
+
if (!pagination_config) return virtual_range
|
|
865
|
+
const start = (current_page - 1) * effective_page_size
|
|
866
|
+
return { start, end: Math.min(sorted_data.length, start + effective_page_size) }
|
|
867
|
+
})
|
|
868
|
+
let display_rows = $derived(sorted_data.slice(display_range.start, display_range.end))
|
|
869
|
+
let spacer_top = $derived(virtual_config ? virtual_range.start * avg_row_height : 0)
|
|
870
|
+
let spacer_bottom = $derived(
|
|
871
|
+
virtual_config ? (sorted_data.length - virtual_range.end) * avg_row_height : 0,
|
|
572
872
|
)
|
|
573
873
|
|
|
874
|
+
// Report the rendered slice so hosts can progressively fetch rows on demand
|
|
875
|
+
$effect(() => {
|
|
876
|
+
on_visible_range?.({ ...display_range, total: sorted_data.length })
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
let total_pages = $derived(Math.ceil(sorted_data.length / effective_page_size))
|
|
880
|
+
|
|
574
881
|
// Track previous values to detect actual changes
|
|
575
882
|
let prev_search_query = $state(``)
|
|
576
883
|
let prev_data_length = $state(0)
|
|
@@ -620,15 +927,20 @@
|
|
|
620
927
|
} else {
|
|
621
928
|
// Toggle direction
|
|
622
929
|
multi_sort = multi_sort.map((sort_entry, idx) =>
|
|
623
|
-
idx === existing_idx
|
|
930
|
+
idx === existing_idx
|
|
931
|
+
? { ...sort_entry, ascending: !sort_entry.ascending }
|
|
932
|
+
: sort_entry,
|
|
624
933
|
)
|
|
625
934
|
}
|
|
626
935
|
} else {
|
|
627
936
|
// Add to multi-sort
|
|
628
|
-
multi_sort = [
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
937
|
+
multi_sort = [
|
|
938
|
+
...multi_sort,
|
|
939
|
+
{
|
|
940
|
+
column: col_id,
|
|
941
|
+
ascending: col.better === `lower`,
|
|
942
|
+
},
|
|
943
|
+
]
|
|
632
944
|
}
|
|
633
945
|
// Clear single sort when using multi-sort
|
|
634
946
|
sort = { column: ``, dir: `asc` }
|
|
@@ -636,9 +948,14 @@
|
|
|
636
948
|
// Regular click - single column sort
|
|
637
949
|
multi_sort = [] // Clear multi-sort
|
|
638
950
|
// Use sort_state.column for comparison since it includes initial_sort fallback
|
|
639
|
-
const new_dir =
|
|
640
|
-
|
|
641
|
-
|
|
951
|
+
const new_dir =
|
|
952
|
+
sort_state.column !== col_id
|
|
953
|
+
? col.better === `lower`
|
|
954
|
+
? `asc`
|
|
955
|
+
: `desc`
|
|
956
|
+
: sort_state.ascending
|
|
957
|
+
? `desc`
|
|
958
|
+
: `asc`
|
|
642
959
|
|
|
643
960
|
// Save previous sort state in case we need to revert on error
|
|
644
961
|
const prev_sort = { ...sort }
|
|
@@ -677,60 +994,62 @@
|
|
|
677
994
|
return typeof val === `string` ? parse_numeric_string(val) : null
|
|
678
995
|
}
|
|
679
996
|
|
|
680
|
-
//
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
997
|
+
// Memoized per-column color scales: the O(rows) numeric scan + min/max +
|
|
998
|
+
// d3 scale construction run once per column when data/filter/overrides
|
|
999
|
+
// change, instead of once per CELL per render (which made a 50-row page over
|
|
1000
|
+
// a 2,000-row snapshot rescan the full column 500+ times). Built from
|
|
1001
|
+
// filtered_data (not sorted_data) since min/max don't depend on row order,
|
|
1002
|
+
// so re-sorting doesn't rebuild the scales. Only visible columns get scales
|
|
1003
|
+
// since calc_color is only ever invoked for visible cells.
|
|
1004
|
+
let column_color_scales = $derived.by(() => {
|
|
1005
|
+
const scales = new SvelteMap<string, (val: number | null | undefined) => CellColor>()
|
|
1006
|
+
if (!show_heatmap) return scales
|
|
1007
|
+
for (const col of visible_columns) {
|
|
684
1008
|
if (col.color_scale === null) continue
|
|
685
1009
|
const col_id = get_col_id(col)
|
|
686
|
-
|
|
1010
|
+
const parsed_vals = filtered_data.map((row) => parse_numeric_val(row[col_id]))
|
|
1011
|
+
const better = better_overrides.get(col_id) ?? col.better
|
|
1012
|
+
const scale = (color_scale_overrides.get(col_id) ??
|
|
1013
|
+
col.color_scale ??
|
|
1014
|
+
`interpolateViridis`) as Parameters<typeof make_cell_color_scale>[2]
|
|
1015
|
+
scales.set(
|
|
1016
|
+
col_id,
|
|
1017
|
+
make_cell_color_scale(parsed_vals, better, scale, col.scale_type || `linear`),
|
|
1018
|
+
)
|
|
687
1019
|
}
|
|
688
|
-
return
|
|
1020
|
+
return scales
|
|
689
1021
|
})
|
|
690
1022
|
|
|
691
|
-
function calc_color(val: CellVal, col: Label) {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
1023
|
+
function calc_color(val: CellVal, col: Label): CellColor {
|
|
1024
|
+
const color_fn = column_color_scales.get(get_col_id(col))
|
|
1025
|
+
if (!color_fn) return { bg: null, text: null }
|
|
695
1026
|
|
|
696
1027
|
// Parse numeric value from strings with uncertainty notation
|
|
697
|
-
const
|
|
698
|
-
if (numeric_val === null) return { bg: null, text: null }
|
|
699
|
-
|
|
700
|
-
const col_id = get_col_id(col)
|
|
701
|
-
// Use memoized parsed values for the column
|
|
702
|
-
const numeric_vals = parsed_column_values.get(col_id) ?? []
|
|
703
|
-
|
|
704
|
-
const better = better_overrides.get(col_id) ?? col.better
|
|
705
|
-
const scale = (color_scale_overrides.get(col_id) ?? col.color_scale ??
|
|
706
|
-
`interpolateViridis`) as Parameters<typeof calc_cell_color>[3]
|
|
707
|
-
const color = calc_cell_color(
|
|
708
|
-
numeric_val,
|
|
709
|
-
numeric_vals,
|
|
710
|
-
better,
|
|
711
|
-
scale,
|
|
712
|
-
col.scale_type || `linear`,
|
|
713
|
-
)
|
|
1028
|
+
const color = color_fn(parse_numeric_val(val))
|
|
714
1029
|
|
|
715
1030
|
// Recompute text contrast against effective bg (cell bg blended with page bg by opacity).
|
|
716
1031
|
// Approximation: blend luminances directly; accurate enough for black/white text choice.
|
|
717
1032
|
if (color.bg && heatmap_opacity < 1) {
|
|
718
|
-
const blended_lum =
|
|
719
|
-
page_bg_lum * (1 - heatmap_opacity)
|
|
720
|
-
color.text
|
|
1033
|
+
const blended_lum =
|
|
1034
|
+
luminance(color.bg) * heatmap_opacity + page_bg_lum * (1 - heatmap_opacity)
|
|
1035
|
+
return { bg: color.bg, text: blended_lum > 0.7 ? `black` : `white` }
|
|
721
1036
|
}
|
|
722
1037
|
return color
|
|
723
1038
|
}
|
|
724
1039
|
|
|
725
1040
|
let visible_columns = $derived(
|
|
726
|
-
ordered_columns.filter(
|
|
727
|
-
col.visible !== false && !hidden_columns.includes(get_col_id(col))
|
|
1041
|
+
ordered_columns.filter(
|
|
1042
|
+
(col) => col.visible !== false && !hidden_columns.includes(get_col_id(col)),
|
|
728
1043
|
),
|
|
729
1044
|
)
|
|
1045
|
+
// total cell count per body row (for spacer + empty-message colspans)
|
|
1046
|
+
let body_colspan = $derived(
|
|
1047
|
+
visible_columns.length + (show_row_select ? 1 : 0) + (show_row_numbers ? 1 : 0),
|
|
1048
|
+
)
|
|
730
1049
|
|
|
731
1050
|
const sort_indicator = (col: Label, current_sort_state: SortState) => {
|
|
732
|
-
const hide_sort_indicator =
|
|
733
|
-
col.style?.includes(`--hide-sort-indicator`)
|
|
1051
|
+
const hide_sort_indicator =
|
|
1052
|
+
col.show_sort_indicator === false || col.style?.includes(`--hide-sort-indicator`)
|
|
734
1053
|
if (hide_sort_indicator) return ``
|
|
735
1054
|
|
|
736
1055
|
const col_id = get_col_id(col)
|
|
@@ -743,56 +1062,264 @@
|
|
|
743
1062
|
return `<span style="font-size: 0.8em;">${arrow}${badge}</span>`
|
|
744
1063
|
}
|
|
745
1064
|
|
|
746
|
-
const is_sorted = current_sort_state.column === col_id
|
|
747
|
-
if (!is_sorted) return ``
|
|
748
1065
|
// Show indicator only for actively sorted columns.
|
|
1066
|
+
if (current_sort_state.column !== col_id) return ``
|
|
749
1067
|
const arrow = current_sort_state.ascending ? `↓` : `↑`
|
|
750
|
-
|
|
751
|
-
return arrow ? `<span style="font-size: 0.8em;">${arrow}</span>` : ``
|
|
1068
|
+
return `<span style="font-size: 0.8em;">${arrow}</span>`
|
|
752
1069
|
}
|
|
753
1070
|
|
|
754
|
-
// Context menu state for column
|
|
1071
|
+
// Context menu state for column right-click (headers and body cells)
|
|
755
1072
|
let context_menu_col = $state<string | null>(null)
|
|
756
1073
|
let context_menu_pos = $state({ x: 0, y: 0 })
|
|
757
1074
|
|
|
758
|
-
const
|
|
1075
|
+
const better_section = {
|
|
1076
|
+
title: `Gradient direction`,
|
|
1077
|
+
options: [
|
|
1078
|
+
{ value: `higher`, label: `▲ Higher is better` },
|
|
1079
|
+
{ value: `lower`, label: `▼ Lower is better` },
|
|
1080
|
+
],
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function open_column_context_menu(event: MouseEvent, col_id: string) {
|
|
1084
|
+
event.preventDefault()
|
|
1085
|
+
event.stopPropagation()
|
|
1086
|
+
context_menu_col = col_id
|
|
1087
|
+
const rect = container_el?.getBoundingClientRect()
|
|
1088
|
+
context_menu_pos = {
|
|
1089
|
+
x: event.clientX - (rect?.left ?? 0),
|
|
1090
|
+
y: event.clientY - (rect?.top ?? 0),
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// ---- Cell range selection: drag selects a rectangle of cells, Shift/Cmd+
|
|
1095
|
+
// drag adds disjoint rectangles, Cmd/Ctrl+C copies as TSV (blocks separated
|
|
1096
|
+
// by newlines), Escape or clicking outside clears. Selection coordinates
|
|
1097
|
+
// are absolute sorted_data row indices plus visible-column indices, cleared
|
|
1098
|
+
// whenever the rendered data changes (sort, page, filter, refresh).
|
|
1099
|
+
interface CellRect {
|
|
1100
|
+
start_row: number
|
|
1101
|
+
start_col: number
|
|
1102
|
+
end_row: number
|
|
1103
|
+
end_col: number
|
|
1104
|
+
}
|
|
1105
|
+
let selected_cell_rects = $state<CellRect[]>([])
|
|
1106
|
+
let cell_drag_active = $state(false)
|
|
1107
|
+
let cell_drag_moved = false
|
|
1108
|
+
let suppress_row_click = false
|
|
1109
|
+
|
|
1110
|
+
const rect_bounds = (rect: CellRect) => ({
|
|
1111
|
+
row_lo: Math.min(rect.start_row, rect.end_row),
|
|
1112
|
+
row_hi: Math.max(rect.start_row, rect.end_row),
|
|
1113
|
+
col_lo: Math.min(rect.start_col, rect.end_col),
|
|
1114
|
+
col_hi: Math.max(rect.start_col, rect.end_col),
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
let selected_cell_keys = $derived.by(() => {
|
|
1118
|
+
const keys = new Set<string>()
|
|
1119
|
+
for (const rect of selected_cell_rects) {
|
|
1120
|
+
const { row_lo, row_hi, col_lo, col_hi } = rect_bounds(rect)
|
|
1121
|
+
for (let row_idx = row_lo; row_idx <= row_hi; row_idx++) {
|
|
1122
|
+
for (let col_idx = col_lo; col_idx <= col_hi; col_idx++) {
|
|
1123
|
+
keys.add(`${row_idx}:${col_idx}`)
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return keys
|
|
1128
|
+
})
|
|
1129
|
+
|
|
1130
|
+
// Stale (row, col) coordinates must not survive sort/page/filter/refresh,
|
|
1131
|
+
// nor column reorder/hide (col indices point into visible_columns). Depends
|
|
1132
|
+
// on sorted_data + current_page + visible_columns (not the virtual window)
|
|
1133
|
+
// so plain scrolling in infinite mode doesn't wipe an active selection.
|
|
1134
|
+
$effect(() => {
|
|
1135
|
+
void sorted_data
|
|
1136
|
+
void current_page
|
|
1137
|
+
void visible_columns
|
|
1138
|
+
selected_cell_rects = []
|
|
1139
|
+
})
|
|
1140
|
+
|
|
1141
|
+
const is_interactive_cell_target = (target: EventTarget | null): boolean =>
|
|
1142
|
+
target instanceof Element && Boolean(target.closest(`button, a, input, select, textarea`))
|
|
1143
|
+
|
|
1144
|
+
function start_cell_drag(event: PointerEvent, row_idx: number, col_idx: number) {
|
|
1145
|
+
if (event.button !== 0 || is_interactive_cell_target(event.target)) return
|
|
1146
|
+
const additive = event.shiftKey || event.metaKey || event.ctrlKey
|
|
1147
|
+
const rect = { start_row: row_idx, start_col: col_idx, end_row: row_idx, end_col: col_idx }
|
|
1148
|
+
selected_cell_rects = additive ? [...selected_cell_rects, rect] : [rect]
|
|
1149
|
+
cell_drag_active = true
|
|
1150
|
+
cell_drag_moved = false
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function extend_cell_drag(event: PointerEvent) {
|
|
1154
|
+
if (!cell_drag_active) return
|
|
1155
|
+
const target_cell =
|
|
1156
|
+
event.target instanceof Element
|
|
1157
|
+
? event.target.closest<HTMLElement>(`td[data-row-idx]`)
|
|
1158
|
+
: null
|
|
1159
|
+
const active_rect = selected_cell_rects.at(-1)
|
|
1160
|
+
if (!target_cell || !active_rect) return
|
|
1161
|
+
const row_idx = Number(target_cell.dataset.rowIdx)
|
|
1162
|
+
const col_idx = Number(target_cell.dataset.colIdx)
|
|
1163
|
+
if (row_idx === active_rect.end_row && col_idx === active_rect.end_col) return
|
|
1164
|
+
if (!cell_drag_moved) {
|
|
1165
|
+
cell_drag_moved = true
|
|
1166
|
+
// A native text selection may have started before user-select: none
|
|
1167
|
+
// kicked in; drop it so the cell selection is the only visible one.
|
|
1168
|
+
globalThis.getSelection()?.removeAllRanges()
|
|
1169
|
+
}
|
|
1170
|
+
selected_cell_rects = [
|
|
1171
|
+
...selected_cell_rects.slice(0, -1),
|
|
1172
|
+
{ ...active_rect, end_row: row_idx, end_col: col_idx },
|
|
1173
|
+
]
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function end_cell_drag() {
|
|
1177
|
+
if (!cell_drag_active) return
|
|
1178
|
+
cell_drag_active = false
|
|
1179
|
+
// A drag that crossed cells must not fire the row click on release
|
|
1180
|
+
if (cell_drag_moved) suppress_row_click = true
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function suppress_click_after_cell_drag(event: MouseEvent) {
|
|
1184
|
+
if (!suppress_row_click) return
|
|
1185
|
+
suppress_row_click = false
|
|
1186
|
+
event.stopPropagation()
|
|
1187
|
+
event.preventDefault()
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function clear_cell_selection_on_outside_pointerdown(event: PointerEvent) {
|
|
1191
|
+
// A drag's suppress flag is consumed by the click right after pointerup;
|
|
1192
|
+
// if that click never fired (released outside the table), any NEW
|
|
1193
|
+
// interaction must not inherit it.
|
|
1194
|
+
suppress_row_click = false
|
|
1195
|
+
if (selected_cell_rects.length === 0) return
|
|
1196
|
+
if (event.target instanceof Node && container_el?.contains(event.target)) return
|
|
1197
|
+
selected_cell_rects = []
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Raw cell value as clipboard text (numbers keep full precision, dates go
|
|
1201
|
+
// ISO, HTML cells lose their markup)
|
|
1202
|
+
const cell_copy_text = (val: CellVal): string => {
|
|
1203
|
+
if (val == null || (typeof val === `number` && Number.isNaN(val))) return ``
|
|
1204
|
+
if (val instanceof Date) return val.toISOString()
|
|
1205
|
+
if (typeof val === `object`) return JSON.stringify(val)
|
|
1206
|
+
return strip_html(String(val)).trim()
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function copy_selected_cells() {
|
|
1210
|
+
const blocks = selected_cell_rects.map((rect) => {
|
|
1211
|
+
const bounds = rect_bounds(rect)
|
|
1212
|
+
const { row_lo, col_lo } = bounds
|
|
1213
|
+
// rects hold absolute indices into the sorted+filtered rows
|
|
1214
|
+
const row_hi = Math.min(bounds.row_hi, sorted_data.length - 1)
|
|
1215
|
+
const col_hi = Math.min(bounds.col_hi, visible_columns.length - 1)
|
|
1216
|
+
const lines: string[] = []
|
|
1217
|
+
for (let row_idx = row_lo; row_idx <= row_hi; row_idx++) {
|
|
1218
|
+
const cells: string[] = []
|
|
1219
|
+
for (let col_idx = col_lo; col_idx <= col_hi; col_idx++) {
|
|
1220
|
+
cells.push(
|
|
1221
|
+
cell_copy_text(sorted_data[row_idx][get_col_id(visible_columns[col_idx])]),
|
|
1222
|
+
)
|
|
1223
|
+
}
|
|
1224
|
+
lines.push(cells.join(`\t`))
|
|
1225
|
+
}
|
|
1226
|
+
return lines.join(`\n`)
|
|
1227
|
+
})
|
|
1228
|
+
void navigator.clipboard?.writeText(blocks.join(`\n`))
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Every sorted+filtered value of one column (all pages), one per line
|
|
1232
|
+
function copy_column_values(col_id: string) {
|
|
1233
|
+
void navigator.clipboard?.writeText(
|
|
1234
|
+
sorted_data.map((row) => cell_copy_text(row[col_id])).join(`\n`),
|
|
1235
|
+
)
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function handle_cell_selection_keydown(event: KeyboardEvent) {
|
|
1239
|
+
if (selected_cell_rects.length === 0) return
|
|
1240
|
+
if (event.key === `Escape`) {
|
|
1241
|
+
selected_cell_rects = []
|
|
1242
|
+
return
|
|
1243
|
+
}
|
|
1244
|
+
if (event.key !== `c` || !(event.metaKey || event.ctrlKey)) return
|
|
1245
|
+
// Native text selections and focused form fields keep native copy
|
|
1246
|
+
if (is_interactive_cell_target(event.target)) return
|
|
1247
|
+
if (globalThis.getSelection()?.toString()) return
|
|
1248
|
+
event.preventDefault()
|
|
1249
|
+
copy_selected_cells()
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
let context_menu_column = $derived(
|
|
1253
|
+
visible_columns.find((col) => get_col_id(col) === context_menu_col),
|
|
1254
|
+
)
|
|
1255
|
+
let context_menu_sections = $derived([
|
|
759
1256
|
{
|
|
760
|
-
title: `
|
|
1257
|
+
title: `Copy`,
|
|
761
1258
|
options: [
|
|
762
|
-
{ value: `
|
|
763
|
-
|
|
1259
|
+
{ value: `copy_column`, label: `Copy column (${sorted_data.length} values)` },
|
|
1260
|
+
...(selected_cell_keys.size > 0
|
|
1261
|
+
? [
|
|
1262
|
+
{
|
|
1263
|
+
value: `copy_selection`,
|
|
1264
|
+
label: `Copy selection (${selected_cell_keys.size} cells)`,
|
|
1265
|
+
},
|
|
1266
|
+
]
|
|
1267
|
+
: []),
|
|
764
1268
|
],
|
|
765
1269
|
},
|
|
766
|
-
|
|
1270
|
+
// Gradient direction only applies to heatmap-colored columns
|
|
1271
|
+
...(allow_better_toggle && context_menu_column?.color_scale != null
|
|
1272
|
+
? [better_section]
|
|
1273
|
+
: []),
|
|
1274
|
+
])
|
|
1275
|
+
|
|
1276
|
+
function handle_context_menu_select(section_title: string, option: { value: string }) {
|
|
1277
|
+
if (section_title === `Copy`) {
|
|
1278
|
+
if (option.value === `copy_column` && context_menu_col) {
|
|
1279
|
+
copy_column_values(context_menu_col)
|
|
1280
|
+
} else if (option.value === `copy_selection`) {
|
|
1281
|
+
copy_selected_cells()
|
|
1282
|
+
}
|
|
1283
|
+
} else if (context_menu_col) {
|
|
1284
|
+
const current = better_overrides.get(context_menu_col)
|
|
1285
|
+
if (current === option.value) better_overrides.delete(context_menu_col)
|
|
1286
|
+
else {
|
|
1287
|
+
better_overrides.set(context_menu_col, option.value as `higher` | `lower`)
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
context_menu_col = null
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Row selection via an ID-indexed Set so per-row checks are O(1) instead of
|
|
1294
|
+
// linear scans over selected_rows (matters for large virtualized datasets)
|
|
1295
|
+
let selected_id_set = $derived(new Set(selected_rows.map((row) => get_row_id(row))))
|
|
767
1296
|
|
|
768
|
-
// Row selection using WeakMap-based ID lookup instead of O(n) JSON.stringify comparison
|
|
769
1297
|
function toggle_row_select(row: RowData) {
|
|
770
1298
|
const row_id = get_row_id(row)
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
} else {
|
|
775
|
-
selected_rows = [...selected_rows, row]
|
|
776
|
-
}
|
|
1299
|
+
selected_rows = selected_id_set.has(row_id)
|
|
1300
|
+
? selected_rows.filter((selected_row) => get_row_id(selected_row) !== row_id)
|
|
1301
|
+
: [...selected_rows, row]
|
|
777
1302
|
}
|
|
778
1303
|
|
|
779
1304
|
function is_row_selected(row: RowData): boolean {
|
|
780
|
-
|
|
781
|
-
return selected_rows.some((selected_row) => get_row_id(selected_row) === row_id)
|
|
1305
|
+
return selected_id_set.has(get_row_id(row))
|
|
782
1306
|
}
|
|
783
1307
|
|
|
784
|
-
// Select-all:
|
|
1308
|
+
// Select-all scope: the current page under pagination, every sorted+filtered
|
|
1309
|
+
// row in infinite-scroll mode (the virtual window is a rendering detail)
|
|
1310
|
+
let select_all_rows = $derived(pagination_config ? display_rows : sorted_data)
|
|
785
1311
|
let all_page_selected = $derived(
|
|
786
|
-
|
|
1312
|
+
select_all_rows.length > 0 &&
|
|
1313
|
+
select_all_rows.every((row) => selected_id_set.has(get_row_id(row))),
|
|
787
1314
|
)
|
|
788
1315
|
|
|
789
1316
|
function toggle_select_all() {
|
|
790
1317
|
if (all_page_selected) {
|
|
791
|
-
const
|
|
792
|
-
selected_rows = selected_rows.filter((row) => !
|
|
1318
|
+
const scope_ids = new Set(select_all_rows.map(get_row_id))
|
|
1319
|
+
selected_rows = selected_rows.filter((row) => !scope_ids.has(get_row_id(row)))
|
|
793
1320
|
} else {
|
|
794
1321
|
const already = new Set(selected_rows.map(get_row_id))
|
|
795
|
-
const new_rows =
|
|
1322
|
+
const new_rows = select_all_rows.filter((row) => !already.has(get_row_id(row)))
|
|
796
1323
|
selected_rows = [...selected_rows, ...new_rows]
|
|
797
1324
|
}
|
|
798
1325
|
}
|
|
@@ -818,32 +1345,26 @@
|
|
|
818
1345
|
const val = row[get_col_id(col)]
|
|
819
1346
|
if (val == null) return ``
|
|
820
1347
|
return quote(strip_html(String(val)))
|
|
821
|
-
})
|
|
1348
|
+
}),
|
|
822
1349
|
)
|
|
823
1350
|
return [headers.join(delimiter), ...rows.map((row) => row.join(delimiter))].join(`\n`)
|
|
824
1351
|
}
|
|
825
1352
|
|
|
826
|
-
function export_csv(filename
|
|
1353
|
+
function export_csv(filename: string) {
|
|
827
1354
|
download(serialize_table(`,`, true), `${filename}.csv`, `text/csv`)
|
|
828
1355
|
}
|
|
829
1356
|
|
|
830
|
-
function export_json(filename
|
|
1357
|
+
function export_json(filename: string) {
|
|
831
1358
|
const rows = export_rows.map((row) => {
|
|
832
1359
|
const clean_row: Record<string, unknown> = {}
|
|
833
1360
|
for (const col of visible_columns) {
|
|
834
1361
|
const col_id = get_col_id(col)
|
|
835
1362
|
const val = row[col_id]
|
|
836
|
-
clean_row[strip_html(col.label)] = typeof val === `string`
|
|
837
|
-
? strip_html(val)
|
|
838
|
-
: val
|
|
1363
|
+
clean_row[strip_html(col.label)] = typeof val === `string` ? strip_html(val) : val
|
|
839
1364
|
}
|
|
840
1365
|
return clean_row
|
|
841
1366
|
})
|
|
842
|
-
download(
|
|
843
|
-
JSON.stringify(rows, null, 2),
|
|
844
|
-
`${filename}.json`,
|
|
845
|
-
`application/json`,
|
|
846
|
-
)
|
|
1367
|
+
download(JSON.stringify(rows, null, 2), `${filename}.json`, `application/json`)
|
|
847
1368
|
}
|
|
848
1369
|
|
|
849
1370
|
function copy_to_clipboard() {
|
|
@@ -852,11 +1373,9 @@
|
|
|
852
1373
|
|
|
853
1374
|
// Column visibility toggle
|
|
854
1375
|
function toggle_column(col_id: string) {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
hidden_columns = [...hidden_columns, col_id]
|
|
859
|
-
}
|
|
1376
|
+
hidden_columns = hidden_columns.includes(col_id)
|
|
1377
|
+
? hidden_columns.filter((id) => id !== col_id)
|
|
1378
|
+
: [...hidden_columns, col_id]
|
|
860
1379
|
}
|
|
861
1380
|
|
|
862
1381
|
// Column resize handlers
|
|
@@ -889,18 +1408,27 @@
|
|
|
889
1408
|
let hint_config = $derived(
|
|
890
1409
|
sort_hint
|
|
891
1410
|
? {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1411
|
+
position: `bottom` as const,
|
|
1412
|
+
permanent: false,
|
|
1413
|
+
...(typeof sort_hint === `string` ? { text: sort_hint } : sort_hint),
|
|
1414
|
+
}
|
|
896
1415
|
: null,
|
|
897
1416
|
)
|
|
898
1417
|
</script>
|
|
899
1418
|
|
|
1419
|
+
<svelte:window
|
|
1420
|
+
onpointerdown={(event) => {
|
|
1421
|
+
close_datetime_select_on_outside_pointerdown(event)
|
|
1422
|
+
clear_cell_selection_on_outside_pointerdown(event)
|
|
1423
|
+
}}
|
|
1424
|
+
onpointerup={end_cell_drag}
|
|
1425
|
+
onkeydown={handle_cell_selection_keydown}
|
|
1426
|
+
/>
|
|
1427
|
+
|
|
900
1428
|
{#snippet sort_hint_element(pos: `top` | `bottom`)}
|
|
901
1429
|
{#if hint_config?.position === pos}
|
|
902
1430
|
<div
|
|
903
|
-
class=
|
|
1431
|
+
class={[`sort-hint`, hint_config.class]}
|
|
904
1432
|
class:permanent={hint_config.permanent}
|
|
905
1433
|
style={hint_config.style}
|
|
906
1434
|
>
|
|
@@ -909,20 +1437,29 @@
|
|
|
909
1437
|
{/if}
|
|
910
1438
|
{/snippet}
|
|
911
1439
|
|
|
1440
|
+
<!-- svelte-ignore a11y_no_static_element_interactions (capture-phase guard swallowing the click that follows a cell-range drag) -->
|
|
912
1441
|
<div
|
|
913
1442
|
{@attach table_tooltips}
|
|
914
1443
|
{...rest_props}
|
|
915
1444
|
bind:this={container_el}
|
|
916
|
-
class=
|
|
1445
|
+
class={[`table-container`, rest_props.class]}
|
|
1446
|
+
class:cell-dragging={cell_drag_active}
|
|
917
1447
|
style:--heatmap-opacity="{heatmap_opacity * 100}%"
|
|
1448
|
+
onclickcapture={suppress_click_after_cell_drag}
|
|
918
1449
|
onmouseleave={() => {
|
|
919
1450
|
show_column_dropdown = false
|
|
920
1451
|
show_export_dropdown = false
|
|
921
1452
|
context_menu_col = null
|
|
922
1453
|
}}
|
|
923
1454
|
>
|
|
924
|
-
<!--
|
|
925
|
-
|
|
1455
|
+
<!-- Control buttons: render inline above the table, or teleport into a host
|
|
1456
|
+
toolbar (controls_target) so embedding panels reuse their own header row -->
|
|
1457
|
+
<section
|
|
1458
|
+
class="control-buttons"
|
|
1459
|
+
class:portaled={Boolean(controls_target)}
|
|
1460
|
+
class:force-visible={controls_open || show_column_dropdown || show_export_dropdown}
|
|
1461
|
+
{@attach portal(controls_target)}
|
|
1462
|
+
>
|
|
926
1463
|
{#if search_config}
|
|
927
1464
|
{#if search_expanded || search_query}
|
|
928
1465
|
<input
|
|
@@ -942,15 +1479,15 @@
|
|
|
942
1479
|
}}
|
|
943
1480
|
{@attach tooltip({ content: `Clear`, placement: `top` })}
|
|
944
1481
|
>
|
|
945
|
-
<Icon icon="Cross"
|
|
1482
|
+
<Icon icon="Cross" />
|
|
946
1483
|
</button>
|
|
947
1484
|
{:else}
|
|
948
1485
|
<button
|
|
949
1486
|
class="icon-btn"
|
|
950
|
-
onclick={() => search_expanded = true}
|
|
1487
|
+
onclick={() => (search_expanded = true)}
|
|
951
1488
|
{@attach tooltip({ content: `Search`, placement: `top` })}
|
|
952
1489
|
>
|
|
953
|
-
<Icon icon="Search"
|
|
1490
|
+
<Icon icon="Search" />
|
|
954
1491
|
</button>
|
|
955
1492
|
{/if}
|
|
956
1493
|
{/if}
|
|
@@ -960,10 +1497,10 @@
|
|
|
960
1497
|
<button
|
|
961
1498
|
class="icon-btn"
|
|
962
1499
|
class:active={show_column_dropdown}
|
|
963
|
-
onclick={() => show_column_dropdown = !show_column_dropdown}
|
|
1500
|
+
onclick={() => (show_column_dropdown = !show_column_dropdown)}
|
|
964
1501
|
{@attach tooltip({ content: `Columns`, placement: `top` })}
|
|
965
1502
|
>
|
|
966
|
-
<Icon icon="Columns"
|
|
1503
|
+
<Icon icon="Columns" />
|
|
967
1504
|
</button>
|
|
968
1505
|
{#if show_column_dropdown}
|
|
969
1506
|
<div class="dropdown-pane">
|
|
@@ -988,10 +1525,10 @@
|
|
|
988
1525
|
<button
|
|
989
1526
|
class="icon-btn"
|
|
990
1527
|
class:active={show_export_dropdown}
|
|
991
|
-
onclick={() => show_export_dropdown = !show_export_dropdown}
|
|
1528
|
+
onclick={() => (show_export_dropdown = !show_export_dropdown)}
|
|
992
1529
|
{@attach tooltip({ content: `Export`, placement: `top` })}
|
|
993
1530
|
>
|
|
994
|
-
<Icon icon="Export"
|
|
1531
|
+
<Icon icon="Export" />
|
|
995
1532
|
</button>
|
|
996
1533
|
{#if show_export_dropdown}
|
|
997
1534
|
<div class="dropdown-pane">
|
|
@@ -1034,118 +1571,114 @@
|
|
|
1034
1571
|
{#if show_row_select && selected_rows.length > 0}
|
|
1035
1572
|
<button
|
|
1036
1573
|
class="icon-btn selection-badge"
|
|
1037
|
-
onclick={() => selected_rows = []}
|
|
1574
|
+
onclick={() => (selected_rows = [])}
|
|
1038
1575
|
title="Clear {selected_rows.length} selected rows"
|
|
1039
1576
|
>
|
|
1040
1577
|
<span class="badge">{selected_rows.length}</span>
|
|
1041
|
-
<Icon icon="Cross"
|
|
1578
|
+
<Icon icon="Cross" />
|
|
1042
1579
|
</button>
|
|
1043
1580
|
{/if}
|
|
1044
1581
|
|
|
1045
|
-
{#if
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
open_icon="Cross"
|
|
1055
|
-
toggle_props={{
|
|
1056
|
-
title: `${controls_open ? `Close` : `Open`} table controls`,
|
|
1057
|
-
style: `position: absolute; top: 5pt; right: 1ex; z-index: 10`,
|
|
1058
|
-
}}
|
|
1059
|
-
pane_props={{ style: `max-height: 60vh; overflow-y: auto; font-size: 0.85em` }}
|
|
1060
|
-
>
|
|
1061
|
-
<SettingsSection
|
|
1062
|
-
title="Heatmap"
|
|
1063
|
-
current_values={{ show_heatmap, heatmap_opacity }}
|
|
1064
|
-
on_reset={() => {
|
|
1065
|
-
show_heatmap = true
|
|
1066
|
-
heatmap_opacity = 1
|
|
1582
|
+
{#if show_controls}
|
|
1583
|
+
<DraggablePane
|
|
1584
|
+
bind:show={controls_open}
|
|
1585
|
+
closed_icon="Settings"
|
|
1586
|
+
open_icon="Cross"
|
|
1587
|
+
toggle_props={{ title: `${controls_open ? `Close` : `Open`} table controls` }}
|
|
1588
|
+
position="fixed"
|
|
1589
|
+
pane_props={{
|
|
1590
|
+
style: `--pane-max-height: 60vh; overflow-y: auto; font-size: 0.85em`,
|
|
1067
1591
|
}}
|
|
1068
1592
|
>
|
|
1069
|
-
<
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
type="
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
title="Display"
|
|
1094
|
-
current_values={{ show_row_numbers }}
|
|
1095
|
-
on_reset={() => {
|
|
1096
|
-
show_row_numbers = false
|
|
1097
|
-
}}
|
|
1098
|
-
>
|
|
1099
|
-
<label><input type="checkbox" bind:checked={show_row_numbers} /> Row
|
|
1100
|
-
numbers</label>
|
|
1101
|
-
</SettingsSection>
|
|
1593
|
+
<SettingsSection
|
|
1594
|
+
title="Heatmap"
|
|
1595
|
+
current_values={{ show_heatmap, heatmap_opacity }}
|
|
1596
|
+
on_reset={() => {
|
|
1597
|
+
show_heatmap = true
|
|
1598
|
+
heatmap_opacity = 1
|
|
1599
|
+
}}
|
|
1600
|
+
>
|
|
1601
|
+
<label><input type="checkbox" bind:checked={show_heatmap} /> Show heatmap</label>
|
|
1602
|
+
{#if show_heatmap}
|
|
1603
|
+
<label>
|
|
1604
|
+
Opacity
|
|
1605
|
+
<input type="range" min="0" max="1" step="0.05" bind:value={heatmap_opacity} />
|
|
1606
|
+
<input
|
|
1607
|
+
type="number"
|
|
1608
|
+
min="0"
|
|
1609
|
+
max="1"
|
|
1610
|
+
step="0.05"
|
|
1611
|
+
bind:value={heatmap_opacity}
|
|
1612
|
+
style="width: 3.5em"
|
|
1613
|
+
/>
|
|
1614
|
+
</label>
|
|
1615
|
+
{/if}
|
|
1616
|
+
</SettingsSection>
|
|
1102
1617
|
|
|
1103
|
-
{#if colored_columns.length > 0}
|
|
1104
1618
|
<SettingsSection
|
|
1105
|
-
title="
|
|
1106
|
-
current_values={
|
|
1619
|
+
title="Display"
|
|
1620
|
+
current_values={{ show_row_numbers }}
|
|
1107
1621
|
on_reset={() => {
|
|
1108
|
-
|
|
1109
|
-
color_scale_overrides.clear()
|
|
1622
|
+
show_row_numbers = false
|
|
1110
1623
|
}}
|
|
1111
1624
|
>
|
|
1112
|
-
{
|
|
1113
|
-
{@const col_id = get_col_id(col)}
|
|
1114
|
-
<div class="col-color-row">
|
|
1115
|
-
<span class="col-color-label">{@html sanitize_html(col.label)}</span>
|
|
1116
|
-
<select
|
|
1117
|
-
value={color_scale_overrides.get(col_id) ?? col.color_scale ??
|
|
1118
|
-
`interpolateViridis`}
|
|
1119
|
-
onchange={(event) => {
|
|
1120
|
-
const val = event.currentTarget.value
|
|
1121
|
-
if (
|
|
1122
|
-
val === (col.color_scale ?? `interpolateViridis`)
|
|
1123
|
-
) color_scale_overrides.delete(col_id)
|
|
1124
|
-
else color_scale_overrides.set(col_id, val)
|
|
1125
|
-
}}
|
|
1126
|
-
>
|
|
1127
|
-
{#each color_scale_options as scale (scale)}
|
|
1128
|
-
<option value={scale}>{scale.replace(`interpolate`, ``)}</option>
|
|
1129
|
-
{/each}
|
|
1130
|
-
</select>
|
|
1131
|
-
<select
|
|
1132
|
-
value={better_overrides.get(col_id) ?? col.better ?? ``}
|
|
1133
|
-
onchange={(event) => {
|
|
1134
|
-
const val = event.currentTarget.value
|
|
1135
|
-
if (!val) better_overrides.delete(col_id)
|
|
1136
|
-
else better_overrides.set(col_id, val as `higher` | `lower`)
|
|
1137
|
-
}}
|
|
1138
|
-
>
|
|
1139
|
-
<option value="">Default</option>
|
|
1140
|
-
<option value="higher">▲ High</option>
|
|
1141
|
-
<option value="lower">▼ Low</option>
|
|
1142
|
-
</select>
|
|
1143
|
-
</div>
|
|
1144
|
-
{/each}
|
|
1625
|
+
<label><input type="checkbox" bind:checked={show_row_numbers} /> Row numbers</label>
|
|
1145
1626
|
</SettingsSection>
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1627
|
+
|
|
1628
|
+
{#if colored_columns.length > 0}
|
|
1629
|
+
<SettingsSection
|
|
1630
|
+
title="Column Colors"
|
|
1631
|
+
current_values={Object.fromEntries([
|
|
1632
|
+
...better_overrides,
|
|
1633
|
+
...color_scale_overrides,
|
|
1634
|
+
])}
|
|
1635
|
+
on_reset={() => {
|
|
1636
|
+
better_overrides.clear()
|
|
1637
|
+
color_scale_overrides.clear()
|
|
1638
|
+
}}
|
|
1639
|
+
>
|
|
1640
|
+
{#each colored_columns as col (get_col_id(col))}
|
|
1641
|
+
{@const col_id = get_col_id(col)}
|
|
1642
|
+
<div class="col-color-row">
|
|
1643
|
+
<span class="col-color-label">{@html sanitize_html(col.label)}</span>
|
|
1644
|
+
<select
|
|
1645
|
+
value={color_scale_overrides.get(col_id) ??
|
|
1646
|
+
col.color_scale ??
|
|
1647
|
+
`interpolateViridis`}
|
|
1648
|
+
onchange={(event) => {
|
|
1649
|
+
const val = event.currentTarget.value
|
|
1650
|
+
if (val === (col.color_scale ?? `interpolateViridis`))
|
|
1651
|
+
color_scale_overrides.delete(col_id)
|
|
1652
|
+
else color_scale_overrides.set(col_id, val)
|
|
1653
|
+
}}
|
|
1654
|
+
>
|
|
1655
|
+
{#each color_scale_options as scale (scale)}
|
|
1656
|
+
<option value={scale}>{scale.replace(`interpolate`, ``)}</option>
|
|
1657
|
+
{/each}
|
|
1658
|
+
</select>
|
|
1659
|
+
<select
|
|
1660
|
+
value={better_overrides.get(col_id) ?? col.better ?? ``}
|
|
1661
|
+
onchange={(event) => {
|
|
1662
|
+
const val = event.currentTarget.value
|
|
1663
|
+
if (!val) better_overrides.delete(col_id)
|
|
1664
|
+
else better_overrides.set(col_id, val as `higher` | `lower`)
|
|
1665
|
+
}}
|
|
1666
|
+
>
|
|
1667
|
+
<option value="">Default</option>
|
|
1668
|
+
<option value="higher">▲ High</option>
|
|
1669
|
+
<option value="lower">▼ Low</option>
|
|
1670
|
+
</select>
|
|
1671
|
+
</div>
|
|
1672
|
+
{/each}
|
|
1673
|
+
</SettingsSection>
|
|
1674
|
+
{/if}
|
|
1675
|
+
</DraggablePane>
|
|
1676
|
+
{/if}
|
|
1677
|
+
|
|
1678
|
+
{#if controls}
|
|
1679
|
+
{@render controls()}
|
|
1680
|
+
{/if}
|
|
1681
|
+
</section>
|
|
1149
1682
|
|
|
1150
1683
|
{@render sort_hint_element(`top`)}
|
|
1151
1684
|
|
|
@@ -1153,6 +1686,8 @@
|
|
|
1153
1686
|
class="table-scroll"
|
|
1154
1687
|
style={scroll_style}
|
|
1155
1688
|
class:has-scroll={scroll_style}
|
|
1689
|
+
bind:this={scroll_el}
|
|
1690
|
+
onscroll={virtual_config ? sync_viewport : undefined}
|
|
1156
1691
|
>
|
|
1157
1692
|
{#if loading}
|
|
1158
1693
|
<div class="loading-overlay">
|
|
@@ -1175,14 +1710,11 @@
|
|
|
1175
1710
|
{#if !col.group}
|
|
1176
1711
|
<th class:sticky-col={col.sticky}></th>
|
|
1177
1712
|
{:else}
|
|
1178
|
-
{@const group_cols = visible_columns.filter(
|
|
1179
|
-
|
|
1180
|
-
|
|
1713
|
+
{@const group_cols = visible_columns.filter(
|
|
1714
|
+
(column) => column.group === col.group,
|
|
1715
|
+
)}
|
|
1181
1716
|
<!-- Only render the group header once for each group by checking if this is the first column of this group -->
|
|
1182
|
-
{#if visible_columns.findIndex((column) => column.group === col.group) ===
|
|
1183
|
-
visible_columns.findIndex((column) =>
|
|
1184
|
-
column.group === col.group && column.label === col.label
|
|
1185
|
-
)}
|
|
1717
|
+
{#if visible_columns.findIndex((column) => column.group === col.group) === visible_columns.findIndex((column) => column.group === col.group && column.label === col.label)}
|
|
1186
1718
|
<th title={col.description} colspan={group_cols.length}>
|
|
1187
1719
|
{@html sanitize_html(col.group)}
|
|
1188
1720
|
</th>
|
|
@@ -1210,61 +1742,47 @@
|
|
|
1210
1742
|
{/if}
|
|
1211
1743
|
{#each visible_columns as col (get_col_id(col))}
|
|
1212
1744
|
{@const col_id = get_col_id(col)}
|
|
1213
|
-
{@const
|
|
1214
|
-
|
|
1215
|
-
|
|
1745
|
+
{@const is_datetime = is_datetime_column(col)}
|
|
1746
|
+
{@const dt_mode = datetime_mode(col)}
|
|
1747
|
+
{@const datetime_label_id = get_datetime_label_id(col_id)}
|
|
1748
|
+
{@const drag_side = drag_over_col_id === col_id ? get_drag_side(col_id) : null}
|
|
1216
1749
|
{@const col_width = column_widths[col_id]}
|
|
1217
1750
|
<th
|
|
1218
1751
|
title={col.description}
|
|
1219
1752
|
tabindex={col.sortable === false ? undefined : 0}
|
|
1220
1753
|
role={col.sortable === false ? undefined : `button`}
|
|
1221
|
-
oncontextmenu={(event) =>
|
|
1222
|
-
if (
|
|
1223
|
-
!allow_better_toggle || col.color_scale === null ||
|
|
1224
|
-
col.color_scale === undefined
|
|
1225
|
-
) return
|
|
1226
|
-
event.preventDefault()
|
|
1227
|
-
event.stopPropagation()
|
|
1228
|
-
context_menu_col = col_id
|
|
1229
|
-
const rect = container_el?.getBoundingClientRect()
|
|
1230
|
-
context_menu_pos = {
|
|
1231
|
-
x: event.clientX - (rect?.left ?? 0),
|
|
1232
|
-
y: event.clientY - (rect?.top ?? 0),
|
|
1233
|
-
}
|
|
1234
|
-
}}
|
|
1754
|
+
oncontextmenu={(event) => open_column_context_menu(event, col_id)}
|
|
1235
1755
|
onclick={(event) => {
|
|
1236
1756
|
if (!drag_col_id && !resize_col_id) {
|
|
1237
|
-
sort_rows(
|
|
1238
|
-
col.label,
|
|
1239
|
-
col.group,
|
|
1240
|
-
event,
|
|
1241
|
-
)
|
|
1757
|
+
sort_rows(col.label, col.group, event)
|
|
1242
1758
|
}
|
|
1243
1759
|
}}
|
|
1244
1760
|
onkeydown={(event) => {
|
|
1245
1761
|
if (
|
|
1246
1762
|
(event.key === `Enter` || event.key === ` `) &&
|
|
1247
|
-
!drag_col_id &&
|
|
1763
|
+
!drag_col_id &&
|
|
1764
|
+
!resize_col_id
|
|
1248
1765
|
) {
|
|
1249
1766
|
event.preventDefault()
|
|
1250
1767
|
sort_rows(col.label, col.group, event)
|
|
1251
1768
|
}
|
|
1252
1769
|
}}
|
|
1253
1770
|
style={`${col.style ?? ``}${
|
|
1254
|
-
col_width
|
|
1255
|
-
? `; width: ${col_width}px; min-width: ${col_width}px`
|
|
1256
|
-
: ``
|
|
1771
|
+
col_width ? `; width: ${col_width}px; min-width: ${col_width}px` : ``
|
|
1257
1772
|
}`}
|
|
1258
1773
|
class:sticky-col={col.sticky}
|
|
1259
1774
|
class:not-sortable={col.sortable === false}
|
|
1260
1775
|
class:dragging={drag_col_id === col_id}
|
|
1261
1776
|
class:resizing={resize_col_id === col_id}
|
|
1777
|
+
class:datetime-select-open={datetime_select_open_col_id === col_id}
|
|
1262
1778
|
data-drag-side={drag_side}
|
|
1263
1779
|
draggable="true"
|
|
1264
1780
|
aria-dropeffect="move"
|
|
1265
1781
|
aria-sort={sort_state.column === col_id
|
|
1266
|
-
|
|
1267
|
-
|
|
1782
|
+
? sort_state.ascending
|
|
1783
|
+
? `ascending`
|
|
1784
|
+
: `descending`
|
|
1785
|
+
: `none`}
|
|
1268
1786
|
ondragstart={(event: DragEvent & { currentTarget: HTMLElement }) => {
|
|
1269
1787
|
handle_drag_start(event, col)
|
|
1270
1788
|
event.currentTarget.setAttribute(`aria-grabbed`, `true`)
|
|
@@ -1283,6 +1801,67 @@
|
|
|
1283
1801
|
{@html sanitize_html(col.label)}
|
|
1284
1802
|
{/if}
|
|
1285
1803
|
{@html sanitize_html(sort_indicator(col, sort_state))}
|
|
1804
|
+
{#if is_datetime}
|
|
1805
|
+
<span class="datetime-format-control">
|
|
1806
|
+
<button
|
|
1807
|
+
type="button"
|
|
1808
|
+
class="datetime-format-trigger"
|
|
1809
|
+
aria-labelledby={datetime_label_id}
|
|
1810
|
+
aria-haspopup="listbox"
|
|
1811
|
+
aria-expanded={datetime_select_open_col_id === col_id}
|
|
1812
|
+
data-mode={dt_mode}
|
|
1813
|
+
onkeydown={(event) => event.stopPropagation()}
|
|
1814
|
+
onmousedown={(event) => event.stopPropagation()}
|
|
1815
|
+
onpointerdown={(event) => event.stopPropagation()}
|
|
1816
|
+
onclick={(event) => {
|
|
1817
|
+
event.stopPropagation()
|
|
1818
|
+
datetime_select_open_col_id =
|
|
1819
|
+
datetime_select_open_col_id === col_id ? null : col_id
|
|
1820
|
+
}}
|
|
1821
|
+
{@attach tooltip({
|
|
1822
|
+
content: `Date/time format: ${datetime_format_labels[dt_mode]}`,
|
|
1823
|
+
placement: `top`,
|
|
1824
|
+
})}
|
|
1825
|
+
>
|
|
1826
|
+
<Icon icon="Calendar" />
|
|
1827
|
+
<span id={datetime_label_id} class="sr-only">
|
|
1828
|
+
Date/time format for {strip_html(col.label)}
|
|
1829
|
+
</span>
|
|
1830
|
+
</button>
|
|
1831
|
+
{#if datetime_select_open_col_id === col_id}
|
|
1832
|
+
<select
|
|
1833
|
+
class="datetime-format-select"
|
|
1834
|
+
aria-labelledby={datetime_label_id}
|
|
1835
|
+
value={dt_mode}
|
|
1836
|
+
size={datetime_format_options(col).length}
|
|
1837
|
+
onclick={(event) => {
|
|
1838
|
+
event.stopPropagation()
|
|
1839
|
+
if (event.currentTarget.value === dt_mode) {
|
|
1840
|
+
datetime_select_open_col_id = null
|
|
1841
|
+
}
|
|
1842
|
+
}}
|
|
1843
|
+
onkeydown={(event) => {
|
|
1844
|
+
event.stopPropagation()
|
|
1845
|
+
if (event.key === `Escape`) datetime_select_open_col_id = null
|
|
1846
|
+
}}
|
|
1847
|
+
onmousedown={(event) => event.stopPropagation()}
|
|
1848
|
+
onpointerdown={(event) => event.stopPropagation()}
|
|
1849
|
+
oninput={(event) => {
|
|
1850
|
+
event.stopPropagation()
|
|
1851
|
+
set_datetime_format(
|
|
1852
|
+
col,
|
|
1853
|
+
event.currentTarget.value as DateTimeFormatMode,
|
|
1854
|
+
)
|
|
1855
|
+
datetime_select_open_col_id = null
|
|
1856
|
+
}}
|
|
1857
|
+
>
|
|
1858
|
+
{#each datetime_format_options(col) as mode (mode)}
|
|
1859
|
+
<option value={mode}>{datetime_format_labels[mode]}</option>
|
|
1860
|
+
{/each}
|
|
1861
|
+
</select>
|
|
1862
|
+
{/if}
|
|
1863
|
+
</span>
|
|
1864
|
+
{/if}
|
|
1286
1865
|
<!-- Column resize handle -->
|
|
1287
1866
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
1288
1867
|
<span
|
|
@@ -1290,7 +1869,7 @@
|
|
|
1290
1869
|
onmousedown={(event) => start_resize(event, col)}
|
|
1291
1870
|
role="separator"
|
|
1292
1871
|
aria-orientation="vertical"
|
|
1293
|
-
aria-valuenow={
|
|
1872
|
+
aria-valuenow={col_width ?? 100}
|
|
1294
1873
|
aria-valuemin={50}
|
|
1295
1874
|
aria-valuemax={500}
|
|
1296
1875
|
></span>
|
|
@@ -1298,33 +1877,48 @@
|
|
|
1298
1877
|
{/each}
|
|
1299
1878
|
</tr>
|
|
1300
1879
|
</thead>
|
|
1301
|
-
|
|
1302
|
-
|
|
1880
|
+
{#snippet virtual_spacer(height: number)}
|
|
1881
|
+
<!-- preserves scroll geometry for the unrendered rows above/below the window -->
|
|
1882
|
+
{#if height > 0}
|
|
1883
|
+
<tr class="virtual-spacer" aria-hidden="true" style:height="{height}px">
|
|
1884
|
+
<td colspan={body_colspan}></td>
|
|
1885
|
+
</tr>
|
|
1886
|
+
{/if}
|
|
1887
|
+
{/snippet}
|
|
1888
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions (drag cell-range selection; keyboard copy handled on window) -->
|
|
1889
|
+
<tbody onpointermove={extend_cell_drag}>
|
|
1890
|
+
{@render virtual_spacer(spacer_top)}
|
|
1891
|
+
{#each display_rows as row, row_idx (get_row_id(row))}
|
|
1892
|
+
{@const abs_idx = display_range.start + row_idx}
|
|
1303
1893
|
{@const row_selected = show_row_select && is_row_selected(row)}
|
|
1304
1894
|
<tr
|
|
1305
|
-
animate:flip={{ duration: 500 }}
|
|
1895
|
+
animate:flip={{ duration: virtual_config ? 0 : 500 }}
|
|
1306
1896
|
style={row.style}
|
|
1307
|
-
class={row.class
|
|
1897
|
+
class={row.class}
|
|
1308
1898
|
class:selected={row_selected}
|
|
1899
|
+
data-title={row_title?.(row) || undefined}
|
|
1309
1900
|
tabindex={onrowclick ? 0 : undefined}
|
|
1901
|
+
onpointerdown={onrowpointerdown
|
|
1902
|
+
? (event) => onrowpointerdown(event, row)
|
|
1903
|
+
: undefined}
|
|
1310
1904
|
onclick={onrowclick ? (event) => onrowclick(event, row) : undefined}
|
|
1311
1905
|
ondblclick={onrowdblclick ? (event) => onrowdblclick(event, row) : undefined}
|
|
1312
1906
|
onkeydown={onrowclick
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1907
|
+
? (event) => {
|
|
1908
|
+
if (event.key === `Enter` || event.key === ` `) {
|
|
1909
|
+
event.preventDefault()
|
|
1910
|
+
onrowclick(event, row)
|
|
1911
|
+
} else if (event.key === `ArrowDown`) {
|
|
1912
|
+
event.preventDefault()
|
|
1913
|
+
const next = event.currentTarget.nextElementSibling
|
|
1914
|
+
if (next instanceof HTMLElement) next.focus()
|
|
1915
|
+
} else if (event.key === `ArrowUp`) {
|
|
1916
|
+
event.preventDefault()
|
|
1917
|
+
const prev = event.currentTarget.previousElementSibling
|
|
1918
|
+
if (prev instanceof HTMLElement) prev.focus()
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
: undefined}
|
|
1328
1922
|
>
|
|
1329
1923
|
{#if show_row_select}
|
|
1330
1924
|
<td class="select-col">
|
|
@@ -1336,36 +1930,45 @@
|
|
|
1336
1930
|
</td>
|
|
1337
1931
|
{/if}
|
|
1338
1932
|
{#if show_row_numbers}
|
|
1339
|
-
<td class="row-num-col">
|
|
1340
|
-
{(current_page - 1) * effective_page_size + row_idx + 1}
|
|
1341
|
-
</td>
|
|
1933
|
+
<td class="row-num-col">{abs_idx + 1}</td>
|
|
1342
1934
|
{/if}
|
|
1343
|
-
{#each visible_columns as col (get_col_id(col))}
|
|
1344
|
-
{@const
|
|
1935
|
+
{#each visible_columns as col, col_idx (get_col_id(col))}
|
|
1936
|
+
{@const col_id = get_col_id(col)}
|
|
1937
|
+
{@const val = row[col_id]}
|
|
1345
1938
|
{@const color = calc_color(val, col)}
|
|
1346
|
-
{@const col_width = column_widths[
|
|
1939
|
+
{@const col_width = column_widths[col_id]}
|
|
1940
|
+
{@const date_val = is_datetime_column(col)
|
|
1941
|
+
? format_datetime_cell(val, col)
|
|
1942
|
+
: null}
|
|
1347
1943
|
<td
|
|
1348
1944
|
data-col={col.label}
|
|
1349
|
-
data-sort-value={
|
|
1945
|
+
data-sort-value={get_cell_sort_attr(val)}
|
|
1946
|
+
data-row-idx={abs_idx}
|
|
1947
|
+
data-col-idx={col_idx}
|
|
1350
1948
|
class:sticky-col={col.sticky}
|
|
1949
|
+
class:cell-selected={selected_cell_keys.has(`${abs_idx}:${col_idx}`)}
|
|
1950
|
+
onpointerdown={(event) => start_cell_drag(event, abs_idx, col_idx)}
|
|
1951
|
+
oncontextmenu={(event) => {
|
|
1952
|
+
// keep the native context menu for links/buttons/inputs inside cells
|
|
1953
|
+
if (is_interactive_cell_target(event.target)) return
|
|
1954
|
+
open_column_context_menu(event, col_id)
|
|
1955
|
+
}}
|
|
1351
1956
|
style:--cell-bg={color.bg}
|
|
1352
1957
|
style:color={color.text}
|
|
1353
1958
|
style={`${col.cell_style ?? col.style ?? ``}${
|
|
1354
|
-
col_width
|
|
1355
|
-
? `; width: ${col_width}px; max-width: ${col_width}px`
|
|
1356
|
-
: ``
|
|
1959
|
+
col_width ? `; width: ${col_width}px; max-width: ${col_width}px` : ``
|
|
1357
1960
|
}`}
|
|
1358
1961
|
>
|
|
1359
1962
|
{#if special_cells?.[col.label]}
|
|
1360
1963
|
{@render special_cells[col.label]({ row, col, val })}
|
|
1361
1964
|
{:else if cell}
|
|
1362
1965
|
{@render cell({ row, col, val })}
|
|
1966
|
+
{:else if date_val != null}
|
|
1967
|
+
{date_val}
|
|
1363
1968
|
{:else if typeof val === `number` && !Number.isNaN(val)}
|
|
1364
1969
|
{format_num(val, col.format ?? default_num_format)}
|
|
1365
1970
|
{:else if val === undefined || val === null || Number.isNaN(val)}
|
|
1366
|
-
<span {@attach tooltip({ content: `Not available` })}>
|
|
1367
|
-
n/a
|
|
1368
|
-
</span>
|
|
1971
|
+
<span {@attach tooltip({ content: `Not available` })}> n/a </span>
|
|
1369
1972
|
{:else}
|
|
1370
1973
|
{@html sanitize_html(val)}
|
|
1371
1974
|
{/if}
|
|
@@ -1375,15 +1978,11 @@
|
|
|
1375
1978
|
{:else}
|
|
1376
1979
|
{#if empty_message}
|
|
1377
1980
|
<tr class="empty-row">
|
|
1378
|
-
<td
|
|
1379
|
-
colspan={visible_columns.length + (show_row_select ? 1 : 0) +
|
|
1380
|
-
(show_row_numbers ? 1 : 0)}
|
|
1381
|
-
>
|
|
1382
|
-
{empty_message}
|
|
1383
|
-
</td>
|
|
1981
|
+
<td colspan={body_colspan}>{empty_message}</td>
|
|
1384
1982
|
</tr>
|
|
1385
1983
|
{/if}
|
|
1386
1984
|
{/each}
|
|
1985
|
+
{@render virtual_spacer(spacer_bottom)}
|
|
1387
1986
|
</tbody>
|
|
1388
1987
|
{#if footer}
|
|
1389
1988
|
<tfoot>
|
|
@@ -1395,12 +1994,18 @@
|
|
|
1395
1994
|
|
|
1396
1995
|
{@render sort_hint_element(`bottom`)}
|
|
1397
1996
|
|
|
1997
|
+
{#if virtual_config && sorted_data.length > display_rows.length}
|
|
1998
|
+
<div class="row-count-info">
|
|
1999
|
+
{display_rows.length} of {sorted_data.length} rows
|
|
2000
|
+
</div>
|
|
2001
|
+
{/if}
|
|
2002
|
+
|
|
1398
2003
|
{#if pagination_config && total_pages > 1}
|
|
1399
2004
|
<div class="pagination">
|
|
1400
2005
|
<button
|
|
1401
2006
|
class="page-btn"
|
|
1402
2007
|
disabled={current_page === 1}
|
|
1403
|
-
onclick={() => current_page = 1}
|
|
2008
|
+
onclick={() => (current_page = 1)}
|
|
1404
2009
|
title="First page"
|
|
1405
2010
|
>
|
|
1406
2011
|
«
|
|
@@ -1441,7 +2046,7 @@
|
|
|
1441
2046
|
<button
|
|
1442
2047
|
class="page-btn"
|
|
1443
2048
|
disabled={current_page === total_pages}
|
|
1444
|
-
onclick={() => current_page = total_pages}
|
|
2049
|
+
onclick={() => (current_page = total_pages)}
|
|
1445
2050
|
title="Last page"
|
|
1446
2051
|
>
|
|
1447
2052
|
»
|
|
@@ -1450,8 +2055,10 @@
|
|
|
1450
2055
|
<select
|
|
1451
2056
|
class="page-size-select"
|
|
1452
2057
|
onchange={(event) => {
|
|
1453
|
-
|
|
2058
|
+
const page_size = parseInt(event.currentTarget.value, 10)
|
|
2059
|
+
effective_page_size = page_size
|
|
1454
2060
|
current_page = 1
|
|
2061
|
+
pagination_config.on_page_size_change?.(page_size)
|
|
1455
2062
|
}}
|
|
1456
2063
|
>
|
|
1457
2064
|
{#each pagination_config.page_sizes as size (size)}
|
|
@@ -1465,11 +2072,13 @@
|
|
|
1465
2072
|
{/if}
|
|
1466
2073
|
|
|
1467
2074
|
<ContextMenu
|
|
1468
|
-
sections={
|
|
1469
|
-
selected_values={{
|
|
2075
|
+
sections={context_menu_sections}
|
|
2076
|
+
selected_values={{
|
|
2077
|
+
'Gradient direction': better_overrides.get(context_menu_col ?? ``) ?? ``,
|
|
2078
|
+
}}
|
|
1470
2079
|
position={context_menu_pos}
|
|
1471
2080
|
visible={context_menu_col !== null}
|
|
1472
|
-
on_close={() => context_menu_col = null}
|
|
2081
|
+
on_close={() => (context_menu_col = null)}
|
|
1473
2082
|
style={[
|
|
1474
2083
|
`--surface-bg: light-dark(#fff, #1e1e1e)`,
|
|
1475
2084
|
`--border-color: light-dark(rgba(0,0,0,0.15), rgba(255,255,255,0.15))`,
|
|
@@ -1479,13 +2088,7 @@
|
|
|
1479
2088
|
`--accent-color: light-dark(rgba(0,0,0,0.1), rgba(255,255,255,0.15))`,
|
|
1480
2089
|
`z-index: 200`,
|
|
1481
2090
|
].join(`; `)}
|
|
1482
|
-
on_select={
|
|
1483
|
-
if (!context_menu_col) return
|
|
1484
|
-
const current = better_overrides.get(context_menu_col)
|
|
1485
|
-
if (current === option.value) better_overrides.delete(context_menu_col)
|
|
1486
|
-
else better_overrides.set(context_menu_col, option.value as `higher` | `lower`)
|
|
1487
|
-
context_menu_col = null
|
|
1488
|
-
}}
|
|
2091
|
+
on_select={handle_context_menu_select}
|
|
1489
2092
|
/>
|
|
1490
2093
|
</div>
|
|
1491
2094
|
|
|
@@ -1507,15 +2110,30 @@
|
|
|
1507
2110
|
.table-scroll.has-scroll {
|
|
1508
2111
|
border: 1px solid light-dark(rgba(0, 0, 0, 0.12), rgba(255, 255, 255, 0.12));
|
|
1509
2112
|
border-radius: var(--border-radius, 3pt);
|
|
1510
|
-
overflow-x: hidden;
|
|
1511
|
-
overflow-y: auto;
|
|
1512
2113
|
}
|
|
1513
2114
|
table {
|
|
1514
2115
|
border-collapse: separate;
|
|
1515
2116
|
border-spacing: 0;
|
|
1516
2117
|
display: table; /* Override global display: block to enable sticky headers */
|
|
1517
2118
|
}
|
|
1518
|
-
|
|
2119
|
+
/* during a cell-range drag, native text selection would fight the
|
|
2120
|
+
rectangle highlight */
|
|
2121
|
+
.table-container.cell-dragging {
|
|
2122
|
+
cursor: cell;
|
|
2123
|
+
user-select: none;
|
|
2124
|
+
}
|
|
2125
|
+
/* background-image stacks on top of the per-cell heatmap background-color,
|
|
2126
|
+
so selected heatmap cells stay tinted underneath */
|
|
2127
|
+
td.cell-selected {
|
|
2128
|
+
background-image: linear-gradient(
|
|
2129
|
+
color-mix(in srgb, var(--accent-color, #4a9eff) 30%, transparent),
|
|
2130
|
+
color-mix(in srgb, var(--accent-color, #4a9eff) 30%, transparent)
|
|
2131
|
+
);
|
|
2132
|
+
box-shadow: inset 0 0 0 1px
|
|
2133
|
+
color-mix(in srgb, var(--accent-color, #4a9eff) 55%, transparent);
|
|
2134
|
+
}
|
|
2135
|
+
th,
|
|
2136
|
+
td {
|
|
1519
2137
|
padding: var(--heatmap-cell-padding, 1pt 5pt);
|
|
1520
2138
|
text-align: var(--heatmap-text-align, left);
|
|
1521
2139
|
border: var(--heatmap-cell-border, none);
|
|
@@ -1541,6 +2159,78 @@
|
|
|
1541
2159
|
th:hover {
|
|
1542
2160
|
background: var(--heatmap-header-hover-bg, var(--nav-bg));
|
|
1543
2161
|
}
|
|
2162
|
+
th.datetime-select-open {
|
|
2163
|
+
overflow: visible;
|
|
2164
|
+
z-index: 30;
|
|
2165
|
+
}
|
|
2166
|
+
.datetime-format-control {
|
|
2167
|
+
display: inline-flex;
|
|
2168
|
+
align-items: center;
|
|
2169
|
+
margin-left: 3px;
|
|
2170
|
+
position: relative;
|
|
2171
|
+
vertical-align: middle;
|
|
2172
|
+
}
|
|
2173
|
+
.datetime-format-trigger {
|
|
2174
|
+
display: inline-grid;
|
|
2175
|
+
place-items: center;
|
|
2176
|
+
width: 14px;
|
|
2177
|
+
height: 14px;
|
|
2178
|
+
padding: 0;
|
|
2179
|
+
border: 0;
|
|
2180
|
+
border-radius: 3px;
|
|
2181
|
+
background: transparent;
|
|
2182
|
+
color: inherit;
|
|
2183
|
+
cursor: pointer;
|
|
2184
|
+
line-height: 1;
|
|
2185
|
+
}
|
|
2186
|
+
.datetime-format-trigger:hover,
|
|
2187
|
+
.datetime-format-trigger[aria-expanded='true'] {
|
|
2188
|
+
background: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.16));
|
|
2189
|
+
}
|
|
2190
|
+
.datetime-format-trigger :global(svg) {
|
|
2191
|
+
width: 10px;
|
|
2192
|
+
height: 10px;
|
|
2193
|
+
opacity: 0.75;
|
|
2194
|
+
transform: translateY(-1px);
|
|
2195
|
+
}
|
|
2196
|
+
.sr-only {
|
|
2197
|
+
position: absolute;
|
|
2198
|
+
width: 1px;
|
|
2199
|
+
height: 1px;
|
|
2200
|
+
padding: 0;
|
|
2201
|
+
margin: -1px;
|
|
2202
|
+
overflow: hidden;
|
|
2203
|
+
clip: rect(0, 0, 0, 0);
|
|
2204
|
+
white-space: nowrap;
|
|
2205
|
+
border: 0;
|
|
2206
|
+
}
|
|
2207
|
+
.datetime-format-select {
|
|
2208
|
+
position: absolute;
|
|
2209
|
+
top: calc(100% + 2px);
|
|
2210
|
+
right: 0;
|
|
2211
|
+
z-index: 20;
|
|
2212
|
+
min-width: max-content;
|
|
2213
|
+
max-width: 10em;
|
|
2214
|
+
padding: 2px;
|
|
2215
|
+
border: 1px solid light-dark(rgba(0, 0, 0, 0.12), rgba(255, 255, 255, 0.18));
|
|
2216
|
+
border-radius: 3px;
|
|
2217
|
+
background: var(--heatmap-header-bg, var(--page-bg, Canvas));
|
|
2218
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
|
|
2219
|
+
color: inherit;
|
|
2220
|
+
cursor: pointer;
|
|
2221
|
+
font-size: 0.9em;
|
|
2222
|
+
line-height: 1.35;
|
|
2223
|
+
outline: none;
|
|
2224
|
+
}
|
|
2225
|
+
.datetime-format-select option {
|
|
2226
|
+
padding: 3px 8px;
|
|
2227
|
+
}
|
|
2228
|
+
.datetime-format-select option:checked {
|
|
2229
|
+
background: light-dark(rgba(74, 158, 255, 0.18), rgba(122, 179, 255, 0.28));
|
|
2230
|
+
box-shadow: 0 0 0 100vmax light-dark(rgba(74, 158, 255, 0.18), rgba(122, 179, 255, 0.28))
|
|
2231
|
+
inset;
|
|
2232
|
+
color: inherit;
|
|
2233
|
+
}
|
|
1544
2234
|
th.dragging {
|
|
1545
2235
|
opacity: 0.4;
|
|
1546
2236
|
cursor: grabbing;
|
|
@@ -1565,10 +2255,20 @@
|
|
|
1565
2255
|
td.sticky-col {
|
|
1566
2256
|
position: sticky;
|
|
1567
2257
|
left: 0;
|
|
1568
|
-
background: var(--page-bg, Canvas);
|
|
2258
|
+
background: var(--heatmap-sticky-cell-bg, var(--page-bg, Canvas));
|
|
1569
2259
|
z-index: 1;
|
|
1570
2260
|
border-right: 1px solid var(--border, #ddd);
|
|
1571
2261
|
}
|
|
2262
|
+
/* separate odd-row var so consumers with striped rows can composite their stripe
|
|
2263
|
+
color over the opaque sticky background (which must stay opaque to occlude columns
|
|
2264
|
+
scrolling beneath it), e.g.
|
|
2265
|
+
--heatmap-sticky-cell-odd-bg: linear-gradient(var(--stripe), var(--stripe)), var(--page-bg) */
|
|
2266
|
+
tbody tr:nth-child(odd) td.sticky-col {
|
|
2267
|
+
background: var(
|
|
2268
|
+
--heatmap-sticky-cell-odd-bg,
|
|
2269
|
+
var(--heatmap-sticky-cell-bg, var(--page-bg, Canvas))
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
1572
2272
|
tbody tr:hover {
|
|
1573
2273
|
filter: var(--heatmap-row-hover-filter, brightness(1.1));
|
|
1574
2274
|
}
|
|
@@ -1602,35 +2302,57 @@
|
|
|
1602
2302
|
transition: opacity 0.15s;
|
|
1603
2303
|
}
|
|
1604
2304
|
.table-container:hover .control-buttons,
|
|
1605
|
-
.control-buttons:focus-within
|
|
2305
|
+
.control-buttons:focus-within,
|
|
2306
|
+
/* keep visible while a dropdown/pane is open or when hosted in a panel
|
|
2307
|
+
toolbar (portaled out of the table, so container hover can't reveal it) */
|
|
2308
|
+
.control-buttons.force-visible,
|
|
2309
|
+
.control-buttons.portaled {
|
|
1606
2310
|
opacity: 1;
|
|
1607
2311
|
pointer-events: auto;
|
|
1608
2312
|
}
|
|
1609
|
-
.
|
|
1610
|
-
|
|
2313
|
+
.control-buttons.portaled {
|
|
2314
|
+
margin: 0;
|
|
2315
|
+
}
|
|
2316
|
+
/* .pane-toggle = the settings-pane gear, which sits in the control row and
|
|
2317
|
+
must match the other .icon-btn buttons: uniform square ghost buttons */
|
|
2318
|
+
.icon-btn,
|
|
2319
|
+
.control-buttons > :global(button.pane-toggle) {
|
|
2320
|
+
box-sizing: border-box;
|
|
2321
|
+
inline-size: 22px;
|
|
2322
|
+
block-size: 22px;
|
|
2323
|
+
padding: 0;
|
|
1611
2324
|
border: none;
|
|
1612
2325
|
border-radius: 3px;
|
|
1613
|
-
background:
|
|
1614
|
-
color
|
|
2326
|
+
background: transparent;
|
|
2327
|
+
/* dim resting color so the hover jump to full contrast reads clearly */
|
|
2328
|
+
color: light-dark(#6b7280, #98a0ae);
|
|
1615
2329
|
cursor: pointer;
|
|
1616
2330
|
display: flex;
|
|
1617
2331
|
align-items: center;
|
|
1618
2332
|
justify-content: center;
|
|
1619
2333
|
gap: 2px;
|
|
1620
2334
|
font-size: 0.8em;
|
|
2335
|
+
transition: color 0.02s linear;
|
|
1621
2336
|
}
|
|
1622
|
-
.icon-btn :global(svg)
|
|
1623
|
-
|
|
1624
|
-
|
|
2337
|
+
.icon-btn :global(svg),
|
|
2338
|
+
.control-buttons > :global(button.pane-toggle svg) {
|
|
2339
|
+
width: 14px;
|
|
2340
|
+
height: 14px;
|
|
1625
2341
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
2342
|
+
/* toolbar buttons give color-only hover feedback — no background shading */
|
|
2343
|
+
.icon-btn:hover,
|
|
2344
|
+
.control-buttons > :global(button.pane-toggle:hover) {
|
|
2345
|
+
background: transparent;
|
|
2346
|
+
color: light-dark(#000, #fff);
|
|
1628
2347
|
}
|
|
1629
2348
|
.icon-btn.active {
|
|
1630
|
-
|
|
2349
|
+
color: var(--active-color, #4a9eff);
|
|
1631
2350
|
}
|
|
1632
2351
|
.selection-badge {
|
|
1633
2352
|
position: relative;
|
|
2353
|
+
/* row-count badge next to the clear icon makes this one wider */
|
|
2354
|
+
inline-size: auto;
|
|
2355
|
+
padding: 0 4px;
|
|
1634
2356
|
}
|
|
1635
2357
|
.selection-badge .badge {
|
|
1636
2358
|
background: var(--highlight, #4a9eff);
|
|
@@ -1716,7 +2438,8 @@
|
|
|
1716
2438
|
tr.highlight {
|
|
1717
2439
|
background-color: var(--nav-bg) !important;
|
|
1718
2440
|
}
|
|
1719
|
-
tr.highlight,
|
|
2441
|
+
tr.highlight,
|
|
2442
|
+
tr.highlight :global(a) {
|
|
1720
2443
|
color: var(--highlight) !important;
|
|
1721
2444
|
}
|
|
1722
2445
|
|
|
@@ -1738,6 +2461,18 @@
|
|
|
1738
2461
|
border-top: 1px solid var(--highlight, #4a9eff);
|
|
1739
2462
|
border-bottom: 1px solid var(--highlight, #4a9eff);
|
|
1740
2463
|
}
|
|
2464
|
+
/* Virtualized rows: spacers keep scroll geometry; the count line replaces
|
|
2465
|
+
pagination in infinite-scroll mode */
|
|
2466
|
+
.virtual-spacer td {
|
|
2467
|
+
padding: 0;
|
|
2468
|
+
border: none;
|
|
2469
|
+
}
|
|
2470
|
+
.row-count-info {
|
|
2471
|
+
padding: 4px 8px;
|
|
2472
|
+
font-size: 0.8em;
|
|
2473
|
+
text-align: right;
|
|
2474
|
+
opacity: 0.6;
|
|
2475
|
+
}
|
|
1741
2476
|
/* Pagination */
|
|
1742
2477
|
.pagination {
|
|
1743
2478
|
display: flex;
|
|
@@ -1857,11 +2592,13 @@
|
|
|
1857
2592
|
font-style: italic;
|
|
1858
2593
|
}
|
|
1859
2594
|
.row-num-col {
|
|
1860
|
-
text-align: right;
|
|
2595
|
+
text-align: var(--heatmap-row-num-align, right);
|
|
1861
2596
|
color: var(--text-muted, #888);
|
|
1862
2597
|
font-size: 0.85em;
|
|
1863
2598
|
width: 2em;
|
|
1864
|
-
padding
|
|
2599
|
+
/* left default matches the th,td --heatmap-cell-padding fallback */
|
|
2600
|
+
padding-left: var(--heatmap-row-num-padding-left, 5pt);
|
|
2601
|
+
padding-right: var(--heatmap-row-num-padding-right, 8px) !important;
|
|
1865
2602
|
}
|
|
1866
2603
|
.page-size-select {
|
|
1867
2604
|
padding: 2px 4px;
|