@opendata-ai/openchart-engine 7.4.0 → 7.4.1

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.4.0",
3
+ "version": "7.4.1",
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.4.0",
51
+ "@opendata-ai/openchart-core": "7.4.1",
52
52
  "d3-array": "^3.2.0",
53
53
  "d3-format": "^3.1.2",
54
54
  "d3-interpolate": "^3.0.0",
@@ -548,4 +548,58 @@ describe('computeLegend', () => {
548
548
  expect(legend.rowHeight).toBeGreaterThan(0);
549
549
  });
550
550
  });
551
+
552
+ describe('rows missing the color field', () => {
553
+ // A layered spec flattens every layer's rows into `spec.data`, but the
554
+ // color encoding belongs to one layer. Rows from sibling layers (e.g. a
555
+ // diagonal reference-line) have no value for the color field and must not
556
+ // manufacture a phantom "undefined" legend entry.
557
+ it('ignores rows whose color field is undefined', () => {
558
+ const spec: NormalizedChartSpec = {
559
+ ...specWithColor,
560
+ data: [
561
+ { date: '2020', value: 10, country: 'US' },
562
+ { date: '2021', value: 20, country: 'UK' },
563
+ // rule-layer rows: no `country` field
564
+ { date: '2020', value: 0 },
565
+ { date: '2022', value: 40 },
566
+ ],
567
+ };
568
+ const legend = computeLegend(spec, fullStrategy, theme, chartArea);
569
+ expect(legend.entries.map((e) => e.label)).toEqual(['US', 'UK']);
570
+ });
571
+
572
+ it('keeps an explicit color domain authoritative despite phantom rows', () => {
573
+ const spec: NormalizedChartSpec = {
574
+ ...specWithColor,
575
+ data: [
576
+ { date: '2020', value: 10, tier: 'One F from takeover' },
577
+ { date: '2021', value: 20, tier: 'One year behind' },
578
+ { date: '2022', value: 30, tier: 'Other D/F campus' },
579
+ // diagonal reference-line layer, no `tier`
580
+ { date: '2020', value: 0 },
581
+ { date: '2022', value: 40 },
582
+ ],
583
+ encoding: {
584
+ x: { field: 'date', type: 'temporal' },
585
+ y: { field: 'value', type: 'quantitative' },
586
+ color: {
587
+ field: 'tier',
588
+ type: 'nominal',
589
+ scale: {
590
+ domain: ['One F from takeover', 'One year behind', 'Other D/F campus'],
591
+ range: ['#fb7185', '#f59e0b', '#64748b'],
592
+ },
593
+ },
594
+ },
595
+ };
596
+ const legend = computeLegend(spec, fullStrategy, theme, chartArea);
597
+ expect(legend.entries.map((e) => e.label)).toEqual([
598
+ 'One F from takeover',
599
+ 'One year behind',
600
+ 'Other D/F campus',
601
+ ]);
602
+ expect(legend.entries).toHaveLength(3);
603
+ });
604
+ });
551
605
  });
@@ -144,7 +144,15 @@ function extractColorEntries(spec: NormalizedChartSpec, theme: ResolvedTheme): L
144
144
  // Sequential (quantitative) color doesn't produce discrete legend entries
145
145
  if (colorEnc.type === 'quantitative') return [];
146
146
 
147
- const dataValues = [...new Set(spec.data.map((d) => String(d[colorEnc.field])))];
147
+ // Rows missing the color field (e.g. a sibling layer in a layered spec whose
148
+ // marks don't participate in the color encoding) must not manufacture a legend
149
+ // category. Without this guard, `String(undefined)` seeds a phantom "undefined"
150
+ // entry and — with an explicit domain — appends it past the authored entries.
151
+ const dataValues = [
152
+ ...new Set(
153
+ spec.data.filter((d) => d[colorEnc.field] != null).map((d) => String(d[colorEnc.field])),
154
+ ),
155
+ ];
148
156
  const explicitDomain = colorEnc.scale?.domain as string[] | undefined;
149
157
  const explicitRange = colorEnc.scale?.range as string[] | undefined;
150
158
  const palette = explicitRange ?? theme.colors.categorical;