@cfasim-ui/charts 0.1.6 → 0.1.8
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cfasim-ui/charts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Chart visualization components for cfasim-ui",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"reka-ui": "^2.9.2",
|
|
26
26
|
"topojson-client": "^3.1.0",
|
|
27
27
|
"us-atlas": "^3.0.1",
|
|
28
|
-
"@cfasim-ui/shared": "0.1.
|
|
28
|
+
"@cfasim-ui/shared": "0.1.8"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"vue": "^3.5.0"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@types/topojson-specification": "^1.0.5",
|
|
39
39
|
"@vitejs/plugin-vue": "^6.0.5",
|
|
40
40
|
"@vue/test-utils": "^2.4.6",
|
|
41
|
-
"happy-dom": "^20.8.
|
|
41
|
+
"happy-dom": "^20.8.9",
|
|
42
42
|
"vitest": "^4.1.0"
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -125,6 +125,48 @@ A responsive SVG line chart with support for multiple series, axis labels, and c
|
|
|
125
125
|
</template>
|
|
126
126
|
</ComponentDemo>
|
|
127
127
|
|
|
128
|
+
### Grid lines
|
|
129
|
+
|
|
130
|
+
<ComponentDemo>
|
|
131
|
+
<LineChart
|
|
132
|
+
:series="[
|
|
133
|
+
{ data: [0, 10, 25, 45, 60, 55, 40, 20, 8], color: '#fb7e38', strokeWidth: 3 },
|
|
134
|
+
{ data: [0, 5, 12, 20, 28, 25, 18, 10, 4], color: '#0057b7', strokeWidth: 3 },
|
|
135
|
+
]"
|
|
136
|
+
:height="200"
|
|
137
|
+
x-label="Weeks"
|
|
138
|
+
y-label="Incidence"
|
|
139
|
+
x-grid
|
|
140
|
+
y-grid
|
|
141
|
+
/>
|
|
142
|
+
|
|
143
|
+
<template #code>
|
|
144
|
+
|
|
145
|
+
```vue
|
|
146
|
+
<LineChart
|
|
147
|
+
:series="[
|
|
148
|
+
{
|
|
149
|
+
data: [0, 10, 25, 45, 60, 55, 40, 20, 8],
|
|
150
|
+
color: '#fb7e38',
|
|
151
|
+
strokeWidth: 3,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
data: [0, 5, 12, 20, 28, 25, 18, 10, 4],
|
|
155
|
+
color: '#0057b7',
|
|
156
|
+
strokeWidth: 3,
|
|
157
|
+
},
|
|
158
|
+
]"
|
|
159
|
+
:height="200"
|
|
160
|
+
x-label="Weeks"
|
|
161
|
+
y-label="Incidence"
|
|
162
|
+
x-grid
|
|
163
|
+
y-grid
|
|
164
|
+
/>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
</template>
|
|
168
|
+
</ComponentDemo>
|
|
169
|
+
|
|
128
170
|
<!--@include: ./_api/line-chart.md-->
|
|
129
171
|
|
|
130
172
|
### Series
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test, expect } from "@playwright/test";
|
|
2
2
|
|
|
3
3
|
test("LineChart page renders demos", async ({ page }) => {
|
|
4
|
-
await page.goto("
|
|
4
|
+
await page.goto("./cfasim-ui/charts/line-chart");
|
|
5
5
|
await expect(page.locator("h1")).toBeVisible();
|
|
6
6
|
const demos = page.locator(".demo-preview");
|
|
7
7
|
await expect(demos.first()).toBeVisible();
|
|
@@ -32,8 +32,11 @@ const props = withDefaults(
|
|
|
32
32
|
yLabel?: string;
|
|
33
33
|
yMin?: number;
|
|
34
34
|
xMin?: number;
|
|
35
|
+
xLabels?: string[];
|
|
35
36
|
debounce?: number;
|
|
36
37
|
menu?: boolean | string;
|
|
38
|
+
xGrid?: boolean;
|
|
39
|
+
yGrid?: boolean;
|
|
37
40
|
}>(),
|
|
38
41
|
{ lineOpacity: 1, menu: true },
|
|
39
42
|
);
|
|
@@ -110,16 +113,19 @@ const extent = computed(() => {
|
|
|
110
113
|
let max = -Infinity;
|
|
111
114
|
for (const s of allSeries.value) {
|
|
112
115
|
for (const v of s.data) {
|
|
116
|
+
if (!isFinite(v)) continue;
|
|
113
117
|
if (v < min) min = v;
|
|
114
118
|
if (v > max) max = v;
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
for (const a of allAreas.value) {
|
|
118
122
|
for (const v of a.upper) {
|
|
123
|
+
if (!isFinite(v)) continue;
|
|
119
124
|
if (v < min) min = v;
|
|
120
125
|
if (v > max) max = v;
|
|
121
126
|
}
|
|
122
127
|
for (const v of a.lower) {
|
|
128
|
+
if (!isFinite(v)) continue;
|
|
123
129
|
if (v < min) min = v;
|
|
124
130
|
if (v > max) max = v;
|
|
125
131
|
}
|
|
@@ -136,9 +142,17 @@ function toPath(data: number[]): string {
|
|
|
136
142
|
const xScale = innerW.value / (len - 1 || 1);
|
|
137
143
|
const yScale = innerH.value / range;
|
|
138
144
|
const py = padding.value.top + innerH.value;
|
|
139
|
-
let d =
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
let d = "";
|
|
146
|
+
let inSegment = false;
|
|
147
|
+
for (let i = 0; i < data.length; i++) {
|
|
148
|
+
if (!isFinite(data[i])) {
|
|
149
|
+
inSegment = false;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const x = padding.value.left + i * xScale;
|
|
153
|
+
const y = py - (data[i] - min) * yScale;
|
|
154
|
+
d += inSegment ? `L${x},${y}` : `M${x},${y}`;
|
|
155
|
+
inSegment = true;
|
|
142
156
|
}
|
|
143
157
|
return d;
|
|
144
158
|
}
|
|
@@ -153,10 +167,27 @@ function toAreaPath(upper: number[], lower: number[]): string {
|
|
|
153
167
|
const py = padding.value.top + innerH.value;
|
|
154
168
|
const x = (i: number) => padding.value.left + i * xScale;
|
|
155
169
|
const y = (v: number) => py - (v - min) * yScale;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
170
|
+
// Collect contiguous segments where both upper and lower are finite
|
|
171
|
+
const segments: number[][] = [];
|
|
172
|
+
let seg: number[] = [];
|
|
173
|
+
for (let i = 0; i < len; i++) {
|
|
174
|
+
if (isFinite(upper[i]) && isFinite(lower[i])) {
|
|
175
|
+
seg.push(i);
|
|
176
|
+
} else if (seg.length) {
|
|
177
|
+
segments.push(seg);
|
|
178
|
+
seg = [];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (seg.length) segments.push(seg);
|
|
182
|
+
let d = "";
|
|
183
|
+
for (const s of segments) {
|
|
184
|
+
d += `M${x(s[0])},${y(upper[s[0]])}`;
|
|
185
|
+
for (let j = 1; j < s.length; j++) d += `L${x(s[j])},${y(upper[s[j]])}`;
|
|
186
|
+
for (let j = s.length - 1; j >= 0; j--)
|
|
187
|
+
d += `L${x(s[j])},${y(lower[s[j]])}`;
|
|
188
|
+
d += "Z";
|
|
189
|
+
}
|
|
190
|
+
return d;
|
|
160
191
|
}
|
|
161
192
|
|
|
162
193
|
function niceStep(range: number, targetTicks: number): number {
|
|
@@ -171,6 +202,11 @@ function niceStep(range: number, targetTicks: number): number {
|
|
|
171
202
|
return step * mag;
|
|
172
203
|
}
|
|
173
204
|
|
|
205
|
+
/** Round to nearest half-pixel so 1px SVG strokes stay sharp. */
|
|
206
|
+
function snap(v: number): number {
|
|
207
|
+
return Math.round(v) + 0.5;
|
|
208
|
+
}
|
|
209
|
+
|
|
174
210
|
const numFmt = new Intl.NumberFormat();
|
|
175
211
|
function formatTick(v: number): string {
|
|
176
212
|
if (Math.abs(v) >= 1000) return numFmt.format(v);
|
|
@@ -184,7 +220,7 @@ const yTicks = computed(() => {
|
|
|
184
220
|
return [
|
|
185
221
|
{
|
|
186
222
|
value: formatTick(min),
|
|
187
|
-
y: padding.value.top + innerH.value / 2,
|
|
223
|
+
y: snap(padding.value.top + innerH.value / 2),
|
|
188
224
|
},
|
|
189
225
|
];
|
|
190
226
|
}
|
|
@@ -195,10 +231,11 @@ const yTicks = computed(() => {
|
|
|
195
231
|
for (let v = start; v <= max; v += step) {
|
|
196
232
|
ticks.push({
|
|
197
233
|
value: formatTick(v),
|
|
198
|
-
y:
|
|
234
|
+
y: snap(
|
|
199
235
|
padding.value.top +
|
|
200
|
-
|
|
201
|
-
|
|
236
|
+
innerH.value -
|
|
237
|
+
((v - min) / extent.value.range) * innerH.value,
|
|
238
|
+
),
|
|
202
239
|
});
|
|
203
240
|
}
|
|
204
241
|
return ticks;
|
|
@@ -207,6 +244,19 @@ const yTicks = computed(() => {
|
|
|
207
244
|
const xTicks = computed(() => {
|
|
208
245
|
const len = maxLen.value;
|
|
209
246
|
if (len <= 1) return [];
|
|
247
|
+
const labels = props.xLabels;
|
|
248
|
+
if (labels && labels.length === len) {
|
|
249
|
+
const targetTicks = Math.max(3, Math.floor(innerW.value / 80));
|
|
250
|
+
const step = Math.max(1, Math.round((len - 1) / targetTicks));
|
|
251
|
+
const ticks: { value: string; x: number }[] = [];
|
|
252
|
+
for (let i = 0; i < len; i += step) {
|
|
253
|
+
ticks.push({
|
|
254
|
+
value: labels[i],
|
|
255
|
+
x: snap(padding.value.left + (i / (len - 1)) * innerW.value),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return ticks;
|
|
259
|
+
}
|
|
210
260
|
const offset = props.xMin ?? 0;
|
|
211
261
|
const targetTicks = Math.max(3, Math.floor(innerW.value / 80));
|
|
212
262
|
const step = niceStep(len - 1, targetTicks);
|
|
@@ -215,7 +265,7 @@ const xTicks = computed(() => {
|
|
|
215
265
|
const idx = Math.round(i);
|
|
216
266
|
ticks.push({
|
|
217
267
|
value: formatTick(idx + offset),
|
|
218
|
-
x: padding.value.left + (idx / (len - 1)) * innerW.value,
|
|
268
|
+
x: snap(padding.value.left + (idx / (len - 1)) * innerW.value),
|
|
219
269
|
});
|
|
220
270
|
}
|
|
221
271
|
return ticks;
|
|
@@ -288,21 +338,47 @@ const menuItems = computed<ChartMenuItem[]>(() => {
|
|
|
288
338
|
</text>
|
|
289
339
|
<!-- axes -->
|
|
290
340
|
<line
|
|
291
|
-
:x1="padding.left"
|
|
292
|
-
:y1="padding.top"
|
|
293
|
-
:x2="padding.left"
|
|
294
|
-
:y2="padding.top + innerH"
|
|
341
|
+
:x1="snap(padding.left)"
|
|
342
|
+
:y1="snap(padding.top)"
|
|
343
|
+
:x2="snap(padding.left)"
|
|
344
|
+
:y2="snap(padding.top + innerH)"
|
|
295
345
|
stroke="currentColor"
|
|
296
346
|
stroke-opacity="0.3"
|
|
297
347
|
/>
|
|
298
348
|
<line
|
|
299
|
-
:x1="padding.left"
|
|
300
|
-
:y1="padding.top + innerH"
|
|
301
|
-
:x2="padding.left + innerW"
|
|
302
|
-
:y2="padding.top + innerH"
|
|
349
|
+
:x1="snap(padding.left)"
|
|
350
|
+
:y1="snap(padding.top + innerH)"
|
|
351
|
+
:x2="snap(padding.left + innerW)"
|
|
352
|
+
:y2="snap(padding.top + innerH)"
|
|
303
353
|
stroke="currentColor"
|
|
304
354
|
stroke-opacity="0.3"
|
|
305
355
|
/>
|
|
356
|
+
<!-- y grid lines -->
|
|
357
|
+
<template v-if="yGrid">
|
|
358
|
+
<line
|
|
359
|
+
v-for="(tick, i) in yTicks"
|
|
360
|
+
:key="'yg' + i"
|
|
361
|
+
:x1="padding.left"
|
|
362
|
+
:y1="tick.y"
|
|
363
|
+
:x2="padding.left + innerW"
|
|
364
|
+
:y2="tick.y"
|
|
365
|
+
stroke="currentColor"
|
|
366
|
+
stroke-opacity="0.1"
|
|
367
|
+
/>
|
|
368
|
+
</template>
|
|
369
|
+
<!-- x grid lines -->
|
|
370
|
+
<template v-if="xGrid">
|
|
371
|
+
<line
|
|
372
|
+
v-for="(tick, i) in xTicks"
|
|
373
|
+
:key="'xg' + i"
|
|
374
|
+
:x1="tick.x"
|
|
375
|
+
:y1="padding.top"
|
|
376
|
+
:x2="tick.x"
|
|
377
|
+
:y2="padding.top + innerH"
|
|
378
|
+
stroke="currentColor"
|
|
379
|
+
stroke-opacity="0.1"
|
|
380
|
+
/>
|
|
381
|
+
</template>
|
|
306
382
|
<!-- y tick labels -->
|
|
307
383
|
<text
|
|
308
384
|
v-for="(tick, i) in yTicks"
|