@opendata-ai/openchart-engine 6.19.2 → 6.19.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 +15 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/axes.test.ts +68 -0
- package/src/layout/axes.ts +21 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-engine",
|
|
3
|
-
"version": "6.19.
|
|
3
|
+
"version": "6.19.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",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typecheck": "tsc --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@opendata-ai/openchart-core": "6.19.
|
|
48
|
+
"@opendata-ai/openchart-core": "6.19.3",
|
|
49
49
|
"d3-array": "^3.2.0",
|
|
50
50
|
"d3-format": "^3.1.2",
|
|
51
51
|
"d3-interpolate": "^3.0.0",
|
|
@@ -655,3 +655,71 @@ describe('axis config properties', () => {
|
|
|
655
655
|
expect(axes.x!.labelFlush).toBeUndefined();
|
|
656
656
|
});
|
|
657
657
|
});
|
|
658
|
+
|
|
659
|
+
// ---------------------------------------------------------------------------
|
|
660
|
+
// Y-axis produces ~5 ticks at standard sizes
|
|
661
|
+
// ---------------------------------------------------------------------------
|
|
662
|
+
|
|
663
|
+
describe('y-axis tick density', () => {
|
|
664
|
+
it('produces 5+ y-axis ticks at standard chart size with full density', () => {
|
|
665
|
+
const scales = computeScales(lineSpec, chartArea, lineSpec.data);
|
|
666
|
+
const axes = computeAxes(scales, chartArea, fullStrategy, theme);
|
|
667
|
+
|
|
668
|
+
// A 500x300 chart with domain [100, 500] should show ~5+ ticks, not just 2
|
|
669
|
+
expect(axes.y!.ticks.length).toBeGreaterThanOrEqual(5);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('y-axis thinning uses vertical overlap, not horizontal text width', () => {
|
|
673
|
+
// Even with a measureText that reports very wide labels, y-axis should
|
|
674
|
+
// not thin aggressively because overlap is checked vertically
|
|
675
|
+
const wideMeasure = () => ({ width: 500, height: 12 });
|
|
676
|
+
const scales = computeScales(lineSpec, chartArea, lineSpec.data);
|
|
677
|
+
const axes = computeAxes(scales, chartArea, fullStrategy, theme, wideMeasure);
|
|
678
|
+
|
|
679
|
+
// Wide label text shouldn't cause y-axis thinning (only height matters)
|
|
680
|
+
expect(axes.y!.ticks.length).toBeGreaterThanOrEqual(5);
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// ---------------------------------------------------------------------------
|
|
685
|
+
// Vertical orientation overlap detection
|
|
686
|
+
// ---------------------------------------------------------------------------
|
|
687
|
+
|
|
688
|
+
describe('ticksOverlap with vertical orientation', () => {
|
|
689
|
+
const fontSize = 12;
|
|
690
|
+
const fontWeight = 400;
|
|
691
|
+
|
|
692
|
+
it('returns false when vertical ticks have sufficient spacing', () => {
|
|
693
|
+
// Labels at 30px intervals with 12px font (14.4px with lineHeight)
|
|
694
|
+
const ticks: AxisTick[] = [
|
|
695
|
+
{ value: 0, position: 0, label: '100' },
|
|
696
|
+
{ value: 1, position: 30, label: '200' },
|
|
697
|
+
{ value: 2, position: 60, label: '300' },
|
|
698
|
+
{ value: 3, position: 90, label: '400' },
|
|
699
|
+
{ value: 4, position: 120, label: '500' },
|
|
700
|
+
];
|
|
701
|
+
expect(ticksOverlap(ticks, fontSize, fontWeight, undefined, 'vertical')).toBe(false);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('returns true when vertical ticks are too close', () => {
|
|
705
|
+
// Labels at 10px intervals with 12px font - should overlap vertically
|
|
706
|
+
const ticks: AxisTick[] = [
|
|
707
|
+
{ value: 0, position: 0, label: '100' },
|
|
708
|
+
{ value: 1, position: 10, label: '200' },
|
|
709
|
+
{ value: 2, position: 20, label: '300' },
|
|
710
|
+
];
|
|
711
|
+
expect(ticksOverlap(ticks, fontSize, fontWeight, undefined, 'vertical')).toBe(true);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('ignores label text width for vertical orientation', () => {
|
|
715
|
+
// Very wide labels but well-spaced vertically - should NOT overlap
|
|
716
|
+
const ticks: AxisTick[] = [
|
|
717
|
+
{ value: 0, position: 0, label: 'Very Long Label Text Here' },
|
|
718
|
+
{ value: 1, position: 40, label: 'Another Very Long Label' },
|
|
719
|
+
{ value: 2, position: 80, label: 'Yet Another Long Label' },
|
|
720
|
+
];
|
|
721
|
+
// Horizontal would detect overlap, vertical should not
|
|
722
|
+
expect(ticksOverlap(ticks, fontSize, fontWeight, undefined, 'horizontal')).toBe(true);
|
|
723
|
+
expect(ticksOverlap(ticks, fontSize, fontWeight, undefined, 'vertical')).toBe(false);
|
|
724
|
+
});
|
|
725
|
+
});
|
package/src/layout/axes.ts
CHANGED
|
@@ -122,15 +122,31 @@ function measureLabel(
|
|
|
122
122
|
: estimateTextWidth(text, fontSize, fontWeight);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
/** Check whether any adjacent tick labels overlap
|
|
125
|
+
/** Check whether any adjacent tick labels overlap along the axis direction. */
|
|
126
126
|
export function ticksOverlap(
|
|
127
127
|
ticks: AxisTick[],
|
|
128
128
|
fontSize: number,
|
|
129
129
|
fontWeight: number,
|
|
130
130
|
measureText?: MeasureTextFn,
|
|
131
|
+
orientation: 'horizontal' | 'vertical' = 'horizontal',
|
|
131
132
|
): boolean {
|
|
132
133
|
if (ticks.length < 2) return false;
|
|
133
134
|
const minGap = fontSize * MIN_TICK_GAP_FACTOR;
|
|
135
|
+
|
|
136
|
+
if (orientation === 'vertical') {
|
|
137
|
+
// Y-axis: labels are stacked vertically. Check if vertical extent
|
|
138
|
+
// (based on font height) overlaps between adjacent ticks.
|
|
139
|
+
// Positions decrease going up in SVG coords, so sort ascending.
|
|
140
|
+
const sorted = [...ticks].sort((a, b) => a.position - b.position);
|
|
141
|
+
const labelHeight = fontSize * 1.2; // lineHeight
|
|
142
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
143
|
+
const aBottom = sorted[i].position + labelHeight / 2;
|
|
144
|
+
const bTop = sorted[i + 1].position - labelHeight / 2;
|
|
145
|
+
if (aBottom + minGap > bTop) return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
134
150
|
for (let i = 0; i < ticks.length - 1; i++) {
|
|
135
151
|
const aWidth = measureLabel(ticks[i].label, fontSize, fontWeight, measureText);
|
|
136
152
|
const bWidth = measureLabel(ticks[i + 1].label, fontSize, fontWeight, measureText);
|
|
@@ -151,8 +167,9 @@ export function thinTicksUntilFit(
|
|
|
151
167
|
fontSize: number,
|
|
152
168
|
fontWeight: number,
|
|
153
169
|
measureText?: MeasureTextFn,
|
|
170
|
+
orientation: 'horizontal' | 'vertical' = 'horizontal',
|
|
154
171
|
): AxisTick[] {
|
|
155
|
-
if (!ticksOverlap(ticks, fontSize, fontWeight, measureText)) return ticks;
|
|
172
|
+
if (!ticksOverlap(ticks, fontSize, fontWeight, measureText, orientation)) return ticks;
|
|
156
173
|
|
|
157
174
|
let current = ticks;
|
|
158
175
|
while (current.length > MIN_TICK_COUNT) {
|
|
@@ -164,7 +181,7 @@ export function thinTicksUntilFit(
|
|
|
164
181
|
if (current.length > 1) thinned.push(current[current.length - 1]);
|
|
165
182
|
current = thinned;
|
|
166
183
|
|
|
167
|
-
if (!ticksOverlap(current, fontSize, fontWeight, measureText)) break;
|
|
184
|
+
if (!ticksOverlap(current, fontSize, fontWeight, measureText, orientation)) break;
|
|
168
185
|
}
|
|
169
186
|
return current;
|
|
170
187
|
}
|
|
@@ -455,7 +472,7 @@ export function computeAxes(
|
|
|
455
472
|
// Thin tick labels to prevent overlap (skip for band scales, explicit tickCount, and values).
|
|
456
473
|
const shouldThin = scales.y.type !== 'band' && !axisConfig?.tickCount && !axisConfig?.values;
|
|
457
474
|
const ticks = shouldThin
|
|
458
|
-
? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText)
|
|
475
|
+
? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText, 'vertical')
|
|
459
476
|
: allTicks;
|
|
460
477
|
|
|
461
478
|
// Gridlines match the tick set so every gridline has a label.
|