@opendata-ai/openchart-engine 7.2.2 → 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.js +31 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +6 -0
- package/src/annotations/__tests__/compute.test.ts +19 -0
- package/src/annotations/resolve-refline.ts +6 -1
- package/src/layout/dimensions.ts +11 -3
- package/src/layout/metrics.ts +34 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-engine",
|
|
3
|
-
"version": "7.2.
|
|
3
|
+
"version": "7.2.3",
|
|
4
4
|
"description": "Headless compiler for openchart: spec validation, data compilation, scales, and layout",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Riley Hilliard",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"typecheck": "tsc --noEmit"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@opendata-ai/openchart-core": "7.2.
|
|
51
|
+
"@opendata-ai/openchart-core": "7.2.3",
|
|
52
52
|
"d3-array": "^3.2.0",
|
|
53
53
|
"d3-format": "^3.1.2",
|
|
54
54
|
"d3-interpolate": "^3.0.0",
|
|
@@ -443,6 +443,8 @@ exports[`compileChart snapshot (Step 7 oracle) > clipped-domain bar chart (data
|
|
|
443
443
|
"sizes": {
|
|
444
444
|
"axisTick": 11,
|
|
445
445
|
"body": 13,
|
|
446
|
+
"metricLabel": 10,
|
|
447
|
+
"metricValue": 22,
|
|
446
448
|
"small": 11,
|
|
447
449
|
"subtitle": 14,
|
|
448
450
|
"title": 26,
|
|
@@ -1419,6 +1421,8 @@ exports[`compileChart snapshot (Step 7 oracle) > legend-heavy multi-series line
|
|
|
1419
1421
|
"sizes": {
|
|
1420
1422
|
"axisTick": 11,
|
|
1421
1423
|
"body": 13,
|
|
1424
|
+
"metricLabel": 10,
|
|
1425
|
+
"metricValue": 22,
|
|
1422
1426
|
"small": 11,
|
|
1423
1427
|
"subtitle": 14,
|
|
1424
1428
|
"title": 26,
|
|
@@ -2010,6 +2014,8 @@ exports[`compileChart snapshot (Step 7 oracle) > watermarked column chart with g
|
|
|
2010
2014
|
"sizes": {
|
|
2011
2015
|
"axisTick": 11,
|
|
2012
2016
|
"body": 13,
|
|
2017
|
+
"metricLabel": 10,
|
|
2018
|
+
"metricValue": 22,
|
|
2013
2019
|
"small": 11,
|
|
2014
2020
|
"subtitle": 14,
|
|
2015
2021
|
"title": 26,
|
|
@@ -624,6 +624,25 @@ describe('computeAnnotations', () => {
|
|
|
624
624
|
expect(annotations[0].strokeDasharray).toBeDefined();
|
|
625
625
|
});
|
|
626
626
|
|
|
627
|
+
it('refline label defaults to fontSize 11', () => {
|
|
628
|
+
const spec = makeSpec([{ type: 'refline', x: '2020-06-01', label: 'Event' }]);
|
|
629
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
630
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
631
|
+
|
|
632
|
+
expect(annotations[0].label!.style.fontSize).toBe(11);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('refline label honors fontSize and fontWeight overrides', () => {
|
|
636
|
+
const spec = makeSpec([
|
|
637
|
+
{ type: 'refline', x: '2020-06-01', label: 'Event', fontSize: 24, fontWeight: 600 },
|
|
638
|
+
]);
|
|
639
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
640
|
+
const annotations = computeAnnotations(spec, scales, chartArea, fullStrategy);
|
|
641
|
+
|
|
642
|
+
expect(annotations[0].label!.style.fontSize).toBe(24);
|
|
643
|
+
expect(annotations[0].label!.style.fontWeight).toBe(600);
|
|
644
|
+
});
|
|
645
|
+
|
|
627
646
|
it('solid refline has no dasharray', () => {
|
|
628
647
|
const spec = makeSpec([{ type: 'refline', y: 20, style: 'solid' }]);
|
|
629
648
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
@@ -123,7 +123,12 @@ export function resolveRefLineAnnotation(
|
|
|
123
123
|
const labelDelta = applyOffset({ dx: baseDx, dy: baseDy }, annotation.labelOffset);
|
|
124
124
|
|
|
125
125
|
const defaultStroke = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
|
|
126
|
-
const style = makeAnnotationLabelStyle(
|
|
126
|
+
const style = makeAnnotationLabelStyle(
|
|
127
|
+
annotation.fontSize ?? 11,
|
|
128
|
+
annotation.fontWeight ?? 400,
|
|
129
|
+
annotation.stroke ?? defaultStroke,
|
|
130
|
+
isDark,
|
|
131
|
+
);
|
|
127
132
|
style.textAnchor = textAnchor;
|
|
128
133
|
|
|
129
134
|
label = {
|
package/src/layout/dimensions.ts
CHANGED
|
@@ -45,7 +45,12 @@ import type { NormalizedChartSpec, NormalizedChrome } from '../compiler/types';
|
|
|
45
45
|
import { predictEndpointLabelsWidth } from '../endpoint-labels/predict';
|
|
46
46
|
import { countColorSeries, resolveSuppression } from '../legend/suppression';
|
|
47
47
|
import { legendGap } from '../legend/wrap';
|
|
48
|
-
import { computeMetricBar, metricBarHeight } from './metrics';
|
|
48
|
+
import { computeMetricBar, type MetricFontSizes, metricBarHeight } from './metrics';
|
|
49
|
+
|
|
50
|
+
/** Pull the metric-row font sizes from the resolved theme. */
|
|
51
|
+
function metricFonts(theme: ResolvedTheme): MetricFontSizes {
|
|
52
|
+
return { label: theme.fonts.sizes.metricLabel, value: theme.fonts.sizes.metricValue };
|
|
53
|
+
}
|
|
49
54
|
|
|
50
55
|
// ---------------------------------------------------------------------------
|
|
51
56
|
// Types
|
|
@@ -372,7 +377,7 @@ export function computeDimensions(
|
|
|
372
377
|
// We reserve optimistically so the chart-area math is correct when the bar
|
|
373
378
|
// is kept; the rollback path subtracts it back when stripped.
|
|
374
379
|
const wantsMetrics = !!spec.metrics && spec.metrics.length > 0 && chromeMode !== 'hidden';
|
|
375
|
-
const tentativeMetricsHeight = wantsMetrics ? metricBarHeight() : 0;
|
|
380
|
+
const tentativeMetricsHeight = wantsMetrics ? metricBarHeight(metricFonts(theme)) : 0;
|
|
376
381
|
// topAxisGap sits between the legend (or chrome, if no legend) and the
|
|
377
382
|
// chart area. It accounts for the general axis margin plus any inline
|
|
378
383
|
// tick-label overhang. Placing it after the legend (below) keeps the
|
|
@@ -749,6 +754,7 @@ export function computeDimensions(
|
|
|
749
754
|
fallbackMetricsArea,
|
|
750
755
|
chartArea.height,
|
|
751
756
|
options.measureText,
|
|
757
|
+
theme,
|
|
752
758
|
)
|
|
753
759
|
: undefined;
|
|
754
760
|
if (wantsMetrics && !fallbackMetrics) {
|
|
@@ -790,7 +796,7 @@ export function computeDimensions(
|
|
|
790
796
|
const metricsTopY = topPad + chrome.topHeight;
|
|
791
797
|
const metricsArea = { x: hPad, width: Math.max(0, width - hPad * 2) };
|
|
792
798
|
const resolvedMetrics = wantsMetrics
|
|
793
|
-
? resolveMetrics(spec, metricsTopY, metricsArea, chartArea.height, options.measureText)
|
|
799
|
+
? resolveMetrics(spec, metricsTopY, metricsArea, chartArea.height, options.measureText, theme)
|
|
794
800
|
: undefined;
|
|
795
801
|
if (wantsMetrics && !resolvedMetrics) {
|
|
796
802
|
// See fallback path above for the clamp rationale.
|
|
@@ -824,6 +830,7 @@ function resolveMetrics(
|
|
|
824
830
|
metricsArea: { x: number; width: number },
|
|
825
831
|
remainingChartHeight: number,
|
|
826
832
|
measureText: import('@opendata-ai/openchart-core').MeasureTextFn | undefined,
|
|
833
|
+
theme: ResolvedTheme,
|
|
827
834
|
): ResolvedMetricBar | undefined {
|
|
828
835
|
return computeMetricBar(
|
|
829
836
|
spec.metrics,
|
|
@@ -831,5 +838,6 @@ function resolveMetrics(
|
|
|
831
838
|
metricsArea,
|
|
832
839
|
remainingChartHeight,
|
|
833
840
|
measureText,
|
|
841
|
+
metricFonts(theme),
|
|
834
842
|
);
|
|
835
843
|
}
|
package/src/layout/metrics.ts
CHANGED
|
@@ -14,19 +14,32 @@ import type {
|
|
|
14
14
|
} from '@opendata-ai/openchart-core';
|
|
15
15
|
import { estimateTextWidth } from '@opendata-ai/openchart-core';
|
|
16
16
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const LABEL_LINE_HEIGHT_RATIO = 1.4;
|
|
23
|
-
const VALUE_LINE_HEIGHT_RATIO = 1.15;
|
|
17
|
+
// Font sizes default to the editorial KPI mock (10px uppercase label above a
|
|
18
|
+
// 22px primary value) but are theme-driven so charts can scale the row. The
|
|
19
|
+
// label/value sizes flow in from `theme.fonts.sizes.metricLabel/metricValue`.
|
|
20
|
+
const DEFAULT_LABEL_FONT_SIZE = 10;
|
|
21
|
+
const DEFAULT_VALUE_FONT_SIZE = 22;
|
|
22
|
+
const LABEL_LINE_HEIGHT_RATIO = 1.4;
|
|
23
|
+
const VALUE_LINE_HEIGHT_RATIO = 1.15;
|
|
24
24
|
const INTER_ROW_GAP = 4;
|
|
25
25
|
// Breathing room above labels (separates metric row from the subtitle).
|
|
26
26
|
const TOP_GAP = 16;
|
|
27
27
|
// Breathing room below values (separates metric row from legend / chart top).
|
|
28
28
|
const BOTTOM_GAP = 20;
|
|
29
29
|
|
|
30
|
+
/** Font sizes the metric layout reserves space for. */
|
|
31
|
+
export interface MetricFontSizes {
|
|
32
|
+
/** Uppercase label size (px). */
|
|
33
|
+
label: number;
|
|
34
|
+
/** Primary value size (px). */
|
|
35
|
+
value: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DEFAULT_METRIC_FONT_SIZES: MetricFontSizes = {
|
|
39
|
+
label: DEFAULT_LABEL_FONT_SIZE,
|
|
40
|
+
value: DEFAULT_VALUE_FONT_SIZE,
|
|
41
|
+
};
|
|
42
|
+
|
|
30
43
|
/** Minimum container width that can fit a metric bar. */
|
|
31
44
|
const MIN_BAR_WIDTH = 480;
|
|
32
45
|
/** Minimum chart-area height after metric reservation. */
|
|
@@ -35,12 +48,12 @@ const MIN_CHART_HEIGHT = 150;
|
|
|
35
48
|
const CELL_INNER_PAD = 8;
|
|
36
49
|
|
|
37
50
|
/**
|
|
38
|
-
* Total height the metric bar reserves above the chart area.
|
|
39
|
-
*
|
|
51
|
+
* Total height the metric bar reserves above the chart area. Derived from the
|
|
52
|
+
* font sizes (theme-driven) plus the fixed gaps; never hardcoded at the call site.
|
|
40
53
|
*/
|
|
41
|
-
export function metricBarHeight(): number {
|
|
42
|
-
const labelLine =
|
|
43
|
-
const valueLine =
|
|
54
|
+
export function metricBarHeight(fonts: MetricFontSizes = DEFAULT_METRIC_FONT_SIZES): number {
|
|
55
|
+
const labelLine = fonts.label * LABEL_LINE_HEIGHT_RATIO;
|
|
56
|
+
const valueLine = fonts.value * VALUE_LINE_HEIGHT_RATIO;
|
|
44
57
|
return TOP_GAP + labelLine + INTER_ROW_GAP + valueLine + BOTTOM_GAP;
|
|
45
58
|
}
|
|
46
59
|
|
|
@@ -66,6 +79,7 @@ export function computeMetricBar(
|
|
|
66
79
|
metricsArea: { x: number; width: number },
|
|
67
80
|
remainingChartHeight: number,
|
|
68
81
|
measureText?: MeasureTextFn,
|
|
82
|
+
fonts: MetricFontSizes = DEFAULT_METRIC_FONT_SIZES,
|
|
69
83
|
): ResolvedMetricBar | undefined {
|
|
70
84
|
if (!metrics || metrics.length === 0) return undefined;
|
|
71
85
|
if (metricsArea.width < MIN_BAR_WIDTH) return undefined;
|
|
@@ -78,14 +92,14 @@ export function computeMetricBar(
|
|
|
78
92
|
for (const metric of metrics) {
|
|
79
93
|
const text = valueRunText(metric);
|
|
80
94
|
const measured = measureText
|
|
81
|
-
? measureText(text,
|
|
82
|
-
: estimateTextWidth(text,
|
|
95
|
+
? measureText(text, fonts.value, 510).width
|
|
96
|
+
: estimateTextWidth(text, fonts.value, 510);
|
|
83
97
|
if (measured > cellWidth - CELL_INNER_PAD) return undefined;
|
|
84
98
|
}
|
|
85
99
|
|
|
86
|
-
const labelLine =
|
|
87
|
-
const labelY = metricsTopY + TOP_GAP +
|
|
88
|
-
const valueY = metricsTopY + TOP_GAP + labelLine + INTER_ROW_GAP +
|
|
100
|
+
const labelLine = fonts.label * LABEL_LINE_HEIGHT_RATIO;
|
|
101
|
+
const labelY = metricsTopY + TOP_GAP + fonts.label; // baseline for uppercase label
|
|
102
|
+
const valueY = metricsTopY + TOP_GAP + labelLine + INTER_ROW_GAP + fonts.value;
|
|
89
103
|
|
|
90
104
|
const cells: ResolvedMetricCell[] = metrics.map((metric, i) => ({
|
|
91
105
|
x: metricsArea.x + i * cellWidth,
|
|
@@ -98,15 +112,15 @@ export function computeMetricBar(
|
|
|
98
112
|
|
|
99
113
|
return {
|
|
100
114
|
y: metricsTopY,
|
|
101
|
-
height: metricBarHeight(),
|
|
115
|
+
height: metricBarHeight(fonts),
|
|
102
116
|
cells,
|
|
103
117
|
};
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
// Exposed for tests and consumers needing the same constants.
|
|
107
121
|
export const METRIC_BAR_INTERNALS = {
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
DEFAULT_LABEL_FONT_SIZE,
|
|
123
|
+
DEFAULT_VALUE_FONT_SIZE,
|
|
110
124
|
LABEL_LINE_HEIGHT_RATIO,
|
|
111
125
|
VALUE_LINE_HEIGHT_RATIO,
|
|
112
126
|
INTER_ROW_GAP,
|