matterviz 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FilePicker.svelte +40 -50
- package/dist/api/optimade.js +10 -7
- package/dist/app.css +15 -0
- package/dist/brillouin/BrillouinZone.svelte +18 -9
- package/dist/brillouin/BrillouinZoneScene.svelte +20 -29
- package/dist/brillouin/compute.js +10 -16
- package/dist/brillouin/geometry.js +2 -2
- package/dist/chempot-diagram/ChemPotDiagram.svelte +18 -15
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +17 -113
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +104 -264
- package/dist/chempot-diagram/async-compute.svelte.js +12 -13
- package/dist/chempot-diagram/color.d.ts +21 -1
- package/dist/chempot-diagram/color.js +70 -1
- package/dist/chempot-diagram/compute.d.ts +7 -0
- package/dist/chempot-diagram/compute.js +85 -26
- package/dist/chempot-diagram/export.js +1 -6
- package/dist/chempot-diagram/temperature.js +9 -10
- package/dist/composition/BarChart.svelte +7 -9
- package/dist/composition/Composition.svelte +3 -8
- package/dist/composition/Formula.svelte +2 -2
- package/dist/composition/FormulaFilter.svelte +27 -93
- package/dist/composition/format.js +6 -8
- package/dist/composition/parse.js +50 -105
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +6 -17
- package/dist/convex-hull/ConvexHull2D.svelte +0 -2
- package/dist/convex-hull/ConvexHull3D.svelte +19 -47
- package/dist/convex-hull/ConvexHull4D.svelte +65 -95
- package/dist/convex-hull/StructurePopup.svelte +1 -1
- package/dist/convex-hull/gas-thermodynamics.js +21 -53
- package/dist/convex-hull/helpers.js +1 -7
- package/dist/convex-hull/thermodynamics.js +19 -27
- package/dist/coordination/CoordinationBarPlot.svelte +12 -21
- package/dist/element/BohrAtom.svelte +0 -1
- package/dist/element/ElementTile.svelte +26 -44
- package/dist/element/data.d.ts +2 -0
- package/dist/element/data.js +5 -1
- package/dist/element/index.d.ts +1 -1
- package/dist/element/index.js +1 -1
- package/dist/fermi-surface/FermiSurface.svelte +22 -23
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +2 -6
- package/dist/fermi-surface/FermiSurfaceScene.svelte +32 -77
- package/dist/fermi-surface/compute.js +22 -55
- package/dist/fermi-surface/parse.js +38 -70
- package/dist/file-viewer/JsonBrowser.svelte +1043 -0
- package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
- package/dist/file-viewer/PlotPanel.svelte +350 -0
- package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
- package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
- package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
- package/dist/file-viewer/detect.d.ts +11 -0
- package/dist/file-viewer/detect.js +381 -0
- package/dist/file-viewer/eligibility.d.ts +4 -0
- package/dist/file-viewer/eligibility.js +53 -0
- package/dist/file-viewer/host-protocol.d.ts +50 -0
- package/dist/file-viewer/host-transfer.d.ts +25 -0
- package/dist/file-viewer/host-transfer.js +42 -0
- package/dist/file-viewer/index.d.ts +5 -0
- package/dist/file-viewer/index.js +5 -0
- package/dist/file-viewer/main.d.ts +44 -0
- package/dist/file-viewer/main.js +535 -0
- package/dist/file-viewer/parse.d.ts +9 -0
- package/dist/file-viewer/parse.js +170 -0
- package/dist/file-viewer/plot-utils.d.ts +24 -0
- package/dist/file-viewer/plot-utils.js +191 -0
- package/dist/file-viewer/types.d.ts +4 -0
- package/dist/file-viewer/types.js +5 -0
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +57 -84
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/io/ExportPane.svelte +1 -1
- package/dist/io/decompress.d.ts +2 -0
- package/dist/io/decompress.js +3 -2
- package/dist/io/export.d.ts +7 -4
- package/dist/io/export.js +70 -18
- package/dist/io/file-drop.js +60 -35
- package/dist/io/url-drop.d.ts +2 -0
- package/dist/io/url-drop.js +28 -16
- package/dist/isosurface/Isosurface.svelte +687 -196
- package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
- package/dist/isosurface/IsosurfaceControls.svelte +564 -85
- package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
- package/dist/isosurface/VolumeSlice.svelte +224 -0
- package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
- package/dist/isosurface/coloring.d.ts +20 -0
- package/dist/isosurface/coloring.js +118 -0
- package/dist/isosurface/geometry-worker-types.d.ts +38 -0
- package/dist/isosurface/geometry.worker.d.ts +1 -0
- package/dist/isosurface/geometry.worker.js +59 -0
- package/dist/isosurface/grid.d.ts +10 -0
- package/dist/isosurface/grid.js +31 -0
- package/dist/isosurface/index.d.ts +4 -0
- package/dist/isosurface/index.js +4 -0
- package/dist/isosurface/profile.d.ts +12 -0
- package/dist/isosurface/profile.js +19 -0
- package/dist/isosurface/sampling.d.ts +34 -0
- package/dist/isosurface/sampling.js +408 -0
- package/dist/isosurface/slice-rendering.d.ts +13 -0
- package/dist/isosurface/slice-rendering.js +79 -0
- package/dist/isosurface/slice.d.ts +22 -2
- package/dist/isosurface/slice.js +200 -116
- package/dist/isosurface/types.d.ts +24 -1
- package/dist/isosurface/types.js +120 -8
- package/dist/json-path.d.ts +4 -0
- package/dist/json-path.js +89 -0
- package/dist/labels.js +1 -1
- package/dist/layout/InfoCard.svelte +1 -3
- package/dist/layout/SettingsSection.svelte +52 -61
- package/dist/layout/SettingsSection.svelte.d.ts +6 -7
- package/dist/layout/ViewerChrome.svelte +7 -1
- package/dist/layout/json-tree/JsonNode.svelte +14 -48
- package/dist/layout/json-tree/JsonTree.svelte +48 -95
- package/dist/layout/json-tree/JsonValue.svelte +3 -18
- package/dist/layout/json-tree/utils.d.ts +0 -3
- package/dist/layout/json-tree/utils.js +50 -179
- package/dist/marching-cubes.d.ts +8 -1
- package/dist/marching-cubes.js +115 -48
- package/dist/math.d.ts +3 -2
- package/dist/math.js +29 -38
- package/dist/overlays/ContextMenu.svelte +15 -32
- package/dist/overlays/DraggablePane.svelte +39 -58
- package/dist/overlays/DraggablePane.svelte.d.ts +1 -1
- package/dist/overlays/GlassChip.svelte +3 -1
- package/dist/periodic-table/PeriodicTableControls.svelte +26 -52
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +18 -34
- package/dist/phase-diagram/build-diagram.js +1 -6
- package/dist/phase-diagram/svg-to-diagram.js +9 -14
- package/dist/plot/bar/BarPlot.svelte +38 -42
- package/dist/plot/bar/SpacegroupBarPlot.svelte +18 -42
- package/dist/plot/box/BoxPlot.svelte +27 -23
- package/dist/plot/core/auto-place.d.ts +1 -1
- package/dist/plot/core/auto-place.js +1 -1
- package/dist/plot/core/data-transform.d.ts +1 -2
- package/dist/plot/core/data-transform.js +0 -10
- package/dist/plot/core/layout.d.ts +12 -5
- package/dist/plot/core/layout.js +37 -27
- package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
- package/dist/plot/core/pan-zoom.svelte.js +34 -17
- package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
- package/dist/plot/core/placed-tween.svelte.js +68 -21
- package/dist/plot/core/svg.js +0 -1
- package/dist/plot/core/utils/hierarchy-chart.d.ts +1 -0
- package/dist/plot/core/utils/hierarchy-chart.js +12 -0
- package/dist/plot/core/utils/hierarchy-labels.js +3 -2
- package/dist/plot/histogram/Histogram.svelte +21 -26
- package/dist/plot/sankey/sankey.js +13 -9
- package/dist/plot/scatter/BinnedScatterPlot.svelte +59 -43
- package/dist/plot/scatter/ScatterPlot.svelte +67 -52
- package/dist/plot/scatter/adaptive-density.d.ts +1 -1
- package/dist/plot/scatter/adaptive-density.js +36 -20
- package/dist/plot/sunburst/Sunburst.svelte +20 -20
- package/dist/plot/treemap/Treemap.svelte +175 -60
- package/dist/plot/treemap/Treemap.svelte.d.ts +6 -0
- package/dist/plot/treemap/index.d.ts +1 -0
- package/dist/plot/treemap/labels.d.ts +38 -0
- package/dist/plot/treemap/labels.js +80 -0
- package/dist/plot/treemap/treemap.d.ts +4 -2
- package/dist/plot/treemap/treemap.js +5 -2
- package/dist/rdf/RdfPlot.svelte +17 -35
- package/dist/rdf/calc-rdf.js +59 -44
- package/dist/rdf/index.d.ts +0 -1
- package/dist/sanitize.js +84 -26
- package/dist/scene/SceneCamera.svelte +1 -1
- package/dist/scene/SceneCamera.svelte.d.ts +1 -1
- package/dist/scene/index.d.ts +1 -2
- package/dist/scene/index.js +1 -1
- package/dist/scene/{props.js → props.svelte.js} +15 -1
- package/dist/settings.js +159 -482
- package/dist/spectral/Bands.svelte +151 -158
- package/dist/spectral/BrillouinBandsDos.svelte +4 -19
- package/dist/spectral/Dos.svelte +26 -24
- package/dist/spectral/helpers.js +40 -105
- package/dist/structure/AtomLegend.svelte +8 -13
- package/dist/structure/AtomLegend.svelte.d.ts +2 -3
- package/dist/structure/CellSelect.svelte +35 -10
- package/dist/structure/Structure.svelte +177 -99
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureCarousel.svelte +10 -3
- package/dist/structure/StructureControls.svelte +8 -26
- package/dist/structure/StructureControls.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +65 -76
- package/dist/structure/StructureScene.svelte +75 -109
- package/dist/structure/StructureScene.svelte.d.ts +3 -1
- package/dist/structure/StructureViewport.svelte +120 -55
- package/dist/structure/StructureViewport.svelte.d.ts +3 -1
- package/dist/structure/atom-properties.d.ts +1 -1
- package/dist/structure/atom-properties.js +4 -3
- package/dist/structure/bond-order-perception.js +3 -8
- package/dist/structure/bonding.d.ts +0 -1
- package/dist/structure/bonding.js +16 -23
- package/dist/structure/export.js +50 -74
- package/dist/structure/format-detect.js +5 -7
- package/dist/structure/index.js +6 -12
- package/dist/structure/parse.js +38 -100
- package/dist/structure/partial-occupancy.js +7 -4
- package/dist/structure/pbc.js +8 -10
- package/dist/structure/polyhedra.js +5 -4
- package/dist/structure/prop-groups.d.ts +85 -0
- package/dist/structure/prop-groups.js +18 -0
- package/dist/symmetry/SymmetryStats.svelte +0 -4
- package/dist/symmetry/WyckoffTable.svelte +5 -4
- package/dist/symmetry/cell-transform.js +5 -14
- package/dist/symmetry/index.js +6 -9
- package/dist/table/HeatmapTable.svelte +61 -103
- package/dist/table/ToggleMenu.svelte +166 -44
- package/dist/table/ToggleMenu.svelte.d.ts +1 -1
- package/dist/theme/embedded.d.ts +18 -0
- package/dist/theme/embedded.js +206 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +1 -2
- package/dist/theme/themes.mjs +31 -60
- package/dist/tooltip/TooltipContent.svelte +1 -1
- package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
- package/dist/tooltip/index.d.ts +8 -1
- package/dist/trajectory/Trajectory.svelte +140 -120
- package/dist/trajectory/TrajectoryError.svelte +1 -66
- package/dist/trajectory/TrajectoryExportPane.svelte +0 -1
- package/dist/trajectory/TrajectoryInfoPane.svelte +55 -80
- package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
- package/dist/trajectory/extract.js +19 -29
- package/dist/trajectory/format-detect.d.ts +2 -0
- package/dist/trajectory/format-detect.js +7 -5
- package/dist/trajectory/frame-reader.js +3 -4
- package/dist/trajectory/helpers.js +16 -49
- package/dist/trajectory/index.js +2 -8
- package/dist/trajectory/parse/ase.js +1 -1
- package/dist/trajectory/parse/index.d.ts +4 -3
- package/dist/trajectory/parse/index.js +12 -17
- package/dist/trajectory/parse/vaspout-h5.d.ts +0 -1
- package/dist/trajectory/parse/vaspout-h5.js +1 -4
- package/dist/trajectory/parse/xyz.js +48 -2
- package/dist/trajectory/plotting.js +14 -37
- package/dist/xrd/XrdPlot.svelte +43 -68
- package/dist/xrd/calc-xrd.js +22 -58
- package/dist/xrd/parse.js +13 -22
- package/package.json +40 -8
- package/readme.md +6 -5
- package/dist/scene/types.d.ts +0 -26
- package/dist/tooltip/types.d.ts +0 -8
- package/dist/trajectory/constants.d.ts +0 -6
- package/dist/trajectory/constants.js +0 -11
- /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
- /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Worker-safe file parsing with no Svelte or DOM imports.
|
|
2
|
+
import { COMPRESSION_EXTENSIONS_REGEX } from '../constants';
|
|
3
|
+
import { parse_fermi_file } from '../fermi-surface/parse';
|
|
4
|
+
import { decompress_data, decompress_data_binary, detect_compression_format, } from '../io/decompress';
|
|
5
|
+
import { parse_volumetric_file } from '../isosurface/parse';
|
|
6
|
+
import { is_vaspwave_filename, parse_vaspwave_charge } from '../isosurface/parse-vaspwave';
|
|
7
|
+
import { parse_structure_file } from '../structure/parse';
|
|
8
|
+
import { is_trajectory_file, parse_trajectory_data } from '../trajectory/parse';
|
|
9
|
+
import { FERMI_FILE_RE, VOLUMETRIC_EXT_RE, VOLUMETRIC_VASP_RE } from './types';
|
|
10
|
+
import { detect_view_type } from './detect';
|
|
11
|
+
// Maps detect.ts RenderableType to ViewType for direct rendering.
|
|
12
|
+
// Types not listed here fall through to json_browser (which can render all types
|
|
13
|
+
// via its internal mount_into, giving the user a tree view alongside the viz).
|
|
14
|
+
// structure and volumetric have special handling in parse_file_content below.
|
|
15
|
+
const DETECTION_TO_VIEW_TYPE = {
|
|
16
|
+
fermi_surface: `fermi_surface`,
|
|
17
|
+
band_grid: `fermi_surface`,
|
|
18
|
+
convex_hull: `convex_hull`,
|
|
19
|
+
phase_diagram: `phase_diagram`,
|
|
20
|
+
};
|
|
21
|
+
// Convert base64 to ArrayBuffer for binary files
|
|
22
|
+
export function base64_to_array_buffer(base64) {
|
|
23
|
+
const binary = atob(base64);
|
|
24
|
+
const bytes = new Uint8Array(binary.length);
|
|
25
|
+
for (let idx = 0; idx < binary.length; idx++) {
|
|
26
|
+
bytes[idx] = binary.charCodeAt(idx);
|
|
27
|
+
}
|
|
28
|
+
return bytes.buffer;
|
|
29
|
+
}
|
|
30
|
+
// Parse file content and determine if it's a structure or trajectory
|
|
31
|
+
export const parse_file_content = async (content, filename, is_base64 = false) => {
|
|
32
|
+
// Handle base64-encoded compressed/binary files by converting them first
|
|
33
|
+
if (is_base64) {
|
|
34
|
+
let buffer = base64_to_array_buffer(content);
|
|
35
|
+
const compression_format = detect_compression_format(filename);
|
|
36
|
+
if (compression_format) {
|
|
37
|
+
const normalized_filename = filename.replace(COMPRESSION_EXTENSIONS_REGEX, ``);
|
|
38
|
+
if (detect_compression_format(normalized_filename)) {
|
|
39
|
+
throw new Error(`Nested compression is not supported: ${filename}`);
|
|
40
|
+
}
|
|
41
|
+
filename = normalized_filename;
|
|
42
|
+
}
|
|
43
|
+
// Compressed binary formats (e.g. vaspwave.h5.gz as ferrox stores them on S3,
|
|
44
|
+
// or compressed ASE .traj files): decompress to binary first — generic text
|
|
45
|
+
// decompression would corrupt their bytes — so routing sees the inner name.
|
|
46
|
+
const is_binary_format = /\.(?:h5|hdf5|traj)$/i.test(filename);
|
|
47
|
+
if (compression_format === `zip`) {
|
|
48
|
+
const { unzipSync } = await import(`fflate`);
|
|
49
|
+
const payload = Object.entries(unzipSync(new Uint8Array(buffer))).find(([entry_name]) => !entry_name.endsWith(`/`))?.[1];
|
|
50
|
+
if (!payload)
|
|
51
|
+
throw new Error(`ZIP archive contains no files: ${filename}`);
|
|
52
|
+
if (is_binary_format)
|
|
53
|
+
buffer = payload.slice().buffer;
|
|
54
|
+
else
|
|
55
|
+
content = new TextDecoder().decode(payload);
|
|
56
|
+
}
|
|
57
|
+
else if (compression_format) {
|
|
58
|
+
// Unified handling for all supported compression formats
|
|
59
|
+
// Unsupported formats fail here with a clear extraction error
|
|
60
|
+
if (is_binary_format)
|
|
61
|
+
buffer = await decompress_data_binary(buffer, compression_format);
|
|
62
|
+
else
|
|
63
|
+
content = await decompress_data(buffer, compression_format);
|
|
64
|
+
}
|
|
65
|
+
// vaspwave.h5 holds charge density (+ wavefunctions), not a trajectory —
|
|
66
|
+
// route to the volumetric parser so it renders as an isosurface.
|
|
67
|
+
if (is_vaspwave_filename(filename)) {
|
|
68
|
+
const data = await parse_vaspwave_charge(buffer, filename);
|
|
69
|
+
return { type: `isosurface`, data, filename };
|
|
70
|
+
}
|
|
71
|
+
// Binary trajectory formats: pass buffer directly to trajectory parser
|
|
72
|
+
if (is_binary_format) {
|
|
73
|
+
const data = await parse_trajectory_data(buffer, filename);
|
|
74
|
+
// DOS/bands-only vaspout.h5 (e.g. phelel band paths): no frames to animate,
|
|
75
|
+
// route the electronic results to the spectral components instead.
|
|
76
|
+
// Require at least one renderable result rather than trusting the
|
|
77
|
+
// parser's invariant (the metadata cast is unchecked): an empty
|
|
78
|
+
// electronic object would otherwise mount <Dos doses={null}>.
|
|
79
|
+
const electronic = data.metadata?.electronic;
|
|
80
|
+
if (data.frames.length === 0 && (electronic?.dos || electronic?.bands)) {
|
|
81
|
+
return { type: `vaspout_electronic`, filename, data: electronic };
|
|
82
|
+
}
|
|
83
|
+
return { type: `trajectory`, filename, data };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Fermi surface files (.bxsf, .frmsf)
|
|
87
|
+
// Use basename for regex matching in case filename retains a directory prefix
|
|
88
|
+
const basename = filename.split(`/`).pop() ?? filename;
|
|
89
|
+
if (FERMI_FILE_RE.test(basename)) {
|
|
90
|
+
// parse_fermi_file throws a descriptive error when parsing fails
|
|
91
|
+
return { type: `fermi_surface`, data: parse_fermi_file(content, filename), filename };
|
|
92
|
+
}
|
|
93
|
+
// Volumetric data files (.cube, CHGCAR, AECCAR*, ELFCAR, LOCPOT, PARCHG)
|
|
94
|
+
if (VOLUMETRIC_EXT_RE.test(basename) || VOLUMETRIC_VASP_RE.test(basename)) {
|
|
95
|
+
const data = parse_volumetric_file(content, filename);
|
|
96
|
+
if (data)
|
|
97
|
+
return { type: `isosurface`, data, filename };
|
|
98
|
+
throw new Error(`Failed to parse volumetric file: ${filename}`);
|
|
99
|
+
}
|
|
100
|
+
let parsed_json;
|
|
101
|
+
let has_parsed_json = false;
|
|
102
|
+
// JSON files: render typed JSON before filename heuristics. Otherwise names like
|
|
103
|
+
// convex-hull.json can be mistaken for trajectory keywords such as nve.
|
|
104
|
+
if (/\.json$/i.test(filename)) {
|
|
105
|
+
try {
|
|
106
|
+
parsed_json = JSON.parse(content);
|
|
107
|
+
has_parsed_json = true;
|
|
108
|
+
// Check if the top-level value matches a known visualization type
|
|
109
|
+
const detected = detect_view_type(parsed_json);
|
|
110
|
+
if (detected) {
|
|
111
|
+
// Structure JSON needs normalization (OPTIMADE, fractional coords, etc.)
|
|
112
|
+
if (detected === `structure`) {
|
|
113
|
+
try {
|
|
114
|
+
const structure = parse_structure_file(content, filename);
|
|
115
|
+
return {
|
|
116
|
+
type: `structure`,
|
|
117
|
+
data: { ...structure, id: filename.replace(/\.[^/.]+$/, ``) },
|
|
118
|
+
filename,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Detailed parse failed despite structure-like shape — fall through to
|
|
123
|
+
// generic JSON handling below
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Volumetric JSON needs wrapping in { structure, volumes } for the isosurface renderer
|
|
127
|
+
if (detected === `volumetric`) {
|
|
128
|
+
const vol = parsed_json;
|
|
129
|
+
return {
|
|
130
|
+
type: `isosurface`,
|
|
131
|
+
data: { structure: { sites: [], lattice: vol.lattice }, volumes: [parsed_json] },
|
|
132
|
+
filename,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
type: DETECTION_TO_VIEW_TYPE[detected] ?? `json_browser`,
|
|
137
|
+
data: parsed_json,
|
|
138
|
+
filename,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// JSON parse failed, fall through to structure parser
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Try trajectory parsing if it looks like a trajectory
|
|
147
|
+
if (is_trajectory_file(filename, content)) {
|
|
148
|
+
try {
|
|
149
|
+
const data = await parse_trajectory_data(content, filename);
|
|
150
|
+
return { type: `trajectory`, data, filename };
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
// Trajectory-looking filename but not trajectory-shaped JSON (e.g. nve-config.json):
|
|
154
|
+
// fall through to the JSON browser instead of failing the render
|
|
155
|
+
if (!has_parsed_json)
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// No top-level match -- show JSON browser for navigation
|
|
160
|
+
if (has_parsed_json)
|
|
161
|
+
return { type: `json_browser`, data: parsed_json, filename };
|
|
162
|
+
// Parse as structure (CIF, POSCAR, XYZ, etc.) — throws descriptive reasons on failure
|
|
163
|
+
const structure = parse_structure_file(content, filename);
|
|
164
|
+
// parse_structure_file throws on parse failure but can still return zero atoms (e.g. a
|
|
165
|
+
// CIF with cell params but no _atom_site records), which is invalid downstream
|
|
166
|
+
if (!structure.sites?.length)
|
|
167
|
+
throw new Error(`No atoms found in ${filename}`);
|
|
168
|
+
const data = { ...structure, id: filename.replace(/\.[^/.]+$/, ``) };
|
|
169
|
+
return { type: `structure`, data, filename };
|
|
170
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BarSeries, DataSeries, DataSeries3D } from '../plot';
|
|
2
|
+
export type PlotType = `scatter` | `scatter3d` | `bar` | `histogram` | `table`;
|
|
3
|
+
export interface ColumnInfo {
|
|
4
|
+
values: unknown[];
|
|
5
|
+
type: `numeric` | `string` | `mixed`;
|
|
6
|
+
n_valid: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AxisMapping {
|
|
9
|
+
x?: string;
|
|
10
|
+
y?: string;
|
|
11
|
+
z?: string;
|
|
12
|
+
color?: string;
|
|
13
|
+
size?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function extract_columns(data: unknown): Map<string, ColumnInfo>;
|
|
16
|
+
export declare const col_keys: (columns: Map<string, ColumnInfo>, type: ColumnInfo[`type`]) => string[];
|
|
17
|
+
export declare function suggest_mapping(columns: Map<string, ColumnInfo>): {
|
|
18
|
+
plot_type: PlotType;
|
|
19
|
+
mapping: AxisMapping;
|
|
20
|
+
};
|
|
21
|
+
export declare function build_scatter_series(columns: Map<string, ColumnInfo>, mapping: AxisMapping): DataSeries;
|
|
22
|
+
export declare function build_scatter3d_series(columns: Map<string, ColumnInfo>, mapping: AxisMapping): DataSeries3D;
|
|
23
|
+
export declare function build_bar_series(columns: Map<string, ColumnInfo>, mapping: AxisMapping): BarSeries;
|
|
24
|
+
export declare function build_histogram_series(columns: Map<string, ColumnInfo>, mapping: AxisMapping): DataSeries;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Utilities for extracting plottable columns from JSON data and building plot series.
|
|
2
|
+
// Extract columns from row-based or column-based tabular data
|
|
3
|
+
export function extract_columns(data) {
|
|
4
|
+
const columns = new Map();
|
|
5
|
+
if (Array.isArray(data) &&
|
|
6
|
+
data.length > 0 &&
|
|
7
|
+
typeof data[0] === `object` &&
|
|
8
|
+
data[0] !== null) {
|
|
9
|
+
// Row-based: [{ a: 1, b: 'x' }, ...]
|
|
10
|
+
const sample = data.slice(0, 100);
|
|
11
|
+
const all_keys = new Set();
|
|
12
|
+
for (const row of sample) {
|
|
13
|
+
if (row && typeof row === `object`) {
|
|
14
|
+
for (const key of Object.keys(row))
|
|
15
|
+
all_keys.add(key);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (const key of all_keys) {
|
|
19
|
+
columns.set(key, classify_column(data.map((row) => row && typeof row === `object` ? row[key] : undefined)));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else if (data && typeof data === `object` && !Array.isArray(data)) {
|
|
23
|
+
// Column-based: { a: [1,2,3], b: ['x','y','z'] }
|
|
24
|
+
const array_entries = Object.entries(data).filter(([, val]) => Array.isArray(val) && val.length > 0);
|
|
25
|
+
if (array_entries.length < 1)
|
|
26
|
+
return columns;
|
|
27
|
+
const target_len = array_entries[0][1].length;
|
|
28
|
+
for (const [key, val] of array_entries) {
|
|
29
|
+
const arr = val;
|
|
30
|
+
if (arr.length === target_len) {
|
|
31
|
+
columns.set(key, classify_column(arr));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return columns;
|
|
36
|
+
}
|
|
37
|
+
function classify_column(values) {
|
|
38
|
+
let n_numeric = 0;
|
|
39
|
+
let n_string = 0;
|
|
40
|
+
let n_valid = 0;
|
|
41
|
+
for (const val of values) {
|
|
42
|
+
if (val == null || (typeof val === `number` && !isFinite(val)))
|
|
43
|
+
continue;
|
|
44
|
+
n_valid++;
|
|
45
|
+
if (typeof val === `number`)
|
|
46
|
+
n_numeric++;
|
|
47
|
+
else if (typeof val === `string`)
|
|
48
|
+
n_string++;
|
|
49
|
+
}
|
|
50
|
+
let type = `mixed`;
|
|
51
|
+
if (n_valid > 0) {
|
|
52
|
+
if (n_numeric >= n_valid * 0.8)
|
|
53
|
+
type = `numeric`;
|
|
54
|
+
else if (n_string >= n_valid * 0.8)
|
|
55
|
+
type = `string`;
|
|
56
|
+
}
|
|
57
|
+
return { values, type, n_valid };
|
|
58
|
+
}
|
|
59
|
+
// Extract column keys of a given type from a column map
|
|
60
|
+
export const col_keys = (columns, type) => [...columns.entries()].filter(([, col]) => col.type === type).map(([key]) => key);
|
|
61
|
+
// Well-known column name patterns for heuristic axis assignment
|
|
62
|
+
const X_NAMES = new Set([`x`, `time`, `step`, `index`, `iteration`, `frame`, `epoch`]);
|
|
63
|
+
const Y_NAMES = new Set([`y`, `energy`, `value`, `force`, `stress`, `loss`, `score`]);
|
|
64
|
+
const Z_NAMES = new Set([`z`, `altitude`, `depth`, `height`]);
|
|
65
|
+
// Heuristic: suggest initial axis mapping and plot type from columns
|
|
66
|
+
export function suggest_mapping(columns) {
|
|
67
|
+
const numeric_cols = col_keys(columns, `numeric`);
|
|
68
|
+
const string_cols = col_keys(columns, `string`);
|
|
69
|
+
const mapping = {};
|
|
70
|
+
// Assign x: prefer named numeric match, then string column (bar chart), then first numeric
|
|
71
|
+
const x_match = numeric_cols.find((key) => X_NAMES.has(key.toLowerCase()));
|
|
72
|
+
const x_string_match = string_cols.find((key) => !Y_NAMES.has(key.toLowerCase()));
|
|
73
|
+
mapping.x =
|
|
74
|
+
x_match ?? x_string_match ?? (numeric_cols.length >= 2 ? numeric_cols[0] : undefined);
|
|
75
|
+
// Assign y
|
|
76
|
+
const remaining_numeric = numeric_cols.filter((key) => key !== mapping.x);
|
|
77
|
+
const y_match = remaining_numeric.find((key) => Y_NAMES.has(key.toLowerCase()));
|
|
78
|
+
mapping.y = y_match ?? remaining_numeric[0];
|
|
79
|
+
// Assign z (only if 3+ numeric columns and one matches z-like name)
|
|
80
|
+
const after_xy = remaining_numeric.filter((key) => key !== mapping.y);
|
|
81
|
+
const z_match = after_xy.find((key) => Z_NAMES.has(key.toLowerCase()));
|
|
82
|
+
if (z_match && numeric_cols.length >= 3)
|
|
83
|
+
mapping.z = z_match;
|
|
84
|
+
// Assign color (first unassigned numeric column)
|
|
85
|
+
const used = new Set([mapping.x, mapping.y, mapping.z].filter(Boolean));
|
|
86
|
+
const color_candidate = numeric_cols.find((key) => !used.has(key));
|
|
87
|
+
if (color_candidate)
|
|
88
|
+
mapping.color = color_candidate;
|
|
89
|
+
// Determine plot type
|
|
90
|
+
let plot_type = `scatter`;
|
|
91
|
+
if (mapping.z) {
|
|
92
|
+
plot_type = `scatter3d`;
|
|
93
|
+
}
|
|
94
|
+
else if (mapping.x && columns.get(mapping.x)?.type === `string`) {
|
|
95
|
+
plot_type = `bar`;
|
|
96
|
+
}
|
|
97
|
+
else if ((!mapping.x || !mapping.y) && numeric_cols.length >= 1) {
|
|
98
|
+
plot_type = `histogram`;
|
|
99
|
+
mapping.x ??= mapping.y ?? numeric_cols[0];
|
|
100
|
+
}
|
|
101
|
+
else if (!mapping.x || !mapping.y) {
|
|
102
|
+
plot_type = `table`;
|
|
103
|
+
}
|
|
104
|
+
return { plot_type, mapping };
|
|
105
|
+
}
|
|
106
|
+
// Convert to numbers, preserving array length (non-finite values become NaN)
|
|
107
|
+
const to_numbers = (values) => values.map((val) => (typeof val === `number` && isFinite(val) ? val : NaN));
|
|
108
|
+
// Convert an arbitrary cell value to a display label, JSON-encoding objects so
|
|
109
|
+
// they don't stringify to the useless "[object Object]".
|
|
110
|
+
const to_label = (val) => {
|
|
111
|
+
if (val == null)
|
|
112
|
+
return ``;
|
|
113
|
+
if (typeof val === `string` ||
|
|
114
|
+
typeof val === `number` ||
|
|
115
|
+
typeof val === `boolean` ||
|
|
116
|
+
typeof val === `bigint`)
|
|
117
|
+
return String(val);
|
|
118
|
+
return JSON.stringify(val);
|
|
119
|
+
};
|
|
120
|
+
// Look up a column by its mapping key, returning undefined if unmapped or missing
|
|
121
|
+
const get_col = (columns, key) => key ? columns.get(key) : undefined;
|
|
122
|
+
// Optional numeric conversion for color/size channels
|
|
123
|
+
const optional_numbers = (col) => col ? to_numbers(col.values) : undefined;
|
|
124
|
+
// Filter N axis arrays to only include indices where all axes are finite,
|
|
125
|
+
// keeping optional color/size arrays aligned
|
|
126
|
+
function filter_finite(axes, color, size) {
|
|
127
|
+
const out = axes.map(() => []);
|
|
128
|
+
const color_values = color ? [] : undefined;
|
|
129
|
+
const size_values = size ? [] : undefined;
|
|
130
|
+
for (let idx = 0; idx < axes[0].length; idx++) {
|
|
131
|
+
if (axes.some((arr) => !isFinite(arr[idx])))
|
|
132
|
+
continue;
|
|
133
|
+
for (let dim = 0; dim < axes.length; dim++)
|
|
134
|
+
out[dim].push(axes[dim][idx]);
|
|
135
|
+
if (color && color_values)
|
|
136
|
+
color_values.push(color[idx]);
|
|
137
|
+
if (size && size_values)
|
|
138
|
+
size_values.push(size[idx]);
|
|
139
|
+
}
|
|
140
|
+
return { axes: out, color_values, size_values };
|
|
141
|
+
}
|
|
142
|
+
export function build_scatter_series(columns, mapping) {
|
|
143
|
+
const x_col = get_col(columns, mapping.x);
|
|
144
|
+
const y_col = get_col(columns, mapping.y);
|
|
145
|
+
if (!x_col || !y_col)
|
|
146
|
+
return { x: [], y: [] };
|
|
147
|
+
const { axes: [x, y], color_values, size_values, } = filter_finite([to_numbers(x_col.values), to_numbers(y_col.values)], optional_numbers(get_col(columns, mapping.color)), optional_numbers(get_col(columns, mapping.size)));
|
|
148
|
+
return {
|
|
149
|
+
x,
|
|
150
|
+
y,
|
|
151
|
+
markers: `points`,
|
|
152
|
+
color_values,
|
|
153
|
+
size_values,
|
|
154
|
+
point_style: { radius: 4, fill: `#4c6ef5`, stroke: `white`, stroke_width: 1 },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export function build_scatter3d_series(columns, mapping) {
|
|
158
|
+
const x_col = get_col(columns, mapping.x);
|
|
159
|
+
const y_col = get_col(columns, mapping.y);
|
|
160
|
+
const z_col = get_col(columns, mapping.z);
|
|
161
|
+
if (!x_col || !y_col || !z_col)
|
|
162
|
+
return { x: [], y: [], z: [] };
|
|
163
|
+
const { axes: [x, y, z], color_values, size_values, } = filter_finite([to_numbers(x_col.values), to_numbers(y_col.values), to_numbers(z_col.values)], optional_numbers(get_col(columns, mapping.color)), optional_numbers(get_col(columns, mapping.size)));
|
|
164
|
+
return { x, y, z, markers: `points`, color_values, size_values };
|
|
165
|
+
}
|
|
166
|
+
export function build_bar_series(columns, mapping) {
|
|
167
|
+
const x_col = get_col(columns, mapping.x);
|
|
168
|
+
const y_col = get_col(columns, mapping.y);
|
|
169
|
+
if (!x_col || !y_col)
|
|
170
|
+
return { x: [], y: [] };
|
|
171
|
+
const raw_y = to_numbers(y_col.values);
|
|
172
|
+
const x = [];
|
|
173
|
+
const y = [];
|
|
174
|
+
for (let idx = 0; idx < raw_y.length; idx++) {
|
|
175
|
+
if (!isFinite(raw_y[idx]))
|
|
176
|
+
continue;
|
|
177
|
+
x.push(to_label(x_col.values[idx]));
|
|
178
|
+
y.push(raw_y[idx]);
|
|
179
|
+
}
|
|
180
|
+
return { x, y, color: `#4c6ef5` };
|
|
181
|
+
}
|
|
182
|
+
export function build_histogram_series(columns, mapping) {
|
|
183
|
+
const col = get_col(columns, mapping.x ?? mapping.y);
|
|
184
|
+
if (!col)
|
|
185
|
+
return { x: [], y: [] };
|
|
186
|
+
const values = to_numbers(col.values).filter(isFinite);
|
|
187
|
+
return {
|
|
188
|
+
x: values.map((_, idx) => idx),
|
|
189
|
+
y: values,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type ViewType = `trajectory` | `structure` | `fermi_surface` | `isosurface` | `convex_hull` | `phase_diagram` | `json_browser` | `vaspout_electronic`;
|
|
2
|
+
export declare const FERMI_FILE_RE: RegExp;
|
|
3
|
+
export declare const VOLUMETRIC_EXT_RE: RegExp;
|
|
4
|
+
export declare const VOLUMETRIC_VASP_RE: RegExp;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Shared file-viewer types and filename patterns.
|
|
2
|
+
// Filename patterns for specialized file types (shared between extension host and webview)
|
|
3
|
+
export const FERMI_FILE_RE = /\.(?:bxsf|frmsf)$/i;
|
|
4
|
+
export const VOLUMETRIC_EXT_RE = /\.cube$/i;
|
|
5
|
+
export const VOLUMETRIC_VASP_RE = /(?:^|[\\/_.-])(?:chgcar|aeccar[012]?|elfcar|locpot|parchg)(?:[\\/_.-]|$)/i;
|
|
@@ -254,29 +254,22 @@
|
|
|
254
254
|
return sorted
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
? all_x.filter((idx) => {
|
|
262
|
-
const item = x_items[idx]
|
|
263
|
-
const key = item.key ?? item.label
|
|
264
|
-
return (
|
|
265
|
-
key.toLowerCase().includes(search_query_norm) ||
|
|
266
|
-
item.label.toLowerCase().includes(search_query_norm)
|
|
267
|
-
)
|
|
268
|
-
})
|
|
269
|
-
: all_x
|
|
270
|
-
const filtered_y = search_query_norm
|
|
271
|
-
? all_y.filter((idx) => {
|
|
272
|
-
const item = y_items[idx]
|
|
273
|
-
const key = item.key ?? item.label
|
|
257
|
+
const filter_by_search = (indices: number[], items: AxisItem[]): number[] =>
|
|
258
|
+
search_query_norm
|
|
259
|
+
? indices.filter((idx) => {
|
|
260
|
+
const item = items[idx]
|
|
274
261
|
return (
|
|
275
|
-
key.toLowerCase().includes(search_query_norm) ||
|
|
262
|
+
(item.key ?? item.label).toLowerCase().includes(search_query_norm) ||
|
|
276
263
|
item.label.toLowerCase().includes(search_query_norm)
|
|
277
264
|
)
|
|
278
265
|
})
|
|
279
|
-
:
|
|
266
|
+
: indices
|
|
267
|
+
|
|
268
|
+
let { vis_x, vis_y } = $derived.by(() => {
|
|
269
|
+
const all_x = Array.from({ length: x_items.length }, (_, idx) => idx)
|
|
270
|
+
const all_y = Array.from({ length: y_items.length }, (_, idx) => idx)
|
|
271
|
+
const filtered_x = filter_by_search(all_x, x_items)
|
|
272
|
+
const filtered_y = filter_by_search(all_y, y_items)
|
|
280
273
|
if (!hide_empty) {
|
|
281
274
|
return {
|
|
282
275
|
vis_x: sort_indices(filtered_x, x_items, x_order),
|
|
@@ -483,14 +476,10 @@
|
|
|
483
476
|
})
|
|
484
477
|
|
|
485
478
|
// Keep selected outlines visible against each cell's background.
|
|
486
|
-
let selected_outline_flat = $derived
|
|
479
|
+
let selected_outline_flat = $derived(to_contrast_colors(bg_flat))
|
|
487
480
|
|
|
488
481
|
const get_flat_idx = (x_idx: number, y_idx: number): number => y_idx * n_x + x_idx
|
|
489
482
|
|
|
490
|
-
// Look up bg color by indices
|
|
491
|
-
const get_bg = (x_idx: number, y_idx: number): string | null =>
|
|
492
|
-
bg_flat[get_flat_idx(x_idx, y_idx)]
|
|
493
|
-
|
|
494
483
|
// === Cell context builder (only called for clicks, not per-hover) ===
|
|
495
484
|
const build_cell_context = (x_idx: number, y_idx: number): CellContext => ({
|
|
496
485
|
x_item: x_items[x_idx],
|
|
@@ -498,16 +487,16 @@
|
|
|
498
487
|
x_idx,
|
|
499
488
|
y_idx,
|
|
500
489
|
value: get_value(x_idx, y_idx),
|
|
501
|
-
bg_color:
|
|
490
|
+
bg_color: bg_flat[get_flat_idx(x_idx, y_idx)],
|
|
502
491
|
})
|
|
503
492
|
|
|
504
493
|
// === Fully imperative hover management ===
|
|
505
494
|
// ZERO $state writes during mouseover — all DOM updates are direct.
|
|
506
495
|
// This avoids Svelte's reactive flush which would re-evaluate effects.
|
|
507
|
-
const is_browser = typeof window !== `undefined`
|
|
508
496
|
let tooltip_div: HTMLDivElement | undefined = $state()
|
|
509
497
|
let active_cell_raf = 0 // rAF handle for deferred active_cell update
|
|
510
498
|
let click_timeout_id: ReturnType<typeof setTimeout> | null = null
|
|
499
|
+
let pending_click_key: string | null = null
|
|
511
500
|
const dblclick_delay_ms = 250
|
|
512
501
|
let last_hover_x = -1
|
|
513
502
|
let last_hover_y = -1
|
|
@@ -634,23 +623,6 @@
|
|
|
634
623
|
return Math.max(1, vis_row + 1)
|
|
635
624
|
}
|
|
636
625
|
|
|
637
|
-
function x_label_diag_grid_col(x_idx: number): number | undefined {
|
|
638
|
-
const vis_col = get_vis_col(x_idx)
|
|
639
|
-
if (vis_col === null) return undefined
|
|
640
|
-
return vis_col + 2
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function y_label_edge_grid_row(y_idx: number): number | undefined {
|
|
644
|
-
const vis_row = get_vis_row(y_idx)
|
|
645
|
-
if (vis_row === null) return undefined
|
|
646
|
-
return vis_row + 2
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function x_label_grid_col(x_idx: number): number | undefined {
|
|
650
|
-
if (use_diagonal_symmetric_labels) return x_label_diag_grid_col(x_idx)
|
|
651
|
-
return cell_grid_col(x_idx)
|
|
652
|
-
}
|
|
653
|
-
|
|
654
626
|
function x_label_grid_row(x_idx: number): number | undefined {
|
|
655
627
|
if (use_diagonal_symmetric_labels) return x_label_diag_grid_row(x_idx)
|
|
656
628
|
if (use_side_split_x_labels && x_idx % 2 !== 0) {
|
|
@@ -679,23 +651,26 @@
|
|
|
679
651
|
return vis_row + 2
|
|
680
652
|
}
|
|
681
653
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
callback()
|
|
685
|
-
return 0
|
|
686
|
-
}
|
|
687
|
-
return globalThis.requestAnimationFrame(callback)
|
|
688
|
-
}
|
|
689
|
-
|
|
654
|
+
// Called by event handlers, the axis-key-change effect, and onDestroy; the 0-handle
|
|
655
|
+
// guard prevents invalid cancelAnimationFrame calls.
|
|
690
656
|
function cancel_raf(raf_handle: number): void {
|
|
691
|
-
if (
|
|
692
|
-
globalThis.cancelAnimationFrame(raf_handle)
|
|
657
|
+
if (raf_handle !== 0) globalThis.cancelAnimationFrame(raf_handle)
|
|
693
658
|
}
|
|
694
659
|
|
|
695
660
|
function clear_pending_click(): void {
|
|
696
|
-
if (click_timeout_id
|
|
697
|
-
clearTimeout(click_timeout_id)
|
|
661
|
+
if (click_timeout_id !== null) clearTimeout(click_timeout_id)
|
|
698
662
|
click_timeout_id = null
|
|
663
|
+
pending_click_key = null
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function schedule_single_click(cell_context: CellContext): void {
|
|
667
|
+
if (!onclick) return
|
|
668
|
+
clear_pending_click()
|
|
669
|
+
pending_click_key = cell_pos_key(cell_context.x_idx, cell_context.y_idx)
|
|
670
|
+
click_timeout_id = setTimeout(() => {
|
|
671
|
+
onclick(cell_context)
|
|
672
|
+
clear_pending_click()
|
|
673
|
+
}, dblclick_delay_ms)
|
|
699
674
|
}
|
|
700
675
|
|
|
701
676
|
function parse_cell_indices(cell_el: HTMLElement): { x_idx: number; y_idx: number } | null {
|
|
@@ -715,25 +690,14 @@
|
|
|
715
690
|
|
|
716
691
|
function trigger_click(cell_context: CellContext): void {
|
|
717
692
|
if (!onclick) return
|
|
718
|
-
if (
|
|
719
|
-
|
|
720
|
-
return
|
|
721
|
-
}
|
|
722
|
-
clear_pending_click()
|
|
723
|
-
click_timeout_id = setTimeout(() => {
|
|
724
|
-
onclick(cell_context)
|
|
725
|
-
click_timeout_id = null
|
|
726
|
-
}, dblclick_delay_ms)
|
|
693
|
+
if (ondblclick) schedule_single_click(cell_context)
|
|
694
|
+
else onclick(cell_context)
|
|
727
695
|
}
|
|
728
696
|
|
|
729
697
|
function get_cell_el_from_target(event_target: EventTarget | null): HTMLElement | null {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
return target_node
|
|
734
|
-
}
|
|
735
|
-
const closest_cell = target_node.closest(`[data-x][data-y]`)
|
|
736
|
-
return closest_cell instanceof HTMLElement ? closest_cell : null
|
|
698
|
+
if (!(event_target instanceof Element)) return null
|
|
699
|
+
const cell_el = event_target.closest(`[data-x][data-y]`) // matches self too
|
|
700
|
+
return cell_el instanceof HTMLElement ? cell_el : null
|
|
737
701
|
}
|
|
738
702
|
|
|
739
703
|
function update_selected_cells(event: MouseEvent, clicked_cell: CellPos): void {
|
|
@@ -764,9 +728,10 @@
|
|
|
764
728
|
const existing_idx = next_cells.findIndex(
|
|
765
729
|
(pos) => cell_pos_key(pos.x_idx, pos.y_idx) === clicked_key,
|
|
766
730
|
)
|
|
731
|
+
// multi mode with Cmd/Ctrl toggles the clicked cell, otherwise replace selection
|
|
767
732
|
const toggle_mode = selection_mode === `multi` && (event.metaKey || event.ctrlKey)
|
|
768
|
-
if (existing_idx !== -1
|
|
769
|
-
else if (
|
|
733
|
+
if (toggle_mode && existing_idx !== -1) next_cells.splice(existing_idx, 1)
|
|
734
|
+
else if (toggle_mode) next_cells.push(clicked_cell)
|
|
770
735
|
else next_cells.splice(0, next_cells.length, clicked_cell)
|
|
771
736
|
selected_cells = next_cells
|
|
772
737
|
last_selected_cell = clicked_cell
|
|
@@ -820,7 +785,7 @@
|
|
|
820
785
|
|
|
821
786
|
// Defer bindable writes out of the hot mouseover path
|
|
822
787
|
cancel_raf(active_cell_raf)
|
|
823
|
-
active_cell_raf =
|
|
788
|
+
active_cell_raf = globalThis.requestAnimationFrame(() => {
|
|
824
789
|
active_cell = { x_idx, y_idx }
|
|
825
790
|
})
|
|
826
791
|
|
|
@@ -852,7 +817,7 @@
|
|
|
852
817
|
}
|
|
853
818
|
// Defer reactive cleanup to rAF
|
|
854
819
|
cancel_raf(active_cell_raf)
|
|
855
|
-
active_cell_raf =
|
|
820
|
+
active_cell_raf = globalThis.requestAnimationFrame(() => {
|
|
856
821
|
active_cell = null
|
|
857
822
|
if (!keep_tooltip_visible) tooltip_cell = null
|
|
858
823
|
})
|
|
@@ -873,7 +838,6 @@
|
|
|
873
838
|
else update_tooltip_content(tooltip_div, x_idx, y_idx)
|
|
874
839
|
}
|
|
875
840
|
}
|
|
876
|
-
if (!onclick) return
|
|
877
841
|
trigger_click(cell_context)
|
|
878
842
|
}
|
|
879
843
|
|
|
@@ -881,8 +845,20 @@
|
|
|
881
845
|
if (disabled || !ondblclick) return
|
|
882
846
|
const cell_context = get_cell_context_from_target(event.target)
|
|
883
847
|
if (!cell_context) return
|
|
848
|
+
// With onclick, require a matching pending click (FilePicker pattern).
|
|
849
|
+
// ondblclick-only stays direct so orphaned dblclicks still work.
|
|
850
|
+
if (!onclick) {
|
|
851
|
+
clear_pending_click()
|
|
852
|
+
ondblclick(cell_context)
|
|
853
|
+
return
|
|
854
|
+
}
|
|
855
|
+
const pending = pending_click_key
|
|
884
856
|
clear_pending_click()
|
|
885
|
-
|
|
857
|
+
if (pending === cell_pos_key(cell_context.x_idx, cell_context.y_idx)) {
|
|
858
|
+
ondblclick(cell_context)
|
|
859
|
+
} else {
|
|
860
|
+
schedule_single_click(cell_context)
|
|
861
|
+
}
|
|
886
862
|
}
|
|
887
863
|
|
|
888
864
|
function handle_contextmenu(event: MouseEvent): void {
|
|
@@ -1044,7 +1020,7 @@
|
|
|
1044
1020
|
let legend_orientation = $derived<ColorBarOrientation>(
|
|
1045
1021
|
legend_position === `right` ? `vertical` : `horizontal`,
|
|
1046
1022
|
)
|
|
1047
|
-
let legend_wrapper_style = $derived
|
|
1023
|
+
let legend_wrapper_style = $derived(
|
|
1048
1024
|
legend_position === `right`
|
|
1049
1025
|
? `--cbar-height: 120px; --cbar-min-height: 120px; --cbar-max-height: 120px;`
|
|
1050
1026
|
: `--cbar-width: 180px;`,
|
|
@@ -1090,11 +1066,8 @@
|
|
|
1090
1066
|
|
|
1091
1067
|
onMount(() => {
|
|
1092
1068
|
update_viewport_state()
|
|
1093
|
-
if (!is_browser) return
|
|
1094
1069
|
globalThis.addEventListener(`mouseup`, handle_mouseup)
|
|
1095
|
-
return () =>
|
|
1096
|
-
globalThis.removeEventListener(`mouseup`, handle_mouseup)
|
|
1097
|
-
}
|
|
1070
|
+
return () => globalThis.removeEventListener(`mouseup`, handle_mouseup)
|
|
1098
1071
|
})
|
|
1099
1072
|
|
|
1100
1073
|
onDestroy(() => {
|
|
@@ -1167,7 +1140,7 @@
|
|
|
1167
1140
|
class:highlighted={highlight_x_by_idx.has(x_idx)}
|
|
1168
1141
|
class:sticky={sticky_x_labels}
|
|
1169
1142
|
style={label_style || undefined}
|
|
1170
|
-
style:grid-column={
|
|
1143
|
+
style:grid-column={cell_grid_col(x_idx)}
|
|
1171
1144
|
style:grid-row={x_label_grid_row(x_idx)}
|
|
1172
1145
|
title={x_label_cell ? undefined : item.label}
|
|
1173
1146
|
>
|
|
@@ -1191,7 +1164,7 @@
|
|
|
1191
1164
|
class:highlighted={highlight_y_by_idx.has(y_idx)}
|
|
1192
1165
|
class:sticky={sticky_y_labels}
|
|
1193
1166
|
style={label_style || undefined}
|
|
1194
|
-
style:grid-row={
|
|
1167
|
+
style:grid-row={cell_grid_row(y_idx)}
|
|
1195
1168
|
style:grid-column={y_label_grid_col(y_idx)}
|
|
1196
1169
|
title={y_label_cell ? undefined : y_item.label}
|
|
1197
1170
|
>
|
|
@@ -25,6 +25,6 @@ type $$ComponentProps = {
|
|
|
25
25
|
controls_open: boolean;
|
|
26
26
|
}]>;
|
|
27
27
|
};
|
|
28
|
-
declare const HeatmapMatrixControls: import("svelte").Component<$$ComponentProps, {}, "normalize" | "theme" | "show_values" | "show_legend" | "controls_open" | "ordering" | "
|
|
28
|
+
declare const HeatmapMatrixControls: import("svelte").Component<$$ComponentProps, {}, "normalize" | "theme" | "show_values" | "show_legend" | "controls_open" | "ordering" | "symmetric" | "search_query" | "toggle_visible" | "domain_mode" | "legend_position" | "show_row_summaries" | "show_col_summaries">;
|
|
29
29
|
type HeatmapMatrixControls = ReturnType<typeof HeatmapMatrixControls>;
|
|
30
30
|
export default HeatmapMatrixControls;
|