matterviz 0.1.12 → 0.1.13
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/ContextMenu.svelte +1 -1
- package/dist/ContextMenu.svelte.d.ts +3 -3
- package/dist/DraggablePane.svelte +10 -5
- package/dist/DraggablePane.svelte.d.ts +3 -3
- package/dist/FilePicker.svelte.d.ts +3 -3
- package/dist/Icon.svelte.d.ts +3 -3
- package/dist/Nav.svelte +76 -31
- package/dist/SettingsSection.svelte.d.ts +3 -3
- package/dist/Spinner.svelte +18 -29
- package/dist/Spinner.svelte.d.ts +3 -3
- package/dist/app.css +4 -0
- package/dist/bands/Bands.svelte +216 -0
- package/dist/bands/Bands.svelte.d.ts +16 -0
- package/dist/bands/BandsAndDos.svelte +26 -0
- package/dist/bands/BandsAndDos.svelte.d.ts +16 -0
- package/dist/bands/Dos.svelte +124 -0
- package/dist/bands/Dos.svelte.d.ts +18 -0
- package/dist/bands/helpers.d.ts +11 -0
- package/dist/bands/helpers.js +195 -0
- package/dist/bands/index.d.ts +5 -0
- package/dist/bands/index.js +6 -0
- package/dist/bands/types.d.ts +54 -0
- package/dist/bands/types.js +2 -0
- package/dist/brillouin/BrillouinZone.svelte +373 -0
- package/dist/brillouin/BrillouinZone.svelte.d.ts +57 -0
- package/dist/brillouin/BrillouinZoneControls.svelte +88 -0
- package/dist/brillouin/BrillouinZoneControls.svelte.d.ts +14 -0
- package/dist/brillouin/BrillouinZoneExportPane.svelte +129 -0
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +15 -0
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +115 -0
- package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +13 -0
- package/dist/brillouin/BrillouinZoneScene.svelte +180 -0
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +34 -0
- package/dist/brillouin/compute.d.ts +7 -0
- package/dist/brillouin/compute.js +215 -0
- package/dist/brillouin/index.d.ts +7 -0
- package/dist/brillouin/index.js +7 -0
- package/dist/brillouin/types.d.ts +23 -0
- package/dist/brillouin/types.js +1 -0
- package/dist/composition/BarChart.svelte.d.ts +3 -3
- package/dist/composition/BubbleChart.svelte +7 -6
- package/dist/composition/BubbleChart.svelte.d.ts +3 -3
- package/dist/composition/Composition.svelte.d.ts +3 -3
- package/dist/composition/PieChart.svelte +1 -1
- package/dist/composition/PieChart.svelte.d.ts +3 -3
- package/dist/composition/parse.d.ts +3 -3
- package/dist/coordination/CoordinationBarPlot.svelte +241 -0
- package/dist/coordination/CoordinationBarPlot.svelte.d.ts +29 -0
- package/dist/coordination/calc-coordination.d.ts +15 -0
- package/dist/coordination/calc-coordination.js +66 -0
- package/dist/coordination/index.d.ts +8 -0
- package/dist/coordination/index.js +7 -0
- package/dist/element/BohrAtom.svelte.d.ts +3 -3
- package/dist/element/ElementHeading.svelte.d.ts +3 -3
- package/dist/element/ElementPhoto.svelte.d.ts +3 -3
- package/dist/element/ElementStats.svelte.d.ts +3 -3
- package/dist/element/ElementTile.svelte +1 -1
- package/dist/element/ElementTile.svelte.d.ts +4 -4
- package/dist/element/Nucleus.svelte.d.ts +3 -3
- package/dist/icons.d.ts +4 -0
- package/dist/icons.js +4 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/io/export.d.ts +1 -6
- package/dist/io/export.js +12 -12
- package/dist/labels.d.ts +19 -0
- package/dist/labels.js +59 -0
- package/dist/math.d.ts +7 -2
- package/dist/math.js +24 -12
- package/dist/periodic-table/PeriodicTable.svelte.d.ts +3 -3
- package/dist/periodic-table/PeriodicTableControls.svelte.d.ts +3 -3
- package/dist/periodic-table/PropertySelect.svelte +1 -1
- package/dist/periodic-table/PropertySelect.svelte.d.ts +5 -4
- package/dist/periodic-table/TableInset.svelte.d.ts +3 -3
- package/dist/phase-diagram/PhaseDiagram2D.svelte +44 -107
- package/dist/phase-diagram/PhaseDiagram2D.svelte.d.ts +7 -27
- package/dist/phase-diagram/PhaseDiagram3D.svelte +67 -170
- package/dist/phase-diagram/PhaseDiagram3D.svelte.d.ts +7 -31
- package/dist/phase-diagram/PhaseDiagram4D.svelte +87 -145
- package/dist/phase-diagram/PhaseDiagram4D.svelte.d.ts +5 -28
- package/dist/phase-diagram/PhaseDiagramControls.svelte +9 -9
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +6 -6
- package/dist/phase-diagram/PhaseDiagramInfoPane.svelte +4 -4
- package/dist/phase-diagram/PhaseDiagramInfoPane.svelte.d.ts +5 -5
- package/dist/phase-diagram/PhaseDiagramStats.svelte +9 -16
- package/dist/phase-diagram/PhaseDiagramStats.svelte.d.ts +3 -3
- package/dist/phase-diagram/StructurePopup.svelte.d.ts +3 -3
- package/dist/phase-diagram/barycentric-coords.js +13 -24
- package/dist/phase-diagram/helpers.d.ts +12 -14
- package/dist/phase-diagram/helpers.js +70 -23
- package/dist/phase-diagram/index.d.ts +58 -0
- package/dist/phase-diagram/index.js +44 -0
- package/dist/phase-diagram/thermodynamics.js +2 -3
- package/dist/phase-diagram/types.d.ts +3 -8
- package/dist/plot/BarPlot.svelte +474 -240
- package/dist/plot/BarPlot.svelte.d.ts +16 -37
- package/dist/plot/BarPlotControls.svelte +33 -201
- package/dist/plot/BarPlotControls.svelte.d.ts +11 -22
- package/dist/plot/ColorBar.svelte.d.ts +3 -3
- package/dist/plot/ColorScaleSelect.svelte +1 -1
- package/dist/plot/ColorScaleSelect.svelte.d.ts +3 -3
- package/dist/plot/ElementScatter.svelte +5 -5
- package/dist/plot/ElementScatter.svelte.d.ts +8 -8
- package/dist/plot/Histogram.svelte +318 -170
- package/dist/plot/Histogram.svelte.d.ts +21 -41
- package/dist/plot/HistogramControls.svelte +115 -347
- package/dist/plot/HistogramControls.svelte.d.ts +13 -28
- package/dist/plot/Line.svelte +1 -1
- package/dist/plot/Line.svelte.d.ts +4 -4
- package/dist/plot/PlotControls.svelte +280 -0
- package/dist/plot/PlotControls.svelte.d.ts +4 -0
- package/dist/plot/PlotLegend.svelte +9 -24
- package/dist/plot/PlotLegend.svelte.d.ts +3 -3
- package/dist/plot/ScatterPlot.svelte +536 -571
- package/dist/plot/ScatterPlot.svelte.d.ts +18 -90
- package/dist/plot/ScatterPlotControls.svelte +108 -316
- package/dist/plot/ScatterPlotControls.svelte.d.ts +11 -38
- package/dist/plot/ScatterPoint.svelte +1 -1
- package/dist/plot/ScatterPoint.svelte.d.ts +5 -5
- package/dist/plot/SpacegroupBarPlot.svelte +235 -0
- package/dist/plot/SpacegroupBarPlot.svelte.d.ts +9 -0
- package/dist/plot/data-transform.d.ts +2 -2
- package/dist/plot/data-transform.js +1 -5
- package/dist/plot/index.d.ts +12 -200
- package/dist/plot/index.js +14 -6
- package/dist/plot/layout.d.ts +10 -16
- package/dist/plot/layout.js +40 -25
- package/dist/plot/scales.d.ts +7 -8
- package/dist/plot/scales.js +22 -7
- package/dist/plot/types.d.ts +303 -0
- package/dist/plot/types.js +1 -0
- package/dist/rdf/RdfPlot.svelte +156 -0
- package/dist/rdf/RdfPlot.svelte.d.ts +20 -0
- package/dist/rdf/calc-rdf.d.ts +4 -0
- package/dist/rdf/calc-rdf.js +107 -0
- package/dist/rdf/index.d.ts +22 -0
- package/dist/rdf/index.js +2 -0
- package/dist/settings.d.ts +86 -75
- package/dist/settings.js +257 -155
- package/dist/structure/Bond.svelte +126 -110
- package/dist/structure/Bond.svelte.d.ts +7 -20
- package/dist/structure/CanvasTooltip.svelte.d.ts +4 -4
- package/dist/structure/Cylinder.svelte +29 -0
- package/dist/structure/Cylinder.svelte.d.ts +10 -0
- package/dist/structure/Lattice.svelte +1 -2
- package/dist/structure/Lattice.svelte.d.ts +3 -3
- package/dist/structure/Structure.svelte +23 -8
- package/dist/structure/Structure.svelte.d.ts +10 -7
- package/dist/structure/StructureControls.svelte +25 -134
- package/dist/structure/StructureControls.svelte.d.ts +4 -9
- package/dist/structure/StructureExportPane.svelte +188 -0
- package/dist/structure/StructureExportPane.svelte.d.ts +17 -0
- package/dist/structure/StructureInfoPane.svelte.d.ts +3 -3
- package/dist/structure/StructureLegend.svelte +91 -27
- package/dist/structure/StructureLegend.svelte.d.ts +5 -4
- package/dist/structure/StructureScene.svelte +93 -100
- package/dist/structure/StructureScene.svelte.d.ts +15 -12
- package/dist/structure/Vector.svelte +19 -25
- package/dist/structure/Vector.svelte.d.ts +3 -3
- package/dist/structure/bonding.d.ts +16 -14
- package/dist/structure/bonding.js +207 -216
- package/dist/structure/export.d.ts +3 -0
- package/dist/structure/export.js +120 -0
- package/dist/structure/index.d.ts +15 -1
- package/dist/structure/index.js +2 -0
- package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -3
- package/dist/symmetry/index.d.ts +1 -0
- package/dist/symmetry/index.js +1 -0
- package/dist/symmetry/spacegroups.d.ts +9 -0
- package/dist/symmetry/spacegroups.js +397 -0
- package/dist/theme/ThemeControl.svelte.d.ts +3 -3
- package/dist/theme/themes.js +2 -16
- package/dist/trajectory/Trajectory.svelte +52 -78
- package/dist/trajectory/Trajectory.svelte.d.ts +5 -5
- package/dist/trajectory/TrajectoryError.svelte.d.ts +3 -3
- package/dist/trajectory/TrajectoryExportPane.svelte +1 -1
- package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +5 -5
- package/dist/trajectory/TrajectoryInfoPane.svelte +1 -1
- package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -3
- package/dist/trajectory/extract.js +2 -1
- package/dist/trajectory/index.d.ts +1 -1
- package/dist/trajectory/index.js +1 -1
- package/dist/trajectory/parse.js +21 -14
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +17 -0
- package/dist/xrd/XrdPlot.svelte +14 -11
- package/dist/xrd/XrdPlot.svelte.d.ts +6 -5
- package/package.json +20 -12
- package/readme.md +1 -1
- package/dist/material/MaterialCard.svelte +0 -123
- package/dist/material/MaterialCard.svelte.d.ts +0 -9
- package/dist/material/index.d.ts +0 -1
- package/dist/material/index.js +0 -1
- package/dist/plot/formatting.d.ts +0 -7
- package/dist/plot/formatting.js +0 -53
package/dist/plot/BarPlot.svelte
CHANGED
|
@@ -1,100 +1,143 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
2
|
-
import { find_best_plot_area, PlotLegend } from './';
|
|
3
|
-
import { format_value } from './formatting';
|
|
1
|
+
<script lang="ts">import { format_value } from '../labels';
|
|
2
|
+
import { BarPlotControls, DEFAULT_GRID_STYLE, find_best_plot_area, PlotLegend, } from './';
|
|
4
3
|
import { get_relative_coords } from './interactions';
|
|
5
4
|
import { create_scale, generate_ticks, get_nice_data_range } from './scales';
|
|
5
|
+
import { DEFAULTS } from '../settings';
|
|
6
6
|
import { SvelteMap } from 'svelte/reactivity';
|
|
7
|
-
import BarPlotControls from './BarPlotControls.svelte';
|
|
8
7
|
import { calc_auto_padding, measure_text_width } from './layout';
|
|
9
|
-
let { series = $bindable([]), orientation = $bindable(`vertical`), mode = $bindable(`overlay`),
|
|
8
|
+
let { series = $bindable([]), orientation = $bindable(`vertical`), mode = $bindable(`overlay`), x_axis = $bindable({}), y_axis = $bindable({}), y2_axis = $bindable({}), display = $bindable(DEFAULTS.bar.display), x_range = [null, null], y_range = [null, null], y2_range = [null, null], range_padding = 0.05, padding = { t: 20, b: 60, l: 60, r: 20 }, legend = {}, show_legend, bar = $bindable({}), line = $bindable({}), tooltip, user_content, hovered = $bindable(false), change = () => { }, on_bar_click, on_bar_hover, show_controls = $bindable(true), controls_open = $bindable(false), controls_toggle_props, controls_pane_props, children, ...rest } = $props();
|
|
9
|
+
// Initialize bar and line styles with defaults (runs once)
|
|
10
|
+
bar = { ...DEFAULTS.bar.bar, ...bar };
|
|
11
|
+
line = { ...DEFAULTS.bar.line, ...line };
|
|
12
|
+
y2_axis = {
|
|
13
|
+
format: ``,
|
|
14
|
+
scale_type: `linear`,
|
|
15
|
+
ticks: 5,
|
|
16
|
+
label_shift: { y: 60 },
|
|
17
|
+
tick_label_shift: { x: 8, y: 0 },
|
|
18
|
+
range: [null, null],
|
|
19
|
+
...y2_axis,
|
|
20
|
+
};
|
|
10
21
|
let [width, height] = $state([0, 0]);
|
|
11
22
|
let svg_element = $state(null);
|
|
12
|
-
let clip_path_id = `chart-clip-${
|
|
23
|
+
let clip_path_id = `chart-clip-${crypto?.randomUUID?.()}`;
|
|
13
24
|
// Compute auto ranges from visible series
|
|
14
|
-
let visible_series = $derived(series.filter((
|
|
25
|
+
let visible_series = $derived(series.filter((srs) => srs?.visible ?? true));
|
|
26
|
+
// Separate series by y-axis
|
|
27
|
+
let y1_series = $derived(visible_series.filter((srs) => (srs.y_axis ?? `y1`) === `y1`));
|
|
28
|
+
let y2_series = $derived(visible_series.filter((srs) => srs.y_axis === `y2`));
|
|
15
29
|
let auto_ranges = $derived.by(() => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
totals.pos
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
...(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return { x: [0, 1], y: [0, 1] };
|
|
45
|
-
}
|
|
46
|
-
// Compute data-driven ranges first (categories from x, magnitudes from y)
|
|
47
|
-
const x_range = get_nice_data_range(all_points, (p) => p.x, x_lim, `linear`, range_padding, x_format?.startsWith(`%`) || false);
|
|
48
|
-
let y_range = get_nice_data_range(all_points, (p) => p.y, y_lim, `linear`, range_padding, false);
|
|
49
|
-
// For bar plots, ensure the value axis starts at 0 unless there are negative values
|
|
50
|
-
// This prevents bars from starting at arbitrary values
|
|
51
|
-
const has_negative = all_points.some((p) => p.y < 0);
|
|
52
|
-
const has_positive = all_points.some((p) => p.y > 0);
|
|
53
|
-
// Only adjust if no explicit y_lim is set
|
|
54
|
-
if (!y_lim?.[0] && !y_lim?.[1]) {
|
|
55
|
-
if (has_positive && !has_negative) {
|
|
56
|
-
// All positive/zero values: always start from 0
|
|
57
|
-
y_range = [0, y_range[1]];
|
|
30
|
+
// Calculate separate ranges for y1 and y2 axes
|
|
31
|
+
const calc_y_range = (series_list, y_limit, scale_type) => {
|
|
32
|
+
let points = series_list.flatMap((srs) => srs.x.map((x_val, idx) => ({ x: x_val, y: srs.y[idx] })));
|
|
33
|
+
// In stacked mode, calculate stacked totals for accurate range (only for bars on the same axis)
|
|
34
|
+
if (mode === `stacked`) {
|
|
35
|
+
const stacked_totals = new SvelteMap();
|
|
36
|
+
// Only include visible bar series (not lines) in stacking
|
|
37
|
+
series_list
|
|
38
|
+
.filter((srs) => srs.render_mode !== `line`)
|
|
39
|
+
.forEach((srs) => srs.x.forEach((x_val, idx) => {
|
|
40
|
+
const y_val = srs.y[idx] ?? 0;
|
|
41
|
+
const totals = stacked_totals.get(x_val) ?? { pos: 0, neg: 0 };
|
|
42
|
+
if (y_val >= 0)
|
|
43
|
+
totals.pos += y_val;
|
|
44
|
+
else
|
|
45
|
+
totals.neg += y_val;
|
|
46
|
+
stacked_totals.set(x_val, totals);
|
|
47
|
+
}));
|
|
48
|
+
// Replace points with stacked totals + line series (which don't stack)
|
|
49
|
+
points = [
|
|
50
|
+
...Array.from(stacked_totals).flatMap(([x_val, { pos, neg }]) => [
|
|
51
|
+
...(pos > 0 ? [{ x: x_val, y: pos }] : []),
|
|
52
|
+
...(neg < 0 ? [{ x: x_val, y: neg }] : []),
|
|
53
|
+
]),
|
|
54
|
+
...series_list
|
|
55
|
+
.filter((srs) => srs.render_mode === `line`)
|
|
56
|
+
.flatMap((srs) => srs.x.map((x_val, idx) => ({ x: x_val, y: srs.y[idx] }))),
|
|
57
|
+
];
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
y_range = [y_range[0], 0];
|
|
59
|
+
if (!points.length) {
|
|
60
|
+
return [0, 1];
|
|
62
61
|
}
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
let y_range = get_nice_data_range(points, (p) => p.y, y_limit, scale_type, range_padding, false);
|
|
63
|
+
// For bar plots, ensure the value axis starts at 0 unless there are negative values
|
|
64
|
+
// Only apply zero-clamping for linear scales
|
|
65
|
+
if (scale_type === `linear`) {
|
|
66
|
+
const has_negative = points.some((p) => p.y < 0);
|
|
67
|
+
const has_positive = points.some((p) => p.y > 0);
|
|
68
|
+
// Only adjust if no explicit y_range is set
|
|
69
|
+
if (y_limit?.[0] == null && y_limit?.[1] == null) {
|
|
70
|
+
if (has_positive && !has_negative)
|
|
71
|
+
y_range = [0, y_range[1]];
|
|
72
|
+
else if (has_negative && !has_positive)
|
|
73
|
+
y_range = [y_range[0], 0];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return y_range;
|
|
77
|
+
};
|
|
78
|
+
// Get all x values for x_range calculation
|
|
79
|
+
const all_x_points = visible_series.flatMap((s) => s.x.map((x_val) => ({ x: x_val, y: 0 })));
|
|
80
|
+
const x_scale_type = x_axis.scale_type ?? `linear`;
|
|
81
|
+
const x_auto_range = all_x_points.length
|
|
82
|
+
? get_nice_data_range(all_x_points, (p) => p.x, x_range, x_scale_type, range_padding, x_axis.format?.startsWith(`%`) || false)
|
|
83
|
+
: [0, 1];
|
|
84
|
+
const y1_range = calc_y_range(y1_series, y_range, y_axis.scale_type ?? `linear`);
|
|
85
|
+
const y2_auto_range = calc_y_range(y2_series, y2_range, y2_axis.scale_type ?? `linear`);
|
|
65
86
|
// Map data ranges to axis ranges depending on orientation
|
|
66
87
|
return orientation === `horizontal`
|
|
67
|
-
? ({ x:
|
|
68
|
-
: ({ x:
|
|
88
|
+
? ({ x: y1_range, y: x_auto_range, y2: y2_auto_range })
|
|
89
|
+
: ({ x: x_auto_range, y: y1_range, y2: y2_auto_range });
|
|
69
90
|
});
|
|
70
91
|
// Initialize and current ranges
|
|
71
92
|
let ranges = $state({
|
|
72
|
-
initial: { x: [0, 1], y: [0, 1] },
|
|
73
|
-
current: { x: [0, 1], y: [0, 1] },
|
|
93
|
+
initial: { x: [0, 1], y: [0, 1], y2: [0, 1] },
|
|
94
|
+
current: { x: [0, 1], y: [0, 1], y2: [0, 1] },
|
|
74
95
|
});
|
|
75
96
|
$effect(() => {
|
|
76
97
|
const new_x = [
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
x_axis.range?.[0] ?? auto_ranges.x[0],
|
|
99
|
+
x_axis.range?.[1] ?? auto_ranges.x[1],
|
|
79
100
|
];
|
|
80
101
|
const new_y = [
|
|
81
|
-
|
|
82
|
-
|
|
102
|
+
y_axis.range?.[0] ?? auto_ranges.y[0],
|
|
103
|
+
y_axis.range?.[1] ?? auto_ranges.y[1],
|
|
83
104
|
];
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
const new_y2 = [
|
|
106
|
+
y2_axis.range?.[0] ?? auto_ranges.y2[0],
|
|
107
|
+
y2_axis.range?.[1] ?? auto_ranges.y2[1],
|
|
108
|
+
];
|
|
109
|
+
// Only update if ranges actually changed
|
|
110
|
+
if (ranges.current.x[0] !== new_x[0] ||
|
|
111
|
+
ranges.current.x[1] !== new_x[1] ||
|
|
112
|
+
ranges.current.y[0] !== new_y[0] ||
|
|
113
|
+
ranges.current.y[1] !== new_y[1] ||
|
|
114
|
+
ranges.current.y2[0] !== new_y2[0] ||
|
|
115
|
+
ranges.current.y2[1] !== new_y2[1]) {
|
|
116
|
+
ranges = {
|
|
117
|
+
initial: { x: new_x, y: new_y, y2: new_y2 },
|
|
118
|
+
current: { x: new_x, y: new_y, y2: new_y2 },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
88
121
|
});
|
|
89
122
|
// Layout: dynamic padding based on tick label widths
|
|
90
123
|
const default_padding = { t: 20, b: 60, l: 60, r: 20 };
|
|
91
124
|
let pad = $state({ ...default_padding, ...padding });
|
|
92
125
|
// Update padding when format or ticks change, but prevent infinite loop
|
|
93
126
|
$effect(() => {
|
|
94
|
-
const base_pad = { ...default_padding, ...padding };
|
|
95
127
|
const new_pad = width && height && ticks.y.length
|
|
96
|
-
? calc_auto_padding({
|
|
97
|
-
|
|
128
|
+
? calc_auto_padding({
|
|
129
|
+
padding,
|
|
130
|
+
default_padding,
|
|
131
|
+
y_axis: { ...y_axis, tick_values: ticks.y },
|
|
132
|
+
y2_axis: { ...y2_axis, tick_values: ticks.y2 },
|
|
133
|
+
})
|
|
134
|
+
: { ...default_padding, ...padding };
|
|
135
|
+
// Expand right padding if y2 ticks are shown (only for vertical orientation)
|
|
136
|
+
if (width && height && y2_series.length && ticks.y2.length &&
|
|
137
|
+
orientation === `vertical`) {
|
|
138
|
+
const y2_tick_width = Math.max(0, ...ticks.y2.map((tick) => measure_text_width(format_value(tick, y2_axis.format), `12px sans-serif`)));
|
|
139
|
+
new_pad.r = Math.max(new_pad.r, 10 + y2_tick_width + (y2_axis.label ? 40 : 0));
|
|
140
|
+
}
|
|
98
141
|
// Only update if padding actually changed (prevents infinite loop)
|
|
99
142
|
if (pad.t !== new_pad.t || pad.b !== new_pad.b || pad.l !== new_pad.l ||
|
|
100
143
|
pad.r !== new_pad.r)
|
|
@@ -102,20 +145,35 @@ $effect(() => {
|
|
|
102
145
|
});
|
|
103
146
|
const chart_width = $derived(Math.max(1, width - pad.l - pad.r));
|
|
104
147
|
const chart_height = $derived(Math.max(1, height - pad.t - pad.b));
|
|
105
|
-
// Scales
|
|
148
|
+
// Scales
|
|
106
149
|
let scales = $derived({
|
|
107
|
-
x: create_scale(`linear`, ranges.current.x, [
|
|
108
|
-
|
|
150
|
+
x: create_scale(x_axis.scale_type ?? `linear`, ranges.current.x, [
|
|
151
|
+
pad.l,
|
|
152
|
+
width - pad.r,
|
|
153
|
+
]),
|
|
154
|
+
y: create_scale(y_axis.scale_type ?? `linear`, ranges.current.y, [
|
|
155
|
+
height - pad.b,
|
|
156
|
+
pad.t,
|
|
157
|
+
]),
|
|
158
|
+
y2: create_scale(y2_axis.scale_type ?? `linear`, ranges.current.y2, [
|
|
159
|
+
height - pad.b,
|
|
160
|
+
pad.t,
|
|
161
|
+
]),
|
|
109
162
|
});
|
|
110
163
|
// Ticks
|
|
111
164
|
let ticks = $derived({
|
|
112
165
|
x: width && height
|
|
113
|
-
? generate_ticks(ranges.current.x, `linear`,
|
|
166
|
+
? generate_ticks(ranges.current.x, x_axis.scale_type ?? `linear`, x_axis.ticks, scales.x, {
|
|
114
167
|
default_count: 8,
|
|
115
168
|
})
|
|
116
169
|
: [],
|
|
117
170
|
y: width && height
|
|
118
|
-
? generate_ticks(ranges.current.y, `linear`,
|
|
171
|
+
? generate_ticks(ranges.current.y, y_axis.scale_type ?? `linear`, y_axis.ticks, scales.y, {
|
|
172
|
+
default_count: 6,
|
|
173
|
+
})
|
|
174
|
+
: [],
|
|
175
|
+
y2: width && height && y2_series.length > 0 && orientation === `vertical`
|
|
176
|
+
? generate_ticks(ranges.current.y2, y2_axis.scale_type ?? `linear`, y2_axis.ticks, scales.y2, {
|
|
119
177
|
default_count: 6,
|
|
120
178
|
})
|
|
121
179
|
: [],
|
|
@@ -132,15 +190,30 @@ const on_window_mouse_move = (evt) => {
|
|
|
132
190
|
};
|
|
133
191
|
const on_window_mouse_up = () => {
|
|
134
192
|
if (drag_state.start && drag_state.current) {
|
|
135
|
-
const
|
|
136
|
-
const
|
|
193
|
+
const x1_raw = scales.x.invert(drag_state.start.x);
|
|
194
|
+
const x2_raw = scales.x.invert(drag_state.current.x);
|
|
137
195
|
const y1 = scales.y.invert(drag_state.start.y);
|
|
138
196
|
const y2 = scales.y.invert(drag_state.current.y);
|
|
197
|
+
const y2_1 = scales.y2.invert(drag_state.start.y);
|
|
198
|
+
const y2_2 = scales.y2.invert(drag_state.current.y);
|
|
139
199
|
const dx = Math.abs(drag_state.start.x - drag_state.current.x);
|
|
140
200
|
const dy = Math.abs(drag_state.start.y - drag_state.current.y);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
201
|
+
let xr1, xr2;
|
|
202
|
+
if (x1_raw instanceof Date && x2_raw instanceof Date) {
|
|
203
|
+
;
|
|
204
|
+
[xr1, xr2] = [x1_raw.getTime(), x2_raw.getTime()];
|
|
205
|
+
}
|
|
206
|
+
else if (typeof x1_raw === `number` && typeof x2_raw === `number`) {
|
|
207
|
+
;
|
|
208
|
+
[xr1, xr2] = [x1_raw, x2_raw];
|
|
209
|
+
}
|
|
210
|
+
else
|
|
211
|
+
[xr1, xr2] = [NaN, NaN]; // bail: mixed types
|
|
212
|
+
if (dx > 5 && dy > 5 && Number.isFinite(xr1) && Number.isFinite(xr2)) {
|
|
213
|
+
// Update axis ranges to trigger reactivity and prevent effect from overriding
|
|
214
|
+
x_axis = { ...x_axis, range: [Math.min(xr1, xr2), Math.max(xr1, xr2)] };
|
|
215
|
+
y_axis = { ...y_axis, range: [Math.min(y1, y2), Math.max(y1, y2)] };
|
|
216
|
+
y2_axis = { ...y2_axis, range: [Math.min(y2_1, y2_2), Math.max(y2_1, y2_2)] };
|
|
144
217
|
}
|
|
145
218
|
}
|
|
146
219
|
drag_state = { start: null, current: null, bounds: null };
|
|
@@ -162,7 +235,10 @@ function handle_mouse_down(evt) {
|
|
|
162
235
|
evt.preventDefault();
|
|
163
236
|
}
|
|
164
237
|
function handle_double_click() {
|
|
165
|
-
ranges
|
|
238
|
+
// Clear axis ranges to reset to auto ranges
|
|
239
|
+
x_axis = { ...x_axis, range: undefined };
|
|
240
|
+
y_axis = { ...y_axis, range: undefined };
|
|
241
|
+
y2_axis = { ...y2_axis, range: undefined };
|
|
166
242
|
}
|
|
167
243
|
// Legend data and handlers
|
|
168
244
|
let legend_data = $derived.by(() => series.map((srs, idx) => ({
|
|
@@ -171,12 +247,12 @@ let legend_data = $derived.by(() => series.map((srs, idx) => ({
|
|
|
171
247
|
visible: srs.visible ?? true,
|
|
172
248
|
display_style: srs.render_mode === `line`
|
|
173
249
|
? {
|
|
174
|
-
line_color: srs.color ??
|
|
250
|
+
line_color: srs.color ?? line.color,
|
|
175
251
|
line_dash: srs.line_style?.line_dash,
|
|
176
252
|
}
|
|
177
253
|
: {
|
|
178
254
|
symbol_type: `Square`,
|
|
179
|
-
symbol_color: srs.color ??
|
|
255
|
+
symbol_color: srs.color ?? bar.color,
|
|
180
256
|
},
|
|
181
257
|
})));
|
|
182
258
|
function toggle_series_visibility(series_idx) {
|
|
@@ -193,34 +269,43 @@ let bar_points_for_placement = $derived.by(() => {
|
|
|
193
269
|
// Use original series index to look up stacked_offsets
|
|
194
270
|
const series_idx = series.indexOf(srs);
|
|
195
271
|
const series_offsets = stacked_offsets[series_idx] ?? [];
|
|
272
|
+
const use_y2 = srs.y_axis === `y2`;
|
|
273
|
+
const y_scale = use_y2 ? scales.y2 : scales.y;
|
|
196
274
|
return srs.x.map((x_val, bar_idx) => {
|
|
197
275
|
const y_val = srs.y[bar_idx];
|
|
198
276
|
const base = !is_line && mode === `stacked`
|
|
199
277
|
? (series_offsets[bar_idx] ?? 0)
|
|
200
278
|
: 0;
|
|
201
279
|
const [bar_x, bar_y] = orientation === `vertical`
|
|
202
|
-
? [scales.x(x_val),
|
|
280
|
+
? [scales.x(x_val), y_scale(base + y_val)]
|
|
203
281
|
: [scales.x(base + y_val), scales.y(x_val)];
|
|
204
282
|
return { x: bar_x, y: bar_y };
|
|
205
283
|
}).filter((p) => isFinite(p.x) && isFinite(p.y));
|
|
206
284
|
});
|
|
207
285
|
});
|
|
208
286
|
// Calculate best legend placement
|
|
209
|
-
let legend_placement = $derived.by(() =>
|
|
210
|
-
?
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
287
|
+
let legend_placement = $derived.by(() => {
|
|
288
|
+
const should_show = show_legend !== undefined ? show_legend : series.length > 1;
|
|
289
|
+
return should_show && width && height
|
|
290
|
+
? find_best_plot_area(bar_points_for_placement, {
|
|
291
|
+
plot_width: chart_width,
|
|
292
|
+
plot_height: chart_height,
|
|
293
|
+
padding: pad,
|
|
294
|
+
margin: 10,
|
|
295
|
+
legend_size: { width: 120, height: 60 },
|
|
296
|
+
})
|
|
297
|
+
: null;
|
|
298
|
+
});
|
|
218
299
|
// Tooltip state
|
|
219
300
|
let hover_info = $state(null);
|
|
220
301
|
function get_bar_data(series_idx, bar_idx, color) {
|
|
221
302
|
const srs = series[series_idx];
|
|
222
303
|
const [x, y] = [srs.x[bar_idx], srs.y[bar_idx]];
|
|
223
304
|
const [orient_x, orient_y] = orientation === `horizontal` ? [y, x] : [x, y];
|
|
305
|
+
const metadata = Array.isArray(srs.metadata)
|
|
306
|
+
? srs.metadata[bar_idx]
|
|
307
|
+
: srs.metadata;
|
|
308
|
+
const label = srs.labels?.[bar_idx] ?? null;
|
|
224
309
|
return {
|
|
225
310
|
x,
|
|
226
311
|
y,
|
|
@@ -228,9 +313,10 @@ function get_bar_data(series_idx, bar_idx, color) {
|
|
|
228
313
|
orient_y,
|
|
229
314
|
series_idx,
|
|
230
315
|
bar_idx,
|
|
231
|
-
|
|
232
|
-
metadata: Array.isArray(srs.metadata) ? srs.metadata[bar_idx] : srs.metadata,
|
|
316
|
+
metadata,
|
|
233
317
|
color,
|
|
318
|
+
label,
|
|
319
|
+
y_axis: srs.y_axis ?? `y1`,
|
|
234
320
|
};
|
|
235
321
|
}
|
|
236
322
|
const handle_bar_hover = (series_idx, bar_idx, color) => (event) => {
|
|
@@ -239,17 +325,23 @@ const handle_bar_hover = (series_idx, bar_idx, color) => (event) => {
|
|
|
239
325
|
change(hover_info);
|
|
240
326
|
on_bar_hover?.({ ...hover_info, event });
|
|
241
327
|
};
|
|
242
|
-
// Stack offsets (only for bar series in stacked mode)
|
|
328
|
+
// Stack offsets (only for bar series in stacked mode, grouped by y-axis)
|
|
243
329
|
let stacked_offsets = $derived.by(() => {
|
|
244
330
|
if (mode !== `stacked`)
|
|
245
331
|
return [];
|
|
246
|
-
const max_len = Math.max(0, ...series.map((
|
|
332
|
+
const max_len = Math.max(0, ...series.map((srs) => srs.y.length));
|
|
247
333
|
const offsets = series.map(() => Array.from({ length: max_len }, () => 0));
|
|
248
|
-
|
|
249
|
-
const
|
|
334
|
+
// Separate accumulators for y1 and y2 axes
|
|
335
|
+
const y1_pos_acc = Array.from({ length: max_len }, () => 0);
|
|
336
|
+
const y1_neg_acc = Array.from({ length: max_len }, () => 0);
|
|
337
|
+
const y2_pos_acc = Array.from({ length: max_len }, () => 0);
|
|
338
|
+
const y2_neg_acc = Array.from({ length: max_len }, () => 0);
|
|
250
339
|
series.forEach((srs, series_idx) => {
|
|
251
340
|
if (!(srs?.visible ?? true) || srs.render_mode === `line`)
|
|
252
341
|
return;
|
|
342
|
+
const use_y2 = srs.y_axis === `y2`;
|
|
343
|
+
const pos_acc = use_y2 ? y2_pos_acc : y1_pos_acc;
|
|
344
|
+
const neg_acc = use_y2 ? y2_neg_acc : y1_neg_acc;
|
|
253
345
|
for (let bar_idx = 0; bar_idx < max_len; bar_idx++) {
|
|
254
346
|
const y_val = srs.y[bar_idx] ?? 0;
|
|
255
347
|
const acc = y_val >= 0 ? pos_acc : neg_acc;
|
|
@@ -292,6 +384,228 @@ let group_info = $derived.by(() => {
|
|
|
292
384
|
tabindex="0"
|
|
293
385
|
aria-label="Interactive bar plot with zoom and tooltip"
|
|
294
386
|
>
|
|
387
|
+
<!-- Zoom rectangle -->
|
|
388
|
+
{#if drag_state.start && drag_state.current}
|
|
389
|
+
{@const x = Math.min(drag_state.start.x, drag_state.current.x)}
|
|
390
|
+
{@const y = Math.min(drag_state.start.y, drag_state.current.y)}
|
|
391
|
+
{@const rect_w = Math.abs(drag_state.start.x - drag_state.current.x)}
|
|
392
|
+
{@const rect_h = Math.abs(drag_state.start.y - drag_state.current.y)}
|
|
393
|
+
<rect class="zoom-rect" {x} {y} width={rect_w} height={rect_h} />
|
|
394
|
+
{/if}
|
|
395
|
+
|
|
396
|
+
<!-- User content (custom overlays, reference lines, etc.) -->
|
|
397
|
+
{@render user_content?.({
|
|
398
|
+
height,
|
|
399
|
+
width,
|
|
400
|
+
x_scale_fn: scales.x,
|
|
401
|
+
y_scale_fn: scales.y,
|
|
402
|
+
y2_scale_fn: scales.y2,
|
|
403
|
+
pad,
|
|
404
|
+
x_range: ranges.current.x,
|
|
405
|
+
y_range: ranges.current.y,
|
|
406
|
+
y2_range: ranges.current.y2,
|
|
407
|
+
})}
|
|
408
|
+
|
|
409
|
+
<!-- X-axis -->
|
|
410
|
+
<g class="x-axis">
|
|
411
|
+
<line
|
|
412
|
+
x1={pad.l}
|
|
413
|
+
x2={width - pad.r}
|
|
414
|
+
y1={height - pad.b}
|
|
415
|
+
y2={height - pad.b}
|
|
416
|
+
stroke={x_axis.color || `var(--border-color, gray)`}
|
|
417
|
+
stroke-width="1"
|
|
418
|
+
/>
|
|
419
|
+
{#each ticks.x as tick (tick)}
|
|
420
|
+
{@const tick_x = scales.x(tick as number)}
|
|
421
|
+
{#if isFinite(tick_x)}
|
|
422
|
+
{@const rotation = x_axis.tick_rotation ?? 0}
|
|
423
|
+
{@const shift_x = x_axis.tick_label_shift?.x ?? 0}
|
|
424
|
+
{@const shift_y = x_axis.tick_label_shift?.y ?? 0}
|
|
425
|
+
{@const text_y = rotation !== 0 ? 8 + shift_y : 18 + shift_y}
|
|
426
|
+
{@const text_anchor = rotation !== 0 ? `start` : `middle`}
|
|
427
|
+
<g class="tick" transform="translate({tick_x}, {height - pad.b})">
|
|
428
|
+
{#if display.x_grid}
|
|
429
|
+
<line
|
|
430
|
+
y1={-(height - pad.b - pad.t)}
|
|
431
|
+
y2="0"
|
|
432
|
+
{...DEFAULT_GRID_STYLE}
|
|
433
|
+
{...(x_axis.grid_style ?? {})}
|
|
434
|
+
/>
|
|
435
|
+
{/if}
|
|
436
|
+
<line
|
|
437
|
+
y1="0"
|
|
438
|
+
y2="5"
|
|
439
|
+
stroke={x_axis.color || `var(--border-color, gray)`}
|
|
440
|
+
stroke-width="1"
|
|
441
|
+
/>
|
|
442
|
+
<text
|
|
443
|
+
x={shift_x}
|
|
444
|
+
y={text_y}
|
|
445
|
+
text-anchor={text_anchor}
|
|
446
|
+
fill={x_axis.color || `var(--text-color)`}
|
|
447
|
+
transform={rotation !== 0
|
|
448
|
+
? `rotate(${rotation}, ${shift_x}, ${text_y})`
|
|
449
|
+
: undefined}
|
|
450
|
+
>
|
|
451
|
+
{format_value(tick, x_axis.format)}
|
|
452
|
+
</text>
|
|
453
|
+
</g>
|
|
454
|
+
{/if}
|
|
455
|
+
{/each}
|
|
456
|
+
{#if x_axis.label}
|
|
457
|
+
{@const shift_x = x_axis.label_shift?.x ?? 0}
|
|
458
|
+
{@const shift_y = x_axis.label_shift?.y ?? 0}
|
|
459
|
+
<text
|
|
460
|
+
x={pad.l + chart_width / 2 + shift_x}
|
|
461
|
+
y={height - (pad.b / 3) + shift_y}
|
|
462
|
+
text-anchor="middle"
|
|
463
|
+
fill={x_axis.color || `var(--text-color)`}
|
|
464
|
+
>
|
|
465
|
+
{@html x_axis.label}
|
|
466
|
+
</text>
|
|
467
|
+
{/if}
|
|
468
|
+
</g>
|
|
469
|
+
|
|
470
|
+
<!-- Y-axis -->
|
|
471
|
+
<g class="y-axis">
|
|
472
|
+
<line
|
|
473
|
+
x1={pad.l}
|
|
474
|
+
x2={pad.l}
|
|
475
|
+
y1={pad.t}
|
|
476
|
+
y2={height - pad.b}
|
|
477
|
+
stroke={y_axis.color || `var(--border-color, gray)`}
|
|
478
|
+
stroke-width="1"
|
|
479
|
+
/>
|
|
480
|
+
{#each ticks.y as tick (tick)}
|
|
481
|
+
{@const tick_y = scales.y(tick as number)}
|
|
482
|
+
{#if isFinite(tick_y)}
|
|
483
|
+
{@const rotation = y_axis.tick_rotation ?? 0}
|
|
484
|
+
{@const shift_x = y_axis.tick_label_shift?.x ?? 0}
|
|
485
|
+
{@const shift_y = y_axis.tick_label_shift?.y ?? 0}
|
|
486
|
+
{@const text_x = -10 + shift_x}
|
|
487
|
+
<g class="tick" transform="translate({pad.l}, {tick_y})">
|
|
488
|
+
{#if display.y_grid}
|
|
489
|
+
<line
|
|
490
|
+
x1="0"
|
|
491
|
+
x2={width - pad.l - pad.r}
|
|
492
|
+
{...DEFAULT_GRID_STYLE}
|
|
493
|
+
{...(y_axis.grid_style ?? {})}
|
|
494
|
+
/>
|
|
495
|
+
{/if}
|
|
496
|
+
<line
|
|
497
|
+
x1="-5"
|
|
498
|
+
x2="0"
|
|
499
|
+
stroke={y_axis.color || `var(--border-color, gray)`}
|
|
500
|
+
stroke-width="1"
|
|
501
|
+
/>
|
|
502
|
+
<text
|
|
503
|
+
x={text_x}
|
|
504
|
+
y={shift_y}
|
|
505
|
+
text-anchor="end"
|
|
506
|
+
dominant-baseline="central"
|
|
507
|
+
fill={y_axis.color || `var(--text-color)`}
|
|
508
|
+
transform={rotation !== 0
|
|
509
|
+
? `rotate(${rotation}, ${text_x}, ${shift_y})`
|
|
510
|
+
: undefined}
|
|
511
|
+
>
|
|
512
|
+
{format_value(tick, y_axis.format)}
|
|
513
|
+
</text>
|
|
514
|
+
</g>
|
|
515
|
+
{/if}
|
|
516
|
+
{/each}
|
|
517
|
+
{#if y_axis.label}
|
|
518
|
+
{@const max_y_tick_width = Math.max(
|
|
519
|
+
0,
|
|
520
|
+
...ticks.y.map((tick) =>
|
|
521
|
+
measure_text_width(
|
|
522
|
+
format_value(tick, y_axis.format),
|
|
523
|
+
`12px sans-serif`,
|
|
524
|
+
)
|
|
525
|
+
),
|
|
526
|
+
)}
|
|
527
|
+
{@const shift_x = y_axis.label_shift?.x ?? 0}
|
|
528
|
+
{@const shift_y = y_axis.label_shift?.y ?? 0}
|
|
529
|
+
{@const y_label_x = Math.max(15, pad.l - max_y_tick_width - 35) + shift_x}
|
|
530
|
+
{@const y_label_y = pad.t + chart_height / 2 + shift_y}
|
|
531
|
+
<text
|
|
532
|
+
x={y_label_x}
|
|
533
|
+
y={y_label_y}
|
|
534
|
+
text-anchor="middle"
|
|
535
|
+
fill={y_axis.color || `var(--text-color)`}
|
|
536
|
+
transform="rotate(-90, {y_label_x}, {y_label_y})"
|
|
537
|
+
>
|
|
538
|
+
{@html y_axis.label}
|
|
539
|
+
</text>
|
|
540
|
+
{/if}
|
|
541
|
+
</g>
|
|
542
|
+
|
|
543
|
+
<!-- Y2-axis (Right) -->
|
|
544
|
+
<!-- Note: y2 axis is only supported for vertical orientation. Implementing x2 for horizontal mode requires additional complexity. -->
|
|
545
|
+
{#if y2_series.length > 0 && orientation === `vertical`}
|
|
546
|
+
<g class="y2-axis">
|
|
547
|
+
<line
|
|
548
|
+
x1={width - pad.r}
|
|
549
|
+
x2={width - pad.r}
|
|
550
|
+
y1={pad.t}
|
|
551
|
+
y2={height - pad.b}
|
|
552
|
+
stroke={y2_axis.color || `var(--border-color, gray)`}
|
|
553
|
+
stroke-width="1"
|
|
554
|
+
/>
|
|
555
|
+
{#each ticks.y2 as tick (tick)}
|
|
556
|
+
{@const tick_y = scales.y2(tick as number)}
|
|
557
|
+
{#if isFinite(tick_y)}
|
|
558
|
+
{@const rotation = y2_axis.tick_rotation ?? 0}
|
|
559
|
+
{@const shift_x = y2_axis.tick_label_shift?.x ?? 8}
|
|
560
|
+
{@const shift_y = y2_axis.tick_label_shift?.y ?? 0}
|
|
561
|
+
<g class="tick" transform="translate({width - pad.r}, {tick_y})">
|
|
562
|
+
{#if display.y2_grid}
|
|
563
|
+
<line
|
|
564
|
+
x1={-(width - pad.l - pad.r)}
|
|
565
|
+
x2="0"
|
|
566
|
+
{...DEFAULT_GRID_STYLE}
|
|
567
|
+
{...(y2_axis.grid_style ?? {})}
|
|
568
|
+
/>
|
|
569
|
+
{/if}
|
|
570
|
+
<line
|
|
571
|
+
x1="0"
|
|
572
|
+
x2="5"
|
|
573
|
+
stroke={y2_axis.color || `var(--border-color, gray)`}
|
|
574
|
+
stroke-width="1"
|
|
575
|
+
/>
|
|
576
|
+
<text
|
|
577
|
+
x={shift_x}
|
|
578
|
+
y={shift_y}
|
|
579
|
+
text-anchor="start"
|
|
580
|
+
dominant-baseline="central"
|
|
581
|
+
fill={y2_axis.color || `var(--text-color)`}
|
|
582
|
+
transform={rotation !== 0
|
|
583
|
+
? `rotate(${rotation}, ${shift_x}, ${shift_y})`
|
|
584
|
+
: undefined}
|
|
585
|
+
>
|
|
586
|
+
{format_value(tick, y2_axis.format)}
|
|
587
|
+
</text>
|
|
588
|
+
</g>
|
|
589
|
+
{/if}
|
|
590
|
+
{/each}
|
|
591
|
+
{#if y2_axis.label}
|
|
592
|
+
{@const shift_x = y2_axis.label_shift?.x ?? 0}
|
|
593
|
+
{@const shift_y = y2_axis.label_shift?.y ?? 0}
|
|
594
|
+
{@const y2_label_x = width - pad.r + 50 + shift_x}
|
|
595
|
+
{@const y2_label_y = pad.t + chart_height / 2 + shift_y}
|
|
596
|
+
<text
|
|
597
|
+
x={y2_label_x}
|
|
598
|
+
y={y2_label_y}
|
|
599
|
+
text-anchor="middle"
|
|
600
|
+
fill={y2_axis.color || `var(--text-color)`}
|
|
601
|
+
transform="rotate(-90, {y2_label_x}, {y2_label_y})"
|
|
602
|
+
>
|
|
603
|
+
{@html y2_axis.label}
|
|
604
|
+
</text>
|
|
605
|
+
{/if}
|
|
606
|
+
</g>
|
|
607
|
+
{/if}
|
|
608
|
+
|
|
295
609
|
<!-- Define clip path for chart area -->
|
|
296
610
|
<defs>
|
|
297
611
|
<clipPath id={clip_path_id}>
|
|
@@ -302,33 +616,37 @@ let group_info = $derived.by(() => {
|
|
|
302
616
|
<!-- Clipped content: zero lines, bars, and lines -->
|
|
303
617
|
<g clip-path="url(#{clip_path_id})">
|
|
304
618
|
<!-- Zero lines -->
|
|
305
|
-
{#if
|
|
619
|
+
{#if display.x_zero_line && (x_axis.scale_type ?? `linear`) === `linear` &&
|
|
620
|
+
ranges.current.x[0] <= 0 && ranges.current.x[1] >= 0}
|
|
306
621
|
{@const zx = scales.x(0)}
|
|
622
|
+
{#if isFinite(zx)}
|
|
623
|
+
<line class="zero-line" x1={zx} x2={zx} y1={pad.t} y2={height - pad.b} />
|
|
624
|
+
{/if}
|
|
625
|
+
{/if}
|
|
626
|
+
{#if display.y_zero_line && (y_axis.scale_type ?? `linear`) === `linear` &&
|
|
627
|
+
ranges.current.y[0] <= 0 && ranges.current.y[1] >= 0}
|
|
307
628
|
{@const zy = scales.y(0)}
|
|
308
|
-
{#if
|
|
309
|
-
<line
|
|
310
|
-
x1={zx}
|
|
311
|
-
x2={zx}
|
|
312
|
-
y1={pad.t}
|
|
313
|
-
y2={height - pad.b}
|
|
314
|
-
stroke="gray"
|
|
315
|
-
stroke-width="0.5"
|
|
316
|
-
/>
|
|
629
|
+
{#if isFinite(zy)}
|
|
630
|
+
<line class="zero-line" x1={pad.l} x2={width - pad.r} y1={zy} y2={zy} />
|
|
317
631
|
{/if}
|
|
318
|
-
|
|
632
|
+
{/if}
|
|
633
|
+
{#if display.y_zero_line && y2_series.length > 0 &&
|
|
634
|
+
(y2_axis.scale_type ?? `linear`) === `linear` &&
|
|
635
|
+
ranges.current.y2[0] <= 0 && ranges.current.y2[1] >= 0}
|
|
636
|
+
{@const zero_y2 = scales.y2(0)}
|
|
637
|
+
{#if isFinite(zero_y2)}
|
|
319
638
|
<line
|
|
639
|
+
class="zero-line"
|
|
320
640
|
x1={pad.l}
|
|
321
641
|
x2={width - pad.r}
|
|
322
|
-
y1={
|
|
323
|
-
y2={
|
|
324
|
-
stroke="gray"
|
|
325
|
-
stroke-width="0.5"
|
|
642
|
+
y1={zero_y2}
|
|
643
|
+
y2={zero_y2}
|
|
326
644
|
/>
|
|
327
645
|
{/if}
|
|
328
646
|
{/if}
|
|
329
647
|
|
|
330
648
|
<!-- Bars and Lines -->
|
|
331
|
-
{#each series as srs, series_idx (series_idx)}
|
|
649
|
+
{#each series as srs, series_idx (srs?.id ?? series_idx)}
|
|
332
650
|
{#if srs?.visible ?? true}
|
|
333
651
|
{@const is_line = srs.render_mode === `line`}
|
|
334
652
|
<g
|
|
@@ -337,9 +655,11 @@ let group_info = $derived.by(() => {
|
|
|
337
655
|
>
|
|
338
656
|
{#if is_line}
|
|
339
657
|
<!-- Render as line -->
|
|
340
|
-
{@const color = srs.color ??
|
|
341
|
-
{@const stroke_width = srs.line_style?.stroke_width ?? 2}
|
|
658
|
+
{@const color = srs.color ?? line.color ?? `steelblue`}
|
|
659
|
+
{@const stroke_width = srs.line_style?.stroke_width ?? line.width ?? 2}
|
|
342
660
|
{@const line_dash = srs.line_style?.line_dash ?? `none`}
|
|
661
|
+
{@const use_y2 = srs.y_axis === `y2`}
|
|
662
|
+
{@const y_scale = use_y2 ? scales.y2 : scales.y}
|
|
343
663
|
{@const points = srs.x.map((x_val, idx) => {
|
|
344
664
|
const y_val = srs.y[idx]
|
|
345
665
|
// Lines don't stack - they show absolute values (useful for totals/trends)
|
|
@@ -347,7 +667,7 @@ let group_info = $derived.by(() => {
|
|
|
347
667
|
? scales.x(x_val)
|
|
348
668
|
: scales.x(y_val)
|
|
349
669
|
const plot_y = orientation === `vertical`
|
|
350
|
-
?
|
|
670
|
+
? y_scale(y_val)
|
|
351
671
|
: scales.y(x_val)
|
|
352
672
|
return { x: plot_x, y: plot_y, idx }
|
|
353
673
|
}).filter((p) => isFinite(p.x) && isFinite(p.y))}
|
|
@@ -423,23 +743,28 @@ let group_info = $derived.by(() => {
|
|
|
423
743
|
{@const base = mode === `stacked`
|
|
424
744
|
? (stacked_offsets[series_idx]?.[bar_idx] ?? 0)
|
|
425
745
|
: 0}
|
|
426
|
-
{@const color = srs.color ??
|
|
746
|
+
{@const color = srs.color ?? bar.color ?? `steelblue`}
|
|
427
747
|
{@const bar_width_val = Array.isArray(srs.bar_width)
|
|
428
748
|
? (srs.bar_width[bar_idx] ?? 0.5)
|
|
429
749
|
: (srs.bar_width ?? 0.5)}
|
|
430
750
|
{@const half = mode === `grouped` && group_info.bar_series_count > 1
|
|
431
751
|
? bar_width_val / (2 * group_info.bar_series_count)
|
|
432
752
|
: bar_width_val / 2}
|
|
753
|
+
{@const calculate_group_offset = (idx: number) => {
|
|
754
|
+
const position = group_info.bar_series_indices.indexOf(idx)
|
|
755
|
+
const offset = position - (group_info.bar_series_count - 1) / 2
|
|
756
|
+
return offset * (bar_width_val / group_info.bar_series_count)
|
|
757
|
+
}}
|
|
433
758
|
{@const group_offset = mode === `grouped` && group_info.bar_series_count > 1
|
|
434
|
-
? (
|
|
435
|
-
(pos - (group_info.bar_series_count - 1) / 2) *
|
|
436
|
-
(bar_width_val / group_info.bar_series_count))()
|
|
759
|
+
? calculate_group_offset(series_idx)
|
|
437
760
|
: 0}
|
|
438
761
|
{@const is_vertical = orientation === `vertical`}
|
|
439
762
|
{@const cat_val = x_val}
|
|
440
763
|
{@const val = y_val}
|
|
764
|
+
{@const use_y2 = srs.y_axis === `y2`}
|
|
765
|
+
{@const y_scale = use_y2 ? scales.y2 : scales.y}
|
|
441
766
|
{@const [cat_scale, val_scale] = is_vertical
|
|
442
|
-
? [scales.x,
|
|
767
|
+
? [scales.x, y_scale]
|
|
443
768
|
: [scales.y, scales.x]}
|
|
444
769
|
{@const c0 = cat_scale(cat_val + group_offset - half)}
|
|
445
770
|
{@const c1 = cat_scale(cat_val + group_offset + half)}
|
|
@@ -458,7 +783,7 @@ let group_info = $derived.by(() => {
|
|
|
458
783
|
width={rect_w}
|
|
459
784
|
height={rect_h}
|
|
460
785
|
fill={color}
|
|
461
|
-
opacity={mode === `overlay` ?
|
|
786
|
+
opacity={mode === `overlay` ? bar.opacity : 1}
|
|
462
787
|
role="button"
|
|
463
788
|
tabindex="0"
|
|
464
789
|
aria-label={`bar ${bar_idx + 1} of ${srs.label ?? `series`}`}
|
|
@@ -502,110 +827,6 @@ let group_info = $derived.by(() => {
|
|
|
502
827
|
{/if}
|
|
503
828
|
{/each}
|
|
504
829
|
</g>
|
|
505
|
-
|
|
506
|
-
<!-- Zoom rectangle -->
|
|
507
|
-
{#if drag_state.start && drag_state.current}
|
|
508
|
-
{@const x = Math.min(drag_state.start.x, drag_state.current.x)}
|
|
509
|
-
{@const y = Math.min(drag_state.start.y, drag_state.current.y)}
|
|
510
|
-
{@const rect_w = Math.abs(drag_state.start.x - drag_state.current.x)}
|
|
511
|
-
{@const rect_h = Math.abs(drag_state.start.y - drag_state.current.y)}
|
|
512
|
-
<rect class="zoom-rect" {x} {y} width={rect_w} height={rect_h} />
|
|
513
|
-
{/if}
|
|
514
|
-
|
|
515
|
-
<!-- X-axis -->
|
|
516
|
-
<g class="x-axis">
|
|
517
|
-
<line
|
|
518
|
-
x1={pad.l}
|
|
519
|
-
x2={width - pad.r}
|
|
520
|
-
y1={height - pad.b}
|
|
521
|
-
y2={height - pad.b}
|
|
522
|
-
stroke="var(--border-color, gray)"
|
|
523
|
-
stroke-width="1"
|
|
524
|
-
/>
|
|
525
|
-
{#each ticks.x as tick (tick)}
|
|
526
|
-
{@const tick_x = scales.x(tick as number)}
|
|
527
|
-
{#if isFinite(tick_x)}
|
|
528
|
-
<g class="tick" transform="translate({tick_x}, {height - pad.b})">
|
|
529
|
-
{#if x_grid}
|
|
530
|
-
<line
|
|
531
|
-
y1={-(height - pad.b - pad.t)}
|
|
532
|
-
y2="0"
|
|
533
|
-
{...typeof x_grid === `object` ? x_grid : {}}
|
|
534
|
-
/>
|
|
535
|
-
{/if}
|
|
536
|
-
<line y1="0" y2="5" stroke="var(--border-color, gray)" stroke-width="1" />
|
|
537
|
-
<text y="18" text-anchor="middle" fill="var(--text-color)">
|
|
538
|
-
{format_value(tick, x_format)}
|
|
539
|
-
</text>
|
|
540
|
-
</g>
|
|
541
|
-
{/if}
|
|
542
|
-
{/each}
|
|
543
|
-
{#if x_label}
|
|
544
|
-
<text
|
|
545
|
-
x={pad.l + chart_width / 2 + (x_label_shift.x ?? 0)}
|
|
546
|
-
y={height - (pad.b / 3) + (x_label_shift.y ?? 0)}
|
|
547
|
-
text-anchor="middle"
|
|
548
|
-
fill="var(--text-color)"
|
|
549
|
-
>
|
|
550
|
-
{x_label}
|
|
551
|
-
</text>
|
|
552
|
-
{/if}
|
|
553
|
-
</g>
|
|
554
|
-
|
|
555
|
-
<!-- Y-axis -->
|
|
556
|
-
<g class="y-axis">
|
|
557
|
-
<line
|
|
558
|
-
x1={pad.l}
|
|
559
|
-
x2={pad.l}
|
|
560
|
-
y1={pad.t}
|
|
561
|
-
y2={height - pad.b}
|
|
562
|
-
stroke="var(--border-color, gray)"
|
|
563
|
-
stroke-width="1"
|
|
564
|
-
/>
|
|
565
|
-
{#each ticks.y as tick (tick)}
|
|
566
|
-
{@const tick_y = scales.y(tick as number)}
|
|
567
|
-
{#if isFinite(tick_y)}
|
|
568
|
-
<g class="tick" transform="translate({pad.l}, {tick_y})">
|
|
569
|
-
{#if y_grid}
|
|
570
|
-
<line
|
|
571
|
-
x1="0"
|
|
572
|
-
x2={width - pad.l - pad.r}
|
|
573
|
-
{...typeof y_grid === `object` ? y_grid : {}}
|
|
574
|
-
/>
|
|
575
|
-
{/if}
|
|
576
|
-
<line x1="-5" x2="0" stroke="var(--border-color, gray)" stroke-width="1" />
|
|
577
|
-
<text
|
|
578
|
-
x="-10"
|
|
579
|
-
text-anchor="end"
|
|
580
|
-
dominant-baseline="central"
|
|
581
|
-
fill="var(--text-color)"
|
|
582
|
-
>
|
|
583
|
-
{format_value(tick, y_format)}
|
|
584
|
-
</text>
|
|
585
|
-
</g>
|
|
586
|
-
{/if}
|
|
587
|
-
{/each}
|
|
588
|
-
{#if y_label}
|
|
589
|
-
{@const max_y_tick_width = Math.max(
|
|
590
|
-
0,
|
|
591
|
-
...ticks.y.map((tick) =>
|
|
592
|
-
measure_text_width(format_value(tick, y_format), `12px sans-serif`)
|
|
593
|
-
),
|
|
594
|
-
)}
|
|
595
|
-
{@const y_label_x = Math.max(15, pad.l - max_y_tick_width - 35) +
|
|
596
|
-
(y_label_shift.x ?? 0)}
|
|
597
|
-
{@const y_label_y = pad.t + chart_height / 2 + (y_label_shift.y ?? 0)}
|
|
598
|
-
<text
|
|
599
|
-
x={y_label_x}
|
|
600
|
-
y={y_label_y}
|
|
601
|
-
text-anchor="middle"
|
|
602
|
-
fill="var(--text-color)"
|
|
603
|
-
transform="rotate(-90, {y_label_x}, {y_label_y})"
|
|
604
|
-
>
|
|
605
|
-
{y_label}
|
|
606
|
-
</text>
|
|
607
|
-
{/if}
|
|
608
|
-
</g>
|
|
609
830
|
</svg>
|
|
610
831
|
|
|
611
832
|
<!-- Legend -->
|
|
@@ -619,8 +840,11 @@ let group_info = $derived.by(() => {
|
|
|
619
840
|
{/if}
|
|
620
841
|
|
|
621
842
|
{#if hover_info && hovered}
|
|
843
|
+
{@const srs = series[hover_info.series_idx]}
|
|
844
|
+
{@const use_y2 = srs?.y_axis === `y2`}
|
|
845
|
+
{@const active_y_axis = use_y2 ? y2_axis : y_axis}
|
|
622
846
|
{@const cx = scales.x(hover_info.orient_x)}
|
|
623
|
-
{@const cy = scales.y(hover_info.orient_y)}
|
|
847
|
+
{@const cy = (use_y2 ? scales.y2 : scales.y)(hover_info.orient_y)}
|
|
624
848
|
<div
|
|
625
849
|
class="tooltip overlay"
|
|
626
850
|
style={`position: absolute; left: ${cx + 6}px; top: ${cy}px; pointer-events: none;`}
|
|
@@ -628,8 +852,16 @@ let group_info = $derived.by(() => {
|
|
|
628
852
|
{#if tooltip}
|
|
629
853
|
{@render tooltip(hover_info)}
|
|
630
854
|
{:else}
|
|
631
|
-
<div>
|
|
632
|
-
|
|
855
|
+
<div>
|
|
856
|
+
{x_axis.label || `x`}: {
|
|
857
|
+
format_value(hover_info.orient_x, x_axis.format || `.3~s`)
|
|
858
|
+
}
|
|
859
|
+
</div>
|
|
860
|
+
<div>
|
|
861
|
+
{active_y_axis.label || `y`}: {
|
|
862
|
+
format_value(hover_info.orient_y, active_y_axis.format || `.3~s`)
|
|
863
|
+
}
|
|
864
|
+
</div>
|
|
633
865
|
{/if}
|
|
634
866
|
</div>
|
|
635
867
|
{/if}
|
|
@@ -637,21 +869,18 @@ let group_info = $derived.by(() => {
|
|
|
637
869
|
{#if show_controls}
|
|
638
870
|
<BarPlotControls
|
|
639
871
|
toggle_props={controls_toggle_props}
|
|
872
|
+
pane_props={controls_pane_props}
|
|
640
873
|
bind:show_controls
|
|
641
874
|
bind:controls_open
|
|
642
875
|
bind:orientation
|
|
643
876
|
bind:mode
|
|
644
|
-
bind:
|
|
645
|
-
bind:
|
|
646
|
-
bind:
|
|
647
|
-
bind:
|
|
648
|
-
bind:x_format
|
|
649
|
-
bind:y_format
|
|
650
|
-
bind:x_range
|
|
651
|
-
bind:y_range
|
|
877
|
+
bind:x_axis
|
|
878
|
+
bind:y_axis
|
|
879
|
+
bind:y2_axis
|
|
880
|
+
bind:display
|
|
652
881
|
auto_x_range={auto_ranges.x as [number, number]}
|
|
653
882
|
auto_y_range={auto_ranges.y as [number, number]}
|
|
654
|
-
{
|
|
883
|
+
auto_y2_range={auto_ranges.y2 as [number, number]}
|
|
655
884
|
/>
|
|
656
885
|
{/if}
|
|
657
886
|
{/if}
|
|
@@ -679,7 +908,7 @@ let group_info = $derived.by(() => {
|
|
|
679
908
|
height: 100%;
|
|
680
909
|
overflow: var(--svg-overflow, visible);
|
|
681
910
|
}
|
|
682
|
-
g:is(.x-axis, .y-axis) .tick text {
|
|
911
|
+
g:is(.x-axis, .y-axis, .y2-axis) .tick text {
|
|
683
912
|
font-size: var(--tick-font-size, 0.8em);
|
|
684
913
|
}
|
|
685
914
|
.zoom-rect {
|
|
@@ -700,4 +929,9 @@ let group_info = $derived.by(() => {
|
|
|
700
929
|
fill: var(--text-color);
|
|
701
930
|
font-size: 11px;
|
|
702
931
|
}
|
|
932
|
+
.zero-line {
|
|
933
|
+
stroke: var(--barplot-zero-line-color, light-dark(black, white));
|
|
934
|
+
stroke-width: var(--barplot-zero-line-width, 1);
|
|
935
|
+
opacity: var(--barplot-zero-line-opacity, 0.3);
|
|
936
|
+
}
|
|
703
937
|
</style>
|