@opendata-ai/openchart-engine 8.0.0-rc.2 → 8.0.0-rc.20
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.d.ts +140 -2
- package/dist/index.js +1403 -210
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/__test-fixtures__/specs.ts +0 -182
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2339
- package/src/__tests__/annotation-behavior.test.ts +0 -721
- package/src/__tests__/axes.test.ts +0 -1579
- package/src/__tests__/compile-animation.test.ts +0 -161
- package/src/__tests__/compile-chart.test.ts +0 -1303
- package/src/__tests__/compile-footnotes.test.ts +0 -277
- package/src/__tests__/compile-layer.test.ts +0 -969
- package/src/__tests__/compile-snapshot.test.ts +0 -156
- package/src/__tests__/compound-labels.test.ts +0 -147
- package/src/__tests__/dimensions.test.ts +0 -918
- package/src/__tests__/encoding-sugar.test.ts +0 -390
- package/src/__tests__/facet.test.ts +0 -215
- package/src/__tests__/helpers/invariants.ts +0 -299
- package/src/__tests__/known-bugs.test.ts +0 -566
- package/src/__tests__/layout-fuzz.test.ts +0 -358
- package/src/__tests__/layout-invariants.test.ts +0 -535
- package/src/__tests__/legend.test.ts +0 -721
- package/src/__tests__/mark-keys.test.ts +0 -294
- package/src/__tests__/ranking-recipes.test.ts +0 -260
- package/src/__tests__/rotation.test.ts +0 -83
- package/src/__tests__/scales.test.ts +0 -323
- package/src/__tests__/series-search.test.ts +0 -92
- package/src/__tests__/spec-sugar.test.ts +0 -801
- package/src/__tests__/you-draw-it.test.ts +0 -160
- package/src/annotations/__tests__/compute.test.ts +0 -1672
- package/src/annotations/__tests__/geometry.test.ts +0 -66
- package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
- package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
- package/src/annotations/__tests__/placement.test.ts +0 -316
- package/src/annotations/__tests__/resolve-text.test.ts +0 -788
- package/src/annotations/__tests__/rich-text.test.ts +0 -91
- package/src/annotations/__tests__/thinning.test.ts +0 -285
- package/src/annotations/collisions.ts +0 -326
- package/src/annotations/compute.ts +0 -333
- package/src/annotations/constants.ts +0 -162
- package/src/annotations/geometry.ts +0 -370
- package/src/annotations/move.ts +0 -135
- package/src/annotations/placement.ts +0 -636
- package/src/annotations/position.ts +0 -131
- package/src/annotations/resolve-range.ts +0 -117
- package/src/annotations/resolve-refline.ts +0 -157
- package/src/annotations/resolve-text.ts +0 -539
- package/src/annotations/rich-text.ts +0 -65
- package/src/annotations/thinning.ts +0 -190
- package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
- package/src/barlist/compile-barlist.ts +0 -391
- package/src/barlist/types.ts +0 -28
- package/src/charts/__tests__/post-process.test.ts +0 -237
- package/src/charts/__tests__/registry.test.ts +0 -116
- package/src/charts/__tests__/utils.test.ts +0 -195
- package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
- package/src/charts/_shared/density-filter.ts +0 -26
- package/src/charts/_shared/format-label-value.ts +0 -16
- package/src/charts/bar/__tests__/compute.test.ts +0 -934
- package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
- package/src/charts/bar/__tests__/labels.test.ts +0 -292
- package/src/charts/bar/compute.ts +0 -539
- package/src/charts/bar/index.ts +0 -60
- package/src/charts/bar/labels.ts +0 -311
- package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
- package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
- package/src/charts/beeswarm/compute.ts +0 -203
- package/src/charts/beeswarm/dodge.ts +0 -155
- package/src/charts/beeswarm/index.ts +0 -32
- package/src/charts/builtin.ts +0 -75
- package/src/charts/calendar/__tests__/compute.test.ts +0 -417
- package/src/charts/calendar/compute.ts +0 -319
- package/src/charts/calendar/index.ts +0 -32
- package/src/charts/column/__tests__/compute.test.ts +0 -540
- package/src/charts/column/__tests__/labels.test.ts +0 -179
- package/src/charts/column/compute.ts +0 -493
- package/src/charts/column/index.ts +0 -53
- package/src/charts/column/labels.ts +0 -229
- package/src/charts/dot/__tests__/compute.test.ts +0 -380
- package/src/charts/dot/__tests__/labels.test.ts +0 -98
- package/src/charts/dot/compute.ts +0 -285
- package/src/charts/dot/index.ts +0 -62
- package/src/charts/dot/labels.ts +0 -121
- package/src/charts/line/__tests__/compute.test.ts +0 -1194
- package/src/charts/line/__tests__/curves.test.ts +0 -51
- package/src/charts/line/__tests__/labels.test.ts +0 -94
- package/src/charts/line/__tests__/mark-options.test.ts +0 -175
- package/src/charts/line/area.ts +0 -484
- package/src/charts/line/compute.ts +0 -356
- package/src/charts/line/curves.ts +0 -85
- package/src/charts/line/index.ts +0 -176
- package/src/charts/line/labels.ts +0 -177
- package/src/charts/parliament/__tests__/compute.test.ts +0 -411
- package/src/charts/parliament/compute.ts +0 -313
- package/src/charts/parliament/index.ts +0 -32
- package/src/charts/pie/__tests__/compute.test.ts +0 -384
- package/src/charts/pie/__tests__/labels.test.ts +0 -132
- package/src/charts/pie/compute.ts +0 -324
- package/src/charts/pie/index.ts +0 -49
- package/src/charts/pie/labels.ts +0 -143
- package/src/charts/post-process.ts +0 -229
- package/src/charts/range/__tests__/compute.test.ts +0 -486
- package/src/charts/range/compute.ts +0 -273
- package/src/charts/range/index.ts +0 -82
- package/src/charts/range/labels.ts +0 -169
- package/src/charts/rect/__tests__/compute.test.ts +0 -120
- package/src/charts/rect/index.ts +0 -127
- package/src/charts/registry.ts +0 -72
- package/src/charts/rule/index.ts +0 -139
- package/src/charts/scatter/__tests__/compute.test.ts +0 -488
- package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
- package/src/charts/scatter/compute.ts +0 -192
- package/src/charts/scatter/index.ts +0 -46
- package/src/charts/scatter/trendline.ts +0 -100
- package/src/charts/text/__tests__/compute.test.ts +0 -307
- package/src/charts/text/index.ts +0 -141
- package/src/charts/tick/__tests__/compute.test.ts +0 -75
- package/src/charts/tick/index.ts +0 -99
- package/src/charts/utils.ts +0 -170
- package/src/charts/waffle/__tests__/compute.test.ts +0 -305
- package/src/charts/waffle/compute.ts +0 -197
- package/src/charts/waffle/index.ts +0 -31
- package/src/compile/__tests__/color-scale-range.test.ts +0 -158
- package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
- package/src/compile/__tests__/data-clip.test.ts +0 -127
- package/src/compile/__tests__/fill-patterns.test.ts +0 -154
- package/src/compile/__tests__/size-scale.test.ts +0 -108
- package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
- package/src/compile/color-scale-range.ts +0 -101
- package/src/compile/data-clip.ts +0 -48
- package/src/compile/fill-patterns.ts +0 -74
- package/src/compile/layer.ts +0 -626
- package/src/compile/size-scale.ts +0 -139
- package/src/compile/spec-sugar.ts +0 -758
- package/src/compile/watermark-obstacle.ts +0 -54
- package/src/compile/you-draw-it.ts +0 -123
- package/src/compile.ts +0 -1450
- package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
- package/src/compiler/__tests__/animation.test.ts +0 -177
- package/src/compiler/__tests__/compile.test.ts +0 -87
- package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
- package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
- package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
- package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
- package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
- package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
- package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
- package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
- package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
- package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
- package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
- package/src/compiler/__tests__/keys.test.ts +0 -83
- package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
- package/src/compiler/__tests__/normalize.test.ts +0 -399
- package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -86
- package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
- package/src/compiler/__tests__/validate-near-miss.test.ts +0 -89
- package/src/compiler/__tests__/validate.test.ts +0 -629
- package/src/compiler/__tests__/vl-parity.test.ts +0 -68
- package/src/compiler/a11y-warnings.ts +0 -110
- package/src/compiler/animation.ts +0 -142
- package/src/compiler/index.ts +0 -47
- package/src/compiler/keys.ts +0 -55
- package/src/compiler/normalize.ts +0 -733
- package/src/compiler/sparkline-defaults.ts +0 -138
- package/src/compiler/types.ts +0 -241
- package/src/compiler/validate.ts +0 -1810
- package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
- package/src/endpoint-labels/compute.ts +0 -621
- package/src/endpoint-labels/constants.ts +0 -54
- package/src/endpoint-labels/format.ts +0 -60
- package/src/endpoint-labels/predict.ts +0 -139
- package/src/format/__tests__/field-format.test.ts +0 -164
- package/src/format/field-format.ts +0 -27
- package/src/geo/__tests__/compile-map.test.ts +0 -621
- package/src/geo/compile-map.ts +0 -856
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -45
- package/src/geo/types.ts +0 -24
- package/src/graphs/__tests__/community.test.ts +0 -228
- package/src/graphs/__tests__/compile-graph.test.ts +0 -396
- package/src/graphs/__tests__/encoding.test.ts +0 -415
- package/src/graphs/community.ts +0 -92
- package/src/graphs/compile-graph.ts +0 -329
- package/src/graphs/encoding.ts +0 -356
- package/src/graphs/types.ts +0 -106
- package/src/index.ts +0 -177
- package/src/layout/__tests__/metrics.test.ts +0 -97
- package/src/layout/__tests__/scales-highlight.test.ts +0 -192
- package/src/layout/axes/rotation.ts +0 -152
- package/src/layout/axes/thinning.ts +0 -98
- package/src/layout/axes/ticks.ts +0 -409
- package/src/layout/axes.ts +0 -757
- package/src/layout/dimensions.ts +0 -912
- package/src/layout/facet.ts +0 -91
- package/src/layout/gridlines.ts +0 -84
- package/src/layout/metrics.ts +0 -132
- package/src/layout/plan.ts +0 -631
- package/src/layout/scales.ts +0 -1053
- package/src/layout/shared.ts +0 -44
- package/src/legend/__tests__/continuous.test.ts +0 -318
- package/src/legend/__tests__/redundancy.test.ts +0 -108
- package/src/legend/__tests__/size.test.ts +0 -348
- package/src/legend/__tests__/suppression.test.ts +0 -382
- package/src/legend/compute.ts +0 -747
- package/src/legend/continuous.ts +0 -327
- package/src/legend/size.ts +0 -407
- package/src/legend/suppression.ts +0 -226
- package/src/legend/wrap.ts +0 -117
- package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
- package/src/sankey/__tests__/layout.test.ts +0 -165
- package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
- package/src/sankey/__tests__/node-sort.test.ts +0 -45
- package/src/sankey/compile-sankey.ts +0 -706
- package/src/sankey/layout.ts +0 -184
- package/src/sankey/types.ts +0 -41
- package/src/tables/__tests__/bar-column.test.ts +0 -147
- package/src/tables/__tests__/category-colors.test.ts +0 -230
- package/src/tables/__tests__/compile-table.test.ts +0 -236
- package/src/tables/__tests__/format-cells.test.ts +0 -143
- package/src/tables/__tests__/heatmap.test.ts +0 -148
- package/src/tables/__tests__/pagination.test.ts +0 -78
- package/src/tables/__tests__/search.test.ts +0 -94
- package/src/tables/__tests__/sort.test.ts +0 -107
- package/src/tables/__tests__/sparkline.test.ts +0 -122
- package/src/tables/bar-column.ts +0 -94
- package/src/tables/category-colors.ts +0 -78
- package/src/tables/compile-table.ts +0 -427
- package/src/tables/format-cells.ts +0 -115
- package/src/tables/heatmap.ts +0 -141
- package/src/tables/pagination.ts +0 -46
- package/src/tables/search.ts +0 -66
- package/src/tables/sort.ts +0 -69
- package/src/tables/sparkline.ts +0 -113
- package/src/tables/utils.ts +0 -16
- package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
- package/src/tilemap/compile-tilemap.ts +0 -755
- package/src/tilemap/layout.ts +0 -172
- package/src/tilemap/types.ts +0 -33
- package/src/tooltips/__tests__/compute.test.ts +0 -582
- package/src/tooltips/compute.ts +0 -700
- package/src/transforms/__tests__/aggregate.test.ts +0 -159
- package/src/transforms/__tests__/bin.test.ts +0 -88
- package/src/transforms/__tests__/calculate.test.ts +0 -146
- package/src/transforms/__tests__/conditional.test.ts +0 -127
- package/src/transforms/__tests__/filter-relative.test.ts +0 -202
- package/src/transforms/__tests__/filter.test.ts +0 -59
- package/src/transforms/__tests__/fold.test.ts +0 -79
- package/src/transforms/__tests__/index.test.ts +0 -93
- package/src/transforms/__tests__/predicates.test.ts +0 -193
- package/src/transforms/__tests__/timeunit.test.ts +0 -129
- package/src/transforms/__tests__/window.test.ts +0 -286
- package/src/transforms/aggregate.ts +0 -131
- package/src/transforms/bin.ts +0 -87
- package/src/transforms/calculate.ts +0 -60
- package/src/transforms/conditional.ts +0 -46
- package/src/transforms/filter.ts +0 -122
- package/src/transforms/fold.ts +0 -49
- package/src/transforms/index.ts +0 -60
- package/src/transforms/predicates.ts +0 -123
- package/src/transforms/timeunit.ts +0 -88
- package/src/transforms/window.ts +0 -185
package/src/layout/plan.ts
DELETED
|
@@ -1,631 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CompileOptions,
|
|
3
|
-
Encoding,
|
|
4
|
-
EncodingChannel,
|
|
5
|
-
FieldFormatContext,
|
|
6
|
-
LayoutStrategy,
|
|
7
|
-
MeasureTextFn,
|
|
8
|
-
Rect,
|
|
9
|
-
ResolvedChrome,
|
|
10
|
-
ResolvedTheme,
|
|
11
|
-
} from '@opendata-ai/openchart-core';
|
|
12
|
-
import {
|
|
13
|
-
AXIS_TITLE_TRAILING_PAD,
|
|
14
|
-
axisTitleOffset,
|
|
15
|
-
BREAKPOINT_COMPACT_MAX,
|
|
16
|
-
COMPACT_WIDTH,
|
|
17
|
-
computeChrome,
|
|
18
|
-
computeFieldFormatContext,
|
|
19
|
-
computeXAxisExtentFromLabels,
|
|
20
|
-
defaultNumberFormatter,
|
|
21
|
-
estimateTextWidth,
|
|
22
|
-
HPAD_COMPACT_FRACTION,
|
|
23
|
-
HPAD_COMPACT_MIN,
|
|
24
|
-
isAxislessMark,
|
|
25
|
-
LABEL_GAP_COMPACT,
|
|
26
|
-
LABEL_GAP_DEFAULT,
|
|
27
|
-
MAX_LEFT_LABEL_FRACTION_COMPACT,
|
|
28
|
-
MAX_LEFT_LABEL_FRACTION_DEFAULT,
|
|
29
|
-
MAX_LEFT_LABEL_FRACTION_MEDIUM,
|
|
30
|
-
MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX,
|
|
31
|
-
NARROW_VIEWPORT_MAX,
|
|
32
|
-
resolveNumberFormatter,
|
|
33
|
-
TOP_PAD_EXTRA_NARROW,
|
|
34
|
-
} from '@opendata-ai/openchart-core';
|
|
35
|
-
|
|
36
|
-
import type { NormalizedChartSpec } from '../compiler/types';
|
|
37
|
-
import { predictEndpointLabelsWidth } from '../endpoint-labels/predict';
|
|
38
|
-
import { computeLegendContent, hasLegendContent, type LegendContent } from '../legend/compute';
|
|
39
|
-
import {
|
|
40
|
-
computeSizeLegendContent,
|
|
41
|
-
SIZE_LEGEND_GAP,
|
|
42
|
-
type SizeLegendContent,
|
|
43
|
-
sizeLegendScaleFor,
|
|
44
|
-
} from '../legend/size';
|
|
45
|
-
import { legendGap, TOP_LEGEND_GAP_ABOVE } from '../legend/wrap';
|
|
46
|
-
import { yTickPositionIsInline } from './axes';
|
|
47
|
-
import { resolveBandTickAngle } from './axes/rotation';
|
|
48
|
-
import { buildContinuousTicks, scaleSupportsTickCount, targetTickCount } from './axes/ticks';
|
|
49
|
-
import { computeScales, estimateBandStep, estimateBandwidth } from './scales';
|
|
50
|
-
import { bottomMargin, chromeToInput, INLINE_TICK_OVERHANG_PAD, scalePadding } from './shared';
|
|
51
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// MeasureFn -- simplified width-only text measurement
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
/** Width-only text measurement function used by the layout planner. */
|
|
57
|
-
export type MeasureFn = (text: string, fontSize: number, fontWeight: number) => number;
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// LayoutPlan -- frozen dimensions from the measure phase
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
/** Frozen layout dimensions produced by the measure phase. */
|
|
64
|
-
export interface LayoutPlan {
|
|
65
|
-
leftGutter: number;
|
|
66
|
-
xAxisExtent: number;
|
|
67
|
-
legendContent: LegendContent;
|
|
68
|
-
/**
|
|
69
|
-
* Size legend content, when a quantitative `size` channel is encoded.
|
|
70
|
-
*
|
|
71
|
-
* Kept beside `legendContent` rather than inside it: a chart can carry BOTH a
|
|
72
|
-
* color legend and a size legend, and folding them into one slot is what made
|
|
73
|
-
* the size channel unkeyable in the first place. `dimensions.ts` reserves
|
|
74
|
-
* right-margin for this independently of the color legend's position.
|
|
75
|
-
*/
|
|
76
|
-
sizeLegendContent?: SizeLegendContent | null;
|
|
77
|
-
chrome: ResolvedChrome;
|
|
78
|
-
yTickValues: unknown[];
|
|
79
|
-
yTickCount: number;
|
|
80
|
-
converged: boolean;
|
|
81
|
-
inlineYLabelInset: number;
|
|
82
|
-
/** True when endpoint labels were demoted by auto-thinning (don't fit at current width). */
|
|
83
|
-
endpointLabelsDemoted?: boolean;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
// createMeasureFn -- adapter from MeasureTextFn to MeasureFn
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Wraps an optional adapter-provided `MeasureTextFn` (returns `{ width, height }`)
|
|
92
|
-
* into a simpler `MeasureFn` that returns width only. Falls back to the
|
|
93
|
-
* heuristic `estimateTextWidth` when no real measurement is available.
|
|
94
|
-
*/
|
|
95
|
-
export function createMeasureFn(measureText?: MeasureTextFn): MeasureFn {
|
|
96
|
-
if (measureText) {
|
|
97
|
-
return (text: string, fontSize: number, fontWeight: number) =>
|
|
98
|
-
measureText(text, fontSize, fontWeight).width;
|
|
99
|
-
}
|
|
100
|
-
return estimateTextWidth;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
// Helpers
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
|
|
107
|
-
function sampleLabelFromContext(
|
|
108
|
-
maxAbsVal: number,
|
|
109
|
-
yAxisFormat: string | undefined,
|
|
110
|
-
ctx: FieldFormatContext,
|
|
111
|
-
): string {
|
|
112
|
-
if (yAxisFormat) {
|
|
113
|
-
const fmt = resolveNumberFormatter(yAxisFormat, ctx);
|
|
114
|
-
if (fmt) return fmt(maxAbsVal);
|
|
115
|
-
}
|
|
116
|
-
return defaultNumberFormatter({ ...ctx, extent: ctx.extent ?? [0, maxAbsVal] })(maxAbsVal);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
// resolveLayoutPlan
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Measure real axis labels and legend content, then freeze the chart layout.
|
|
125
|
-
*
|
|
126
|
-
* Runs up to 2 iterations: build provisional scales at an estimated gutter,
|
|
127
|
-
* generate real y-axis tick labels, measure their widths, and check whether
|
|
128
|
-
* the gutter changed. If it converged on the first pass, we're done;
|
|
129
|
-
* otherwise a second pass locks in the final gutter as max(pass0, pass1).
|
|
130
|
-
*
|
|
131
|
-
* @param chartSpec - UNFILTERED spec (used for legend -- retains all series)
|
|
132
|
-
* @param renderSpec - FILTERED spec (used for measuring data and scales)
|
|
133
|
-
* @param options - CompileOptions (width, height, measureText, etc.)
|
|
134
|
-
* @param theme - Resolved theme
|
|
135
|
-
* @param strategy - Responsive layout strategy
|
|
136
|
-
* @param watermark - Whether the brand watermark is shown
|
|
137
|
-
* @param measure - Width-only text measurement function
|
|
138
|
-
*/
|
|
139
|
-
export function resolveLayoutPlan(
|
|
140
|
-
chartSpec: NormalizedChartSpec,
|
|
141
|
-
renderSpec: NormalizedChartSpec,
|
|
142
|
-
options: CompileOptions,
|
|
143
|
-
theme: ResolvedTheme,
|
|
144
|
-
strategy: LayoutStrategy,
|
|
145
|
-
watermark: boolean,
|
|
146
|
-
measure: MeasureFn,
|
|
147
|
-
): LayoutPlan {
|
|
148
|
-
const { width, height } = options;
|
|
149
|
-
const padding = scalePadding(theme.spacing.padding, width, height);
|
|
150
|
-
const hPad =
|
|
151
|
-
width < BREAKPOINT_COMPACT_MAX
|
|
152
|
-
? Math.max(Math.round(padding * HPAD_COMPACT_FRACTION), HPAD_COMPACT_MIN)
|
|
153
|
-
: padding;
|
|
154
|
-
const encoding = renderSpec.encoding as Encoding;
|
|
155
|
-
const isRadial = isAxislessMark(renderSpec.markType);
|
|
156
|
-
const axisMargin = theme.spacing.axisMargin;
|
|
157
|
-
|
|
158
|
-
// Resolve chromeMode
|
|
159
|
-
let chromeMode = strategy?.chromeMode ?? 'full';
|
|
160
|
-
if (renderSpec.display === 'sparkline' && !renderSpec.userExplicit?.chrome) {
|
|
161
|
-
chromeMode = 'hidden';
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// -----------------------------------------------------------------------
|
|
165
|
-
// Sparkline / radial: trivial plan (no y-axis measurement needed)
|
|
166
|
-
// -----------------------------------------------------------------------
|
|
167
|
-
if (renderSpec.display === 'sparkline' || isRadial) {
|
|
168
|
-
const legendContent = computeLegendContent(
|
|
169
|
-
chartSpec,
|
|
170
|
-
strategy,
|
|
171
|
-
theme,
|
|
172
|
-
width,
|
|
173
|
-
height,
|
|
174
|
-
watermark,
|
|
175
|
-
measure,
|
|
176
|
-
);
|
|
177
|
-
const bottomLegendReservation =
|
|
178
|
-
hasLegendContent(legendContent) && legendContent.position === 'bottom'
|
|
179
|
-
? legendContent.height + legendGap(width)
|
|
180
|
-
: 0;
|
|
181
|
-
const chrome = computeChrome(
|
|
182
|
-
chromeToInput(renderSpec.chrome),
|
|
183
|
-
theme,
|
|
184
|
-
width,
|
|
185
|
-
options.measureText,
|
|
186
|
-
chromeMode,
|
|
187
|
-
padding,
|
|
188
|
-
watermark,
|
|
189
|
-
bottomLegendReservation,
|
|
190
|
-
);
|
|
191
|
-
return {
|
|
192
|
-
leftGutter: hPad,
|
|
193
|
-
xAxisExtent: 0,
|
|
194
|
-
legendContent,
|
|
195
|
-
// Sparkline / radial: no positional axes and no bubbles, so no size key.
|
|
196
|
-
sizeLegendContent: null,
|
|
197
|
-
chrome,
|
|
198
|
-
yTickValues: [],
|
|
199
|
-
yTickCount: 0,
|
|
200
|
-
converged: true,
|
|
201
|
-
inlineYLabelInset: 0,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// -----------------------------------------------------------------------
|
|
206
|
-
// Resolve y-axis inline status
|
|
207
|
-
// -----------------------------------------------------------------------
|
|
208
|
-
const yAxisCfg = (encoding.y?.axis as Record<string, unknown> | undefined) ?? undefined;
|
|
209
|
-
const yIsContinuous = encoding.y?.type === 'quantitative' || encoding.y?.type === 'temporal';
|
|
210
|
-
// Inline y-tick placement must match computeAxes exactly (shared predicate),
|
|
211
|
-
// or the reserved left margin won't line up with where the title is drawn.
|
|
212
|
-
// Sparkline display already returned early above, so no sparkline guard needed.
|
|
213
|
-
const yIsInline = yTickPositionIsInline(encoding.y, renderSpec.markType);
|
|
214
|
-
const yAxisSuppressed = encoding.y?.axis === false;
|
|
215
|
-
|
|
216
|
-
// -----------------------------------------------------------------------
|
|
217
|
-
// Seed gutter from formatted max |y|
|
|
218
|
-
// -----------------------------------------------------------------------
|
|
219
|
-
let leftGutter = hPad + axisMargin; // base: pad + axis margin
|
|
220
|
-
if (encoding.y && !isRadial && !yAxisSuppressed && !yIsInline) {
|
|
221
|
-
if (
|
|
222
|
-
encoding.y.type === 'nominal' ||
|
|
223
|
-
encoding.y.type === 'ordinal' ||
|
|
224
|
-
renderSpec.markType === 'bar' ||
|
|
225
|
-
renderSpec.markType === 'circle' ||
|
|
226
|
-
renderSpec.markType === 'lollipop'
|
|
227
|
-
) {
|
|
228
|
-
// Category labels: measure real data values
|
|
229
|
-
const yField = encoding.y.field;
|
|
230
|
-
const yLabelField = yAxisCfg?.labelField as string | undefined;
|
|
231
|
-
let maxLabelWidth = 0;
|
|
232
|
-
for (const row of renderSpec.data) {
|
|
233
|
-
const label = String(row[yField] ?? '');
|
|
234
|
-
let w = measure(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
235
|
-
if (yLabelField) {
|
|
236
|
-
const subtitle = String(row[yLabelField] ?? '');
|
|
237
|
-
if (subtitle) {
|
|
238
|
-
const gap = theme.fonts.sizes.axisTick * 0.6;
|
|
239
|
-
const subtitleWidth = measure(
|
|
240
|
-
subtitle,
|
|
241
|
-
theme.fonts.sizes.axisTick,
|
|
242
|
-
theme.fonts.weights.normal,
|
|
243
|
-
);
|
|
244
|
-
w += gap + subtitleWidth;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
248
|
-
}
|
|
249
|
-
if (maxLabelWidth > 0) {
|
|
250
|
-
const labelGap = width < NARROW_VIEWPORT_MAX ? LABEL_GAP_COMPACT : LABEL_GAP_DEFAULT;
|
|
251
|
-
const maxLeftFraction =
|
|
252
|
-
width < BREAKPOINT_COMPACT_MAX
|
|
253
|
-
? MAX_LEFT_LABEL_FRACTION_COMPACT
|
|
254
|
-
: width < MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX
|
|
255
|
-
? MAX_LEFT_LABEL_FRACTION_MEDIUM
|
|
256
|
-
: MAX_LEFT_LABEL_FRACTION_DEFAULT;
|
|
257
|
-
const maxLeftReserved = Math.floor(width * maxLeftFraction);
|
|
258
|
-
const reserved = Math.min(hPad + maxLabelWidth + labelGap, maxLeftReserved);
|
|
259
|
-
leftGutter = Math.max(leftGutter, reserved);
|
|
260
|
-
}
|
|
261
|
-
} else {
|
|
262
|
-
// Quantitative: seed with a formatted label
|
|
263
|
-
const yField = encoding.y.field;
|
|
264
|
-
const yAxisFormat = yAxisCfg?.format as string | undefined;
|
|
265
|
-
let maxAbsVal = 0;
|
|
266
|
-
for (const row of renderSpec.data) {
|
|
267
|
-
const v = Number(row[yField]);
|
|
268
|
-
if (Number.isFinite(v) && Math.abs(v) > maxAbsVal) maxAbsVal = Math.abs(v);
|
|
269
|
-
}
|
|
270
|
-
const ctx = computeFieldFormatContext(renderSpec.data.map((r) => r[yField]));
|
|
271
|
-
const sampleLabel = sampleLabelFromContext(maxAbsVal, yAxisFormat, ctx);
|
|
272
|
-
const negPrefix = renderSpec.data.some((r) => Number(r[yField]) < 0) ? '-' : '';
|
|
273
|
-
const labelWidth = measure(
|
|
274
|
-
negPrefix + sampleLabel,
|
|
275
|
-
theme.fonts.sizes.axisTick,
|
|
276
|
-
theme.fonts.weights.normal,
|
|
277
|
-
);
|
|
278
|
-
leftGutter = Math.max(leftGutter, hPad + labelWidth + 10);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// -----------------------------------------------------------------------
|
|
283
|
-
// X-axis setup (doesn't change between iterations)
|
|
284
|
-
// -----------------------------------------------------------------------
|
|
285
|
-
const xAxisSuppressed = encoding.x?.axis === false;
|
|
286
|
-
const xAxis = (!xAxisSuppressed && encoding.x?.axis) as Record<string, unknown> | undefined;
|
|
287
|
-
const hasXAxisTitle = !!xAxis?.title;
|
|
288
|
-
const xTickAngle = xAxis?.labelAngle as number | undefined;
|
|
289
|
-
|
|
290
|
-
// Collect x labels for extent computation
|
|
291
|
-
const xLabels: string[] = [];
|
|
292
|
-
if (!isRadial && !xAxisSuppressed && encoding.x) {
|
|
293
|
-
const xField = encoding.x.field;
|
|
294
|
-
if (encoding.x.type === 'nominal' || encoding.x.type === 'ordinal') {
|
|
295
|
-
const seen = new Set<string>();
|
|
296
|
-
for (const row of renderSpec.data) {
|
|
297
|
-
const label = String(row[xField] ?? '');
|
|
298
|
-
if (!seen.has(label)) {
|
|
299
|
-
seen.add(label);
|
|
300
|
-
xLabels.push(label);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// -----------------------------------------------------------------------
|
|
307
|
-
// Iteration loop (max 2)
|
|
308
|
-
// -----------------------------------------------------------------------
|
|
309
|
-
let prev: { gutter: number; rowCount: number; xAxisExtent: number } | null = null;
|
|
310
|
-
let finalPlan: LayoutPlan | null = null;
|
|
311
|
-
|
|
312
|
-
// Endpoint-label fit check: when autoThin is on, demote endpoint labels to
|
|
313
|
-
// legend if the column would consume more than 30% of the total width.
|
|
314
|
-
const MAX_ENDPOINT_FRACTION = 0.3;
|
|
315
|
-
let endpointLabelsDemoted = false;
|
|
316
|
-
if (chartSpec.autoThin) {
|
|
317
|
-
const epWidth = predictEndpointLabelsWidth(chartSpec, theme);
|
|
318
|
-
if (epWidth > 0 && epWidth / width > MAX_ENDPOINT_FRACTION) {
|
|
319
|
-
endpointLabelsDemoted = true;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Size legend: keys a quantitative `size` channel with graduated circles.
|
|
324
|
-
// Independent of the color legend -- a bubble chart keys BOTH continent
|
|
325
|
-
// (color) and population (size), so this is computed alongside, not instead.
|
|
326
|
-
// Its content is width-independent, so it sits outside the convergence loop.
|
|
327
|
-
//
|
|
328
|
-
// Dropped below COMPACT_WIDTH. The block is a fixed ~90px (it can't reflow the
|
|
329
|
-
// way a categorical legend's entries can), which on a phone-width chart is a
|
|
330
|
-
// quarter of the SVG spent on the key and a plot too narrow to read the bubbles
|
|
331
|
-
// it explains. The same threshold the color legend goes compact at.
|
|
332
|
-
const sizeScaleOpts = sizeLegendScaleFor(renderSpec.markType);
|
|
333
|
-
const sizeLegendContent =
|
|
334
|
-
sizeScaleOpts && chartSpec.legend?.show !== false && width >= COMPACT_WIDTH
|
|
335
|
-
? computeSizeLegendContent(
|
|
336
|
-
renderSpec.encoding.size as EncodingChannel | undefined,
|
|
337
|
-
renderSpec.data,
|
|
338
|
-
theme,
|
|
339
|
-
sizeScaleOpts,
|
|
340
|
-
)
|
|
341
|
-
: null;
|
|
342
|
-
|
|
343
|
-
for (let iter = 0; iter < 2; iter++) {
|
|
344
|
-
// Legend content
|
|
345
|
-
const legendAvailWidth = width - padding - leftGutter;
|
|
346
|
-
const legendContent = computeLegendContent(
|
|
347
|
-
chartSpec,
|
|
348
|
-
strategy,
|
|
349
|
-
theme,
|
|
350
|
-
legendAvailWidth,
|
|
351
|
-
height,
|
|
352
|
-
watermark,
|
|
353
|
-
measure,
|
|
354
|
-
endpointLabelsDemoted,
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
// Chrome
|
|
358
|
-
const bottomLegendReservation =
|
|
359
|
-
hasLegendContent(legendContent) && legendContent.position === 'bottom'
|
|
360
|
-
? legendContent.height + legendGap(width)
|
|
361
|
-
: 0;
|
|
362
|
-
const chrome = computeChrome(
|
|
363
|
-
chromeToInput(renderSpec.chrome),
|
|
364
|
-
theme,
|
|
365
|
-
width,
|
|
366
|
-
options.measureText,
|
|
367
|
-
chromeMode,
|
|
368
|
-
padding,
|
|
369
|
-
watermark,
|
|
370
|
-
bottomLegendReservation,
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
// Effective x tick angle. Band x-axes auto-rotate to -45° when horizontal
|
|
374
|
-
// labels don't fit their band; computeAxes makes the same call later. The
|
|
375
|
-
// planner must know the angle now so it reserves the rotated-label
|
|
376
|
-
// footprint in the bottom margin instead of the (smaller) flat one.
|
|
377
|
-
let effectiveXTickAngle = xTickAngle;
|
|
378
|
-
if (xTickAngle === undefined && xLabels.length > 1) {
|
|
379
|
-
// The right-hand legend column narrows the plot (dimensions.ts reserves
|
|
380
|
-
// it), so it has to be in this estimate too. Without it the planner sizes
|
|
381
|
-
// bands against a plot up to ~100px wider than the real one, concludes the
|
|
382
|
-
// labels fit flat, and computeAxes then rotates them against the true
|
|
383
|
-
// bandwidth -- reserving the flat footprint for rotated labels and clipping
|
|
384
|
-
// the axis. Bites a nominal-x beeswarm with a size channel.
|
|
385
|
-
//
|
|
386
|
-
// Mirrors the shared-column reservation in dimensions.ts: the color and
|
|
387
|
-
// size legends stack in ONE column, so it is the wider of the two, not the
|
|
388
|
-
// sum. These two must agree -- an estimate wider than the real margin
|
|
389
|
-
// rotates labels that would have fit.
|
|
390
|
-
const colorInGutter =
|
|
391
|
-
hasLegendContent(legendContent) &&
|
|
392
|
-
(legendContent.position === 'right' || legendContent.position === 'bottom-right');
|
|
393
|
-
const colorLegendReserve = colorInGutter ? legendContent.legendWidth + 8 : 0;
|
|
394
|
-
const sizeLegendReserve = sizeLegendContent ? sizeLegendContent.width + SIZE_LEGEND_GAP : 0;
|
|
395
|
-
const legendReserve = Math.max(colorLegendReserve, sizeLegendReserve);
|
|
396
|
-
const rightMarginEst = hPad + (isRadial ? hPad : axisMargin) + legendReserve;
|
|
397
|
-
const plotWidth = Math.max(0, width - leftGutter - Math.max(rightMarginEst, hPad));
|
|
398
|
-
// Mirror d3 scaleBand: step = plotWidth / (n - paddingInner + 2*paddingOuter),
|
|
399
|
-
// bandwidth = step * (1 - paddingInner). Uses the same override resolution
|
|
400
|
-
// as buildBandScale so paddingInner/paddingOuter are honored here too.
|
|
401
|
-
const bandwidth = estimateBandwidth(encoding.x?.scale, plotWidth, xLabels.length);
|
|
402
|
-
const step = estimateBandStep(encoding.x?.scale, plotWidth, xLabels.length);
|
|
403
|
-
// Mirror categoricalTicks' explicit tickCount cap: retained ticks sit a
|
|
404
|
-
// multiple of the band step apart, so the drawn labels and their anchor
|
|
405
|
-
// spacing — not the full category set — drive the angle here, matching
|
|
406
|
-
// what computeAxes measures from actual tick positions.
|
|
407
|
-
const xTickCount = xAxis?.tickCount as number | undefined;
|
|
408
|
-
let drawnLabels = xLabels;
|
|
409
|
-
let anchorSpacing = step;
|
|
410
|
-
if (xTickCount && xLabels.length > xTickCount) {
|
|
411
|
-
const capStride = Math.ceil(xLabels.length / xTickCount);
|
|
412
|
-
drawnLabels = xLabels.filter((_, i) => i % capStride === 0);
|
|
413
|
-
anchorSpacing = step * capStride;
|
|
414
|
-
}
|
|
415
|
-
let maxXLabelWidth = 0;
|
|
416
|
-
for (const label of drawnLabels) {
|
|
417
|
-
const w = measure(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
418
|
-
if (w > maxXLabelWidth) maxXLabelWidth = w;
|
|
419
|
-
}
|
|
420
|
-
effectiveXTickAngle = resolveBandTickAngle(
|
|
421
|
-
undefined,
|
|
422
|
-
maxXLabelWidth,
|
|
423
|
-
bandwidth,
|
|
424
|
-
drawnLabels.length,
|
|
425
|
-
anchorSpacing,
|
|
426
|
-
theme.fonts.sizes.axisTick,
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// X-axis extent
|
|
431
|
-
const xAxisExtent =
|
|
432
|
-
isRadial || xAxisSuppressed
|
|
433
|
-
? 0
|
|
434
|
-
: computeXAxisExtentFromLabels({
|
|
435
|
-
labels: xLabels,
|
|
436
|
-
tickAngle: effectiveXTickAngle,
|
|
437
|
-
hasTitle: hasXAxisTitle,
|
|
438
|
-
tickFontSize: theme.fonts.sizes.axisTick,
|
|
439
|
-
tickFontWeight: theme.fonts.weights.normal,
|
|
440
|
-
xAxisHeight: theme.spacing.xAxisHeight,
|
|
441
|
-
measure,
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
// Top margin
|
|
445
|
-
const topPad = width < NARROW_VIEWPORT_MAX ? padding + TOP_PAD_EXTRA_NARROW : padding;
|
|
446
|
-
const hasTopLegend = hasLegendContent(legendContent) && legendContent.position === 'top';
|
|
447
|
-
const gap = legendGap(width);
|
|
448
|
-
const inlineTickOverhang = yIsInline
|
|
449
|
-
? theme.fonts.sizes.axisTick + INLINE_TICK_OVERHANG_PAD
|
|
450
|
-
: 0;
|
|
451
|
-
const topAxisGap = axisMargin + inlineTickOverhang;
|
|
452
|
-
const effectiveTopAxisGap = hasTopLegend ? inlineTickOverhang : topAxisGap;
|
|
453
|
-
|
|
454
|
-
let topMargin = topPad + chrome.topHeight;
|
|
455
|
-
// Mirrors the dimensions.ts margin stack: min gap above the top legend.
|
|
456
|
-
if (hasTopLegend) topMargin += TOP_LEGEND_GAP_ABOVE + legendContent.height + gap;
|
|
457
|
-
topMargin += effectiveTopAxisGap;
|
|
458
|
-
|
|
459
|
-
const bMargin = bottomMargin(chrome.bottomHeight, padding, xAxisExtent);
|
|
460
|
-
const chartHeight = Math.max(0, height - topMargin - bMargin);
|
|
461
|
-
|
|
462
|
-
// Provisional area
|
|
463
|
-
const rightMargin = hPad + (isRadial ? hPad : axisMargin);
|
|
464
|
-
const rightAxisReserve = options.rightAxisReserve ?? 0;
|
|
465
|
-
const provisionalArea: Rect = {
|
|
466
|
-
x: leftGutter,
|
|
467
|
-
y: topMargin,
|
|
468
|
-
width: Math.max(0, width - leftGutter - Math.max(rightMargin, hPad + rightAxisReserve)),
|
|
469
|
-
height: chartHeight,
|
|
470
|
-
};
|
|
471
|
-
|
|
472
|
-
// Build provisional scales and generate real y ticks
|
|
473
|
-
let yTickValues: unknown[] = [];
|
|
474
|
-
let yTickCount = 0;
|
|
475
|
-
let newGutter = leftGutter;
|
|
476
|
-
|
|
477
|
-
if (encoding.y && yIsContinuous && !yAxisSuppressed) {
|
|
478
|
-
const scales = computeScales(renderSpec, provisionalArea, renderSpec.data);
|
|
479
|
-
if (scales.y && scaleSupportsTickCount(scales.y)) {
|
|
480
|
-
const yTargetCount = targetTickCount(chartHeight, 'full', 'y');
|
|
481
|
-
const axisTickCount = yAxisCfg?.tickCount as number | undefined;
|
|
482
|
-
const effectiveCount = axisTickCount ?? yTargetCount;
|
|
483
|
-
const ticks = buildContinuousTicks(scales.y, effectiveCount);
|
|
484
|
-
yTickValues = ticks.map((t) => t.value);
|
|
485
|
-
yTickCount = ticks.length;
|
|
486
|
-
|
|
487
|
-
// Measure real tick labels for gutter
|
|
488
|
-
if (!yIsInline) {
|
|
489
|
-
let maxLabelWidth = 0;
|
|
490
|
-
for (const t of ticks) {
|
|
491
|
-
const w = measure(t.label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
492
|
-
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
493
|
-
}
|
|
494
|
-
newGutter = hPad + maxLabelWidth + 10;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Convergence check
|
|
500
|
-
const converged =
|
|
501
|
-
prev != null &&
|
|
502
|
-
Math.abs(newGutter - prev.gutter) <= 0.5 &&
|
|
503
|
-
legendContent.rowCount === prev.rowCount &&
|
|
504
|
-
Math.abs(xAxisExtent - prev.xAxisExtent) <= 0.5;
|
|
505
|
-
|
|
506
|
-
if (converged || iter === 1) {
|
|
507
|
-
// On non-convergence at iter=1: gutter = max(iter0, iter1)
|
|
508
|
-
const finalGutter = prev && !converged ? Math.max(prev.gutter, newGutter) : newGutter;
|
|
509
|
-
|
|
510
|
-
// If non-convergent, regenerate ticks once at the final height
|
|
511
|
-
if (prev && !converged && encoding.y && yIsContinuous && !yAxisSuppressed) {
|
|
512
|
-
const finalProvArea: Rect = {
|
|
513
|
-
x: finalGutter,
|
|
514
|
-
y: topMargin,
|
|
515
|
-
width: Math.max(0, width - finalGutter - Math.max(rightMargin, hPad + rightAxisReserve)),
|
|
516
|
-
height: chartHeight,
|
|
517
|
-
};
|
|
518
|
-
const scales = computeScales(renderSpec, finalProvArea, renderSpec.data);
|
|
519
|
-
if (scales.y && scaleSupportsTickCount(scales.y)) {
|
|
520
|
-
const yTargetCount = targetTickCount(chartHeight, 'full', 'y');
|
|
521
|
-
const axisTickCount = yAxisCfg?.tickCount as number | undefined;
|
|
522
|
-
const effectiveCount = axisTickCount ?? yTargetCount;
|
|
523
|
-
const ticks = buildContinuousTicks(scales.y, effectiveCount);
|
|
524
|
-
yTickValues = ticks.map((t) => t.value);
|
|
525
|
-
yTickCount = ticks.length;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Compute inline y-label inset
|
|
530
|
-
let inlineYLabelInset = 0;
|
|
531
|
-
if (yIsInline && encoding.y && yIsContinuous && encoding.y.axis !== false) {
|
|
532
|
-
if (yTickValues.length > 0) {
|
|
533
|
-
// Build ticks from the final scales to get formatted labels
|
|
534
|
-
const finalProvArea: Rect = {
|
|
535
|
-
x: finalGutter,
|
|
536
|
-
y: topMargin,
|
|
537
|
-
width: Math.max(0, width - finalGutter - rightMargin),
|
|
538
|
-
height: chartHeight,
|
|
539
|
-
};
|
|
540
|
-
const scales = computeScales(renderSpec, finalProvArea, renderSpec.data);
|
|
541
|
-
if (scales.y && scaleSupportsTickCount(scales.y)) {
|
|
542
|
-
const ticks = buildContinuousTicks(scales.y, yTickCount || 5);
|
|
543
|
-
let maxLabelWidth = 0;
|
|
544
|
-
for (const t of ticks) {
|
|
545
|
-
const w = measure(t.label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
546
|
-
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
547
|
-
}
|
|
548
|
-
if (maxLabelWidth > 0) {
|
|
549
|
-
inlineYLabelInset = Math.ceil(maxLabelWidth + 8);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
} else {
|
|
553
|
-
// Fallback: measure from data max
|
|
554
|
-
const yField = encoding.y.field;
|
|
555
|
-
const yAxisFormat = yAxisCfg?.format as string | undefined;
|
|
556
|
-
let maxAbsVal = 0;
|
|
557
|
-
for (const row of renderSpec.data) {
|
|
558
|
-
const v = Number(row[yField]);
|
|
559
|
-
if (Number.isFinite(v) && Math.abs(v) > maxAbsVal) maxAbsVal = Math.abs(v);
|
|
560
|
-
}
|
|
561
|
-
const ctx = computeFieldFormatContext(renderSpec.data.map((r) => r[yField]));
|
|
562
|
-
const sample = sampleLabelFromContext(maxAbsVal, yAxisFormat, ctx);
|
|
563
|
-
const neg = renderSpec.data.some((r) => Number(r[encoding.y!.field]) < 0) ? '-' : '';
|
|
564
|
-
const maxLabelWidth = measure(
|
|
565
|
-
neg + sample,
|
|
566
|
-
theme.fonts.sizes.axisTick,
|
|
567
|
-
theme.fonts.weights.normal,
|
|
568
|
-
);
|
|
569
|
-
if (maxLabelWidth > 0) {
|
|
570
|
-
inlineYLabelInset = Math.ceil(maxLabelWidth + 8);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Account for rotated y-axis title in the gutter
|
|
576
|
-
let gutterWithTitle = finalGutter;
|
|
577
|
-
const yAxisDef = encoding.y?.axis as Record<string, unknown> | undefined;
|
|
578
|
-
if (yAxisDef && (yAxisDef.title || yAxisDef.label) && !isRadial) {
|
|
579
|
-
let estTickLabelWidth = 0;
|
|
580
|
-
// Inline y-tick labels sit above their gridlines inside the chart area,
|
|
581
|
-
// not in a left gutter, so they add no width the title must clear.
|
|
582
|
-
// Reserving their width here would leave a dead gap between the title
|
|
583
|
-
// and the plot. Only gutter (non-inline) tick labels count.
|
|
584
|
-
if (encoding.y?.field && yIsContinuous && !yIsInline) {
|
|
585
|
-
if (yTickValues.length > 0) {
|
|
586
|
-
const titleProvArea: Rect = {
|
|
587
|
-
x: finalGutter,
|
|
588
|
-
y: topMargin,
|
|
589
|
-
width: Math.max(0, width - finalGutter - hPad),
|
|
590
|
-
height: chartHeight,
|
|
591
|
-
};
|
|
592
|
-
const scales = computeScales(renderSpec, titleProvArea, renderSpec.data);
|
|
593
|
-
if (scales.y && scaleSupportsTickCount(scales.y)) {
|
|
594
|
-
const ticks = buildContinuousTicks(scales.y, yTickCount || 5);
|
|
595
|
-
for (const t of ticks) {
|
|
596
|
-
const w = measure(t.label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
597
|
-
if (w > estTickLabelWidth) estTickLabelWidth = w;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
const titleFontSize = theme.fonts.sizes.body;
|
|
603
|
-
const offset = axisTitleOffset(estTickLabelWidth, titleFontSize, width, yIsInline);
|
|
604
|
-
const halfGlyph = Math.ceil(titleFontSize / 2);
|
|
605
|
-
const rotatedLabelMargin =
|
|
606
|
-
offset + halfGlyph + (width < BREAKPOINT_COMPACT_MAX ? 0 : AXIS_TITLE_TRAILING_PAD);
|
|
607
|
-
gutterWithTitle = Math.max(finalGutter, hPad + rotatedLabelMargin);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
finalPlan = {
|
|
611
|
-
leftGutter: gutterWithTitle,
|
|
612
|
-
xAxisExtent,
|
|
613
|
-
legendContent,
|
|
614
|
-
sizeLegendContent,
|
|
615
|
-
chrome,
|
|
616
|
-
yTickValues,
|
|
617
|
-
yTickCount,
|
|
618
|
-
converged: prev === null || converged,
|
|
619
|
-
inlineYLabelInset,
|
|
620
|
-
endpointLabelsDemoted,
|
|
621
|
-
};
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
prev = { gutter: newGutter, rowCount: legendContent.rowCount, xAxisExtent };
|
|
626
|
-
leftGutter = newGutter;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
if (!finalPlan) throw new Error('resolveLayoutPlan: loop exited without producing a plan');
|
|
630
|
-
return finalPlan;
|
|
631
|
-
}
|