@opendata-ai/openchart-engine 8.0.0-rc.9 → 8.0.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.d.ts +149 -12
- package/dist/index.js +804 -155
- 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 -2401
- package/src/__tests__/annotation-behavior.test.ts +0 -852
- 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 -376
- 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 -545
- 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 -630
- package/src/compile/size-scale.ts +0 -141
- 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 -1503
- 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 -170
- package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
- package/src/compiler/__tests__/validate-near-miss.test.ts +0 -251
- 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 -734
- package/src/compiler/sparkline-defaults.ts +0 -138
- package/src/compiler/types.ts +0 -241
- package/src/compiler/validate.ts +0 -2041
- 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 -1128
- package/src/geo/compile-map.ts +0 -1376
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -65
- package/src/geo/types.ts +0 -26
- 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 -748
- 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
|
@@ -1,755 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TileMap compilation pipeline.
|
|
3
|
-
*
|
|
4
|
-
* Takes a raw tilemap spec (unknown shape), validates, normalizes, resolves
|
|
5
|
-
* theme, computes chrome, builds a color scale, computes tile positions and
|
|
6
|
-
* marks, builds legend and tooltips, and returns a TileMapLayout.
|
|
7
|
-
*
|
|
8
|
-
* Supports two color modes:
|
|
9
|
-
* - Quantitative (default): opacity-based encoding with a single base color
|
|
10
|
-
* - Categorical: distinct fill colors per category (via `colors` map or
|
|
11
|
-
* `encoding.color` channel)
|
|
12
|
-
*
|
|
13
|
-
* Pipeline:
|
|
14
|
-
* validate -> normalize -> resolve theme -> dark mode adapt ->
|
|
15
|
-
* compute chrome -> extract data -> build color scale -> compute positions ->
|
|
16
|
-
* build tile marks -> legend -> tooltips -> a11y -> animation ->
|
|
17
|
-
* return TileMapLayout
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import type {
|
|
21
|
-
CategoricalLegendLayout,
|
|
22
|
-
CompileOptions,
|
|
23
|
-
GradientColorStop,
|
|
24
|
-
GradientLegendLayout,
|
|
25
|
-
LegendEntry,
|
|
26
|
-
ResolvedAnimation,
|
|
27
|
-
ResolvedTheme,
|
|
28
|
-
TextStyle,
|
|
29
|
-
TileMapLayout,
|
|
30
|
-
TileMapTileMark,
|
|
31
|
-
TooltipContent,
|
|
32
|
-
TooltipField,
|
|
33
|
-
} from '@opendata-ai/openchart-core';
|
|
34
|
-
import {
|
|
35
|
-
adaptTheme,
|
|
36
|
-
CATEGORICAL_PALETTE,
|
|
37
|
-
computeChrome,
|
|
38
|
-
estimateTextWidth,
|
|
39
|
-
getBreakpoint,
|
|
40
|
-
HPAD_COMPACT_FRACTION,
|
|
41
|
-
HPAD_COMPACT_MIN,
|
|
42
|
-
pickLabelColor,
|
|
43
|
-
resolveTheme,
|
|
44
|
-
SEQUENTIAL_PALETTES,
|
|
45
|
-
} from '@opendata-ai/openchart-core';
|
|
46
|
-
import { scaleLinear } from 'd3-scale';
|
|
47
|
-
import { emitSpecWarnings, expandSpecSugar } from '../compile/spec-sugar';
|
|
48
|
-
import { resolveAnimation } from '../compiler/animation';
|
|
49
|
-
import { compile as compileSpec } from '../compiler/index';
|
|
50
|
-
import { resolveFieldFormatter } from '../format/field-format';
|
|
51
|
-
import { computeTilePositions, STATE_CODE_SET, STATE_NAMES, US_STATE_TILES } from './layout';
|
|
52
|
-
import type { NormalizedTileMapSpec } from './types';
|
|
53
|
-
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
// Constants
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
const TILE_CORNER_RADIUS = 6;
|
|
59
|
-
const TILE_STROKE_WIDTH = 1;
|
|
60
|
-
const LEGEND_SWATCH_SIZE = 10;
|
|
61
|
-
const LEGEND_SWATCH_GAP = 6;
|
|
62
|
-
const LEGEND_ENTRY_GAP = 16;
|
|
63
|
-
|
|
64
|
-
function clamp(value: number, min: number, max: number): number {
|
|
65
|
-
return Math.min(max, Math.max(min, value));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
// Public API
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Compile a tilemap spec into a TileMapLayout.
|
|
74
|
-
*
|
|
75
|
-
* @param spec - Raw tilemap spec (validated and normalized internally).
|
|
76
|
-
* @param options - Compile options (width, height, theme, darkMode).
|
|
77
|
-
* @returns TileMapLayout with computed positions and visual properties.
|
|
78
|
-
* @throws Error if spec is invalid or not a tilemap type.
|
|
79
|
-
*/
|
|
80
|
-
export function compileTileMap(spec: unknown, options: CompileOptions): TileMapLayout {
|
|
81
|
-
// 1. Expand deprecated top-level sugar (valueFormat -> encoding.value.format)
|
|
82
|
-
// before validation, then validate + normalize via the shared compiler
|
|
83
|
-
// pipeline. Surface any normalize warnings (e.g. "only N of M rows have
|
|
84
|
-
// valid US state codes", mixed-type coercion) the same way the chart path
|
|
85
|
-
// does; without this the primary "your data did not map" diagnostic is
|
|
86
|
-
// computed and then dropped.
|
|
87
|
-
const sugarWarnings: string[] = [];
|
|
88
|
-
const expandedSpec =
|
|
89
|
-
spec && typeof spec === 'object' && !Array.isArray(spec)
|
|
90
|
-
? expandSpecSugar(spec as Record<string, unknown>, sugarWarnings)
|
|
91
|
-
: spec;
|
|
92
|
-
const { spec: normalized, warnings } = compileSpec(expandedSpec);
|
|
93
|
-
emitSpecWarnings([...sugarWarnings, ...warnings], options.onWarn);
|
|
94
|
-
|
|
95
|
-
if (!('type' in normalized) || normalized.type !== 'tilemap') {
|
|
96
|
-
throw new Error(
|
|
97
|
-
'compileTileMap received a non-tilemap spec. Use compileChart, compileTable, compileGraph, or compileSankey instead.',
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const tilemapSpec = normalized as NormalizedTileMapSpec;
|
|
102
|
-
|
|
103
|
-
// Resolve watermark: explicit spec value wins, then options fallback, then default true.
|
|
104
|
-
const rawWatermark = (spec as Record<string, unknown>).watermark;
|
|
105
|
-
const watermark =
|
|
106
|
-
rawWatermark !== undefined ? tilemapSpec.watermark : (options.watermark ?? true);
|
|
107
|
-
|
|
108
|
-
// 2. Resolve theme
|
|
109
|
-
const mergedThemeConfig = options.theme
|
|
110
|
-
? { ...tilemapSpec.theme, ...options.theme }
|
|
111
|
-
: tilemapSpec.theme;
|
|
112
|
-
const lightTheme: ResolvedTheme = resolveTheme(mergedThemeConfig);
|
|
113
|
-
let theme: ResolvedTheme = lightTheme;
|
|
114
|
-
if (options.darkMode) {
|
|
115
|
-
theme = adaptTheme(theme);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const isDarkMode = options.darkMode;
|
|
119
|
-
|
|
120
|
-
// 3. Compute chrome
|
|
121
|
-
const chrome = computeChrome(
|
|
122
|
-
{
|
|
123
|
-
title: tilemapSpec.chrome.title,
|
|
124
|
-
subtitle: tilemapSpec.chrome.subtitle,
|
|
125
|
-
source: tilemapSpec.chrome.source,
|
|
126
|
-
byline: tilemapSpec.chrome.byline,
|
|
127
|
-
footer: tilemapSpec.chrome.footer,
|
|
128
|
-
},
|
|
129
|
-
theme,
|
|
130
|
-
options.width,
|
|
131
|
-
options.measureText,
|
|
132
|
-
'full',
|
|
133
|
-
undefined,
|
|
134
|
-
watermark,
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
// 4. Compute drawing area (total space minus chrome)
|
|
138
|
-
// On compact (mobile) widths, reclaim horizontal space by shrinking the
|
|
139
|
-
// padding — every reclaimed pixel widens the tiles, which are width-bound at
|
|
140
|
-
// these sizes. Mirrors how the chart pipeline uses HPAD_COMPACT_* on narrow
|
|
141
|
-
// viewports. The tile-area, chrome, and content-height math all read this one
|
|
142
|
-
// value, so overriding it here keeps every derived measurement consistent.
|
|
143
|
-
const padding =
|
|
144
|
-
getBreakpoint(options.width) === 'compact'
|
|
145
|
-
? Math.max(HPAD_COMPACT_MIN, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION))
|
|
146
|
-
: theme.spacing.padding;
|
|
147
|
-
const fullArea = {
|
|
148
|
-
x: padding,
|
|
149
|
-
y: padding + chrome.topHeight,
|
|
150
|
-
width: options.width - padding * 2,
|
|
151
|
-
height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2,
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
// Guard against negative dimensions
|
|
155
|
-
if (fullArea.width <= 0 || fullArea.height <= 0) {
|
|
156
|
-
return emptyLayout(chrome, theme, options, watermark);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// 5. Detect categorical vs quantitative mode
|
|
160
|
-
const isCategorical =
|
|
161
|
-
tilemapSpec.encoding.color !== undefined || tilemapSpec.colors !== undefined;
|
|
162
|
-
|
|
163
|
-
if (isCategorical) {
|
|
164
|
-
return compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark);
|
|
165
|
-
}
|
|
166
|
-
return compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ---------------------------------------------------------------------------
|
|
170
|
-
// Quantitative mode (opacity-based sequential coloring)
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
|
-
|
|
173
|
-
function compileQuantitative(
|
|
174
|
-
tilemapSpec: NormalizedTileMapSpec,
|
|
175
|
-
options: CompileOptions,
|
|
176
|
-
theme: ResolvedTheme,
|
|
177
|
-
chrome: ReturnType<typeof computeChrome>,
|
|
178
|
-
fullArea: { x: number; y: number; width: number; height: number },
|
|
179
|
-
isDarkMode: boolean | undefined,
|
|
180
|
-
watermark: boolean,
|
|
181
|
-
): TileMapLayout {
|
|
182
|
-
const stateField = tilemapSpec.encoding.state.field;
|
|
183
|
-
const valueField = tilemapSpec.encoding.value.field;
|
|
184
|
-
|
|
185
|
-
const stateValueMap = new Map<string, number>();
|
|
186
|
-
for (const row of tilemapSpec.data) {
|
|
187
|
-
const stateCode = String(row[stateField]);
|
|
188
|
-
const raw = row[valueField];
|
|
189
|
-
if (STATE_CODE_SET.has(stateCode) && raw !== null && raw !== undefined) {
|
|
190
|
-
const value = Number(raw);
|
|
191
|
-
if (!Number.isNaN(value)) {
|
|
192
|
-
stateValueMap.set(stateCode, value);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const values = Array.from(stateValueMap.values());
|
|
198
|
-
const min = values.length > 0 ? Math.min(...values) : 0;
|
|
199
|
-
const max = values.length > 0 ? Math.max(...values) : 100;
|
|
200
|
-
|
|
201
|
-
const paletteStops = [...(SEQUENTIAL_PALETTES[tilemapSpec.palette] ?? SEQUENTIAL_PALETTES.blue)];
|
|
202
|
-
const baseColor = isDarkMode ? paletteStops[0] : paletteStops[paletteStops.length - 1];
|
|
203
|
-
const opacityRange: [number, number] = isDarkMode ? [0.15, 1] : [0.2, 1];
|
|
204
|
-
const opacityScale = scaleLinear<number>().domain([min, max]).range(opacityRange).clamp(true);
|
|
205
|
-
|
|
206
|
-
const showLegend = tilemapSpec.legend?.show !== false;
|
|
207
|
-
const legendBarHeight = 6;
|
|
208
|
-
const legendLabelGap = 6;
|
|
209
|
-
const legendTotalHeight = showLegend ? legendBarHeight + legendLabelGap + 14 : 0;
|
|
210
|
-
|
|
211
|
-
const legendGap = showLegend ? 8 : 0;
|
|
212
|
-
const tileAreaHeight = fullArea.height - legendTotalHeight - legendGap;
|
|
213
|
-
const tilePositions = computeTilePositions(fullArea.width, tileAreaHeight, 5);
|
|
214
|
-
|
|
215
|
-
const tileGridOffsetX = fullArea.x + (fullArea.width - tilePositions.gridWidth) / 2;
|
|
216
|
-
const tileGridOffsetY = fullArea.y;
|
|
217
|
-
const legendX = tileGridOffsetX;
|
|
218
|
-
const legendY = tileGridOffsetY + tilePositions.gridHeight + legendGap;
|
|
219
|
-
const legendWidth = tilePositions.gridWidth;
|
|
220
|
-
|
|
221
|
-
// Encoding-level format (v8 canonical) wins over deprecated top-level valueFormat
|
|
222
|
-
const formatter = resolveFieldFormatter({
|
|
223
|
-
surfaceFormat: tilemapSpec.encoding.value.format ?? tilemapSpec.valueFormat,
|
|
224
|
-
values: Array.from(stateValueMap.values()),
|
|
225
|
-
});
|
|
226
|
-
const neutralFill = isDarkMode ? '#1e2a30' : '#e0e0e0';
|
|
227
|
-
const neutralStroke = isDarkMode ? 'rgba(255,255,255,0.08)' : '#d0d0d0';
|
|
228
|
-
|
|
229
|
-
const tiles: TileMapTileMark[] = [];
|
|
230
|
-
for (const { state: stateCode } of US_STATE_TILES) {
|
|
231
|
-
const pos = tilePositions.positions.get(stateCode);
|
|
232
|
-
if (!pos) continue;
|
|
233
|
-
|
|
234
|
-
const hasData = stateValueMap.has(stateCode);
|
|
235
|
-
const value = hasData ? stateValueMap.get(stateCode)! : null;
|
|
236
|
-
const opacity = hasData ? opacityScale(value!) : 0;
|
|
237
|
-
const fill = hasData ? baseColor : neutralFill;
|
|
238
|
-
const stroke = hasData
|
|
239
|
-
? isDarkMode
|
|
240
|
-
? 'rgba(255,255,255,0.1)'
|
|
241
|
-
: 'rgba(0,0,0,0.1)'
|
|
242
|
-
: neutralStroke;
|
|
243
|
-
const formattedValue = hasData ? formatter(value!) : '–';
|
|
244
|
-
|
|
245
|
-
tiles.push(
|
|
246
|
-
buildTileMark({
|
|
247
|
-
stateCode,
|
|
248
|
-
pos,
|
|
249
|
-
tileSize: tilePositions.tileSize,
|
|
250
|
-
gridOffsetX: tileGridOffsetX,
|
|
251
|
-
gridOffsetY: tileGridOffsetY,
|
|
252
|
-
fill,
|
|
253
|
-
fillOpacity: hasData ? opacity : 1,
|
|
254
|
-
stroke,
|
|
255
|
-
value,
|
|
256
|
-
formattedValue,
|
|
257
|
-
hasData,
|
|
258
|
-
theme,
|
|
259
|
-
isDarkMode: !!isDarkMode,
|
|
260
|
-
}),
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
assignAnimationIndices(tiles);
|
|
265
|
-
|
|
266
|
-
let gradientLegend: GradientLegendLayout | null = null;
|
|
267
|
-
if (showLegend) {
|
|
268
|
-
const numStops = 5;
|
|
269
|
-
const gradientColorStops: GradientColorStop[] = Array.from({ length: numStops }, (_, i) => {
|
|
270
|
-
const t = i / (numStops - 1);
|
|
271
|
-
const o = opacityRange[0] + t * (opacityRange[1] - opacityRange[0]);
|
|
272
|
-
return { offset: t, color: baseColor, opacity: o };
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
gradientLegend = {
|
|
276
|
-
type: 'gradient',
|
|
277
|
-
position: 'bottom',
|
|
278
|
-
bounds: { x: legendX, y: legendY, width: legendWidth, height: legendBarHeight },
|
|
279
|
-
labelStyle: {
|
|
280
|
-
fontFamily: theme.fonts.family,
|
|
281
|
-
fontSize: 11,
|
|
282
|
-
fontWeight: 400,
|
|
283
|
-
fill: theme.colors.text,
|
|
284
|
-
lineHeight: 1.2,
|
|
285
|
-
},
|
|
286
|
-
colorStops: gradientColorStops,
|
|
287
|
-
minLabel: formatter(min),
|
|
288
|
-
maxLabel: formatter(max),
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const tooltipDescriptors = buildTooltips(tiles);
|
|
293
|
-
const a11y = {
|
|
294
|
-
altText: `Tile map of US states showing values from ${formatter(min)} to ${formatter(max)}`,
|
|
295
|
-
dataTableFallback: tiles.map((t) => [t.stateCode, t.formattedValue]),
|
|
296
|
-
role: 'img',
|
|
297
|
-
keyboardNavigable: tiles.length > 0,
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
const resolvedAnimation: ResolvedAnimation | undefined = resolveAnimation(tilemapSpec.animation);
|
|
301
|
-
// fullArea.x is the (possibly compact-reduced) padding chosen in compileTileMap;
|
|
302
|
-
// reuse it so contentHeight matches the drawing area exactly.
|
|
303
|
-
const padding = fullArea.x;
|
|
304
|
-
const contentHeight =
|
|
305
|
-
tileGridOffsetY +
|
|
306
|
-
tilePositions.gridHeight +
|
|
307
|
-
legendGap +
|
|
308
|
-
legendTotalHeight +
|
|
309
|
-
chrome.bottomHeight +
|
|
310
|
-
padding;
|
|
311
|
-
|
|
312
|
-
return {
|
|
313
|
-
area: fullArea,
|
|
314
|
-
chrome,
|
|
315
|
-
tiles,
|
|
316
|
-
gradientLegend,
|
|
317
|
-
categoricalLegend: null,
|
|
318
|
-
tooltipDescriptors,
|
|
319
|
-
a11y,
|
|
320
|
-
theme,
|
|
321
|
-
width: options.width,
|
|
322
|
-
height: contentHeight,
|
|
323
|
-
animation: resolvedAnimation,
|
|
324
|
-
watermark,
|
|
325
|
-
measureText:
|
|
326
|
-
options.measureText ??
|
|
327
|
-
((text, fontSize) => ({ width: estimateTextWidth(text, fontSize), height: fontSize })),
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// ---------------------------------------------------------------------------
|
|
332
|
-
// Categorical mode (distinct fill colors per category)
|
|
333
|
-
// ---------------------------------------------------------------------------
|
|
334
|
-
|
|
335
|
-
function compileCategorical(
|
|
336
|
-
tilemapSpec: NormalizedTileMapSpec,
|
|
337
|
-
options: CompileOptions,
|
|
338
|
-
theme: ResolvedTheme,
|
|
339
|
-
chrome: ReturnType<typeof computeChrome>,
|
|
340
|
-
fullArea: { x: number; y: number; width: number; height: number },
|
|
341
|
-
isDarkMode: boolean | undefined,
|
|
342
|
-
watermark: boolean,
|
|
343
|
-
): TileMapLayout {
|
|
344
|
-
const stateField = tilemapSpec.encoding.state.field;
|
|
345
|
-
const colorField = tilemapSpec.encoding.color?.field ?? tilemapSpec.encoding.value.field;
|
|
346
|
-
|
|
347
|
-
// Extract category per state
|
|
348
|
-
const stateCategoryMap = new Map<string, string>();
|
|
349
|
-
for (const row of tilemapSpec.data) {
|
|
350
|
-
const stateCode = String(row[stateField]);
|
|
351
|
-
const raw = row[colorField];
|
|
352
|
-
if (STATE_CODE_SET.has(stateCode) && raw !== null && raw !== undefined) {
|
|
353
|
-
stateCategoryMap.set(stateCode, String(raw));
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Collect unique categories in data order
|
|
358
|
-
const categories: string[] = [];
|
|
359
|
-
const seen = new Set<string>();
|
|
360
|
-
for (const row of tilemapSpec.data) {
|
|
361
|
-
const raw = row[colorField];
|
|
362
|
-
if (raw !== null && raw !== undefined) {
|
|
363
|
-
const cat = String(raw);
|
|
364
|
-
if (!seen.has(cat)) {
|
|
365
|
-
seen.add(cat);
|
|
366
|
-
categories.push(cat);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Build category -> color mapping
|
|
372
|
-
const categoryColors = new Map<string, string>();
|
|
373
|
-
if (tilemapSpec.colors) {
|
|
374
|
-
for (const cat of categories) {
|
|
375
|
-
categoryColors.set(
|
|
376
|
-
cat,
|
|
377
|
-
tilemapSpec.colors[cat] ??
|
|
378
|
-
CATEGORICAL_PALETTE[categoryColors.size % CATEGORICAL_PALETTE.length],
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
} else {
|
|
382
|
-
for (let i = 0; i < categories.length; i++) {
|
|
383
|
-
categoryColors.set(categories[i], CATEGORICAL_PALETTE[i % CATEGORICAL_PALETTE.length]);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const showLegend = tilemapSpec.legend?.show !== false;
|
|
388
|
-
const legendLabelFontSize = 11;
|
|
389
|
-
const legendRowHeight = LEGEND_SWATCH_SIZE + 6;
|
|
390
|
-
const legendLabels = categories.map(formatCategoryLabel);
|
|
391
|
-
|
|
392
|
-
// Estimate legend rows against the full width to reserve tile-area height.
|
|
393
|
-
// The renderer wraps against the (narrower) grid width, so we recompute the
|
|
394
|
-
// real row count below and use that for final placement — never under-reserve.
|
|
395
|
-
const legendGap = showLegend ? 8 : 0;
|
|
396
|
-
const estimatedRows = showLegend
|
|
397
|
-
? countLegendRows(legendLabels, fullArea.width, legendLabelFontSize)
|
|
398
|
-
: 0;
|
|
399
|
-
const tileAreaHeight = fullArea.height - legendRowHeight * estimatedRows - legendGap;
|
|
400
|
-
const tilePositions = computeTilePositions(fullArea.width, tileAreaHeight, 5);
|
|
401
|
-
|
|
402
|
-
const tileGridOffsetX = fullArea.x + (fullArea.width - tilePositions.gridWidth) / 2;
|
|
403
|
-
const tileGridOffsetY = fullArea.y;
|
|
404
|
-
const legendX = tileGridOffsetX;
|
|
405
|
-
const legendY = tileGridOffsetY + tilePositions.gridHeight + legendGap;
|
|
406
|
-
const legendWidth = tilePositions.gridWidth;
|
|
407
|
-
|
|
408
|
-
// Reserve height for every wrapped row so a legend that spills onto a second
|
|
409
|
-
// line (common on narrow mobile widths) doesn't collide with the source text.
|
|
410
|
-
const legendRows = showLegend
|
|
411
|
-
? countLegendRows(legendLabels, legendWidth, legendLabelFontSize)
|
|
412
|
-
: 0;
|
|
413
|
-
const legendTotalHeight = showLegend ? legendRowHeight * legendRows : 0;
|
|
414
|
-
|
|
415
|
-
const neutralFill = isDarkMode ? '#1e2a30' : '#e0e0e0';
|
|
416
|
-
const neutralStroke = isDarkMode ? 'rgba(255,255,255,0.08)' : '#d0d0d0';
|
|
417
|
-
|
|
418
|
-
const tiles: TileMapTileMark[] = [];
|
|
419
|
-
for (const { state: stateCode } of US_STATE_TILES) {
|
|
420
|
-
const pos = tilePositions.positions.get(stateCode);
|
|
421
|
-
if (!pos) continue;
|
|
422
|
-
|
|
423
|
-
const hasData = stateCategoryMap.has(stateCode);
|
|
424
|
-
const category = hasData ? stateCategoryMap.get(stateCode)! : null;
|
|
425
|
-
const fill = hasData ? (categoryColors.get(category!) ?? neutralFill) : neutralFill;
|
|
426
|
-
const stroke = hasData
|
|
427
|
-
? isDarkMode
|
|
428
|
-
? 'rgba(255,255,255,0.1)'
|
|
429
|
-
: 'rgba(0,0,0,0.1)'
|
|
430
|
-
: neutralStroke;
|
|
431
|
-
const formattedValue = category ? formatCategoryLabel(category) : '–';
|
|
432
|
-
|
|
433
|
-
tiles.push(
|
|
434
|
-
buildTileMark({
|
|
435
|
-
stateCode,
|
|
436
|
-
pos,
|
|
437
|
-
tileSize: tilePositions.tileSize,
|
|
438
|
-
gridOffsetX: tileGridOffsetX,
|
|
439
|
-
gridOffsetY: tileGridOffsetY,
|
|
440
|
-
fill,
|
|
441
|
-
fillOpacity: 1,
|
|
442
|
-
stroke,
|
|
443
|
-
value: null,
|
|
444
|
-
formattedValue,
|
|
445
|
-
hasData,
|
|
446
|
-
theme,
|
|
447
|
-
isDarkMode: !!isDarkMode,
|
|
448
|
-
category: hasData ? category : undefined,
|
|
449
|
-
showValueLabels: false,
|
|
450
|
-
}),
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
assignAnimationIndices(tiles);
|
|
455
|
-
|
|
456
|
-
let categoricalLegend: CategoricalLegendLayout | null = null;
|
|
457
|
-
if (showLegend) {
|
|
458
|
-
const labelStyle: TextStyle = {
|
|
459
|
-
fontFamily: theme.fonts.family,
|
|
460
|
-
fontSize: legendLabelFontSize,
|
|
461
|
-
fontWeight: 400,
|
|
462
|
-
fill: theme.colors.text,
|
|
463
|
-
lineHeight: 1.2,
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
const entries: LegendEntry[] = categories.map((cat) => ({
|
|
467
|
-
label: formatCategoryLabel(cat),
|
|
468
|
-
color: categoryColors.get(cat)!,
|
|
469
|
-
shape: 'square' as const,
|
|
470
|
-
}));
|
|
471
|
-
|
|
472
|
-
categoricalLegend = {
|
|
473
|
-
type: 'categorical',
|
|
474
|
-
position: 'bottom',
|
|
475
|
-
bounds: { x: legendX, y: legendY, width: legendWidth, height: legendTotalHeight },
|
|
476
|
-
labelStyle,
|
|
477
|
-
entries,
|
|
478
|
-
swatchSize: LEGEND_SWATCH_SIZE,
|
|
479
|
-
swatchGap: LEGEND_SWATCH_GAP,
|
|
480
|
-
entryGap: LEGEND_ENTRY_GAP,
|
|
481
|
-
swatchChipFill: 'transparent',
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const tooltipDescriptors = buildTooltips(tiles, 'Category');
|
|
486
|
-
const categoryList = categories.map(formatCategoryLabel).join(', ');
|
|
487
|
-
const a11y = {
|
|
488
|
-
altText: `Tile map of US states showing categories: ${categoryList}`,
|
|
489
|
-
dataTableFallback: tiles.map((t) => [t.stateCode, t.formattedValue]),
|
|
490
|
-
role: 'img',
|
|
491
|
-
keyboardNavigable: tiles.length > 0,
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
const resolvedAnimation: ResolvedAnimation | undefined = resolveAnimation(tilemapSpec.animation);
|
|
495
|
-
// fullArea.x is the (possibly compact-reduced) padding chosen in compileTileMap;
|
|
496
|
-
// reuse it so contentHeight matches the drawing area exactly.
|
|
497
|
-
const padding = fullArea.x;
|
|
498
|
-
const contentHeight =
|
|
499
|
-
tileGridOffsetY +
|
|
500
|
-
tilePositions.gridHeight +
|
|
501
|
-
legendGap +
|
|
502
|
-
legendTotalHeight +
|
|
503
|
-
chrome.bottomHeight +
|
|
504
|
-
padding;
|
|
505
|
-
|
|
506
|
-
return {
|
|
507
|
-
area: fullArea,
|
|
508
|
-
chrome,
|
|
509
|
-
tiles,
|
|
510
|
-
gradientLegend: null,
|
|
511
|
-
categoricalLegend,
|
|
512
|
-
tooltipDescriptors,
|
|
513
|
-
a11y,
|
|
514
|
-
theme,
|
|
515
|
-
width: options.width,
|
|
516
|
-
height: contentHeight,
|
|
517
|
-
animation: resolvedAnimation,
|
|
518
|
-
watermark,
|
|
519
|
-
measureText:
|
|
520
|
-
options.measureText ??
|
|
521
|
-
((text, fontSize) => ({ width: estimateTextWidth(text, fontSize), height: fontSize })),
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// ---------------------------------------------------------------------------
|
|
526
|
-
// Shared helpers
|
|
527
|
-
// ---------------------------------------------------------------------------
|
|
528
|
-
|
|
529
|
-
interface TileMarkOptions {
|
|
530
|
-
stateCode: string;
|
|
531
|
-
pos: { x: number; y: number };
|
|
532
|
-
tileSize: number;
|
|
533
|
-
gridOffsetX: number;
|
|
534
|
-
gridOffsetY: number;
|
|
535
|
-
fill: string;
|
|
536
|
-
fillOpacity: number;
|
|
537
|
-
stroke: string;
|
|
538
|
-
value: number | null;
|
|
539
|
-
formattedValue: string;
|
|
540
|
-
hasData: boolean;
|
|
541
|
-
theme: ResolvedTheme;
|
|
542
|
-
isDarkMode: boolean;
|
|
543
|
-
category?: string | null;
|
|
544
|
-
showValueLabels?: boolean;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function buildTileMark(opts: TileMarkOptions): TileMapTileMark {
|
|
548
|
-
const {
|
|
549
|
-
stateCode,
|
|
550
|
-
pos,
|
|
551
|
-
tileSize,
|
|
552
|
-
gridOffsetX,
|
|
553
|
-
gridOffsetY,
|
|
554
|
-
fill,
|
|
555
|
-
fillOpacity,
|
|
556
|
-
stroke,
|
|
557
|
-
value,
|
|
558
|
-
formattedValue,
|
|
559
|
-
hasData,
|
|
560
|
-
theme,
|
|
561
|
-
isDarkMode,
|
|
562
|
-
category,
|
|
563
|
-
showValueLabels = true,
|
|
564
|
-
} = opts;
|
|
565
|
-
const tileCenterX = gridOffsetX + pos.x + tileSize / 2;
|
|
566
|
-
const tileTopY = gridOffsetY + pos.y;
|
|
567
|
-
|
|
568
|
-
const textColor = hasData ? pickLabelColor(fill, isDarkMode) : '#ffffff';
|
|
569
|
-
const isLightText = textColor === '#ffffff';
|
|
570
|
-
|
|
571
|
-
// A two-line layout (code over value) only fits once tiles are large enough;
|
|
572
|
-
// below that we drop the value and center the code so nothing overflows.
|
|
573
|
-
const showValue = showValueLabels && tileSize >= 24;
|
|
574
|
-
|
|
575
|
-
// Scale the code font with the tile so a 2-char label always fits its box,
|
|
576
|
-
// rather than snapping between two fixed sizes. A 2-char string is ~1.2x the
|
|
577
|
-
// font size wide; capping font at ~0.5x tile width keeps it inside the tile
|
|
578
|
-
// with margin, then clamp to a legible [6.5, 11] range.
|
|
579
|
-
const codeFontSize = clamp(Math.round(tileSize * 0.5 * 10) / 10, 6.5, 11);
|
|
580
|
-
const valueFontSize = clamp(Math.round(tileSize * 0.42 * 10) / 10, 6.5, 10);
|
|
581
|
-
|
|
582
|
-
const labelStyle: TextStyle = {
|
|
583
|
-
fontFamily: theme.fonts.family,
|
|
584
|
-
fontSize: codeFontSize,
|
|
585
|
-
fontWeight: 700,
|
|
586
|
-
fill: textColor,
|
|
587
|
-
lineHeight: 1.2,
|
|
588
|
-
};
|
|
589
|
-
|
|
590
|
-
const valueLabelStyle: TextStyle = {
|
|
591
|
-
fontFamily: theme.fonts.family,
|
|
592
|
-
fontSize: valueFontSize,
|
|
593
|
-
fontWeight: 300,
|
|
594
|
-
fill: isLightText ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.45)',
|
|
595
|
-
lineHeight: 1.2,
|
|
596
|
-
};
|
|
597
|
-
|
|
598
|
-
// Code sits in the upper third when paired with a value, otherwise centered.
|
|
599
|
-
const codeCenterY = showValue ? tileTopY + tileSize * 0.28 : tileTopY + tileSize * 0.5;
|
|
600
|
-
|
|
601
|
-
const valueLabel = showValue
|
|
602
|
-
? {
|
|
603
|
-
text: formattedValue,
|
|
604
|
-
x: tileCenterX,
|
|
605
|
-
y: tileTopY + tileSize * 0.78,
|
|
606
|
-
style: valueLabelStyle,
|
|
607
|
-
visible: true,
|
|
608
|
-
}
|
|
609
|
-
: { text: '', x: 0, y: 0, style: valueLabelStyle, visible: false };
|
|
610
|
-
|
|
611
|
-
// Keep corners visibly rounded-rectangular, never near-circular on small tiles.
|
|
612
|
-
const cornerRadius = Math.min(TILE_CORNER_RADIUS, Math.round(tileSize * 0.22));
|
|
613
|
-
|
|
614
|
-
const data: Record<string, unknown> = {
|
|
615
|
-
state: stateCode,
|
|
616
|
-
value,
|
|
617
|
-
stateName: STATE_NAMES[stateCode] ?? stateCode,
|
|
618
|
-
};
|
|
619
|
-
if (category !== undefined) {
|
|
620
|
-
data.category = category;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
return {
|
|
624
|
-
type: 'tile' as const,
|
|
625
|
-
stateCode,
|
|
626
|
-
x: gridOffsetX + pos.x,
|
|
627
|
-
y: tileTopY,
|
|
628
|
-
size: tileSize,
|
|
629
|
-
fill,
|
|
630
|
-
fillOpacity,
|
|
631
|
-
stroke,
|
|
632
|
-
strokeWidth: TILE_STROKE_WIDTH,
|
|
633
|
-
cornerRadius,
|
|
634
|
-
value,
|
|
635
|
-
formattedValue,
|
|
636
|
-
hasData,
|
|
637
|
-
label: {
|
|
638
|
-
text: stateCode,
|
|
639
|
-
x: tileCenterX,
|
|
640
|
-
y: codeCenterY,
|
|
641
|
-
style: labelStyle,
|
|
642
|
-
visible: true,
|
|
643
|
-
},
|
|
644
|
-
valueLabel,
|
|
645
|
-
data,
|
|
646
|
-
aria: {
|
|
647
|
-
role: 'img',
|
|
648
|
-
label: `${STATE_NAMES[stateCode] ?? stateCode}: ${formattedValue}`,
|
|
649
|
-
},
|
|
650
|
-
animationIndex: 0,
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function assignAnimationIndices(tiles: TileMapTileMark[]): void {
|
|
655
|
-
const indices = Array.from({ length: tiles.length }, (_, i) => i);
|
|
656
|
-
let seed = 42;
|
|
657
|
-
for (let i = indices.length - 1; i > 0; i--) {
|
|
658
|
-
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
|
659
|
-
const j = seed % (i + 1);
|
|
660
|
-
[indices[i], indices[j]] = [indices[j], indices[i]];
|
|
661
|
-
}
|
|
662
|
-
for (let i = 0; i < tiles.length; i++) {
|
|
663
|
-
tiles[i].animationIndex = indices[i];
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function buildTooltips(
|
|
668
|
-
tiles: TileMapTileMark[],
|
|
669
|
-
fieldLabel = 'Value',
|
|
670
|
-
): Map<string, TooltipContent> {
|
|
671
|
-
const tooltipDescriptors = new Map<string, TooltipContent>();
|
|
672
|
-
for (const tile of tiles) {
|
|
673
|
-
const fields: TooltipField[] = [{ label: fieldLabel, value: tile.formattedValue }];
|
|
674
|
-
tooltipDescriptors.set(tile.stateCode, {
|
|
675
|
-
title: STATE_NAMES[tile.stateCode] ?? tile.stateCode,
|
|
676
|
-
fields,
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
return tooltipDescriptors;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
function formatCategoryLabel(category: string): string {
|
|
683
|
-
return category.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Count how many rows a horizontal categorical legend wraps into for a given
|
|
688
|
-
* available width. Mirrors the wrap logic in the legend renderer so the compiler
|
|
689
|
-
* can reserve enough vertical space and avoid the legend overlapping the source
|
|
690
|
-
* line on narrow (mobile) containers.
|
|
691
|
-
*/
|
|
692
|
-
function countLegendRows(labels: string[], availableWidth: number, fontSize: number): number {
|
|
693
|
-
if (labels.length === 0) return 0;
|
|
694
|
-
let rows = 1;
|
|
695
|
-
let offsetX = 0;
|
|
696
|
-
for (let i = 0; i < labels.length; i++) {
|
|
697
|
-
const labelWidth = estimateTextWidth(labels[i], fontSize);
|
|
698
|
-
const entryWidth = LEGEND_SWATCH_SIZE + LEGEND_SWATCH_GAP + labelWidth + LEGEND_ENTRY_GAP;
|
|
699
|
-
if (i > 0 && offsetX + entryWidth > availableWidth) {
|
|
700
|
-
rows++;
|
|
701
|
-
offsetX = entryWidth;
|
|
702
|
-
} else {
|
|
703
|
-
offsetX += entryWidth;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
return rows;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// ---------------------------------------------------------------------------
|
|
710
|
-
// Empty layout fallback
|
|
711
|
-
// ---------------------------------------------------------------------------
|
|
712
|
-
|
|
713
|
-
function emptyLayout(
|
|
714
|
-
chrome: ReturnType<typeof computeChrome>,
|
|
715
|
-
theme: ResolvedTheme,
|
|
716
|
-
options: CompileOptions,
|
|
717
|
-
watermark: boolean,
|
|
718
|
-
): TileMapLayout {
|
|
719
|
-
return {
|
|
720
|
-
area: { x: 0, y: 0, width: 0, height: 0 },
|
|
721
|
-
chrome,
|
|
722
|
-
tiles: [],
|
|
723
|
-
gradientLegend: {
|
|
724
|
-
type: 'gradient',
|
|
725
|
-
position: 'bottom',
|
|
726
|
-
bounds: { x: 0, y: 0, width: 0, height: 0 },
|
|
727
|
-
labelStyle: {
|
|
728
|
-
fontFamily: theme.fonts.family,
|
|
729
|
-
fontSize: 11,
|
|
730
|
-
fontWeight: 400,
|
|
731
|
-
fill: theme.colors.text,
|
|
732
|
-
lineHeight: 1.2,
|
|
733
|
-
},
|
|
734
|
-
colorStops: [],
|
|
735
|
-
minLabel: '0',
|
|
736
|
-
maxLabel: '0',
|
|
737
|
-
},
|
|
738
|
-
categoricalLegend: null,
|
|
739
|
-
tooltipDescriptors: new Map(),
|
|
740
|
-
a11y: {
|
|
741
|
-
altText: 'Empty tile map',
|
|
742
|
-
dataTableFallback: [],
|
|
743
|
-
role: 'img',
|
|
744
|
-
keyboardNavigable: false,
|
|
745
|
-
},
|
|
746
|
-
theme,
|
|
747
|
-
width: options.width,
|
|
748
|
-
height: options.height,
|
|
749
|
-
watermark,
|
|
750
|
-
animation: undefined,
|
|
751
|
-
measureText:
|
|
752
|
-
options.measureText ??
|
|
753
|
-
((text, fontSize) => ({ width: estimateTextWidth(text, fontSize), height: fontSize })),
|
|
754
|
-
};
|
|
755
|
-
}
|