@opendata-ai/openchart-engine 2.12.1 → 2.13.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.
- package/dist/index.js +11 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/axes.test.ts +32 -0
- package/src/layout/axes.ts +27 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-engine",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
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": "2.
|
|
48
|
+
"@opendata-ai/openchart-core": "2.13.0",
|
|
49
49
|
"d3-array": "^3.2.0",
|
|
50
50
|
"d3-format": "^3.1.2",
|
|
51
51
|
"d3-interpolate": "^3.0.0",
|
|
@@ -455,6 +455,38 @@ describe('text-aware tick density', () => {
|
|
|
455
455
|
expect(axes.x!.ticks.length).toBe(categories.length);
|
|
456
456
|
});
|
|
457
457
|
|
|
458
|
+
it('gridlines survive tick thinning', () => {
|
|
459
|
+
// Force thinning by using a measureText that reports wide labels
|
|
460
|
+
const wideMeasure = () => ({ width: 200, height: 12 });
|
|
461
|
+
const scales = computeScales(lineSpec, chartArea, lineSpec.data);
|
|
462
|
+
const axes = computeAxes(scales, chartArea, fullStrategy, theme, wideMeasure);
|
|
463
|
+
|
|
464
|
+
// Ticks should be thinned (fewer labels) but gridlines should remain at
|
|
465
|
+
// all original tick positions
|
|
466
|
+
expect(axes.y!.gridlines.length).toBeGreaterThanOrEqual(axes.y!.ticks.length);
|
|
467
|
+
// With wide labels forcing thinning, gridlines should outnumber ticks
|
|
468
|
+
if (axes.y!.ticks.length < axes.y!.gridlines.length) {
|
|
469
|
+
// Gridlines retained positions that ticks lost — the fix is working
|
|
470
|
+
expect(axes.y!.gridlines.length).toBeGreaterThan(axes.y!.ticks.length);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('x-axis gridlines survive tick thinning when grid is enabled', () => {
|
|
475
|
+
const specWithGrid: NormalizedChartSpec = {
|
|
476
|
+
...lineSpec,
|
|
477
|
+
encoding: {
|
|
478
|
+
x: { field: 'date', type: 'temporal', axis: { grid: true } },
|
|
479
|
+
y: { field: 'value', type: 'quantitative' },
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const wideMeasure = () => ({ width: 200, height: 12 });
|
|
484
|
+
const scales = computeScales(specWithGrid, chartArea, specWithGrid.data);
|
|
485
|
+
const axes = computeAxes(scales, chartArea, fullStrategy, theme, wideMeasure);
|
|
486
|
+
|
|
487
|
+
expect(axes.x!.gridlines.length).toBeGreaterThanOrEqual(axes.x!.ticks.length);
|
|
488
|
+
});
|
|
489
|
+
|
|
458
490
|
it('passes measureText to auto-rotation detection', () => {
|
|
459
491
|
const barSpec: NormalizedChartSpec = {
|
|
460
492
|
...lineSpec,
|
package/src/layout/axes.ts
CHANGED
|
@@ -173,13 +173,7 @@ export function thinTicksUntilFit(
|
|
|
173
173
|
// ---------------------------------------------------------------------------
|
|
174
174
|
|
|
175
175
|
/** Generate ticks for a continuous scale (linear, time, log). */
|
|
176
|
-
function continuousTicks(
|
|
177
|
-
resolvedScale: ResolvedScale,
|
|
178
|
-
density: AxisLabelDensity,
|
|
179
|
-
fontSize: number,
|
|
180
|
-
fontWeight: number,
|
|
181
|
-
measureText?: MeasureTextFn,
|
|
182
|
-
): AxisTick[] {
|
|
176
|
+
function continuousTicks(resolvedScale: ResolvedScale, density: AxisLabelDensity): AxisTick[] {
|
|
183
177
|
const scale = resolvedScale.scale as D3ContinuousScale;
|
|
184
178
|
const explicitCount = resolvedScale.channel.axis?.tickCount;
|
|
185
179
|
const count = explicitCount ?? TICK_COUNTS[density];
|
|
@@ -191,20 +185,11 @@ function continuousTicks(
|
|
|
191
185
|
label: formatTickLabel(value, resolvedScale),
|
|
192
186
|
}));
|
|
193
187
|
|
|
194
|
-
|
|
195
|
-
if (explicitCount) return ticks;
|
|
196
|
-
|
|
197
|
-
return thinTicksUntilFit(ticks, fontSize, fontWeight, measureText);
|
|
188
|
+
return ticks;
|
|
198
189
|
}
|
|
199
190
|
|
|
200
191
|
/** Generate ticks for a band/point/ordinal scale. */
|
|
201
|
-
function categoricalTicks(
|
|
202
|
-
resolvedScale: ResolvedScale,
|
|
203
|
-
density: AxisLabelDensity,
|
|
204
|
-
fontSize: number,
|
|
205
|
-
fontWeight: number,
|
|
206
|
-
measureText?: MeasureTextFn,
|
|
207
|
-
): AxisTick[] {
|
|
192
|
+
function categoricalTicks(resolvedScale: ResolvedScale, density: AxisLabelDensity): AxisTick[] {
|
|
208
193
|
const scale = resolvedScale.scale as D3CategoricalScale;
|
|
209
194
|
const domain: string[] = scale.domain();
|
|
210
195
|
const explicitTickCount = resolvedScale.channel.axis?.tickCount;
|
|
@@ -232,11 +217,6 @@ function categoricalTicks(
|
|
|
232
217
|
};
|
|
233
218
|
});
|
|
234
219
|
|
|
235
|
-
// For non-band scales without explicit tickCount, thin based on label width
|
|
236
|
-
if (resolvedScale.type !== 'band' && !explicitTickCount) {
|
|
237
|
-
return thinTicksUntilFit(ticks, fontSize, fontWeight, measureText);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
220
|
return ticks;
|
|
241
221
|
}
|
|
242
222
|
|
|
@@ -328,16 +308,25 @@ export function computeAxes(
|
|
|
328
308
|
const { fontWeight } = tickLabelStyle;
|
|
329
309
|
|
|
330
310
|
if (scales.x) {
|
|
331
|
-
const
|
|
311
|
+
const allTicks =
|
|
332
312
|
scales.x.type === 'band' || scales.x.type === 'point' || scales.x.type === 'ordinal'
|
|
333
|
-
? categoricalTicks(scales.x, xDensity
|
|
334
|
-
: continuousTicks(scales.x, xDensity
|
|
313
|
+
? categoricalTicks(scales.x, xDensity)
|
|
314
|
+
: continuousTicks(scales.x, xDensity);
|
|
335
315
|
|
|
336
|
-
|
|
316
|
+
// Gridlines use the full tick set so they remain visible even when labels
|
|
317
|
+
// are thinned to prevent overlap.
|
|
318
|
+
const gridlines: Gridline[] = allTicks.map((t) => ({
|
|
337
319
|
position: t.position,
|
|
338
320
|
major: true,
|
|
339
321
|
}));
|
|
340
322
|
|
|
323
|
+
// Thin tick labels to prevent overlap (skip for band scales which use
|
|
324
|
+
// auto-rotation, and when the user set an explicit tickCount).
|
|
325
|
+
const shouldThin = scales.x.type !== 'band' && !scales.x.channel.axis?.tickCount;
|
|
326
|
+
const ticks = shouldThin
|
|
327
|
+
? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText)
|
|
328
|
+
: allTicks;
|
|
329
|
+
|
|
341
330
|
// Auto-rotate labels when band scale labels would overlap.
|
|
342
331
|
// Uses max label width (not average) since one long label is enough to overlap.
|
|
343
332
|
let tickAngle = scales.x.channel.axis?.tickAngle;
|
|
@@ -367,16 +356,23 @@ export function computeAxes(
|
|
|
367
356
|
}
|
|
368
357
|
|
|
369
358
|
if (scales.y) {
|
|
370
|
-
const
|
|
359
|
+
const allTicks =
|
|
371
360
|
scales.y.type === 'band' || scales.y.type === 'point' || scales.y.type === 'ordinal'
|
|
372
|
-
? categoricalTicks(scales.y, yDensity
|
|
373
|
-
: continuousTicks(scales.y, yDensity
|
|
361
|
+
? categoricalTicks(scales.y, yDensity)
|
|
362
|
+
: continuousTicks(scales.y, yDensity);
|
|
374
363
|
|
|
375
|
-
|
|
364
|
+
// Gridlines use the full tick set (label thinning shouldn't remove gridlines).
|
|
365
|
+
const gridlines: Gridline[] = allTicks.map((t) => ({
|
|
376
366
|
position: t.position,
|
|
377
367
|
major: true,
|
|
378
368
|
}));
|
|
379
369
|
|
|
370
|
+
// Thin tick labels to prevent overlap (skip for band scales and explicit tickCount).
|
|
371
|
+
const shouldThin = scales.y.type !== 'band' && !scales.y.channel.axis?.tickCount;
|
|
372
|
+
const ticks = shouldThin
|
|
373
|
+
? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText)
|
|
374
|
+
: allTicks;
|
|
375
|
+
|
|
380
376
|
result.y = {
|
|
381
377
|
ticks,
|
|
382
378
|
// Y-axis gridlines are shown by default (standard editorial practice)
|