@cfasim-ui/charts 0.7.8 → 0.8.1
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/BarChart/BarChart.d.ts +9 -26
- package/dist/ChartMenu/ChartMenu.d.ts +3 -2
- package/dist/ChartTooltip/ChartTooltip.d.ts +9 -12
- package/dist/ChoroplethMap/ChoroplethMap.d.ts +42 -190
- package/dist/ChoroplethMap/ChoroplethTooltip.d.ts +20 -27
- package/dist/ChoroplethMap/canvasLayer.d.ts +23 -6
- package/dist/ChoroplethMap/cityLayout.d.ts +3 -2
- package/dist/ChoroplethMap/mixedGeo.d.ts +74 -0
- package/dist/ChoroplethMap/mixedGeo.test.d.ts +1 -0
- package/dist/DataTable/DataTable.d.ts +3 -2
- package/dist/LineChart/LineChart.d.ts +9 -26
- package/dist/_shared/ChartAnnotations.d.ts +3 -2
- package/dist/_shared/ChartZoomControls.d.ts +3 -2
- package/dist/_shared/index.d.ts +2 -1
- package/dist/_shared/mapTheme.d.ts +159 -0
- package/dist/_shared/mapTheme.test.d.ts +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1821 -1462
- package/docs/BarChart.md +776 -0
- package/docs/ChoroplethMap.md +1394 -0
- package/docs/DataTable.md +386 -0
- package/docs/LineChart.md +1267 -0
- package/docs/index.json +56 -0
- package/package.json +25 -21
- package/src/BarChart/BarChart.md +743 -0
- package/src/BarChart/BarChart.vue +1901 -0
- package/src/ChartMenu/ChartMenu.vue +220 -0
- package/src/ChartMenu/download.ts +120 -0
- package/src/ChartTooltip/ChartTooltip.vue +97 -0
- package/src/ChoroplethMap/ChoroplethMap.md +1354 -0
- package/src/ChoroplethMap/ChoroplethMap.vue +3778 -0
- package/src/ChoroplethMap/ChoroplethTooltip.vue +103 -0
- package/src/ChoroplethMap/canvasLayer.ts +373 -0
- package/src/ChoroplethMap/cityLayout.ts +262 -0
- package/src/ChoroplethMap/hsaMapping.ts +4116 -0
- package/src/ChoroplethMap/mixedGeo.ts +201 -0
- package/src/DataTable/DataTable.md +372 -0
- package/src/DataTable/DataTable.vue +406 -0
- package/src/LineChart/LineChart.md +1225 -0
- package/src/LineChart/LineChart.vue +1555 -0
- package/src/_shared/ChartAnnotations.vue +420 -0
- package/src/_shared/ChartZoomControls.vue +138 -0
- package/src/_shared/annotations.ts +106 -0
- package/src/_shared/axes.ts +69 -0
- package/src/_shared/chartProps.ts +201 -0
- package/src/_shared/computeTicks.ts +42 -0
- package/src/_shared/contrast.ts +100 -0
- package/src/_shared/dateAxis.ts +501 -0
- package/src/_shared/index.ts +78 -0
- package/src/_shared/mapTheme.ts +551 -0
- package/src/_shared/scale.ts +86 -0
- package/src/_shared/seriesCsv.ts +68 -0
- package/src/_shared/touch.ts +8 -0
- package/src/_shared/useChartFoundation.ts +175 -0
- package/src/_shared/useChartFullscreen.ts +254 -0
- package/src/_shared/useChartMenu.ts +111 -0
- package/src/_shared/useChartPadding.ts +235 -0
- package/src/_shared/useChartSize.ts +58 -0
- package/src/_shared/useChartTooltip.ts +205 -0
- package/src/env.d.ts +4 -0
- package/src/hsa-mapping.ts +1 -0
- package/src/index.ts +41 -0
- package/src/tooltip-position.ts +55 -0
- package/src/us-cities/data.ts +1371 -0
- package/src/us-cities/index.ts +122 -0
- package/src/us-cities.ts +7 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Pure layout for the ChoroplethMap city overlay: project each city, apply the
|
|
2
|
+
// current zoom transform, and greedily place its name label so labels never
|
|
3
|
+
// overlap. Kept free of Vue/DOM so it can be unit tested in isolation.
|
|
4
|
+
//
|
|
5
|
+
// All output coordinates are in the map's canonical space (0..width, 0..height)
|
|
6
|
+
// — the same space the projection emits — so both render backends can consume
|
|
7
|
+
// them. Pixel-denominated sizes (font, dot radius) are divided by `viewScale`
|
|
8
|
+
// (rendered-px / canonical-width) to convert into canonical units, which keeps
|
|
9
|
+
// markers and labels a constant on-screen size regardless of zoom.
|
|
10
|
+
|
|
11
|
+
/** A city to render. `coordinates` is `[longitude, latitude]`. */
|
|
12
|
+
export interface CityMarker {
|
|
13
|
+
name: string;
|
|
14
|
+
coordinates: [number, number];
|
|
15
|
+
/**
|
|
16
|
+
* Mark as a capital: the label gets first pick during placement and is never
|
|
17
|
+
* dropped for collisions. It renders like any other label (with a
|
|
18
|
+
* `choropleth-city-label-capital` class to style if you want), and the marker
|
|
19
|
+
* is a plain dot, same as any other city.
|
|
20
|
+
*/
|
|
21
|
+
capital?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Minimum zoom scale at which this city appears (level-of-detail): it shows
|
|
24
|
+
* only once the map is zoomed to `scaleK >= minZoom`. Lets you reveal the
|
|
25
|
+
* biggest cities first and progressively add more as the user zooms in.
|
|
26
|
+
* Falls back to the map's `citiesMinZoom` prop when unset. Only applies on a
|
|
27
|
+
* zoomable map. See `nationalCityMarkers` / `stateCityMarkers`, which assign
|
|
28
|
+
* tiers by population.
|
|
29
|
+
*/
|
|
30
|
+
minZoom?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A resolved label anchor in canonical coordinates. */
|
|
34
|
+
export interface PlacedLabel {
|
|
35
|
+
/** Anchor x (interpretation depends on `anchor`). */
|
|
36
|
+
x: number;
|
|
37
|
+
/** Vertical center of the text (render with `dominant-baseline: central`). */
|
|
38
|
+
y: number;
|
|
39
|
+
anchor: "start" | "middle" | "end";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A city resolved to canonical coordinates, with an optional placed label. */
|
|
43
|
+
export interface PlacedCity {
|
|
44
|
+
name: string;
|
|
45
|
+
capital: boolean;
|
|
46
|
+
/** Dot/star center. */
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
/** Marker radius in canonical units. */
|
|
50
|
+
radius: number;
|
|
51
|
+
/** Null when no non-overlapping label position was found. */
|
|
52
|
+
label: PlacedLabel | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** d3-zoom-style transform: `screen = k * point + [x, y]`. */
|
|
56
|
+
export interface ZoomTransformLike {
|
|
57
|
+
k: number;
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface LayoutOptions {
|
|
63
|
+
/** Canonical viewport width (e.g. 1000). */
|
|
64
|
+
width: number;
|
|
65
|
+
/** Canonical viewport height. */
|
|
66
|
+
height: number;
|
|
67
|
+
/** Rendered px per canonical unit; converts px sizes into canonical units. */
|
|
68
|
+
viewScale: number;
|
|
69
|
+
/** Label font size in px. */
|
|
70
|
+
labelPx?: number;
|
|
71
|
+
/** Dot radius in px (capitals render larger, see CAPITAL_SCALE). */
|
|
72
|
+
dotPx?: number;
|
|
73
|
+
/** Gap between marker edge and label in px. */
|
|
74
|
+
gapPx?: number;
|
|
75
|
+
/** How far outside the viewport (canonical units) a dot may sit and still
|
|
76
|
+
* render. Cities beyond this are culled. */
|
|
77
|
+
margin?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** An axis-aligned bounding box in canonical coordinates. */
|
|
81
|
+
export interface LabelRect {
|
|
82
|
+
x0: number;
|
|
83
|
+
y0: number;
|
|
84
|
+
x1: number;
|
|
85
|
+
y1: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type Box = LabelRect;
|
|
89
|
+
|
|
90
|
+
const IDENTITY: ZoomTransformLike = { k: 1, x: 0, y: 0 };
|
|
91
|
+
|
|
92
|
+
// Average glyph advance as a fraction of font size for a typical sans font.
|
|
93
|
+
// Only used to estimate label width for collision — exactness isn't required.
|
|
94
|
+
const AVG_ADVANCE = 0.56;
|
|
95
|
+
// Text box height as a multiple of font size (a little vertical padding).
|
|
96
|
+
const LINE_HEIGHT = 1.15;
|
|
97
|
+
|
|
98
|
+
/** Rough label width in px (no DOM measurement). */
|
|
99
|
+
export function estimateTextWidth(text: string, fontPx: number): number {
|
|
100
|
+
return text.length * AVG_ADVANCE * fontPx;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function round(n: number): number {
|
|
104
|
+
return Math.round(n * 100) / 100;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** True when two axis-aligned boxes overlap (shared edges don't count). */
|
|
108
|
+
export function rectsOverlap(a: LabelRect, b: LabelRect): boolean {
|
|
109
|
+
return !(a.x1 <= b.x0 || a.x0 >= b.x1 || a.y1 <= b.y0 || a.y0 >= b.y1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The canonical bounding box of a placed label, or null if it has none. Uses
|
|
114
|
+
* the same width/height model as the layout, so callers can reason about label
|
|
115
|
+
* extents (e.g. tests asserting non-overlap).
|
|
116
|
+
*/
|
|
117
|
+
export function placedLabelBox(
|
|
118
|
+
placed: PlacedCity,
|
|
119
|
+
opts: { labelPx?: number; viewScale: number },
|
|
120
|
+
): LabelRect | null {
|
|
121
|
+
if (!placed.label) return null;
|
|
122
|
+
const { labelPx = 11, viewScale } = opts;
|
|
123
|
+
const vs = viewScale || 1;
|
|
124
|
+
const w = estimateTextWidth(placed.name, labelPx) / vs;
|
|
125
|
+
const h = (labelPx * LINE_HEIGHT) / vs;
|
|
126
|
+
return boxFor(placed.label.x, placed.label.y, w, h, placed.label.anchor);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function within(box: Box, width: number, height: number): boolean {
|
|
130
|
+
return box.x0 >= 0 && box.y0 >= 0 && box.x1 <= width && box.y1 <= height;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function boxFor(
|
|
134
|
+
anchorX: number,
|
|
135
|
+
centerY: number,
|
|
136
|
+
w: number,
|
|
137
|
+
h: number,
|
|
138
|
+
anchor: PlacedLabel["anchor"],
|
|
139
|
+
): Box {
|
|
140
|
+
const y0 = centerY - h / 2;
|
|
141
|
+
const y1 = centerY + h / 2;
|
|
142
|
+
if (anchor === "start") return { x0: anchorX, y0, x1: anchorX + w, y1 };
|
|
143
|
+
if (anchor === "end") return { x0: anchorX - w, y0, x1: anchorX, y1 };
|
|
144
|
+
return { x0: anchorX - w / 2, y0, x1: anchorX + w / 2, y1 };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Project + place cities. Returns one `PlacedCity` per city that projects
|
|
149
|
+
* inside the (margin-expanded) viewport, in draw order. `label` is null when no
|
|
150
|
+
* candidate anchor avoided every already-placed label and every dot.
|
|
151
|
+
*/
|
|
152
|
+
export function layoutCities(
|
|
153
|
+
cities: readonly CityMarker[],
|
|
154
|
+
project: (lngLat: [number, number]) => [number, number] | null,
|
|
155
|
+
transform: ZoomTransformLike = IDENTITY,
|
|
156
|
+
options: LayoutOptions,
|
|
157
|
+
): PlacedCity[] {
|
|
158
|
+
const {
|
|
159
|
+
width,
|
|
160
|
+
height,
|
|
161
|
+
viewScale,
|
|
162
|
+
labelPx = 11,
|
|
163
|
+
dotPx = 2.4,
|
|
164
|
+
gapPx = 4,
|
|
165
|
+
margin = 0,
|
|
166
|
+
} = options;
|
|
167
|
+
const vs = viewScale || 1;
|
|
168
|
+
const font = labelPx / vs;
|
|
169
|
+
const dotR = dotPx / vs;
|
|
170
|
+
const gap = gapPx / vs;
|
|
171
|
+
const labelH = font * LINE_HEIGHT;
|
|
172
|
+
|
|
173
|
+
// Project + cull, preserving input order and capital priority.
|
|
174
|
+
type Kept = {
|
|
175
|
+
marker: CityMarker;
|
|
176
|
+
x: number;
|
|
177
|
+
y: number;
|
|
178
|
+
radius: number;
|
|
179
|
+
order: number;
|
|
180
|
+
};
|
|
181
|
+
const kept: Kept[] = [];
|
|
182
|
+
cities.forEach((marker, i) => {
|
|
183
|
+
const p = project(marker.coordinates);
|
|
184
|
+
if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) return;
|
|
185
|
+
const x = transform.k * p[0] + transform.x;
|
|
186
|
+
const y = transform.k * p[1] + transform.y;
|
|
187
|
+
if (x < -margin || x > width + margin || y < -margin || y > height + margin)
|
|
188
|
+
return;
|
|
189
|
+
kept.push({
|
|
190
|
+
marker,
|
|
191
|
+
x,
|
|
192
|
+
y,
|
|
193
|
+
radius: dotR,
|
|
194
|
+
order: i,
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Capitals first (stable), then input order.
|
|
199
|
+
kept.sort(
|
|
200
|
+
(a, b) =>
|
|
201
|
+
(b.marker.capital ? 1 : 0) - (a.marker.capital ? 1 : 0) ||
|
|
202
|
+
a.order - b.order,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Every dot is an obstacle for every label, so precompute all dot boxes.
|
|
206
|
+
const dotBoxes: Box[] = kept.map((k) => ({
|
|
207
|
+
x0: k.x - k.radius,
|
|
208
|
+
y0: k.y - k.radius,
|
|
209
|
+
x1: k.x + k.radius,
|
|
210
|
+
y1: k.y + k.radius,
|
|
211
|
+
}));
|
|
212
|
+
|
|
213
|
+
const placedLabels: Box[] = [];
|
|
214
|
+
const result: PlacedCity[] = [];
|
|
215
|
+
|
|
216
|
+
for (const k of kept) {
|
|
217
|
+
const w = estimateTextWidth(k.marker.name, labelPx) / vs;
|
|
218
|
+
const mR = k.radius;
|
|
219
|
+
const candidates: {
|
|
220
|
+
anchorX: number;
|
|
221
|
+
cy: number;
|
|
222
|
+
anchor: PlacedLabel["anchor"];
|
|
223
|
+
}[] = [
|
|
224
|
+
{ anchorX: k.x + mR + gap, cy: k.y, anchor: "start" },
|
|
225
|
+
{ anchorX: k.x - mR - gap, cy: k.y, anchor: "end" },
|
|
226
|
+
{ anchorX: k.x, cy: k.y - mR - gap - labelH / 2, anchor: "middle" },
|
|
227
|
+
{ anchorX: k.x, cy: k.y + mR + gap + labelH / 2, anchor: "middle" },
|
|
228
|
+
{ anchorX: k.x + mR * 0.7 + gap, cy: k.y - mR - gap, anchor: "start" },
|
|
229
|
+
{ anchorX: k.x + mR * 0.7 + gap, cy: k.y + mR + gap, anchor: "start" },
|
|
230
|
+
{ anchorX: k.x - mR * 0.7 - gap, cy: k.y - mR - gap, anchor: "end" },
|
|
231
|
+
{ anchorX: k.x - mR * 0.7 - gap, cy: k.y + mR + gap, anchor: "end" },
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
// Capitals are placed first (so no labels exist yet to collide with) and
|
|
235
|
+
// are exempt from dot-avoidance: the capital is the most important label
|
|
236
|
+
// and must not be dropped just because it sits amid dense neighboring
|
|
237
|
+
// dots (e.g. DC in the mid-Atlantic). It still stays label-to-label
|
|
238
|
+
// disjoint and in-bounds, and other labels then avoid its box.
|
|
239
|
+
const avoidDots = !k.marker.capital;
|
|
240
|
+
let label: PlacedLabel | null = null;
|
|
241
|
+
for (const c of candidates) {
|
|
242
|
+
const box = boxFor(c.anchorX, c.cy, w, labelH, c.anchor);
|
|
243
|
+
if (!within(box, width, height)) continue;
|
|
244
|
+
if (placedLabels.some((b) => rectsOverlap(box, b))) continue;
|
|
245
|
+
if (avoidDots && dotBoxes.some((b) => rectsOverlap(box, b))) continue;
|
|
246
|
+
label = { x: round(c.anchorX), y: round(c.cy), anchor: c.anchor };
|
|
247
|
+
placedLabels.push(box);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
result.push({
|
|
252
|
+
name: k.marker.name,
|
|
253
|
+
capital: !!k.marker.capital,
|
|
254
|
+
x: round(k.x),
|
|
255
|
+
y: round(k.y),
|
|
256
|
+
radius: round(mR),
|
|
257
|
+
label,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return result;
|
|
262
|
+
}
|