@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-engine",
3
- "version": "7.2.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.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(11, 400, annotation.stroke ?? defaultStroke, isDark);
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 = {
@@ -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
  }
@@ -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
- // Visual constants. Sized to match the editorial KPI mock: a 10px uppercase
18
- // label sits above a 22px primary value, with breathing room below before the
19
- // chart area starts. Derived from the 4px grid.
20
- const LABEL_FONT_SIZE = 10;
21
- const VALUE_FONT_SIZE = 22;
22
- const LABEL_LINE_HEIGHT_RATIO = 1.4; // 14px
23
- const VALUE_LINE_HEIGHT_RATIO = 1.15; // ~25.3px
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
- * Always derived from the constants above; never hardcoded at the call site.
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 = LABEL_FONT_SIZE * LABEL_LINE_HEIGHT_RATIO;
43
- const valueLine = VALUE_FONT_SIZE * VALUE_LINE_HEIGHT_RATIO;
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, VALUE_FONT_SIZE, 510).width
82
- : estimateTextWidth(text, VALUE_FONT_SIZE, 510);
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 = LABEL_FONT_SIZE * LABEL_LINE_HEIGHT_RATIO;
87
- const labelY = metricsTopY + TOP_GAP + LABEL_FONT_SIZE; // baseline for uppercase label
88
- const valueY = metricsTopY + TOP_GAP + labelLine + INTER_ROW_GAP + VALUE_FONT_SIZE;
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
- LABEL_FONT_SIZE,
109
- VALUE_FONT_SIZE,
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,