@leavepulse/ui 0.29.4 → 0.29.5
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/assets/LpChart-CzgCGaAA.css +8 -0
- package/dist/components/LpChart.vue.js +814 -2
- package/dist/index.js +120 -120
- package/package.json +1 -1
- package/src/components/LpChart.vue +337 -63
- package/dist/chunks/LpChart-Bmw7y34Z.js +0 -672
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { Motion } from "motion-v"
|
|
13
13
|
import { computed, ref, shallowRef, watch, onMounted, onBeforeUnmount } from "vue"
|
|
14
|
+
import { easeTravel, prefersReducedMotion } from "../composables/easing"
|
|
14
15
|
import { usePillTransition } from "../composables/usePillTransition"
|
|
15
16
|
import { useReveal } from "../composables/useReveal"
|
|
16
17
|
import {
|
|
@@ -147,6 +148,10 @@ onMounted(() => {
|
|
|
147
148
|
onBeforeUnmount(() => {
|
|
148
149
|
ro?.disconnect()
|
|
149
150
|
ro = null
|
|
151
|
+
if (presenceFrame) cancelAnimationFrame(presenceFrame)
|
|
152
|
+
if (scaleFrame) cancelAnimationFrame(scaleFrame)
|
|
153
|
+
if (rightFrame) cancelAnimationFrame(rightFrame)
|
|
154
|
+
if (gutterFrame) cancelAnimationFrame(gutterFrame)
|
|
150
155
|
})
|
|
151
156
|
|
|
152
157
|
// ── data ───────────────────────────────────────
|
|
@@ -163,6 +168,74 @@ const toggleSeries = (name: string) => {
|
|
|
163
168
|
const allSeries = computed(() => normalise(props.series))
|
|
164
169
|
const visible = computed(() => allSeries.value.filter((s) => !hidden.value.has(s.name)))
|
|
165
170
|
|
|
171
|
+
/*
|
|
172
|
+
* How present each series is, 1 to 0, driven per frame rather than by CSS.
|
|
173
|
+
*
|
|
174
|
+
* A curve leaves by collapsing onto the baseline: every point travels to zero
|
|
175
|
+
* together and the shape flattens into the axis before it fades. Moving the
|
|
176
|
+
* whole layer down instead slides it past the axis as a picture of a chart —
|
|
177
|
+
* the marks keep their shape on the way out, which reads as the plot being
|
|
178
|
+
* dragged away rather than the readings going to nothing.
|
|
179
|
+
*
|
|
180
|
+
* It has to be JS: the collapse is a new `d` on every frame, and no browser
|
|
181
|
+
* interpolates a path's `d` between two arbitrary shapes.
|
|
182
|
+
*/
|
|
183
|
+
/* One duration for the series leaving and the axis it was measured against, so
|
|
184
|
+
the two read as a single change rather than two. 260ms sits in the 250-400ms
|
|
185
|
+
band a legend toggle wants: long enough to follow, short enough to feel like
|
|
186
|
+
a control responding. */
|
|
187
|
+
const SETTLE_MS = 260
|
|
188
|
+
|
|
189
|
+
const presence = ref(new Map<string, number>())
|
|
190
|
+
let presenceFrame: number | undefined
|
|
191
|
+
|
|
192
|
+
/*
|
|
193
|
+
* A series with no entry yet is fully present, NOT "0 because it is hidden".
|
|
194
|
+
*
|
|
195
|
+
* Falling back to the hidden flag raced the animation it was meant to start:
|
|
196
|
+
* the computed that draws the curve and the watcher that animates it both run
|
|
197
|
+
* on the same tick, and whichever order they land in, the fallback had already
|
|
198
|
+
* reported the end state — so a series being hidden jumped to the floor while
|
|
199
|
+
* one being shown eased up from it.
|
|
200
|
+
*/
|
|
201
|
+
const presenceOf = (name: string) => presence.value.get(name) ?? 1
|
|
202
|
+
|
|
203
|
+
watch(
|
|
204
|
+
// A string, not the Set: `hidden` is replaced wholesale on every toggle, and a
|
|
205
|
+
// watcher handed the object itself compares the new reference to the old one
|
|
206
|
+
// and sees a change it cannot describe — or, with a stale copy in the array,
|
|
207
|
+
// no change at all. The names that are currently off say it exactly.
|
|
208
|
+
() =>
|
|
209
|
+
allSeries.value.map((s) => `${s.name}:${hidden.value.has(s.name) ? 0 : 1}`).join(","),
|
|
210
|
+
() => {
|
|
211
|
+
const targets = new Map(allSeries.value.map((s) => [s.name, hidden.value.has(s.name) ? 0 : 1]))
|
|
212
|
+
if (!props.animate || prefersReducedMotion()) {
|
|
213
|
+
presence.value = targets
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const from = new Map(allSeries.value.map((s) => [s.name, presenceOf(s.name)]))
|
|
218
|
+
if ([...targets].every(([name, to]) => from.get(name) === to)) return
|
|
219
|
+
|
|
220
|
+
const start = performance.now()
|
|
221
|
+
if (presenceFrame) cancelAnimationFrame(presenceFrame)
|
|
222
|
+
|
|
223
|
+
const step = (now: number) => {
|
|
224
|
+
const t = Math.min(1, (now - start) / SETTLE_MS)
|
|
225
|
+
// Same curve as the axis: the curve and its scale travel together.
|
|
226
|
+
const eased = easeTravel(t)
|
|
227
|
+
const next = new Map<string, number>()
|
|
228
|
+
for (const [name, to] of targets) {
|
|
229
|
+
const a = from.get(name) ?? to
|
|
230
|
+
next.set(name, a + (to - a) * eased)
|
|
231
|
+
}
|
|
232
|
+
presence.value = next
|
|
233
|
+
presenceFrame = t < 1 ? requestAnimationFrame(step) : undefined
|
|
234
|
+
}
|
|
235
|
+
presenceFrame = requestAnimationFrame(step)
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
|
|
166
239
|
const count = computed(() => pointCount(allSeries.value))
|
|
167
240
|
const labels = computed(() => resolveLabels(props.series, props.labels, count.value))
|
|
168
241
|
const banded = computed(() => props.type === "bar")
|
|
@@ -194,14 +267,29 @@ const stackTotals = computed(() => {
|
|
|
194
267
|
return { pos, neg }
|
|
195
268
|
})
|
|
196
269
|
|
|
270
|
+
/*
|
|
271
|
+
* Which axes EXIST is decided by the series as declared; only their ranges
|
|
272
|
+
* follow what is visible.
|
|
273
|
+
*
|
|
274
|
+
* Hiding the one series on the right axis used to remove that axis outright:
|
|
275
|
+
* its labels went, the gutter they reserved collapsed, the plot widened into
|
|
276
|
+
* the space, and any remaining right-axis series jumped across to the left
|
|
277
|
+
* scale — a toggle of one curve rearranging the whole frame. Keeping the axis
|
|
278
|
+
* while it has a series assigned to it means hiding a curve changes the curve.
|
|
279
|
+
*/
|
|
280
|
+
const declaredRight = computed(() => allSeries.value.filter((s) => s.axis === "right"))
|
|
281
|
+
const declaredLeft = computed(() => allSeries.value.filter((s) => s.axis === "left"))
|
|
282
|
+
|
|
197
283
|
const onRight = computed(() => visible.value.filter((s) => s.axis === "right"))
|
|
198
284
|
const onLeft = computed(() => {
|
|
199
285
|
// A chart whose every series asked for the right axis still needs the left
|
|
200
286
|
// one to carry the grid, so treat that as "no split at all".
|
|
201
287
|
const left = visible.value.filter((s) => s.axis === "left")
|
|
202
|
-
return left.length || !
|
|
288
|
+
return left.length || !declaredRight.value.length ? left : visible.value
|
|
203
289
|
})
|
|
204
|
-
const hasRightAxis = computed(
|
|
290
|
+
const hasRightAxis = computed(
|
|
291
|
+
() => declaredRight.value.length > 0 && declaredLeft.value.length > 0,
|
|
292
|
+
)
|
|
205
293
|
|
|
206
294
|
const scaleFor = (members: typeof visible.value) => {
|
|
207
295
|
const totals = stackTotals.value
|
|
@@ -211,22 +299,110 @@ const scaleFor = (members: typeof visible.value) => {
|
|
|
211
299
|
return niceScale(raw.min, raw.max, props.yTicks)
|
|
212
300
|
}
|
|
213
301
|
|
|
214
|
-
const
|
|
302
|
+
const targetScale = computed(() =>
|
|
303
|
+
scaleFor(onLeft.value.length ? onLeft.value : declaredLeft.value),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
/*
|
|
307
|
+
* The axis animates by interpolating its own BOUNDS, not by transitioning the
|
|
308
|
+
* elements drawn from them.
|
|
309
|
+
*
|
|
310
|
+
* Everything on the plot is positioned from min/max — the curves, the gridlines
|
|
311
|
+
* and the tick labels — so moving the range moves all three as one gesture, and
|
|
312
|
+
* the numbers arrive with the shape that made them change. Transitioning the
|
|
313
|
+
* marks instead would need the axis to animate separately, and it cannot: `y1`
|
|
314
|
+
* and `y2` on an SVG <line> never became CSS properties, so a gridline jumps
|
|
315
|
+
* while the curve eases, which is the two-beat effect this avoids.
|
|
316
|
+
*/
|
|
317
|
+
const scale = ref(targetScale.value)
|
|
318
|
+
let scaleFrame: number | undefined
|
|
319
|
+
|
|
320
|
+
watch(targetScale, (to, from) => {
|
|
321
|
+
if (!props.animate || prefersReducedMotion() || !from) {
|
|
322
|
+
scale.value = to
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
if (from.min === to.min && from.max === to.max && from.step === to.step) return
|
|
326
|
+
|
|
327
|
+
const start = performance.now()
|
|
328
|
+
if (scaleFrame) cancelAnimationFrame(scaleFrame)
|
|
329
|
+
|
|
330
|
+
const tick = (now: number) => {
|
|
331
|
+
const t = Math.min(1, (now - start) / SETTLE_MS)
|
|
332
|
+
/* easeTravel, not easeOut: an ease-out covers most of the range in the first
|
|
333
|
+
frames, which on a big rescale (800 down to 40) looks like a snap with a
|
|
334
|
+
slow tail rather than a scale moving. */
|
|
335
|
+
const eased = easeTravel(t)
|
|
336
|
+
scale.value =
|
|
337
|
+
t < 1
|
|
338
|
+
? {
|
|
339
|
+
min: from.min + (to.min - from.min) * eased,
|
|
340
|
+
max: from.max + (to.max - from.max) * eased,
|
|
341
|
+
/* The step stays the target's: it decides how many ticks there are,
|
|
342
|
+
and a travelling one would add and drop labels mid-move. */
|
|
343
|
+
step: to.step,
|
|
344
|
+
}
|
|
345
|
+
: to
|
|
346
|
+
scaleFrame = t < 1 ? requestAnimationFrame(tick) : undefined
|
|
347
|
+
}
|
|
348
|
+
scaleFrame = requestAnimationFrame(tick)
|
|
349
|
+
})
|
|
215
350
|
|
|
216
351
|
/*
|
|
217
352
|
* The right axis reuses the LEFT axis's tick COUNT so both sets of labels land
|
|
218
353
|
* on the same gridlines. Two independent tick runs would draw two interleaved
|
|
219
354
|
* grids across one plot, which reads as noise rather than as two scales.
|
|
220
355
|
*/
|
|
221
|
-
const
|
|
356
|
+
const targetRightScale = computed(() => {
|
|
222
357
|
if (!hasRightAxis.value) return null
|
|
223
|
-
|
|
224
|
-
|
|
358
|
+
/* With every right-hand series hidden there is nothing to measure, so the
|
|
359
|
+
axis holds the range it had — an axis that keeps its place while its curve
|
|
360
|
+
leaves, rather than collapsing to a degenerate 0..1 behind it. */
|
|
361
|
+
const members = onRight.value.length ? onRight.value : declaredRight.value
|
|
362
|
+
const raw = extent(members, zeroBased.value)
|
|
363
|
+
/* Steps come from the TARGET left scale. Reading the animated one rebuilt this
|
|
364
|
+
axis from a fractional range every frame, so a right-hand series jittered
|
|
365
|
+
against a scale being redrawn underneath it. */
|
|
366
|
+
const steps = Math.max(
|
|
367
|
+
1,
|
|
368
|
+
axisTicks(targetScale.value.min, targetScale.value.max, targetScale.value.step).length - 1,
|
|
369
|
+
)
|
|
225
370
|
const nice = niceScale(raw.min, raw.max, steps)
|
|
226
371
|
// Stretch the top so the run divides into exactly `steps` intervals.
|
|
227
372
|
return { min: nice.min, max: nice.min + nice.step * steps, step: nice.step }
|
|
228
373
|
})
|
|
229
374
|
|
|
375
|
+
// The right axis travels on the same clock as the left, or a series measured
|
|
376
|
+
// against it arrives before the one beside it.
|
|
377
|
+
const rightScale = ref(targetRightScale.value)
|
|
378
|
+
let rightFrame: number | undefined
|
|
379
|
+
|
|
380
|
+
watch(targetRightScale, (to, from) => {
|
|
381
|
+
if (!to || !from || !props.animate || prefersReducedMotion()) {
|
|
382
|
+
rightScale.value = to
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
if (from.min === to.min && from.max === to.max && from.step === to.step) return
|
|
386
|
+
|
|
387
|
+
const start = performance.now()
|
|
388
|
+
if (rightFrame) cancelAnimationFrame(rightFrame)
|
|
389
|
+
|
|
390
|
+
const tick = (now: number) => {
|
|
391
|
+
const t = Math.min(1, (now - start) / SETTLE_MS)
|
|
392
|
+
const eased = easeTravel(t)
|
|
393
|
+
rightScale.value =
|
|
394
|
+
t < 1
|
|
395
|
+
? {
|
|
396
|
+
min: from.min + (to.min - from.min) * eased,
|
|
397
|
+
max: from.max + (to.max - from.max) * eased,
|
|
398
|
+
step: to.step,
|
|
399
|
+
}
|
|
400
|
+
: to
|
|
401
|
+
rightFrame = t < 1 ? requestAnimationFrame(tick) : undefined
|
|
402
|
+
}
|
|
403
|
+
rightFrame = requestAnimationFrame(tick)
|
|
404
|
+
})
|
|
405
|
+
|
|
230
406
|
const fmtWith = (v: number, unit?: string, f?: (n: number) => string): string =>
|
|
231
407
|
`${f ? f(v) : formatTick(v)}${unit ?? ""}`
|
|
232
408
|
|
|
@@ -243,23 +419,76 @@ const scaleOf = (s: { axis: "left" | "right" }) =>
|
|
|
243
419
|
|
|
244
420
|
// ── layout ─────────────────────────────────────
|
|
245
421
|
// Side padding tracks the widest tick label so long numbers never clip.
|
|
422
|
+
/*
|
|
423
|
+
* Labels come from the TARGET scale, never the one mid-flight.
|
|
424
|
+
*
|
|
425
|
+
* The animated bounds are fractional for most of their travel, so formatting
|
|
426
|
+
* them printed "173.4" where "200" belongs — and since the gutter is measured
|
|
427
|
+
* from the widest label, the whole plot breathed in and out for the length of
|
|
428
|
+
* every transition. The numbers a tick can read are the ones it will settle on;
|
|
429
|
+
* only where each sits is in motion.
|
|
430
|
+
*/
|
|
246
431
|
const yLabels = computed(() =>
|
|
247
|
-
axisTicks(
|
|
432
|
+
axisTicks(targetScale.value.min, targetScale.value.max, targetScale.value.step).map(fmt),
|
|
248
433
|
)
|
|
249
434
|
|
|
250
435
|
const rightLabels = computed(() =>
|
|
251
|
-
|
|
252
|
-
? axisTicks(
|
|
436
|
+
targetRightScale.value
|
|
437
|
+
? axisTicks(
|
|
438
|
+
targetRightScale.value.min,
|
|
439
|
+
targetRightScale.value.max,
|
|
440
|
+
targetRightScale.value.step,
|
|
441
|
+
).map(fmtRight)
|
|
253
442
|
: [],
|
|
254
443
|
)
|
|
255
444
|
|
|
256
445
|
const gutter = (labels: string[]) => 12 + Math.max(...labels.map((l) => l.length), 1) * 7
|
|
257
446
|
|
|
447
|
+
/*
|
|
448
|
+
* The gutters ease to their new width instead of resizing on the frame the
|
|
449
|
+
* labels change. Going from "800" to "30" narrows the left margin by a third of
|
|
450
|
+
* an inch in one step, and because the plot is measured from it, every mark
|
|
451
|
+
* shifted sideways while it was still travelling vertically — a diagonal slide
|
|
452
|
+
* nothing in the data asked for.
|
|
453
|
+
*/
|
|
454
|
+
const targetGutters = computed(() => ({
|
|
455
|
+
left: props.showYAxis ? gutter(yLabels.value) : 8,
|
|
456
|
+
right: props.showYAxis && rightLabels.value.length ? gutter(rightLabels.value) : 12,
|
|
457
|
+
}))
|
|
458
|
+
|
|
459
|
+
const gutters = ref(targetGutters.value)
|
|
460
|
+
let gutterFrame: number | undefined
|
|
461
|
+
|
|
462
|
+
watch(targetGutters, (to, from) => {
|
|
463
|
+
if (!props.animate || prefersReducedMotion() || !from) {
|
|
464
|
+
gutters.value = to
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
if (from.left === to.left && from.right === to.right) return
|
|
468
|
+
|
|
469
|
+
const start = performance.now()
|
|
470
|
+
if (gutterFrame) cancelAnimationFrame(gutterFrame)
|
|
471
|
+
|
|
472
|
+
const tick = (now: number) => {
|
|
473
|
+
const t = Math.min(1, (now - start) / SETTLE_MS)
|
|
474
|
+
const eased = easeTravel(t)
|
|
475
|
+
gutters.value =
|
|
476
|
+
t < 1
|
|
477
|
+
? {
|
|
478
|
+
left: from.left + (to.left - from.left) * eased,
|
|
479
|
+
right: from.right + (to.right - from.right) * eased,
|
|
480
|
+
}
|
|
481
|
+
: to
|
|
482
|
+
gutterFrame = t < 1 ? requestAnimationFrame(tick) : undefined
|
|
483
|
+
}
|
|
484
|
+
gutterFrame = requestAnimationFrame(tick)
|
|
485
|
+
})
|
|
486
|
+
|
|
258
487
|
const box = computed<Box>(() => ({
|
|
259
488
|
width: width.value,
|
|
260
489
|
height: props.height,
|
|
261
|
-
left:
|
|
262
|
-
right:
|
|
490
|
+
left: gutters.value.left,
|
|
491
|
+
right: gutters.value.right,
|
|
263
492
|
top: 10,
|
|
264
493
|
bottom: props.showXAxis ? 26 : 8,
|
|
265
494
|
}))
|
|
@@ -276,16 +505,44 @@ const geo = computed(() => ({
|
|
|
276
505
|
const geoOf = (s: { axis: "left" | "right" }) => ({ ...geo.value, ...scaleOf(s) })
|
|
277
506
|
|
|
278
507
|
const gridLines = computed(() => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
508
|
+
// Ticks are the target's — they are the figures the axis will read — while
|
|
509
|
+
// the position each one takes comes from the scale as it currently stands.
|
|
510
|
+
const left = axisTicks(targetScale.value.min, targetScale.value.max, targetScale.value.step)
|
|
511
|
+
const right = targetRightScale.value
|
|
512
|
+
? axisTicks(
|
|
513
|
+
targetRightScale.value.min,
|
|
514
|
+
targetRightScale.value.max,
|
|
515
|
+
targetRightScale.value.step,
|
|
516
|
+
)
|
|
282
517
|
: []
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
518
|
+
/*
|
|
519
|
+
* A tick sits at its FRACTION of the axis, not at its value on the scale
|
|
520
|
+
* currently in flight.
|
|
521
|
+
*
|
|
522
|
+
* Projecting the value instead put "800" where 800 falls on a range still
|
|
523
|
+
* reading 0..30 — roughly seventeen hundred pixels above a plot 240 tall.
|
|
524
|
+
* The labels shot off the top, came back as the range caught up, and the
|
|
525
|
+
* chart looked like it had thrown them. As a fraction, the top tick is the
|
|
526
|
+
* top of the axis throughout, whatever number it happens to carry.
|
|
527
|
+
*/
|
|
528
|
+
const span = targetScale.value.max - targetScale.value.min || 1
|
|
529
|
+
const plot = plotHeight(box.value)
|
|
530
|
+
return left.map((v, i) => {
|
|
531
|
+
const fraction = (v - targetScale.value.min) / span
|
|
532
|
+
return {
|
|
533
|
+
v,
|
|
534
|
+
/*
|
|
535
|
+
* Keyed by POSITION, not by value. A tick keyed by its value is a
|
|
536
|
+
* different element every time the range moves, so hiding a series
|
|
537
|
+
* replaced the whole axis at once — the numbers jumped to their new
|
|
538
|
+
* places instead of moving there.
|
|
539
|
+
*/
|
|
540
|
+
key: i,
|
|
541
|
+
y: box.value.top + plot - fraction * plot,
|
|
542
|
+
label: fmt(v),
|
|
543
|
+
rightLabel: right[i] != null ? fmtRight(right[i]) : "",
|
|
544
|
+
}
|
|
545
|
+
})
|
|
289
546
|
})
|
|
290
547
|
|
|
291
548
|
/** Thin out x labels so they never collide at narrow widths. */
|
|
@@ -312,17 +569,33 @@ const baseY = computed(() =>
|
|
|
312
569
|
),
|
|
313
570
|
)
|
|
314
571
|
|
|
572
|
+
/*
|
|
573
|
+
* Hidden series are still drawn — off the bottom of the plot and transparent.
|
|
574
|
+
* Dropping them from the list instead makes the legend feel like a switch that
|
|
575
|
+
* deletes a reading: the curve is simply not there on the next frame, while the
|
|
576
|
+
* axis it was scaled against slides to a new range behind it. Kept in the list,
|
|
577
|
+
* it falls out of the plot while the remaining series keep their place, which is
|
|
578
|
+
* what "stop showing this one" should look like.
|
|
579
|
+
*/
|
|
315
580
|
const lines = computed(() =>
|
|
316
|
-
|
|
581
|
+
allSeries.value.map((s) => {
|
|
317
582
|
const g = geoOf(s)
|
|
583
|
+
const p = presenceOf(s.name)
|
|
584
|
+
// Values pulled toward the axis floor, so the curve flattens into it.
|
|
585
|
+
const base = Math.min(Math.max(0, g.min), g.max)
|
|
586
|
+
const values = p === 1 ? s.values : s.values.map((v) => (v == null ? null : base + (v - base) * p))
|
|
318
587
|
return {
|
|
319
588
|
...s,
|
|
320
|
-
|
|
321
|
-
|
|
589
|
+
values,
|
|
590
|
+
d: linePath(values, g, props.smooth),
|
|
591
|
+
fill: props.type === "area" ? areaPath(values, g, props.smooth) : "",
|
|
322
592
|
/* A dashed series is a reference level (a budget, a target), not a
|
|
323
593
|
measurement — dotting it implies readings that aren't there. */
|
|
324
594
|
points: props.showPoints && !s.dashed,
|
|
325
595
|
scale: scaleOf(s),
|
|
596
|
+
/* Fades only at the end of the collapse: going transparent while it is
|
|
597
|
+
still visibly moving loses the travel that explains what happened. */
|
|
598
|
+
alpha: Math.min(1, p * 3),
|
|
326
599
|
}
|
|
327
600
|
}),
|
|
328
601
|
)
|
|
@@ -679,7 +952,7 @@ const gradientId = (name: string, index: number) =>
|
|
|
679
952
|
|
|
680
953
|
<!-- grid + y ticks -->
|
|
681
954
|
<g v-if="showGrid || showYAxis">
|
|
682
|
-
<template v-for="g in gridLines" :key="g.
|
|
955
|
+
<template v-for="g in gridLines" :key="g.key">
|
|
683
956
|
<line
|
|
684
957
|
v-if="showGrid"
|
|
685
958
|
:x1="box.left"
|
|
@@ -688,7 +961,7 @@ const gradientId = (name: string, index: number) =>
|
|
|
688
961
|
:y2="g.y"
|
|
689
962
|
stroke="var(--color-line)"
|
|
690
963
|
stroke-width="1"
|
|
691
|
-
|
|
964
|
+
/>
|
|
692
965
|
<text
|
|
693
966
|
v-if="showYAxis"
|
|
694
967
|
:x="box.left - 8"
|
|
@@ -745,45 +1018,35 @@ const gradientId = (name: string, index: number) =>
|
|
|
745
1018
|
:opacity="active < 0 || active === b.index ? 1 : 0.45"
|
|
746
1019
|
/>
|
|
747
1020
|
|
|
748
|
-
<!--
|
|
749
|
-
<
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
<circle
|
|
776
|
-
v-for="(v, i) in s.values"
|
|
777
|
-
v-show="v != null"
|
|
778
|
-
:key="i"
|
|
779
|
-
:cx="xAt(i, count, box, banded)"
|
|
780
|
-
:cy="v == null ? 0 : yAt(v, s.scale.min, s.scale.max, box)"
|
|
781
|
-
r="2.5"
|
|
782
|
-
:fill="s.color"
|
|
783
|
-
/>
|
|
784
|
-
</template>
|
|
1021
|
+
<!-- one group per series, so hiding one drops only that curve -->
|
|
1022
|
+
<g v-for="(s, i) in lines" :key="`s-${s.name}`" :style="{ opacity: s.alpha }">
|
|
1023
|
+
<path
|
|
1024
|
+
v-show="type === 'area'"
|
|
1025
|
+
:d="s.fill"
|
|
1026
|
+
:fill="`url(#${gradientId(s.name, i)})`"
|
|
1027
|
+
/>
|
|
1028
|
+
<path
|
|
1029
|
+
v-show="type !== 'bar'"
|
|
1030
|
+
:d="s.d"
|
|
1031
|
+
fill="none"
|
|
1032
|
+
:stroke="s.color"
|
|
1033
|
+
stroke-width="2"
|
|
1034
|
+
stroke-linecap="round"
|
|
1035
|
+
stroke-linejoin="round"
|
|
1036
|
+
:stroke-dasharray="s.dashed ? '5 4' : undefined"
|
|
1037
|
+
/>
|
|
1038
|
+
<template v-if="type !== 'bar' && s.points">
|
|
1039
|
+
<circle
|
|
1040
|
+
v-for="(v, j) in s.values"
|
|
1041
|
+
v-show="v != null"
|
|
1042
|
+
:key="j"
|
|
1043
|
+
:cx="xAt(j, count, box, banded)"
|
|
1044
|
+
:cy="v == null ? 0 : yAt(v, s.scale.min, s.scale.max, box)"
|
|
1045
|
+
r="2.5"
|
|
1046
|
+
:fill="s.color"
|
|
1047
|
+
/>
|
|
785
1048
|
</template>
|
|
786
|
-
</
|
|
1049
|
+
</g>
|
|
787
1050
|
</g>
|
|
788
1051
|
|
|
789
1052
|
<!-- crosshair -->
|
|
@@ -863,3 +1126,14 @@ const gradientId = (name: string, index: number) =>
|
|
|
863
1126
|
</LpContextMenu>
|
|
864
1127
|
</div>
|
|
865
1128
|
</template>
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
<style scoped>
|
|
1132
|
+
/*
|
|
1133
|
+
* A series leaving, and the axis it was measured against following it.
|
|
1134
|
+
*
|
|
1135
|
+
* Both run on the same duration so the curve and its numbers settle together:
|
|
1136
|
+
* the axis arriving first would read as the scale having changed for its own
|
|
1137
|
+
* reasons, rather than because a series left.
|
|
1138
|
+
*/
|
|
1139
|
+
</style>
|