Skip to content

Commit 60f9a04

Browse files
committed
refactor(engine): extract density filter from bar/column/dot/pie labels
The 'none' and 'endpoints' density branches were byte-identical across four label modules (~3 lines each). Consolidate into `_shared/density-filter.ts` exposing `filterByDensity<T>(marks, density)`. Line chart labels are intentionally left alone: their 'none' returns a Map, and 'endpoints' semantically collapses to 'auto' (end-of-line label per series), so there's no density filter to extract. No behavior change: preserves the `marks.length > 1` guard for single-element arrays and short-circuits 'none' before candidate construction.
1 parent 67a5792 commit 60f9a04

6 files changed

Lines changed: 68 additions & 24 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { filterByDensity } from '../density-filter';
3+
4+
describe('filterByDensity', () => {
5+
const marks = ['a', 'b', 'c', 'd'];
6+
7+
it("returns [] for 'none'", () => {
8+
expect(filterByDensity(marks, 'none')).toEqual([]);
9+
});
10+
11+
it("returns first + last for 'endpoints'", () => {
12+
expect(filterByDensity(marks, 'endpoints')).toEqual(['a', 'd']);
13+
});
14+
15+
it("returns marks unchanged for 'all'", () => {
16+
expect(filterByDensity(marks, 'all')).toBe(marks);
17+
});
18+
19+
it("returns marks unchanged for 'auto'", () => {
20+
expect(filterByDensity(marks, 'auto')).toBe(marks);
21+
});
22+
23+
it("returns single-element array unchanged for 'endpoints'", () => {
24+
const single = ['only'];
25+
expect(filterByDensity(single, 'endpoints')).toBe(single);
26+
});
27+
28+
it("returns empty array unchanged for 'endpoints'", () => {
29+
const empty: string[] = [];
30+
expect(filterByDensity(empty, 'endpoints')).toBe(empty);
31+
});
32+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Shared density filter for data labels.
3+
*
4+
* Maps a `LabelDensity` setting to the subset of marks eligible for
5+
* labeling. The `'auto'` branch is a pass-through — auto resolution
6+
* happens upstream in the per-chart label modules (typically via
7+
* collision detection on the full candidate set).
8+
*/
9+
10+
import type { LabelDensity } from '@opendata-ai/openchart-core';
11+
12+
/**
13+
* Filter a mark array by label density.
14+
*
15+
* - `'none'` returns `[]` (no labels)
16+
* - `'endpoints'` returns first + last marks when `marks.length > 1`,
17+
* otherwise the input unchanged (preserves single-element arrays as-is)
18+
* - `'all'` and `'auto'` return the input unchanged
19+
*/
20+
export function filterByDensity<T>(marks: T[], density: LabelDensity): T[] {
21+
if (density === 'none') return [];
22+
if (density === 'endpoints' && marks.length > 1) {
23+
return [marks[0], marks[marks.length - 1]];
24+
}
25+
return marks;
26+
}

packages/engine/src/charts/bar/labels.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
getRepresentativeColor,
2424
resolveCollisions,
2525
} from '@opendata-ai/openchart-core';
26+
import { filterByDensity } from '../_shared/density-filter';
2627
import { formatLabelValue } from '../_shared/format-label-value';
2728

2829
// ---------------------------------------------------------------------------
@@ -93,12 +94,7 @@ export function computeBarLabels(
9394
labelPrefix?: string,
9495
valueField?: string,
9596
): ResolvedLabel[] {
96-
// 'none': no labels at all
97-
if (density === 'none') return [];
98-
99-
// Filter marks for 'endpoints' density
100-
const targetMarks =
101-
density === 'endpoints' && marks.length > 1 ? [marks[0], marks[marks.length - 1]] : marks;
97+
const targetMarks = filterByDensity(marks, density);
10298

10399
const candidates: LabelCandidate[] = [];
104100
// Track whether each candidate fits within its stacked segment.

packages/engine/src/charts/column/labels.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
getRepresentativeColor,
2424
resolveCollisions,
2525
} from '@opendata-ai/openchart-core';
26+
import { filterByDensity } from '../_shared/density-filter';
2627
import { formatLabelValue } from '../_shared/format-label-value';
2728

2829
// ---------------------------------------------------------------------------
@@ -50,12 +51,7 @@ export function computeColumnLabels(
5051
labelPrefix?: string,
5152
valueField?: string,
5253
): ResolvedLabel[] {
53-
// 'none': no labels at all
54-
if (density === 'none') return [];
55-
56-
// Filter marks for 'endpoints' density
57-
const targetMarks =
58-
density === 'endpoints' && marks.length > 1 ? [marks[0], marks[marks.length - 1]] : marks;
54+
const targetMarks = filterByDensity(marks, density);
5955

6056
const formatter = buildD3Formatter(labelFormat);
6157

packages/engine/src/charts/dot/labels.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
getRepresentativeColor,
2424
resolveCollisions,
2525
} from '@opendata-ai/openchart-core';
26+
import { filterByDensity } from '../_shared/density-filter';
2627
import { formatLabelValue } from '../_shared/format-label-value';
2728

2829
// ---------------------------------------------------------------------------
@@ -50,12 +51,7 @@ export function computeDotLabels(
5051
labelFormat?: string,
5152
valueField?: string,
5253
): ResolvedLabel[] {
53-
// 'none': no labels at all
54-
if (density === 'none') return [];
55-
56-
// Filter marks for 'endpoints' density
57-
const targetMarks =
58-
density === 'endpoints' && marks.length > 1 ? [marks[0], marks[marks.length - 1]] : marks;
54+
const targetMarks = filterByDensity(marks, density);
5955

6056
const formatter = buildD3Formatter(labelFormat);
6157
const candidates: LabelCandidate[] = [];

packages/engine/src/charts/pie/labels.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
ResolvedLabel,
2121
} from '@opendata-ai/openchart-core';
2222
import { estimateTextWidth, resolveCollisions } from '@opendata-ai/openchart-core';
23+
import { filterByDensity } from '../_shared/density-filter';
2324

2425
// ---------------------------------------------------------------------------
2526
// Constants
@@ -48,16 +49,13 @@ export function computePieLabels(
4849
): ResolvedLabel[] {
4950
if (marks.length === 0) return [];
5051

51-
// 'none': no labels at all
52-
if (density === 'none') return [];
53-
5452
// Get the pie center from the first mark's center property
53+
// (read before filtering — 'endpoints' still needs the original center)
5554
const centerX = marks[0].center.x;
5655
const centerY = marks[0].center.y;
5756

58-
// Filter marks for 'endpoints' density
59-
const targetMarks =
60-
density === 'endpoints' && marks.length > 1 ? [marks[0], marks[marks.length - 1]] : marks;
57+
const targetMarks = filterByDensity(marks, density);
58+
if (targetMarks.length === 0) return [];
6159

6260
const candidates: LabelCandidate[] = [];
6361
const targetMarkIndices: number[] = [];

0 commit comments

Comments
 (0)