@opendata-ai/openchart-engine 7.2.3 → 7.3.0

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.
Files changed (40) hide show
  1. package/dist/index.d.ts +84 -36
  2. package/dist/index.js +4013 -2872
  3. package/dist/index.js.map +1 -1
  4. package/package.json +2 -2
  5. package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +139 -63
  6. package/src/__tests__/axes.test.ts +67 -1
  7. package/src/__tests__/compile-chart.test.ts +6 -0
  8. package/src/__tests__/compile-layer.test.ts +49 -0
  9. package/src/__tests__/dimensions.test.ts +7 -2
  10. package/src/__tests__/helpers/invariants.ts +267 -0
  11. package/src/__tests__/known-bugs.test.ts +113 -0
  12. package/src/__tests__/layout-fuzz.test.ts +342 -0
  13. package/src/__tests__/layout-invariants.test.ts +533 -0
  14. package/src/__tests__/legend.test.ts +37 -0
  15. package/src/annotations/__tests__/compute.test.ts +8 -15
  16. package/src/annotations/__tests__/placement-fuzz.test.ts +256 -0
  17. package/src/annotations/__tests__/placement-invariants.test.ts +345 -0
  18. package/src/annotations/__tests__/placement.test.ts +316 -0
  19. package/src/annotations/collisions.ts +19 -7
  20. package/src/annotations/compute.ts +257 -16
  21. package/src/annotations/geometry.ts +73 -15
  22. package/src/annotations/placement.ts +588 -0
  23. package/src/annotations/resolve-text.ts +58 -10
  24. package/src/charts/__tests__/post-process.test.ts +3 -3
  25. package/src/charts/bar/__tests__/compute.test.ts +67 -0
  26. package/src/charts/bar/compute.ts +8 -1
  27. package/src/charts/post-process.ts +21 -7
  28. package/src/compile/layer.ts +31 -9
  29. package/src/compile.ts +85 -148
  30. package/src/index.ts +9 -0
  31. package/src/layout/__tests__/metrics.test.ts +97 -0
  32. package/src/layout/axes.ts +93 -1
  33. package/src/layout/dimensions.ts +80 -98
  34. package/src/layout/plan.ts +524 -0
  35. package/src/layout/shared.ts +44 -0
  36. package/src/legend/compute.ts +315 -78
  37. package/src/legend/wrap.ts +11 -7
  38. package/src/sankey/__tests__/node-label-wrap.test.ts +2 -2
  39. package/src/tables/__tests__/heatmap.test.ts +24 -20
  40. package/src/tables/heatmap.ts +25 -9
@@ -20,13 +20,14 @@ describe('computeHeatmapColors', () => {
20
20
  const theme = getTheme();
21
21
  const colors = computeHeatmapColors(data, column, theme, false);
22
22
 
23
- expect(colors.size).toBe(5);
23
+ // Domain-minimum row (value=0) is skipped so the normal row
24
+ // background shows through instead of an opaque low-end color.
25
+ expect(colors.size).toBe(4);
26
+ expect(colors.has(0)).toBe(false);
24
27
 
25
- // Lowest value should have a lighter background
26
- const lowBg = colors.get(0)!.backgroundColor!;
28
+ const lowBg = colors.get(1)!.backgroundColor!;
27
29
  const highBg = colors.get(4)!.backgroundColor!;
28
30
 
29
- // Check that they're different colors
30
31
  expect(lowBg).not.toBe(highBg);
31
32
  });
32
33
 
@@ -51,8 +52,8 @@ describe('computeHeatmapColors', () => {
51
52
  const theme = getTheme();
52
53
  const colors = computeHeatmapColors(data, col, theme, false);
53
54
 
54
- // All values are in the lower half of the domain, so should all have lighter colors
55
- expect(colors.size).toBe(5);
55
+ // value=0 sits at the domain minimum and is skipped
56
+ expect(colors.size).toBe(4);
56
57
  });
57
58
 
58
59
  it('values outside custom domain are clamped', () => {
@@ -63,8 +64,9 @@ describe('computeHeatmapColors', () => {
63
64
  const theme = getTheme();
64
65
  const colors = computeHeatmapColors(data, col, theme, false);
65
66
 
66
- // value=0 is clamped to domain min, value=100 to domain max
67
- expect(colors.has(0)).toBe(true);
67
+ // value=0 clamps to domain[0]=25, which is the minimum, so it's skipped
68
+ expect(colors.has(0)).toBe(false);
69
+ // value=100 clamps to domain max and gets colored
68
70
  expect(colors.has(4)).toBe(true);
69
71
  });
70
72
 
@@ -80,8 +82,9 @@ describe('computeHeatmapColors', () => {
80
82
  const theme = getTheme();
81
83
  const colors = computeHeatmapColors(dataWithLabel, col, theme, false);
82
84
 
83
- // Should have colors for both rows since score has numeric values
84
- expect(colors.size).toBe(2);
85
+ // score=10 is the domain minimum and is skipped; only score=90 gets colored
86
+ expect(colors.size).toBe(1);
87
+ expect(colors.has(1)).toBe(true);
85
88
  });
86
89
 
87
90
  it('returns empty map for non-numeric columns', () => {
@@ -102,8 +105,8 @@ describe('computeHeatmapColors', () => {
102
105
  const theme = getTheme(true);
103
106
  const colors = computeHeatmapColors(data, column, theme, true);
104
107
 
105
- expect(colors.size).toBe(5);
106
- // In dark mode, text colors should still have adequate contrast
108
+ // Domain-minimum row skipped
109
+ expect(colors.size).toBe(4);
107
110
  for (const [, style] of colors) {
108
111
  const bg = style.backgroundColor!;
109
112
  const fg = style.color!;
@@ -112,7 +115,7 @@ describe('computeHeatmapColors', () => {
112
115
  }
113
116
  });
114
117
 
115
- it('custom palette arrays are not adapted in dark mode', () => {
118
+ it('custom palette arrays are adapted for dark mode', () => {
116
119
  const col: ColumnConfig = {
117
120
  key: 'value',
118
121
  heatmap: { palette: ['#fca5a5', '#c44e52'], domain: [0, 100] },
@@ -122,14 +125,14 @@ describe('computeHeatmapColors', () => {
122
125
  const lightColors = computeHeatmapColors(data, col, lightTheme, false);
123
126
  const darkColors = computeHeatmapColors(data, col, darkTheme, true);
124
127
 
125
- const lightLowBg = lightColors.get(0)!.backgroundColor!;
128
+ // Domain-minimum row (value=0) is skipped in both modes
129
+ expect(lightColors.has(0)).toBe(false);
130
+ expect(darkColors.has(0)).toBe(false);
131
+
132
+ // The high-end color gets adapted for dark mode
126
133
  const lightHighBg = lightColors.get(4)!.backgroundColor!;
127
- const darkLowBg = darkColors.get(0)!.backgroundColor!;
128
134
  const darkHighBg = darkColors.get(4)!.backgroundColor!;
129
-
130
- // Custom palettes produce the same colors in both modes
131
- expect(darkLowBg).toBe(lightLowBg);
132
- expect(darkHighBg).toBe(lightHighBg);
135
+ expect(darkHighBg).not.toBe(lightHighBg);
133
136
  });
134
137
 
135
138
  it('supports array of color stops as palette', () => {
@@ -139,6 +142,7 @@ describe('computeHeatmapColors', () => {
139
142
  };
140
143
  const theme = getTheme();
141
144
  const colors = computeHeatmapColors(data, col, theme, false);
142
- expect(colors.size).toBe(5);
145
+ // Domain-minimum row skipped
146
+ expect(colors.size).toBe(4);
143
147
  });
144
148
  });
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { CellStyle, ColumnConfig, ResolvedTheme } from '@opendata-ai/openchart-core';
9
- import { adaptColorForDarkMode } from '@opendata-ai/openchart-core';
9
+ import { adaptColorForDarkMode, contrastRatio } from '@opendata-ai/openchart-core';
10
10
  import { interpolateRgb } from 'd3-interpolate';
11
11
  import { scaleSequential } from 'd3-scale';
12
12
  import { accessibleTextColor } from './utils';
@@ -96,22 +96,38 @@ export function computeHeatmapColors(
96
96
  }
97
97
 
98
98
  // Resolve palette and build scale.
99
- // Only adapt theme-derived palettes for dark mode. Custom color arrays
100
- // are used as-is since the author chose them for a reason, and
101
- // adaptColorForDarkMode inverts the perceived intensity ordering
102
- // (light pink -> dark red becomes dark red -> pink on dark backgrounds).
99
+ // For dark mode with custom palettes: replace near-white low-end stops
100
+ // with the dark background so the gradient blends into the table surface.
101
+ // Theme-named palettes use the standard adaptColorForDarkMode path.
103
102
  let stops = resolvePalette(config.palette, theme);
104
- if (darkMode && !Array.isArray(config.palette)) {
103
+ if (darkMode) {
105
104
  const lightBg = '#ffffff';
106
- const darkBg = theme.colors.background;
107
- stops = stops.map((c) => adaptColorForDarkMode(c, lightBg, darkBg));
105
+ const themeBg = theme.colors.background;
106
+ // Use an opaque dark surface for palette blending even when the theme
107
+ // bg is transparent, so interpolation never passes through alpha=0
108
+ // (which breaks accessibleTextColor via NaN contrast ratios).
109
+ const darkBg = themeBg === 'transparent' ? '#09090b' : themeBg;
110
+ if (Array.isArray(config.palette)) {
111
+ // Custom arrays: swap near-white stops for the dark bg so the low end
112
+ // blends in, then adapt the remaining (saturated) stops normally.
113
+ stops = stops.map((c) => {
114
+ const ratio = contrastRatio(c, lightBg);
115
+ if (ratio < 1.15) return darkBg;
116
+ return adaptColorForDarkMode(c, lightBg, darkBg);
117
+ });
118
+ } else {
119
+ stops = stops.map((c) => adaptColorForDarkMode(c, lightBg, darkBg));
120
+ }
108
121
  }
109
122
 
110
123
  const interpolator = interpolatorFromStops(stops);
111
124
  const scale = scaleSequential(interpolator).domain(domain).clamp(true);
112
125
 
113
- // Apply to each row
126
+ // Apply to each row. Skip cells at the domain minimum so the normal
127
+ // row background (striping) shows through instead of painting an
128
+ // opaque low-end color that clashes with the table chrome.
114
129
  for (const { index, value } of numericValues) {
130
+ if (value <= domain[0]) continue;
115
131
  const bg = scale(value);
116
132
  const textColor = accessibleTextColor(bg);
117
133