@opendata-ai/openchart-core 7.2.1 → 7.2.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/index.d.ts +51 -7
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/responsive/__tests__/metrics.test.ts +52 -0
- package/src/responsive/index.ts +1 -0
- package/src/responsive/metrics.ts +32 -6
- package/src/theme/defaults.ts +6 -1
- package/src/types/spec.ts +22 -0
- package/src/types/theme.ts +5 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
AXIS_TITLE_GAP,
|
|
4
|
+
AXIS_TITLE_OFFSET_DEFAULT,
|
|
5
|
+
axisTitleOffset,
|
|
6
|
+
TICK_LABEL_OFFSET,
|
|
7
|
+
} from '../metrics';
|
|
8
|
+
|
|
9
|
+
// The rotated y-axis title is anchored at its center and its glyph box extends
|
|
10
|
+
// half the title font size toward the tick labels. axisTitleOffset() returns the
|
|
11
|
+
// distance from the chart edge to that center; visible clearance between the
|
|
12
|
+
// widest tick label and the title's near edge is therefore:
|
|
13
|
+
// offset - TICK_LABEL_OFFSET - tickLabelWidth - titleFontSize / 2
|
|
14
|
+
const visibleClearance = (offset: number, tickLabelWidth: number, titleFontSize: number): number =>
|
|
15
|
+
offset - TICK_LABEL_OFFSET - tickLabelWidth - titleFontSize / 2;
|
|
16
|
+
|
|
17
|
+
describe('axisTitleOffset', () => {
|
|
18
|
+
const WIDE = 1200; // wide viewport so the dynamic value wins over the floor
|
|
19
|
+
|
|
20
|
+
it('leaves the title clear of the tick labels at the default font size', () => {
|
|
21
|
+
const offset = axisTitleOffset(40, 13, WIDE);
|
|
22
|
+
// ~AXIS_TITLE_GAP of clearance, never overlapping (negative).
|
|
23
|
+
expect(visibleClearance(offset, 40, 13)).toBeGreaterThanOrEqual(AXIS_TITLE_GAP - 1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('keeps clearance constant as the title font size grows', () => {
|
|
27
|
+
// The original bug: a fixed gap let large title fonts overlap the tick
|
|
28
|
+
// labels because the title's half-glyph ate into the gap. Clearance must
|
|
29
|
+
// stay ~AXIS_TITLE_GAP regardless of how big the title font is.
|
|
30
|
+
const small = visibleClearance(axisTitleOffset(40, 13, WIDE), 40, 13);
|
|
31
|
+
const large = visibleClearance(axisTitleOffset(40, 30, WIDE), 40, 30);
|
|
32
|
+
|
|
33
|
+
expect(large).toBeGreaterThanOrEqual(AXIS_TITLE_GAP - 1);
|
|
34
|
+
expect(Math.abs(large - small)).toBeLessThanOrEqual(1); // rounding only
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('pushes the title further left as tick labels get wider', () => {
|
|
38
|
+
// Both label widths sit above the viewport floor, so the offset tracks the
|
|
39
|
+
// label width 1:1 (a 60px wider label moves the title 60px further left).
|
|
40
|
+
const narrow = axisTitleOffset(50, 13, WIDE);
|
|
41
|
+
const wide = axisTitleOffset(110, 13, WIDE);
|
|
42
|
+
expect(narrow).toBeGreaterThan(AXIS_TITLE_OFFSET_DEFAULT); // floor not in play
|
|
43
|
+
expect(wide - narrow).toBeCloseTo(60, 0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('falls back to the viewport-minimum offset for short labels on wide containers', () => {
|
|
47
|
+
// Tiny label + small font: the dynamic value is below the floor, so the
|
|
48
|
+
// floor wins (prevents the title from hugging the chart edge).
|
|
49
|
+
const offset = axisTitleOffset(2, 11, WIDE);
|
|
50
|
+
expect(offset).toBe(AXIS_TITLE_OFFSET_DEFAULT);
|
|
51
|
+
});
|
|
52
|
+
});
|
package/src/responsive/index.ts
CHANGED
|
@@ -64,15 +64,41 @@ export const AXIS_TITLE_TRAILING_PAD = 4;
|
|
|
64
64
|
// ---------------------------------------------------------------------------
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
*
|
|
68
|
-
* y-axis title
|
|
69
|
-
*
|
|
70
|
-
*
|
|
67
|
+
* Visible breathing room between the widest tick label's far edge and the
|
|
68
|
+
* rotated y-axis title's near edge.
|
|
69
|
+
*
|
|
70
|
+
* The title is rotated -90deg and anchored at its center, so its glyph box
|
|
71
|
+
* extends half the title font size toward the tick labels. To keep the gap
|
|
72
|
+
* constant regardless of title font size, callers add that half-glyph on top
|
|
73
|
+
* of this value (see axisTitleOffset() below). At the old fixed gap of 14 the
|
|
74
|
+
* clearance silently shrank as the font grew (14 - 11 = 3px at body size 21),
|
|
75
|
+
* which let large-font titles collide with the tick labels.
|
|
71
76
|
*
|
|
72
77
|
* Used in both the engine (dimensions.ts margin reservation) and the renderer
|
|
73
|
-
* (axes.ts title placement). Both must agree on this value.
|
|
78
|
+
* (axes.ts title placement) via axisTitleOffset(). Both must agree on this value.
|
|
74
79
|
*/
|
|
75
|
-
export const AXIS_TITLE_GAP =
|
|
80
|
+
export const AXIS_TITLE_GAP = 7;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Computes the distance from the chart edge to the rotated y-axis title's center.
|
|
84
|
+
*
|
|
85
|
+
* The title center must sit far enough left that, after accounting for the
|
|
86
|
+
* widest tick label and the title's own half-glyph height, AXIS_TITLE_GAP of
|
|
87
|
+
* visible clearance remains. Falls back to the viewport-minimum offset when the
|
|
88
|
+
* dynamic value would be smaller (short tick labels on wide containers).
|
|
89
|
+
*
|
|
90
|
+
* Shared by the engine (margin reservation) and the renderer (title placement)
|
|
91
|
+
* so the reserved space always matches where the title is drawn.
|
|
92
|
+
*/
|
|
93
|
+
export function axisTitleOffset(
|
|
94
|
+
maxTickLabelWidth: number,
|
|
95
|
+
titleFontSize: number,
|
|
96
|
+
width: number,
|
|
97
|
+
): number {
|
|
98
|
+
const halfTitleGlyph = Math.ceil(titleFontSize / 2);
|
|
99
|
+
const dynamic = TICK_LABEL_OFFSET + maxTickLabelWidth + AXIS_TITLE_GAP + halfTitleGlyph;
|
|
100
|
+
return Math.max(dynamic, getAxisTitleOffset(width));
|
|
101
|
+
}
|
|
76
102
|
|
|
77
103
|
// ---------------------------------------------------------------------------
|
|
78
104
|
// Narrow viewport threshold (between compact and medium)
|
package/src/theme/defaults.ts
CHANGED
|
@@ -42,6 +42,8 @@ export const DEFAULT_THEME: Theme = {
|
|
|
42
42
|
body: 13,
|
|
43
43
|
small: 11,
|
|
44
44
|
axisTick: 11,
|
|
45
|
+
metricLabel: 10,
|
|
46
|
+
metricValue: 22,
|
|
45
47
|
},
|
|
46
48
|
weights: {
|
|
47
49
|
normal: 450,
|
|
@@ -57,7 +59,10 @@ export const DEFAULT_THEME: Theme = {
|
|
|
57
59
|
chartToFooter: 8,
|
|
58
60
|
axisMargin: 6,
|
|
59
61
|
xAxisHeight: 26,
|
|
60
|
-
|
|
62
|
+
// Gap (px) between the x-axis line and the TOP of the tick labels. The
|
|
63
|
+
// renderer anchors x-tick labels at their top edge (hanging baseline), so
|
|
64
|
+
// this is a literal top gap that holds regardless of font size.
|
|
65
|
+
xAxisLabelPadding: 4,
|
|
61
66
|
},
|
|
62
67
|
borderRadius: 2,
|
|
63
68
|
chrome: {
|
package/src/types/spec.ts
CHANGED
|
@@ -727,10 +727,24 @@ export interface RangeAnnotation extends AnnotationBase {
|
|
|
727
727
|
y1?: string | number;
|
|
728
728
|
/** End of the range on the y-axis. */
|
|
729
729
|
y2?: string | number;
|
|
730
|
+
/**
|
|
731
|
+
* For band/point (ordinal) x or y scales, whether the range extends from the
|
|
732
|
+
* data point's center out to the band/step edge. Default `true`: a range on a
|
|
733
|
+
* bar chart spans full columns, and a range on a line chart extends half a step
|
|
734
|
+
* past the first/last point so the band reaches the plot edge. Set `false` to
|
|
735
|
+
* anchor the range exactly at the data point centers instead — useful when you
|
|
736
|
+
* want the band inset from the axis (e.g. starting at the first data point
|
|
737
|
+
* rather than flush against the y-axis guide). No effect on linear/time scales.
|
|
738
|
+
*/
|
|
739
|
+
extendToEdges?: boolean;
|
|
730
740
|
/** Pixel offset for the range label. */
|
|
731
741
|
labelOffset?: AnnotationOffset;
|
|
732
742
|
/** Anchor direction for the range label. */
|
|
733
743
|
labelAnchor?: AnnotationAnchor;
|
|
744
|
+
/** Font size override for the range label (px). Default: 11. */
|
|
745
|
+
fontSize?: number;
|
|
746
|
+
/** Font weight override for the range label. Default: 500. */
|
|
747
|
+
fontWeight?: number;
|
|
734
748
|
}
|
|
735
749
|
|
|
736
750
|
/**
|
|
@@ -753,6 +767,10 @@ export interface RefLineAnnotation extends AnnotationBase {
|
|
|
753
767
|
labelOffset?: AnnotationOffset;
|
|
754
768
|
/** Anchor direction for the reference line label. */
|
|
755
769
|
labelAnchor?: AnnotationAnchor;
|
|
770
|
+
/** Label font size in pixels. Default: 11. */
|
|
771
|
+
fontSize?: number;
|
|
772
|
+
/** Label font weight. Default: 400. */
|
|
773
|
+
fontWeight?: number;
|
|
756
774
|
}
|
|
757
775
|
|
|
758
776
|
/** Discriminated union of all annotation types. */
|
|
@@ -824,6 +842,10 @@ export interface ThemeConfig {
|
|
|
824
842
|
small?: number;
|
|
825
843
|
/** Axis tick labels. Default: 11. */
|
|
826
844
|
axisTick?: number;
|
|
845
|
+
/** KPI metric uppercase label. Default: 10. */
|
|
846
|
+
metricLabel?: number;
|
|
847
|
+
/** KPI metric primary value. Default: 22. Delta/secondary derive from this. */
|
|
848
|
+
metricValue?: number;
|
|
827
849
|
};
|
|
828
850
|
};
|
|
829
851
|
/** Spacing overrides in pixels. */
|
package/src/types/theme.ts
CHANGED
|
@@ -51,6 +51,11 @@ export interface ThemeFontSizes {
|
|
|
51
51
|
small: number;
|
|
52
52
|
/** Axis tick label font size in pixels. */
|
|
53
53
|
axisTick: number;
|
|
54
|
+
/** KPI metric uppercase label font size in pixels. */
|
|
55
|
+
metricLabel: number;
|
|
56
|
+
/** KPI metric primary value font size in pixels. The delta and secondary
|
|
57
|
+
* values derive from this (each ~0.55x), so scaling this scales the whole cell. */
|
|
58
|
+
metricValue: number;
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
/** Font weight presets. */
|