@cfasim-ui/docs 0.4.10 → 0.4.12
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/charts/BarChart/BarChart.md +54 -1
- package/charts/BarChart/BarChart.vue +248 -48
- package/charts/ChartMenu/ChartMenu.vue +3 -0
- package/charts/ChoroplethMap/ChoroplethMap.md +8 -2
- package/charts/ChoroplethMap/ChoroplethMap.vue +102 -11
- package/charts/LineChart/LineChart.md +226 -9
- package/charts/LineChart/LineChart.vue +381 -120
- package/charts/_shared/ChartAnnotations.vue +56 -17
- package/charts/_shared/annotations.ts +14 -9
- package/charts/_shared/chartProps.ts +39 -0
- package/charts/_shared/dateAxis.ts +507 -0
- package/charts/_shared/fullscreen.css +51 -0
- package/charts/_shared/index.ts +25 -1
- package/charts/_shared/useChartFoundation.ts +57 -27
- package/charts/_shared/useChartFullscreen.ts +104 -0
- package/charts/_shared/useChartMenu.ts +19 -4
- package/charts/_shared/useChartPadding.ts +152 -18
- package/charts/_shared/useChartSize.ts +14 -5
- package/charts/hsa-mapping.ts +1 -0
- package/charts/index.ts +2 -1
- package/index.json +1 -1
- package/package.json +1 -1
- package/shared/useUrlParams.ts +203 -40
package/shared/useUrlParams.ts
CHANGED
|
@@ -28,6 +28,13 @@ export interface UrlParamsRoute {
|
|
|
28
28
|
*/
|
|
29
29
|
export type DefaultsInput<T> = T | Ref<T> | (() => T | undefined);
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* A path into the params object. Either a top-level key (autocompleted from
|
|
33
|
+
* `T`) or a dotted path to a nested leaf (`"nested.foo"`). Matches the leaf
|
|
34
|
+
* exactly or any descendant when used as `include`/`ignore`.
|
|
35
|
+
*/
|
|
36
|
+
export type ParamPath<T> = keyof T | (string & {});
|
|
37
|
+
|
|
31
38
|
export type UrlParamsOptions<T> = {
|
|
32
39
|
debounceMs?: number;
|
|
33
40
|
/**
|
|
@@ -39,14 +46,15 @@ export type UrlParamsOptions<T> = {
|
|
|
39
46
|
router?: UrlParamsRouter;
|
|
40
47
|
route?: UrlParamsRoute;
|
|
41
48
|
/**
|
|
42
|
-
* Only sync these
|
|
43
|
-
*
|
|
49
|
+
* Only sync these paths. Accepts top-level keys or dotted paths to nested
|
|
50
|
+
* leaves; a parent path implicitly covers all of its descendants.
|
|
44
51
|
*/
|
|
45
|
-
include?:
|
|
52
|
+
include?: ParamPath<T>[];
|
|
46
53
|
/**
|
|
47
|
-
* Sync all
|
|
54
|
+
* Sync all paths except these. Same matching rules as `include`. Ignored
|
|
55
|
+
* if `include` is provided.
|
|
48
56
|
*/
|
|
49
|
-
ignore?:
|
|
57
|
+
ignore?: ParamPath<T>[];
|
|
50
58
|
};
|
|
51
59
|
|
|
52
60
|
export type ResetOptions = {
|
|
@@ -74,16 +82,134 @@ export function deserialize(raw: string, defaultValue: unknown): unknown {
|
|
|
74
82
|
return raw;
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
// A "container" is any non-null object — plain object or array. A "branch"
|
|
86
|
+
// is a container we recurse into when flattening defaults: plain objects
|
|
87
|
+
// always recurse; arrays only recurse when at least one element is itself a
|
|
88
|
+
// plain object (so `[1, 2, 3]` stays a comma-joined leaf, while
|
|
89
|
+
// `[{foo: 1}, {foo: 2}]` becomes `items.0.foo=…&items.1.foo=…`).
|
|
90
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
91
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isContainer(
|
|
95
|
+
value: unknown,
|
|
96
|
+
): value is Record<string, unknown> | unknown[] {
|
|
97
|
+
return value !== null && typeof value === "object";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isBranch(value: unknown): boolean {
|
|
101
|
+
if (isPlainObject(value)) return true;
|
|
102
|
+
// Recurse into arrays whose elements are themselves containers (plain
|
|
103
|
+
// objects or arrays), so array-of-arrays and array-of-objects map onto
|
|
104
|
+
// indexed dotted paths. Arrays of primitives stay as comma-joined leaves.
|
|
105
|
+
if (Array.isArray(value)) return value.some(isContainer);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function* flattenLeaves(
|
|
110
|
+
obj: Record<string, unknown> | unknown[],
|
|
111
|
+
prefix: string[] = [],
|
|
112
|
+
): Generator<[string[], unknown]> {
|
|
113
|
+
const entries: Array<[string, unknown]> = Array.isArray(obj)
|
|
114
|
+
? obj.map((v, i) => [String(i), v])
|
|
115
|
+
: Object.entries(obj);
|
|
116
|
+
for (const [k, v] of entries) {
|
|
117
|
+
const path = [...prefix, k];
|
|
118
|
+
if (isBranch(v)) {
|
|
119
|
+
yield* flattenLeaves(v as Record<string, unknown> | unknown[], path);
|
|
120
|
+
} else {
|
|
121
|
+
yield [path, v];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getAtSegments(obj: unknown, segments: string[]): unknown {
|
|
127
|
+
let cur: unknown = obj;
|
|
128
|
+
for (const p of segments) {
|
|
129
|
+
if (!isContainer(cur)) return undefined;
|
|
130
|
+
cur = (cur as Record<string, unknown>)[p];
|
|
131
|
+
}
|
|
132
|
+
return cur;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Sets `value` at `segments` inside `obj`. When `defaults` is supplied,
|
|
136
|
+
// missing intermediate slots match the defaults' shape (array vs object) at
|
|
137
|
+
// the same path — so a URL like `items.0.foo=5` rebuilds an array, not an
|
|
138
|
+
// object with numeric keys.
|
|
139
|
+
function setAtSegments(
|
|
140
|
+
obj: Record<string, unknown> | unknown[],
|
|
141
|
+
segments: string[],
|
|
142
|
+
value: unknown,
|
|
143
|
+
defaults?: unknown,
|
|
144
|
+
): void {
|
|
145
|
+
let cur: Record<string, unknown> | unknown[] = obj;
|
|
146
|
+
let defCur: unknown = defaults;
|
|
147
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
148
|
+
const p = segments[i];
|
|
149
|
+
const defNext = isContainer(defCur)
|
|
150
|
+
? (defCur as Record<string, unknown>)[p]
|
|
151
|
+
: undefined;
|
|
152
|
+
const slot = (cur as Record<string, unknown>)[p];
|
|
153
|
+
if (!isContainer(slot)) {
|
|
154
|
+
(cur as Record<string, unknown>)[p] = Array.isArray(defNext) ? [] : {};
|
|
155
|
+
}
|
|
156
|
+
cur = (cur as Record<string, unknown>)[p] as
|
|
157
|
+
| Record<string, unknown>
|
|
158
|
+
| unknown[];
|
|
159
|
+
defCur = defNext;
|
|
160
|
+
}
|
|
161
|
+
(cur as Record<string, unknown>)[segments[segments.length - 1]] = value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Encode/decode segment arrays to/from URL query keys. Each segment is
|
|
165
|
+
// percent-encoded so a literal `.` inside a key name won't collide with the
|
|
166
|
+
// nesting separator. `encodeURIComponent` leaves `.` alone, so we replace
|
|
167
|
+
// it manually to `%2E`. The encoded key still passes cleanly through
|
|
168
|
+
// `URLSearchParams` and vue-router.
|
|
169
|
+
function encodeKey(segments: string[]): string {
|
|
170
|
+
return segments
|
|
171
|
+
.map((s) => encodeURIComponent(s).replace(/\./g, "%2E"))
|
|
172
|
+
.join(".");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function decodeKey(key: string): string[] {
|
|
176
|
+
return key.split(".").map(decodeURIComponent);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Recursive merge used to layer defaults onto the cloned current params.
|
|
180
|
+
// Descends into matching containers (objects and arrays) so sibling
|
|
181
|
+
// branches survive. URL overrides bypass this and use `setAtSegments`
|
|
182
|
+
// directly, since a comma-encoded leaf array must replace its slot
|
|
183
|
+
// wholesale rather than merge element-wise with the default.
|
|
184
|
+
function deepMerge(
|
|
185
|
+
target: Record<string, unknown> | unknown[],
|
|
186
|
+
source: Record<string, unknown> | unknown[],
|
|
187
|
+
): Record<string, unknown> | unknown[] {
|
|
188
|
+
for (const [k, v] of Object.entries(source)) {
|
|
189
|
+
const slot = (target as Record<string, unknown>)[k];
|
|
190
|
+
if (isContainer(v) && isContainer(slot)) {
|
|
191
|
+
deepMerge(slot, v);
|
|
192
|
+
} else {
|
|
193
|
+
(target as Record<string, unknown>)[k] = v;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return target;
|
|
197
|
+
}
|
|
198
|
+
|
|
77
199
|
export function paramsToQuery<T extends object>(
|
|
78
200
|
params: T,
|
|
79
201
|
defaults: T,
|
|
80
202
|
): Record<string, string> {
|
|
81
203
|
const query: Record<string, string> = {};
|
|
82
|
-
for (const
|
|
83
|
-
|
|
84
|
-
|
|
204
|
+
for (const [segments, defaultLeaf] of flattenLeaves(
|
|
205
|
+
defaults as Record<string, unknown>,
|
|
206
|
+
)) {
|
|
207
|
+
const paramLeaf = getAtSegments(params, segments);
|
|
208
|
+
if (paramLeaf === undefined) continue;
|
|
209
|
+
const serialized = serialize(paramLeaf);
|
|
210
|
+
const defaultSerialized = serialize(defaultLeaf);
|
|
85
211
|
if (serialized !== defaultSerialized) {
|
|
86
|
-
query[
|
|
212
|
+
query[encodeKey(segments)] = serialized;
|
|
87
213
|
}
|
|
88
214
|
}
|
|
89
215
|
return query;
|
|
@@ -94,11 +220,14 @@ export function queryToParams<T extends object>(
|
|
|
94
220
|
defaults: T,
|
|
95
221
|
): Partial<T> {
|
|
96
222
|
const result: Record<string, unknown> = {};
|
|
97
|
-
const defaultsRecord = defaults as Record<string, unknown>;
|
|
98
223
|
for (const [key, raw] of Object.entries(query)) {
|
|
99
|
-
if (!(key in defaultsRecord)) continue;
|
|
100
224
|
if (typeof raw !== "string") continue;
|
|
101
|
-
|
|
225
|
+
const segments = decodeKey(key);
|
|
226
|
+
const defaultLeaf = getAtSegments(defaults, segments);
|
|
227
|
+
// Drop unknown dotted keys: matches the flat behavior and keeps the
|
|
228
|
+
// params shape stable.
|
|
229
|
+
if (defaultLeaf === undefined) continue;
|
|
230
|
+
setAtSegments(result, segments, deserialize(raw, defaultLeaf), defaults);
|
|
102
231
|
}
|
|
103
232
|
return result as Partial<T>;
|
|
104
233
|
}
|
|
@@ -127,22 +256,38 @@ function toGetter<T>(input: DefaultsInput<T>): () => T | undefined {
|
|
|
127
256
|
return () => input as T;
|
|
128
257
|
}
|
|
129
258
|
|
|
130
|
-
|
|
259
|
+
// `include`/`ignore` entries are dotted strings using `.` as a JS-side path
|
|
260
|
+
// separator (one segment per key). A path is in scope iff it (or any
|
|
261
|
+
// ancestor) matches an include entry, or it (and all its ancestors) avoid
|
|
262
|
+
// every ignore entry. `include` wins when both are supplied.
|
|
263
|
+
function segmentsStartWith(path: string[], prefix: string[]): boolean {
|
|
264
|
+
if (prefix.length > path.length) return false;
|
|
265
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
266
|
+
if (path[i] !== prefix[i]) return false;
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function pathInScope(
|
|
272
|
+
path: string[],
|
|
273
|
+
include?: string[][],
|
|
274
|
+
ignore?: string[][],
|
|
275
|
+
): boolean {
|
|
276
|
+
if (include) return include.some((entry) => segmentsStartWith(path, entry));
|
|
277
|
+
if (ignore) return !ignore.some((entry) => segmentsStartWith(path, entry));
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function filterDefaults<T extends object>(
|
|
131
282
|
obj: T,
|
|
132
|
-
include?:
|
|
133
|
-
ignore?:
|
|
283
|
+
include?: string[][],
|
|
284
|
+
ignore?: string[][],
|
|
134
285
|
): T {
|
|
135
286
|
if (!include && !ignore) return obj;
|
|
136
|
-
const src = obj as Record<string, unknown>;
|
|
137
287
|
const out: Record<string, unknown> = {};
|
|
138
|
-
for (const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (include.includes(key)) out[k] = src[k];
|
|
142
|
-
} else if (ignore) {
|
|
143
|
-
if (!ignore.includes(key)) out[k] = src[k];
|
|
144
|
-
} else {
|
|
145
|
-
out[k] = src[k];
|
|
288
|
+
for (const [path, leaf] of flattenLeaves(obj as Record<string, unknown>)) {
|
|
289
|
+
if (pathInScope(path, include, ignore)) {
|
|
290
|
+
setAtSegments(out, path, leaf, obj);
|
|
146
291
|
}
|
|
147
292
|
}
|
|
148
293
|
return out as T;
|
|
@@ -151,7 +296,8 @@ function filterKeys<T extends object>(
|
|
|
151
296
|
/**
|
|
152
297
|
* Syncs a reactive params object with the URL query string. Only values
|
|
153
298
|
* that differ from defaults appear in the URL. Accepts either a `ref()`
|
|
154
|
-
* or a `reactive()` object.
|
|
299
|
+
* or a `reactive()` object. Nested objects are encoded as dotted keys
|
|
300
|
+
* (`nested.foo=5`); arrays of primitives remain comma-joined leaves.
|
|
155
301
|
*
|
|
156
302
|
* For async defaults (e.g. loaded from WASM/network), pass a getter or ref
|
|
157
303
|
* that returns `undefined` until ready, and call `hydrate()` once defaults
|
|
@@ -172,10 +318,14 @@ export function useUrlParams<T extends object>(
|
|
|
172
318
|
) {
|
|
173
319
|
const debounceMs = options.debounceMs ?? 300;
|
|
174
320
|
const { router, route, include, ignore } = options;
|
|
321
|
+
const includeSegments = include?.map((e) => String(e).split("."));
|
|
322
|
+
const ignoreSegments = ignore?.map((e) => String(e).split("."));
|
|
175
323
|
const getDefaults = toGetter<T>(defaults);
|
|
176
324
|
const scopedDefaults = (): T | undefined => {
|
|
177
325
|
const d = getDefaults();
|
|
178
|
-
return d === undefined
|
|
326
|
+
return d === undefined
|
|
327
|
+
? undefined
|
|
328
|
+
: filterDefaults(d, includeSegments, ignoreSegments);
|
|
179
329
|
};
|
|
180
330
|
|
|
181
331
|
function readQuery(): Record<string, unknown> {
|
|
@@ -195,18 +345,20 @@ export function useUrlParams<T extends object>(
|
|
|
195
345
|
return isRef(params) ? params.value : params;
|
|
196
346
|
}
|
|
197
347
|
|
|
198
|
-
function apply(overrides:
|
|
348
|
+
function apply(overrides: Array<[string[], unknown]>, d: T) {
|
|
199
349
|
// Layer order (later wins): current params -> defaults -> URL overrides.
|
|
200
350
|
// Starting from current preserves keys outside the sync scope (i.e.
|
|
201
351
|
// those filtered out by `include`/`ignore`), which matters when `params`
|
|
202
|
-
// is a ref and the whole object gets replaced below.
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
352
|
+
// is a ref and the whole object gets replaced below. Defaults deep-merge
|
|
353
|
+
// so sibling branches survive; URL overrides are applied at leaf
|
|
354
|
+
// precision so a leaf array like `tags=1,2` replaces the whole leaf
|
|
355
|
+
// instead of merging element-wise with the default array.
|
|
356
|
+
const current = structuredClone(toRaw(read())) as Record<string, unknown>;
|
|
357
|
+
deepMerge(current, structuredClone(toRaw(d)) as Record<string, unknown>);
|
|
358
|
+
for (const [segments, value] of overrides) {
|
|
359
|
+
setAtSegments(current, segments, value, d);
|
|
360
|
+
}
|
|
361
|
+
const merged = current as T;
|
|
210
362
|
if (isRef(params)) {
|
|
211
363
|
params.value = merged;
|
|
212
364
|
} else {
|
|
@@ -214,13 +366,25 @@ export function useUrlParams<T extends object>(
|
|
|
214
366
|
}
|
|
215
367
|
}
|
|
216
368
|
|
|
369
|
+
function readOverrides(d: T): Array<[string[], unknown]> {
|
|
370
|
+
const out: Array<[string[], unknown]> = [];
|
|
371
|
+
for (const [key, raw] of Object.entries(readQuery())) {
|
|
372
|
+
if (typeof raw !== "string") continue;
|
|
373
|
+
const segments = decodeKey(key);
|
|
374
|
+
const defaultLeaf = getAtSegments(d, segments);
|
|
375
|
+
if (defaultLeaf === undefined) continue;
|
|
376
|
+
out.push([segments, deserialize(raw, defaultLeaf)]);
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
|
|
217
381
|
let hydrated = false;
|
|
218
382
|
|
|
219
383
|
function hydrate(): boolean {
|
|
220
384
|
const d = scopedDefaults();
|
|
221
385
|
if (d === undefined) return false;
|
|
222
|
-
const overrides =
|
|
223
|
-
if (
|
|
386
|
+
const overrides = readOverrides(d);
|
|
387
|
+
if (overrides.length > 0) apply(overrides, d);
|
|
224
388
|
hydrated = true;
|
|
225
389
|
return true;
|
|
226
390
|
}
|
|
@@ -232,8 +396,7 @@ export function useUrlParams<T extends object>(
|
|
|
232
396
|
}
|
|
233
397
|
const d = scopedDefaults();
|
|
234
398
|
if (d === undefined) return;
|
|
235
|
-
|
|
236
|
-
apply(overrides, d);
|
|
399
|
+
apply(readOverrides(d), d);
|
|
237
400
|
}
|
|
238
401
|
|
|
239
402
|
onMounted(() => {
|
|
@@ -281,7 +444,7 @@ export function useUrlParams<T extends object>(
|
|
|
281
444
|
const { clearUrl = true } = opts;
|
|
282
445
|
const d = scopedDefaults();
|
|
283
446
|
if (d === undefined) return;
|
|
284
|
-
apply(
|
|
447
|
+
apply([], d);
|
|
285
448
|
if (clearUrl) {
|
|
286
449
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
287
450
|
writeQuery({});
|