@opendata-ai/openchart-engine 6.12.0 → 6.15.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 +1022 -648
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/axes.test.ts +12 -30
- package/src/__tests__/compile-chart.test.ts +4 -4
- package/src/__tests__/dimensions.test.ts +2 -2
- package/src/__tests__/encoding-sugar.test.ts +390 -0
- package/src/annotations/collisions.ts +268 -0
- package/src/annotations/compute.ts +9 -912
- package/src/annotations/constants.ts +32 -0
- package/src/annotations/geometry.ts +167 -0
- package/src/annotations/position.ts +95 -0
- package/src/annotations/resolve-range.ts +98 -0
- package/src/annotations/resolve-refline.ts +148 -0
- package/src/annotations/resolve-text.ts +134 -0
- package/src/charts/__tests__/post-process.test.ts +258 -0
- package/src/charts/bar/__tests__/labels.test.ts +31 -0
- package/src/charts/bar/compute.ts +27 -6
- package/src/charts/bar/index.ts +3 -0
- package/src/charts/bar/labels.ts +38 -14
- package/src/charts/column/__tests__/compute.test.ts +99 -0
- package/src/charts/column/compute.ts +27 -6
- package/src/charts/column/index.ts +3 -0
- package/src/charts/column/labels.ts +35 -13
- package/src/charts/dot/index.ts +10 -1
- package/src/charts/dot/labels.ts +37 -6
- package/src/charts/line/area.ts +31 -6
- package/src/charts/line/compute.ts +7 -2
- package/src/charts/line/index.ts +33 -2
- package/src/charts/post-process.ts +215 -0
- package/src/compile.ts +91 -158
- package/src/compiler/normalize.ts +2 -2
- package/src/layout/axes.ts +12 -15
- package/src/layout/dimensions.ts +3 -3
- package/src/layout/scales.ts +116 -36
- package/src/legend/compute.ts +2 -4
- package/src/tooltips/__tests__/compute.test.ts +188 -0
- package/src/tooltips/compute.ts +54 -12
- package/src/transforms/__tests__/aggregate.test.ts +159 -0
- package/src/transforms/__tests__/fold.test.ts +79 -0
- package/src/transforms/aggregate.ts +130 -0
- package/src/transforms/fold.ts +49 -0
- package/src/transforms/index.ts +8 -0
package/src/compile.ts
CHANGED
|
@@ -13,18 +13,22 @@
|
|
|
13
13
|
|
|
14
14
|
import type {
|
|
15
15
|
AnimationSpec,
|
|
16
|
+
BinParams,
|
|
17
|
+
BinTransform,
|
|
16
18
|
ChartLayout,
|
|
17
19
|
ChartSpec,
|
|
18
20
|
CompileOptions,
|
|
19
21
|
CompileTableOptions,
|
|
22
|
+
EncodingChannel,
|
|
20
23
|
LayerSpec,
|
|
21
24
|
Mark,
|
|
22
|
-
PointMark,
|
|
23
25
|
Rect,
|
|
24
|
-
RectMark,
|
|
25
26
|
ResolvedAnnotation,
|
|
26
27
|
ResolvedTheme,
|
|
27
28
|
TableLayout,
|
|
29
|
+
TimeUnit,
|
|
30
|
+
TimeUnitTransform,
|
|
31
|
+
Transform,
|
|
28
32
|
} from '@opendata-ai/openchart-core';
|
|
29
33
|
import {
|
|
30
34
|
adaptTheme,
|
|
@@ -43,6 +47,11 @@ import { columnRenderer } from './charts/column';
|
|
|
43
47
|
import { dotRenderer } from './charts/dot';
|
|
44
48
|
import { areaRenderer, lineRenderer } from './charts/line';
|
|
45
49
|
import { donutRenderer, pieRenderer } from './charts/pie';
|
|
50
|
+
import {
|
|
51
|
+
assignAnimationIndices,
|
|
52
|
+
computeMarkObstacles,
|
|
53
|
+
resolveRendererKey,
|
|
54
|
+
} from './charts/post-process';
|
|
46
55
|
import { type ChartRenderer, getChartRenderer, registerChartRenderer } from './charts/registry';
|
|
47
56
|
import { ruleRenderer } from './charts/rule';
|
|
48
57
|
import { scatterRenderer } from './charts/scatter';
|
|
@@ -90,7 +99,7 @@ import type { GraphCompilation } from './graphs/types';
|
|
|
90
99
|
import { computeAxes } from './layout/axes';
|
|
91
100
|
import { computeDimensions } from './layout/dimensions';
|
|
92
101
|
import { computeGridlines } from './layout/gridlines';
|
|
93
|
-
import { computeScales
|
|
102
|
+
import { computeScales } from './layout/scales';
|
|
94
103
|
import { computeLegend } from './legend/compute';
|
|
95
104
|
import { compileSankey as compileSankeyImpl } from './sankey/compile-sankey';
|
|
96
105
|
import { compileTableLayout } from './tables/compile-table';
|
|
@@ -98,93 +107,77 @@ import { computeTooltipDescriptors } from './tooltips/compute';
|
|
|
98
107
|
import { runTransforms } from './transforms';
|
|
99
108
|
|
|
100
109
|
// ---------------------------------------------------------------------------
|
|
101
|
-
//
|
|
110
|
+
// Encoding sugar expansion (bin, timeUnit on encoding channels)
|
|
102
111
|
// ---------------------------------------------------------------------------
|
|
103
112
|
|
|
104
113
|
/**
|
|
105
|
-
*
|
|
114
|
+
* Expand encoding-level `bin` and `timeUnit` shorthand into explicit transforms.
|
|
106
115
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
116
|
+
* Vega-Lite allows `encoding.x.bin: true` as sugar for a BinTransform.
|
|
117
|
+
* This function detects those shorthands, generates the corresponding transforms,
|
|
118
|
+
* updates encoding field references to the output field names, and prepends the
|
|
119
|
+
* transforms to the spec's transform array.
|
|
109
120
|
*
|
|
110
|
-
*
|
|
111
|
-
* annotations avoid overlapping any visible data mark.
|
|
121
|
+
* Mutates nothing; returns a new spec object (shallow copy).
|
|
112
122
|
*/
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
export function expandEncodingSugar(spec: Record<string, unknown>): Record<string, unknown> {
|
|
124
|
+
const encoding = spec.encoding as Record<string, EncodingChannel | undefined> | undefined;
|
|
125
|
+
if (!encoding) return spec;
|
|
126
|
+
|
|
127
|
+
const generatedTransforms: Transform[] = [];
|
|
128
|
+
const updatedEncoding = { ...encoding };
|
|
129
|
+
let changed = false;
|
|
130
|
+
|
|
131
|
+
for (const channel of Object.keys(encoding)) {
|
|
132
|
+
const ch = encoding[channel];
|
|
133
|
+
if (!ch || !ch.field) continue;
|
|
134
|
+
|
|
135
|
+
// Expand bin shorthand
|
|
136
|
+
if (ch.bin != null && ch.bin !== false) {
|
|
137
|
+
const field = ch.field;
|
|
138
|
+
const outputField = `bin_${field}`;
|
|
139
|
+
const binTransform: BinTransform = {
|
|
140
|
+
bin: ch.bin === true ? true : (ch.bin as BinParams),
|
|
141
|
+
field,
|
|
142
|
+
as: outputField,
|
|
143
|
+
};
|
|
144
|
+
generatedTransforms.push(binTransform);
|
|
118
145
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const rm = mark as RectMark;
|
|
124
|
-
obstacles.push({ x: rm.x, y: rm.y, width: rm.width, height: rm.height });
|
|
125
|
-
} else if (mark.type === 'point') {
|
|
126
|
-
const pm = mark as PointMark;
|
|
127
|
-
obstacles.push({
|
|
128
|
-
x: pm.cx - pm.r,
|
|
129
|
-
y: pm.cy - pm.r,
|
|
130
|
-
width: pm.r * 2,
|
|
131
|
-
height: pm.r * 2,
|
|
132
|
-
});
|
|
146
|
+
// Update encoding to reference binned output field, remove bin property
|
|
147
|
+
const { bin: _bin, ...rest } = ch;
|
|
148
|
+
updatedEncoding[channel] = { ...rest, field: outputField } as EncodingChannel;
|
|
149
|
+
changed = true;
|
|
133
150
|
}
|
|
134
|
-
}
|
|
135
|
-
return obstacles;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Group band-scale marks by row, returning one obstacle per band. */
|
|
139
|
-
function computeBandRowObstacles(marks: Mark[], scales: ResolvedScales): Rect[] {
|
|
140
|
-
const rows = new Map<number, { minX: number; maxX: number; bandY: number }>();
|
|
141
151
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
cy = rm.y + rm.height / 2;
|
|
155
|
-
left = rm.x;
|
|
156
|
-
right = rm.x + rm.width;
|
|
157
|
-
} else {
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
152
|
+
// Expand timeUnit shorthand (read from updated encoding in case bin already ran)
|
|
153
|
+
const current = updatedEncoding[channel] ?? ch;
|
|
154
|
+
if (current.timeUnit) {
|
|
155
|
+
const field = current.field;
|
|
156
|
+
const unit = current.timeUnit as TimeUnit;
|
|
157
|
+
const outputField = `${unit}_${field}`;
|
|
158
|
+
const timeUnitTransform: TimeUnitTransform = {
|
|
159
|
+
timeUnit: unit,
|
|
160
|
+
field,
|
|
161
|
+
as: outputField,
|
|
162
|
+
};
|
|
163
|
+
generatedTransforms.push(timeUnitTransform);
|
|
160
164
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
existing.minX = Math.min(existing.minX, left);
|
|
166
|
-
existing.maxX = Math.max(existing.maxX, right);
|
|
167
|
-
} else {
|
|
168
|
-
rows.set(key, { minX: left, maxX: right, bandY: cy });
|
|
165
|
+
// Update encoding to reference timeUnit output field, remove timeUnit property
|
|
166
|
+
const { timeUnit: _tu, ...rest } = current;
|
|
167
|
+
updatedEncoding[channel] = { ...rest, field: outputField } as EncodingChannel;
|
|
168
|
+
changed = true;
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
const bandScale = scales.y!.scale as { bandwidth?: () => number };
|
|
174
|
-
const bandwidth = bandScale.bandwidth?.() ?? 0;
|
|
175
|
-
if (bandwidth === 0) return [];
|
|
172
|
+
if (!changed) return spec;
|
|
176
173
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return obstacles;
|
|
174
|
+
// Prepend generated transforms before any user-defined transforms
|
|
175
|
+
const existingTransforms = (spec.transform as Transform[] | undefined) ?? [];
|
|
176
|
+
return {
|
|
177
|
+
...spec,
|
|
178
|
+
encoding: updatedEncoding,
|
|
179
|
+
transform: [...generatedTransforms, ...existingTransforms],
|
|
180
|
+
};
|
|
188
181
|
}
|
|
189
182
|
|
|
190
183
|
// ---------------------------------------------------------------------------
|
|
@@ -204,8 +197,15 @@ function computeBandRowObstacles(marks: Mark[], scales: ResolvedScales): Rect[]
|
|
|
204
197
|
* @throws Error if spec is invalid or not a chart type.
|
|
205
198
|
*/
|
|
206
199
|
export function compileChart(spec: unknown, options: CompileOptions): ChartLayout {
|
|
200
|
+
// Expand encoding-level bin/timeUnit sugar before validation + normalization.
|
|
201
|
+
// This converts shorthand (e.g. encoding.x.bin: true) into explicit transforms.
|
|
202
|
+
const expandedSpec =
|
|
203
|
+
spec && typeof spec === 'object' && !Array.isArray(spec)
|
|
204
|
+
? expandEncodingSugar(spec as Record<string, unknown>)
|
|
205
|
+
: spec;
|
|
206
|
+
|
|
207
207
|
// Validate + normalize
|
|
208
|
-
const { spec: normalized } = compileSpec(
|
|
208
|
+
const { spec: normalized } = compileSpec(expandedSpec);
|
|
209
209
|
|
|
210
210
|
if ('type' in normalized && (normalized as unknown as Record<string, unknown>).type === 'table') {
|
|
211
211
|
throw new Error('compileChart received a table spec. Use compileTable instead.');
|
|
@@ -223,13 +223,14 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
223
223
|
let chartSpec = normalized as NormalizedChartSpec;
|
|
224
224
|
|
|
225
225
|
// Resolve watermark: explicit spec value wins, then options fallback, then default true.
|
|
226
|
-
const rawWatermark = (
|
|
226
|
+
const rawWatermark = (expandedSpec as Record<string, unknown>).watermark;
|
|
227
227
|
const watermark = rawWatermark !== undefined ? chartSpec.watermark : (options.watermark ?? true);
|
|
228
228
|
|
|
229
229
|
// Run data transforms (filter, bin, calculate, timeUnit) before any other data processing.
|
|
230
|
-
// Transforms are defined on the
|
|
231
|
-
//
|
|
232
|
-
|
|
230
|
+
// Transforms are defined on the expanded spec (which includes any auto-generated
|
|
231
|
+
// transforms from encoding-level bin/timeUnit sugar), not the normalized spec,
|
|
232
|
+
// since NormalizedChartSpec doesn't carry the transform field.
|
|
233
|
+
const rawTransforms = (expandedSpec as Record<string, unknown>).transform as
|
|
233
234
|
| import('@opendata-ai/openchart-core').Transform[]
|
|
234
235
|
| undefined;
|
|
235
236
|
if (rawTransforms && rawTransforms.length > 0) {
|
|
@@ -241,8 +242,8 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
241
242
|
const heightClass = getHeightClass(options.height);
|
|
242
243
|
const strategy = getLayoutStrategy(breakpoint, heightClass);
|
|
243
244
|
|
|
244
|
-
// Apply breakpoint-conditional overrides from the
|
|
245
|
-
const rawSpec =
|
|
245
|
+
// Apply breakpoint-conditional overrides from the expanded spec
|
|
246
|
+
const rawSpec = expandedSpec as Record<string, unknown>;
|
|
246
247
|
const overrides = rawSpec.overrides as
|
|
247
248
|
| Partial<
|
|
248
249
|
Record<
|
|
@@ -408,26 +409,11 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
408
409
|
}
|
|
409
410
|
|
|
410
411
|
// Get chart renderer and compute marks (using filtered data).
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
// If x is categorical and y is quantitative, vertical (old 'column')
|
|
417
|
-
const xType = renderSpec.encoding.x?.type;
|
|
418
|
-
const yType = renderSpec.encoding.y?.type;
|
|
419
|
-
const isVertical =
|
|
420
|
-
(xType === 'nominal' || xType === 'ordinal' || xType === 'temporal') &&
|
|
421
|
-
yType === 'quantitative';
|
|
422
|
-
if (isVertical) {
|
|
423
|
-
rendererKey = 'bar:vertical';
|
|
424
|
-
}
|
|
425
|
-
} else if (rendererKey === 'arc') {
|
|
426
|
-
const innerRadius = renderSpec.markDef.innerRadius;
|
|
427
|
-
if (innerRadius && innerRadius > 0) {
|
|
428
|
-
rendererKey = 'arc:donut';
|
|
429
|
-
}
|
|
430
|
-
}
|
|
412
|
+
const rendererKey = resolveRendererKey(
|
|
413
|
+
renderSpec.markType,
|
|
414
|
+
renderSpec.encoding,
|
|
415
|
+
renderSpec.markDef,
|
|
416
|
+
);
|
|
431
417
|
const renderer = getChartRenderer(rendererKey);
|
|
432
418
|
const marks: Mark[] = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme) : [];
|
|
433
419
|
|
|
@@ -491,44 +477,7 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
491
477
|
);
|
|
492
478
|
|
|
493
479
|
// Assign animationIndex for stagger ordering when animation is enabled
|
|
494
|
-
|
|
495
|
-
// since they get group-based indices below (avoids wasted work that gets overwritten).
|
|
496
|
-
if (resolvedAnimation?.enabled && resolvedAnimation.staggerOrder === 'value') {
|
|
497
|
-
const indexed = marks.map((m, i) => ({ mark: m, idx: i }));
|
|
498
|
-
indexed.sort((a, b) => {
|
|
499
|
-
const av = getMarkPrimaryValue(a.mark);
|
|
500
|
-
const bv = getMarkPrimaryValue(b.mark);
|
|
501
|
-
return av - bv;
|
|
502
|
-
});
|
|
503
|
-
for (let i = 0; i < indexed.length; i++) {
|
|
504
|
-
const m = indexed[i].mark;
|
|
505
|
-
if (m.type === 'rect' && (m as RectMark).stackGroup) continue;
|
|
506
|
-
m.animationIndex = i;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// For stacked bars/columns, assign the same animationIndex to all segments
|
|
511
|
-
// sharing a stackGroup so they animate as one contiguous bar per category.
|
|
512
|
-
// Also compute stackPos (segment position within each group: 0, 1, 2...)
|
|
513
|
-
// so the renderer can chain segment animations sequentially.
|
|
514
|
-
if (resolvedAnimation?.enabled) {
|
|
515
|
-
const groupIndexMap = new Map<string, number>();
|
|
516
|
-
const groupStackPos = new Map<string, number>();
|
|
517
|
-
let nextGroupIndex = 0;
|
|
518
|
-
for (const mark of marks) {
|
|
519
|
-
if (mark.type === 'rect' && (mark as RectMark).stackGroup) {
|
|
520
|
-
const rect = mark as RectMark;
|
|
521
|
-
const group = rect.stackGroup!;
|
|
522
|
-
if (!groupIndexMap.has(group)) {
|
|
523
|
-
groupIndexMap.set(group, nextGroupIndex++);
|
|
524
|
-
}
|
|
525
|
-
rect.animationIndex = groupIndexMap.get(group)!;
|
|
526
|
-
const pos = groupStackPos.get(group) ?? 0;
|
|
527
|
-
rect.stackPos = pos;
|
|
528
|
-
groupStackPos.set(group, pos + 1);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
480
|
+
assignAnimationIndices(marks, resolvedAnimation);
|
|
532
481
|
|
|
533
482
|
return {
|
|
534
483
|
area: chartArea,
|
|
@@ -554,26 +503,10 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
554
503
|
},
|
|
555
504
|
animation: resolvedAnimation,
|
|
556
505
|
watermark,
|
|
506
|
+
measureText: options.measureText,
|
|
557
507
|
};
|
|
558
508
|
}
|
|
559
509
|
|
|
560
|
-
/** Extract the primary quantitative value from a mark for value-based stagger ordering. */
|
|
561
|
-
function getMarkPrimaryValue(mark: Mark): number {
|
|
562
|
-
switch (mark.type) {
|
|
563
|
-
case 'rect':
|
|
564
|
-
return mark.height; // bar height is the primary value encoding
|
|
565
|
-
case 'point':
|
|
566
|
-
return mark.cy; // y position for scatter
|
|
567
|
-
case 'arc':
|
|
568
|
-
return mark.endAngle - mark.startAngle; // arc angle extent
|
|
569
|
-
case 'line':
|
|
570
|
-
case 'area':
|
|
571
|
-
return 0; // series marks don't have individual values
|
|
572
|
-
default:
|
|
573
|
-
return 0;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
510
|
// ---------------------------------------------------------------------------
|
|
578
511
|
// Layer compilation
|
|
579
512
|
// ---------------------------------------------------------------------------
|
|
@@ -69,8 +69,8 @@ function normalizeChrome(chrome: Chrome | undefined): NormalizedChrome {
|
|
|
69
69
|
|
|
70
70
|
/** Sample values from a data column and infer the field type. */
|
|
71
71
|
function inferFieldType(data: DataRow[], field: string): FieldType {
|
|
72
|
-
// Sample up to
|
|
73
|
-
const sampleSize = Math.min(
|
|
72
|
+
// Sample up to 50 rows for more reliable inference on mixed/messy data
|
|
73
|
+
const sampleSize = Math.min(50, data.length);
|
|
74
74
|
let numericCount = 0;
|
|
75
75
|
let dateCount = 0;
|
|
76
76
|
let totalNonNull = 0;
|
package/src/layout/axes.ts
CHANGED
|
@@ -38,8 +38,8 @@ import type {
|
|
|
38
38
|
|
|
39
39
|
/** Base tick counts by axis label density. */
|
|
40
40
|
const TICK_COUNTS: Record<AxisLabelDensity, number> = {
|
|
41
|
-
full:
|
|
42
|
-
reduced:
|
|
41
|
+
full: 10,
|
|
42
|
+
reduced: 7,
|
|
43
43
|
minimal: 3,
|
|
44
44
|
};
|
|
45
45
|
|
|
@@ -399,8 +399,7 @@ export function computeAxes(
|
|
|
399
399
|
|
|
400
400
|
// Auto-rotate labels when band scale labels would overlap.
|
|
401
401
|
// Uses max label width (not average) since one long label is enough to overlap.
|
|
402
|
-
|
|
403
|
-
let tickAngle = axisConfig?.labelAngle ?? axisConfig?.tickAngle;
|
|
402
|
+
let tickAngle = axisConfig?.labelAngle;
|
|
404
403
|
if (tickAngle === undefined && scales.x.type === 'band' && ticks.length > 1) {
|
|
405
404
|
const bandwidth = (scales.x.scale as ScaleBand<string>).bandwidth();
|
|
406
405
|
let maxLabelWidth = 0;
|
|
@@ -414,8 +413,7 @@ export function computeAxes(
|
|
|
414
413
|
}
|
|
415
414
|
}
|
|
416
415
|
|
|
417
|
-
|
|
418
|
-
const axisTitle = axisConfig?.title ?? axisConfig?.label;
|
|
416
|
+
const axisTitle = axisConfig?.title;
|
|
419
417
|
|
|
420
418
|
result.x = {
|
|
421
419
|
ticks,
|
|
@@ -454,21 +452,20 @@ export function computeAxes(
|
|
|
454
452
|
allTicks = continuousTicks(scales.y, yDensity);
|
|
455
453
|
}
|
|
456
454
|
|
|
457
|
-
// Gridlines use the full tick set (label thinning shouldn't remove gridlines).
|
|
458
|
-
const gridlines: Gridline[] = allTicks.map((t) => ({
|
|
459
|
-
position: t.position,
|
|
460
|
-
major: true,
|
|
461
|
-
}));
|
|
462
|
-
|
|
463
455
|
// Thin tick labels to prevent overlap (skip for band scales, explicit tickCount, and values).
|
|
464
456
|
const shouldThin = scales.y.type !== 'band' && !axisConfig?.tickCount && !axisConfig?.values;
|
|
465
457
|
const ticks = shouldThin
|
|
466
458
|
? thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText)
|
|
467
459
|
: allTicks;
|
|
468
460
|
|
|
469
|
-
//
|
|
470
|
-
const
|
|
471
|
-
|
|
461
|
+
// Gridlines match the tick set so every gridline has a label.
|
|
462
|
+
const gridlines: Gridline[] = ticks.map((t) => ({
|
|
463
|
+
position: t.position,
|
|
464
|
+
major: true,
|
|
465
|
+
}));
|
|
466
|
+
|
|
467
|
+
const axisTitle = axisConfig?.title;
|
|
468
|
+
const tickAngle = axisConfig?.labelAngle;
|
|
472
469
|
|
|
473
470
|
result.y = {
|
|
474
471
|
ticks,
|
package/src/layout/dimensions.ts
CHANGED
|
@@ -125,9 +125,9 @@ export function computeDimensions(
|
|
|
125
125
|
// Estimate x-axis height below chart area: tick labels sit 14px below,
|
|
126
126
|
// axis title sits 35px below. These extend past the chart area bottom
|
|
127
127
|
// and source/footer chrome must be positioned below them.
|
|
128
|
-
const xAxis = encoding.x?.axis as (Record<string, unknown> & {
|
|
129
|
-
const hasXAxisLabel = !!xAxis?.
|
|
130
|
-
const xTickAngle = xAxis?.
|
|
128
|
+
const xAxis = encoding.x?.axis as (Record<string, unknown> & { labelAngle?: number }) | undefined;
|
|
129
|
+
const hasXAxisLabel = !!xAxis?.title;
|
|
130
|
+
const xTickAngle = xAxis?.labelAngle;
|
|
131
131
|
|
|
132
132
|
let xAxisHeight: number;
|
|
133
133
|
if (isRadial) {
|