@opendata-ai/openchart-engine 1.2.0 → 2.1.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.
@@ -218,6 +218,182 @@ describe('computeLineMarks', () => {
218
218
  });
219
219
  });
220
220
 
221
+ describe('x-axis sorting', () => {
222
+ it('sorts unsorted temporal data so points increase left-to-right', () => {
223
+ const spec: NormalizedChartSpec = {
224
+ ...makeSingleSeriesSpec(),
225
+ data: [
226
+ { date: '2022-01-01', value: 30 },
227
+ { date: '2020-01-01', value: 10 },
228
+ { date: '2021-01-01', value: 40 },
229
+ ],
230
+ };
231
+ const scales = computeScales(spec, chartArea, spec.data);
232
+ const marks = computeLineMarks(spec, scales, chartArea, fullStrategy);
233
+
234
+ const lineMark = marks.find((m): m is LineMark => m.type === 'line')!;
235
+ // Points should have monotonically increasing x pixel values
236
+ for (let i = 1; i < lineMark.points.length; i++) {
237
+ expect(lineMark.points[i].x).toBeGreaterThan(lineMark.points[i - 1].x);
238
+ }
239
+ });
240
+
241
+ it('sorts reverse-ordered dates correctly', () => {
242
+ const spec: NormalizedChartSpec = {
243
+ ...makeSingleSeriesSpec(),
244
+ data: [
245
+ { date: '2025-01-01', value: 50 },
246
+ { date: '2024-01-01', value: 40 },
247
+ { date: '2023-01-01', value: 30 },
248
+ { date: '2022-01-01', value: 20 },
249
+ { date: '2021-01-01', value: 10 },
250
+ ],
251
+ };
252
+ const scales = computeScales(spec, chartArea, spec.data);
253
+ const marks = computeLineMarks(spec, scales, chartArea, fullStrategy);
254
+
255
+ const lineMark = marks.find((m): m is LineMark => m.type === 'line')!;
256
+ expect(lineMark.points).toHaveLength(5);
257
+ for (let i = 1; i < lineMark.points.length; i++) {
258
+ expect(lineMark.points[i].x).toBeGreaterThan(lineMark.points[i - 1].x);
259
+ }
260
+ });
261
+
262
+ it('sorts unsorted numeric x-axis data', () => {
263
+ const spec: NormalizedChartSpec = {
264
+ ...makeSingleSeriesSpec(),
265
+ data: [
266
+ { date: 2022, value: 30 },
267
+ { date: 2020, value: 10 },
268
+ { date: 2021, value: 40 },
269
+ ],
270
+ encoding: {
271
+ x: { field: 'date', type: 'quantitative' },
272
+ y: { field: 'value', type: 'quantitative' },
273
+ },
274
+ };
275
+ const scales = computeScales(spec, chartArea, spec.data);
276
+ const marks = computeLineMarks(spec, scales, chartArea, fullStrategy);
277
+
278
+ const lineMark = marks.find((m): m is LineMark => m.type === 'line')!;
279
+ for (let i = 1; i < lineMark.points.length; i++) {
280
+ expect(lineMark.points[i].x).toBeGreaterThan(lineMark.points[i - 1].x);
281
+ }
282
+ });
283
+
284
+ it('sorts each series independently in multi-series', () => {
285
+ const spec: NormalizedChartSpec = {
286
+ ...makeMultiSeriesSpec(),
287
+ data: [
288
+ { date: '2022-01-01', value: 30, country: 'US' },
289
+ { date: '2020-01-01', value: 10, country: 'US' },
290
+ { date: '2021-01-01', value: 40, country: 'US' },
291
+ { date: '2022-01-01', value: 45, country: 'UK' },
292
+ { date: '2020-01-01', value: 15, country: 'UK' },
293
+ { date: '2021-01-01', value: 35, country: 'UK' },
294
+ ],
295
+ };
296
+ const scales = computeScales(spec, chartArea, spec.data);
297
+ const marks = computeLineMarks(spec, scales, chartArea, fullStrategy);
298
+
299
+ const lineMarks = marks.filter((m): m is LineMark => m.type === 'line');
300
+ for (const lm of lineMarks) {
301
+ for (let i = 1; i < lm.points.length; i++) {
302
+ expect(lm.points[i].x).toBeGreaterThan(lm.points[i - 1].x);
303
+ }
304
+ }
305
+ });
306
+
307
+ it('attaches data rows in sorted order on marks', () => {
308
+ const spec: NormalizedChartSpec = {
309
+ ...makeSingleSeriesSpec(),
310
+ data: [
311
+ { date: '2022-01-01', value: 30 },
312
+ { date: '2020-01-01', value: 10 },
313
+ { date: '2021-01-01', value: 40 },
314
+ ],
315
+ };
316
+ const scales = computeScales(spec, chartArea, spec.data);
317
+ const marks = computeLineMarks(spec, scales, chartArea, fullStrategy);
318
+
319
+ const lineMark = marks.find((m): m is LineMark => m.type === 'line')!;
320
+ // The data array on the mark should be chronologically ordered
321
+ const dates = lineMark.data!.map((r) => r.date);
322
+ expect(dates).toEqual(['2020-01-01', '2021-01-01', '2022-01-01']);
323
+ });
324
+
325
+ it('sorts data before handling null y-value line breaks', () => {
326
+ // Unsorted data with a null in the middle chronologically.
327
+ // After sorting: 2020 (10), 2021 (null), 2022 (30) -> line breaks at 2021
328
+ const spec: NormalizedChartSpec = {
329
+ ...makeSingleSeriesSpec(),
330
+ data: [
331
+ { date: '2022-01-01', value: 30 },
332
+ { date: '2021-01-01', value: null },
333
+ { date: '2020-01-01', value: 10 },
334
+ ],
335
+ };
336
+ const scales = computeScales(spec, chartArea, spec.data);
337
+ const marks = computeLineMarks(spec, scales, chartArea, fullStrategy);
338
+
339
+ const lineMark = marks.find((m): m is LineMark => m.type === 'line')!;
340
+ // Null is excluded, so only 2 valid points
341
+ expect(lineMark.points).toHaveLength(2);
342
+ // The two valid points should still be left-to-right
343
+ expect(lineMark.points[1].x).toBeGreaterThan(lineMark.points[0].x);
344
+ });
345
+
346
+ it('produces identical output for already-sorted data', () => {
347
+ // Verify sorting doesn't break pre-sorted input (regression check)
348
+ const sorted = makeSingleSeriesSpec(); // already chronological
349
+ const shuffled: NormalizedChartSpec = {
350
+ ...sorted,
351
+ data: [
352
+ { date: '2021-01-01', value: 40 },
353
+ { date: '2020-01-01', value: 10 },
354
+ { date: '2022-01-01', value: 30 },
355
+ ],
356
+ };
357
+
358
+ const sortedScales = computeScales(sorted, chartArea, sorted.data);
359
+ const sortedMarks = computeLineMarks(sorted, sortedScales, chartArea, fullStrategy);
360
+
361
+ const shuffledScales = computeScales(shuffled, chartArea, shuffled.data);
362
+ const shuffledMarks = computeLineMarks(shuffled, shuffledScales, chartArea, fullStrategy);
363
+
364
+ const sortedLine = sortedMarks.find((m): m is LineMark => m.type === 'line')!;
365
+ const shuffledLine = shuffledMarks.find((m): m is LineMark => m.type === 'line')!;
366
+
367
+ // Both should produce the same pixel positions
368
+ expect(sortedLine.points).toEqual(shuffledLine.points);
369
+ });
370
+
371
+ it('sorts within-year dates by month in multi-series', () => {
372
+ const spec: NormalizedChartSpec = {
373
+ ...makeMultiSeriesSpec(),
374
+ data: [
375
+ { date: '2020-12-01', value: 30, country: 'US' },
376
+ { date: '2020-03-01', value: 10, country: 'US' },
377
+ { date: '2020-07-01', value: 20, country: 'US' },
378
+ { date: '2020-12-01', value: 45, country: 'UK' },
379
+ { date: '2020-03-01', value: 15, country: 'UK' },
380
+ { date: '2020-07-01', value: 25, country: 'UK' },
381
+ ],
382
+ };
383
+ const scales = computeScales(spec, chartArea, spec.data);
384
+ const marks = computeLineMarks(spec, scales, chartArea, fullStrategy);
385
+
386
+ const lineMarks = marks.filter((m): m is LineMark => m.type === 'line');
387
+ expect(lineMarks).toHaveLength(2);
388
+
389
+ for (const lm of lineMarks) {
390
+ // Data rows should be Mar -> Jul -> Dec
391
+ const dates = lm.data!.map((r) => r.date);
392
+ expect(dates).toEqual(['2020-03-01', '2020-07-01', '2020-12-01']);
393
+ }
394
+ });
395
+ });
396
+
221
397
  describe('edge cases', () => {
222
398
  it('returns empty array when no x encoding', () => {
223
399
  const spec: NormalizedChartSpec = {
@@ -307,6 +483,122 @@ describe('computeAreaMarks', () => {
307
483
  expect(seriesKeys).toContain('UK');
308
484
  });
309
485
 
486
+ describe('x-axis sorting', () => {
487
+ it('sorts unsorted temporal data for single area', () => {
488
+ const spec: NormalizedChartSpec = {
489
+ ...makeSingleSeriesSpec(),
490
+ data: [
491
+ { date: '2022-01-01', value: 30 },
492
+ { date: '2020-01-01', value: 10 },
493
+ { date: '2021-01-01', value: 40 },
494
+ ],
495
+ };
496
+ const scales = computeScales(spec, chartArea, spec.data);
497
+ const marks = computeAreaMarks(spec, scales, chartArea);
498
+
499
+ expect(marks).toHaveLength(1);
500
+ for (let i = 1; i < marks[0].topPoints.length; i++) {
501
+ expect(marks[0].topPoints[i].x).toBeGreaterThan(marks[0].topPoints[i - 1].x);
502
+ }
503
+ });
504
+
505
+ it('sorts unsorted temporal data for stacked area', () => {
506
+ const spec: NormalizedChartSpec = {
507
+ ...makeMultiSeriesSpec(),
508
+ data: [
509
+ { date: '2022-01-01', value: 30, country: 'US' },
510
+ { date: '2020-01-01', value: 10, country: 'US' },
511
+ { date: '2021-01-01', value: 40, country: 'US' },
512
+ { date: '2022-01-01', value: 45, country: 'UK' },
513
+ { date: '2020-01-01', value: 15, country: 'UK' },
514
+ { date: '2021-01-01', value: 35, country: 'UK' },
515
+ ],
516
+ };
517
+ const scales = computeScales(spec, chartArea, spec.data);
518
+ const marks = computeAreaMarks(spec, scales, chartArea);
519
+
520
+ for (const mark of marks) {
521
+ for (let i = 1; i < mark.topPoints.length; i++) {
522
+ expect(mark.topPoints[i].x).toBeGreaterThan(mark.topPoints[i - 1].x);
523
+ }
524
+ }
525
+ });
526
+
527
+ it('attaches sorted data rows on single area marks', () => {
528
+ const spec: NormalizedChartSpec = {
529
+ ...makeSingleSeriesSpec(),
530
+ data: [
531
+ { date: '2022-01-01', value: 30 },
532
+ { date: '2020-01-01', value: 10 },
533
+ { date: '2021-01-01', value: 40 },
534
+ ],
535
+ };
536
+ const scales = computeScales(spec, chartArea, spec.data);
537
+ const marks = computeAreaMarks(spec, scales, chartArea);
538
+
539
+ const dates = marks[0].data!.map((r) => r.date);
540
+ expect(dates).toEqual(['2020-01-01', '2021-01-01', '2022-01-01']);
541
+ });
542
+
543
+ it('sorts stacked area with 3+ series and shuffled dates', () => {
544
+ const spec: NormalizedChartSpec = {
545
+ type: 'line',
546
+ data: [
547
+ { date: '2022-01-01', value: 30, region: 'A' },
548
+ { date: '2020-01-01', value: 10, region: 'A' },
549
+ { date: '2021-01-01', value: 20, region: 'A' },
550
+ { date: '2021-01-01', value: 25, region: 'B' },
551
+ { date: '2022-01-01', value: 35, region: 'B' },
552
+ { date: '2020-01-01', value: 15, region: 'B' },
553
+ { date: '2022-01-01', value: 40, region: 'C' },
554
+ { date: '2020-01-01', value: 5, region: 'C' },
555
+ { date: '2021-01-01', value: 30, region: 'C' },
556
+ ],
557
+ encoding: {
558
+ x: { field: 'date', type: 'temporal' },
559
+ y: { field: 'value', type: 'quantitative' },
560
+ color: { field: 'region', type: 'nominal' },
561
+ },
562
+ chrome: {},
563
+ annotations: [],
564
+ responsive: true,
565
+ theme: {},
566
+ darkMode: 'off',
567
+ labels: { density: 'auto', format: '' },
568
+ };
569
+ const scales = computeScales(spec, chartArea, spec.data);
570
+ const marks = computeAreaMarks(spec, scales, chartArea);
571
+
572
+ expect(marks).toHaveLength(3);
573
+ for (const mark of marks) {
574
+ for (let i = 1; i < mark.topPoints.length; i++) {
575
+ expect(mark.topPoints[i].x).toBeGreaterThan(mark.topPoints[i - 1].x);
576
+ }
577
+ }
578
+ });
579
+
580
+ it('produces identical output for pre-sorted and shuffled single area data', () => {
581
+ const preSorted = makeSingleSeriesSpec();
582
+ const shuffled: NormalizedChartSpec = {
583
+ ...preSorted,
584
+ data: [
585
+ { date: '2021-01-01', value: 40 },
586
+ { date: '2020-01-01', value: 10 },
587
+ { date: '2022-01-01', value: 30 },
588
+ ],
589
+ };
590
+
591
+ const preSortedScales = computeScales(preSorted, chartArea, preSorted.data);
592
+ const preSortedMarks = computeAreaMarks(preSorted, preSortedScales, chartArea);
593
+
594
+ const shuffledScales = computeScales(shuffled, chartArea, shuffled.data);
595
+ const shuffledMarks = computeAreaMarks(shuffled, shuffledScales, chartArea);
596
+
597
+ expect(preSortedMarks[0].topPoints).toEqual(shuffledMarks[0].topPoints);
598
+ expect(preSortedMarks[0].bottomPoints).toEqual(shuffledMarks[0].bottomPoints);
599
+ });
600
+ });
601
+
310
602
  it('stacked areas: each layer has different baselines', () => {
311
603
  const spec = makeMultiSeriesSpec();
312
604
  const scales = computeScales(spec, chartArea, spec.data);
@@ -320,6 +612,78 @@ describe('computeAreaMarks', () => {
320
612
  expect(firstBottom).not.toBe(secondBottom);
321
613
  }
322
614
  });
615
+
616
+ it('stacked areas: y-domain covers the stacked sum, not individual max', () => {
617
+ // Three series each with value 100 at the same x point. The stacked sum
618
+ // is 300, so the y-scale domain must go up to at least 300. Without the
619
+ // stacked domain fix, the domain only reaches 100 and the top layers clip.
620
+ const spec: NormalizedChartSpec = {
621
+ type: 'area',
622
+ data: [
623
+ { date: '2020-01-01', value: 100, group: 'A' },
624
+ { date: '2021-01-01', value: 100, group: 'A' },
625
+ { date: '2020-01-01', value: 100, group: 'B' },
626
+ { date: '2021-01-01', value: 100, group: 'B' },
627
+ { date: '2020-01-01', value: 100, group: 'C' },
628
+ { date: '2021-01-01', value: 100, group: 'C' },
629
+ ],
630
+ encoding: {
631
+ x: { field: 'date', type: 'temporal' },
632
+ y: { field: 'value', type: 'quantitative' },
633
+ color: { field: 'group', type: 'nominal' },
634
+ },
635
+ chrome: {},
636
+ annotations: [],
637
+ responsive: true,
638
+ theme: {},
639
+ darkMode: 'off',
640
+ labels: { density: 'auto', format: '' },
641
+ };
642
+ const scales = computeScales(spec, chartArea, spec.data);
643
+ const marks = computeAreaMarks(spec, scales, chartArea);
644
+
645
+ expect(marks).toHaveLength(3);
646
+
647
+ // The topmost layer's top points should be within the chart area, not
648
+ // clipped beyond it. With a proper stacked domain the y-scale covers
649
+ // 0..300 (niced), so all pixel positions stay within bounds.
650
+ const lastLayer = marks[marks.length - 1];
651
+ for (const pt of lastLayer.topPoints) {
652
+ expect(pt.y).toBeGreaterThanOrEqual(chartArea.y);
653
+ expect(pt.y).toBeLessThanOrEqual(chartArea.y + chartArea.height);
654
+ }
655
+ });
656
+
657
+ it('returns empty marks for unparseable temporal data', () => {
658
+ // Quarterly strings like '2022-Q1' are not valid Date strings. The engine
659
+ // should handle this gracefully (empty marks) rather than crashing or
660
+ // producing NaN-filled paths.
661
+ const spec: NormalizedChartSpec = {
662
+ type: 'area',
663
+ data: [
664
+ { quarter: '2022-Q1', revenue: 45, segment: 'Services' },
665
+ { quarter: '2022-Q2', revenue: 52, segment: 'Services' },
666
+ { quarter: '2022-Q1', revenue: 120, segment: 'Products' },
667
+ { quarter: '2022-Q2', revenue: 135, segment: 'Products' },
668
+ ],
669
+ encoding: {
670
+ x: { field: 'quarter', type: 'temporal' },
671
+ y: { field: 'revenue', type: 'quantitative' },
672
+ color: { field: 'segment', type: 'nominal' },
673
+ },
674
+ chrome: {},
675
+ annotations: [],
676
+ responsive: true,
677
+ theme: {},
678
+ darkMode: 'off',
679
+ labels: { density: 'auto', format: '' },
680
+ };
681
+ const scales = computeScales(spec, chartArea, spec.data);
682
+ const marks = computeAreaMarks(spec, scales, chartArea);
683
+
684
+ // Should produce empty marks since dates can't be parsed, not crash
685
+ expect(marks).toHaveLength(0);
686
+ });
323
687
  });
324
688
 
325
689
  // ---------------------------------------------------------------------------
@@ -12,7 +12,7 @@ import { area, curveMonotoneX, line, stack, stackOffsetNone, stackOrderNone } fr
12
12
 
13
13
  import type { NormalizedChartSpec } from '../../compiler/types';
14
14
  import type { ResolvedScales } from '../../layout/scales';
15
- import { getColor, scaleValue } from '../utils';
15
+ import { getColor, scaleValue, sortByField } from '../utils';
16
16
 
17
17
  // ---------------------------------------------------------------------------
18
18
  // Constants
@@ -64,10 +64,13 @@ function computeSingleArea(
64
64
  for (const [seriesKey, rows] of groups) {
65
65
  const color = getColor(scales, seriesKey);
66
66
 
67
+ // Sort rows by x-axis field so areas draw left-to-right
68
+ const sortedRows = sortByField(rows, xChannel.field);
69
+
67
70
  // Compute points, filtering out null values
68
71
  const validPoints: { x: number; yTop: number; yBottom: number; row: DataRow }[] = [];
69
72
 
70
- for (const row of rows) {
73
+ for (const row of sortedRows) {
71
74
  const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
72
75
  const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
73
76
 
@@ -147,6 +150,9 @@ function computeStackedArea(
147
150
  return computeSingleArea(spec, scales, chartArea);
148
151
  }
149
152
 
153
+ // Sort data by x field so stacked areas render left-to-right
154
+ const sortedData = sortByField(spec.data, xChannel.field);
155
+
150
156
  // Collect unique series keys and x values, and build a lookup from
151
157
  // (x-value, series-key) -> original data row so stacked area marks
152
158
  // get original rows instead of pivot rows.
@@ -155,7 +161,7 @@ function computeStackedArea(
155
161
  const rowsByXSeries = new Map<string, DataRow>();
156
162
  const rowsByX = new Map<string, DataRow[]>();
157
163
 
158
- for (const row of spec.data) {
164
+ for (const row of sortedData) {
159
165
  const xStr = String(row[xChannel.field]);
160
166
  const series = String(row[colorField]);
161
167
  seriesKeys.add(series);
@@ -20,7 +20,7 @@ import { curveMonotoneX, line } from 'd3-shape';
20
20
 
21
21
  import type { NormalizedChartSpec } from '../../compiler/types';
22
22
  import type { ResolvedScales } from '../../layout/scales';
23
- import { getColor, groupByField, scaleValue } from '../utils';
23
+ import { getColor, groupByField, scaleValue, sortByField } from '../utils';
24
24
 
25
25
  // ---------------------------------------------------------------------------
26
26
  // Constants
@@ -64,6 +64,9 @@ export function computeLineMarks(
64
64
  for (const [seriesKey, rows] of groups) {
65
65
  const color = getColor(scales, seriesKey);
66
66
 
67
+ // Sort rows by x-axis field so lines draw left-to-right
68
+ const sortedRows = sortByField(rows, xChannel.field);
69
+
67
70
  // Compute pixel positions for each data point, preserving nulls
68
71
  // for line break handling
69
72
  const pointsWithData: {
@@ -76,7 +79,7 @@ export function computeLineMarks(
76
79
  const segments: { x: number; y: number }[][] = [];
77
80
  let currentSegment: { x: number; y: number }[] = [];
78
81
 
79
- for (const row of rows) {
82
+ for (const row of sortedRows) {
80
83
  const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
81
84
  const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
82
85
 
@@ -80,6 +80,54 @@ export function groupByField(data: DataRow[], field: string | undefined): Map<st
80
80
  return groups;
81
81
  }
82
82
 
83
+ // ---------------------------------------------------------------------------
84
+ // Sorting
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /**
88
+ * Sort data rows by a field value in ascending order.
89
+ *
90
+ * Type-aware: numbers compared numerically, Date objects by timestamp,
91
+ * string-encoded numbers parsed and compared numerically, and everything
92
+ * else compared lexicographically (which also handles ISO date strings).
93
+ * Nulls are sorted last. Returns a new array (no mutation).
94
+ */
95
+ export function sortByField(data: DataRow[], field: string): DataRow[] {
96
+ if (data.length <= 1) return [...data];
97
+
98
+ return [...data].sort((a, b) => {
99
+ const aVal = a[field];
100
+ const bVal = b[field];
101
+
102
+ // Nulls last
103
+ if (aVal == null && bVal == null) return 0;
104
+ if (aVal == null) return 1;
105
+ if (bVal == null) return -1;
106
+
107
+ // Both numbers
108
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
109
+ return aVal - bVal;
110
+ }
111
+
112
+ // Both Dates
113
+ if (aVal instanceof Date && bVal instanceof Date) {
114
+ return aVal.getTime() - bVal.getTime();
115
+ }
116
+
117
+ // String values: try numeric parse, then lexicographic
118
+ const aStr = String(aVal);
119
+ const bStr = String(bVal);
120
+
121
+ const aNum = Number(aStr);
122
+ const bNum = Number(bStr);
123
+ if (Number.isFinite(aNum) && Number.isFinite(bNum)) {
124
+ return aNum - bNum;
125
+ }
126
+
127
+ return aStr.localeCompare(bStr);
128
+ });
129
+ }
130
+
83
131
  // ---------------------------------------------------------------------------
84
132
  // Color helpers
85
133
  // ---------------------------------------------------------------------------
@@ -56,12 +56,13 @@ function continuousTicks(resolvedScale: ResolvedScale, density: AxisLabelDensity
56
56
  function categoricalTicks(resolvedScale: ResolvedScale, density: AxisLabelDensity): AxisTick[] {
57
57
  const scale = resolvedScale.scale as D3CategoricalScale;
58
58
  const domain: string[] = scale.domain();
59
- const maxTicks = TICK_COUNTS[density];
59
+ const explicitTickCount = resolvedScale.channel.axis?.tickCount;
60
+ const maxTicks = explicitTickCount ?? TICK_COUNTS[density];
60
61
 
61
- // Band scales (bar charts) should always show all category labels.
62
- // Only thin point/ordinal scales used for continuous-like axes (e.g. line charts).
62
+ // Band scales (bar charts) show all category labels by default.
63
+ // Only thin when there's an explicit tickCount override or for point/ordinal scales.
63
64
  let selectedValues = domain;
64
- if (resolvedScale.type !== 'band' && domain.length > maxTicks) {
65
+ if ((resolvedScale.type !== 'band' || explicitTickCount) && domain.length > maxTicks) {
65
66
  const step = Math.ceil(domain.length / maxTicks);
66
67
  selectedValues = domain.filter((_: string, i: number) => i % step === 0);
67
68
  }
@@ -367,10 +367,15 @@ export function computeScales(
367
367
  }
368
368
 
369
369
  if (encoding.y) {
370
- // For stacked columns, the y-domain needs the max category sum, not max individual value.
371
- // Without this, stacked bars would clip above the chart area.
370
+ // For stacked columns and stacked areas, the y-domain needs the max category
371
+ // sum, not the max individual value. Without this, stacked marks would clip
372
+ // above the chart area.
372
373
  let yData = data;
373
- if (spec.type === 'column' && encoding.color && encoding.y.type === 'quantitative') {
374
+ if (
375
+ (spec.type === 'column' || spec.type === 'area') &&
376
+ encoding.color &&
377
+ encoding.y.type === 'quantitative'
378
+ ) {
374
379
  const xField = encoding.x?.field;
375
380
  const yField = encoding.y.field;
376
381
  if (xField) {