@liiift-studio/sanity-visitor-insights 0.52.0 → 0.54.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.mts +0 -11
- package/dist/index.d.ts +0 -11
- package/dist/index.js +78 -44
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +78 -44
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/studio/CrossSourceTimeline.tsx +36 -11
- package/src/studio/Figure.tsx +65 -24
- package/src/studio/palette.test.ts +59 -1
- package/src/studio/palette.ts +83 -0
- package/src/studio/panels.test.tsx +114 -7
- package/src/studio/panels.tsx +17 -4
package/dist/index.d.mts
CHANGED
|
@@ -190,17 +190,6 @@ interface DeltaProps {
|
|
|
190
190
|
/** Render as a percentage-point change rather than a percentage change of a percentage. */
|
|
191
191
|
unit?: 'count' | 'percent';
|
|
192
192
|
}
|
|
193
|
-
/**
|
|
194
|
-
* A period-over-period change.
|
|
195
|
-
*
|
|
196
|
-
* Every figure in this tool used to be a bare level, which at these volumes is close to
|
|
197
|
-
* meaningless: "389 sessions" is neither good nor bad without last week beside it, and the
|
|
198
|
-
* 24 August collapse would have announced itself on every panel as a delta while going unnoticed
|
|
199
|
-
* for over a week as a level.
|
|
200
|
-
*
|
|
201
|
-
* Renders nothing at all when there is no comparison — an absent delta must never be drawn as
|
|
202
|
-
* "no change", which is a different and much more reassuring claim.
|
|
203
|
-
*/
|
|
204
193
|
declare function Delta({ current, previous, riseIsGood, unit }: DeltaProps): React.ReactElement | null;
|
|
205
194
|
interface MetricFigureProps {
|
|
206
195
|
metric: MetricValue;
|
package/dist/index.d.ts
CHANGED
|
@@ -190,17 +190,6 @@ interface DeltaProps {
|
|
|
190
190
|
/** Render as a percentage-point change rather than a percentage change of a percentage. */
|
|
191
191
|
unit?: 'count' | 'percent';
|
|
192
192
|
}
|
|
193
|
-
/**
|
|
194
|
-
* A period-over-period change.
|
|
195
|
-
*
|
|
196
|
-
* Every figure in this tool used to be a bare level, which at these volumes is close to
|
|
197
|
-
* meaningless: "389 sessions" is neither good nor bad without last week beside it, and the
|
|
198
|
-
* 24 August collapse would have announced itself on every panel as a delta while going unnoticed
|
|
199
|
-
* for over a week as a level.
|
|
200
|
-
*
|
|
201
|
-
* Renders nothing at all when there is no comparison — an absent delta must never be drawn as
|
|
202
|
-
* "no change", which is a different and much more reassuring claim.
|
|
203
|
-
*/
|
|
204
193
|
declare function Delta({ current, previous, riseIsGood, unit }: DeltaProps): React.ReactElement | null;
|
|
205
194
|
interface MetricFigureProps {
|
|
206
195
|
metric: MetricValue;
|
package/dist/index.js
CHANGED
|
@@ -288,6 +288,32 @@ function seriesFill(key, alpha = REGION_ALPHA) {
|
|
|
288
288
|
const b = parseInt(hex.slice(5, 7), 16);
|
|
289
289
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
290
290
|
}
|
|
291
|
+
var MARKS = {
|
|
292
|
+
"chart.line": { key: "vercel", alpha: 1, role: "data", note: "A source series in the timeline" },
|
|
293
|
+
"chart.lossyLine": { key: "ga4Pageviews", alpha: 1, role: "data", note: "The lossier source, dashed" },
|
|
294
|
+
"chart.coverage": { key: "ga4Sessions", alpha: 1, role: "data", note: "Share one source saw of another" },
|
|
295
|
+
"chart.stem": { key: "orders", alpha: 1, role: "data", note: "One day that had orders" },
|
|
296
|
+
"chart.zeroTick": { key: "orders", alpha: 1, role: "data", note: "A day MEASURED at zero \u2014 the package's founding distinction" },
|
|
297
|
+
"chart.regionEdge": { key: "ga4Pageviews", alpha: 1, role: "data", note: "The upper boundary of the shortfall region" },
|
|
298
|
+
"chart.region": {
|
|
299
|
+
key: "ga4Pageviews",
|
|
300
|
+
alpha: 0.22,
|
|
301
|
+
role: "fill",
|
|
302
|
+
boundary: "chart.regionEdge",
|
|
303
|
+
note: "What the lossier source missed. Light on purpose \u2014 the line it covers must stay readable"
|
|
304
|
+
},
|
|
305
|
+
"chart.ghost": { key: "neutral", alpha: 0.55, role: "furniture", note: "The same window last period, behind everything" },
|
|
306
|
+
"chart.grid": { key: "neutral", alpha: 0.18, role: "furniture", note: "Grid rules" },
|
|
307
|
+
"bar.fill": { key: "vercel", alpha: 1, role: "data", note: "Every proportion, comparison and funnel bar" },
|
|
308
|
+
"bar.track": { key: "neutral", alpha: 0.12, role: "furniture", note: "The rail a bar sits in" },
|
|
309
|
+
"bar.lost": { key: "revenue", alpha: 1, role: "data", note: "People who did not continue past a funnel rung" }
|
|
310
|
+
};
|
|
311
|
+
function mark(name) {
|
|
312
|
+
const m = MARKS[name];
|
|
313
|
+
if (!m) throw new Error(`Unknown mark: ${name}`);
|
|
314
|
+
if (m.key === "neutral") return `rgba(127, 127, 127, ${m.alpha})`;
|
|
315
|
+
return seriesFill(m.key, m.alpha);
|
|
316
|
+
}
|
|
291
317
|
|
|
292
318
|
// src/studio/Figure.tsx
|
|
293
319
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -328,6 +354,7 @@ function formatMoney(value, currency, fractionDigits = 0) {
|
|
|
328
354
|
return `${formatCount(Math.round(value))} ${currency}`;
|
|
329
355
|
}
|
|
330
356
|
}
|
|
357
|
+
var MIN_DELTA_BASE = 25;
|
|
331
358
|
function Delta({ current, previous, riseIsGood = true, unit = "count" }) {
|
|
332
359
|
if (!Number.isFinite(current) || !Number.isFinite(previous)) return null;
|
|
333
360
|
const now = current;
|
|
@@ -342,7 +369,8 @@ function Delta({ current, previous, riseIsGood = true, unit = "count" }) {
|
|
|
342
369
|
const tone = rising === riseIsGood ? "good" : "bad";
|
|
343
370
|
const arrow = rising ? "\u2191" : "\u2193";
|
|
344
371
|
const baseline = unit === "percent" ? `${before.toFixed(1)}%` : formatCount(before);
|
|
345
|
-
const
|
|
372
|
+
const tooFewToRate = change !== null && unit !== "percent" && Math.abs(before) < MIN_DELTA_BASE;
|
|
373
|
+
const magnitude = change === null ? rising ? "new" : "gone" : tooFewToRate ? "" : unit === "percent" ? `${rising ? "+" : ""}${absolute.toFixed(1)} pts` : `${rising ? "+" : ""}${formatPercent(change, 0)}`;
|
|
346
374
|
return (
|
|
347
375
|
// The baseline is printed, not hidden in a `title`. At seven orders a quarter an 18% move
|
|
348
376
|
// might be one order, so "from 6" is the fact and the percentage is the decoration — and a
|
|
@@ -548,9 +576,20 @@ function FunnelChart({ stages, measurement }) {
|
|
|
548
576
|
const active = activeKey === stage.key;
|
|
549
577
|
const delta = previous ? previous.value - stage.value : 0;
|
|
550
578
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: funnelItem, children: [
|
|
551
|
-
previous && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: funnelGap,
|
|
552
|
-
measurement === "sequence" && delta > 0 && entry > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
553
|
-
|
|
579
|
+
previous && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: funnelGap, children: [
|
|
580
|
+
measurement === "sequence" && delta > 0 && entry > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-hidden": "true", style: { ...barTrack, height: 6, flex: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
581
|
+
"div",
|
|
582
|
+
{
|
|
583
|
+
style: {
|
|
584
|
+
marginLeft: "auto",
|
|
585
|
+
height: "100%",
|
|
586
|
+
borderRadius: 2,
|
|
587
|
+
background: mark("bar.lost"),
|
|
588
|
+
width: `${Math.max(1, Math.min(1, delta / entry) * 100)}%`
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
) }),
|
|
592
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: gapLabel(delta, measurement, entry) })
|
|
554
593
|
] }),
|
|
555
594
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
556
595
|
"div",
|
|
@@ -582,9 +621,13 @@ function FunnelChart({ stages, measurement }) {
|
|
|
582
621
|
] }, stage.key);
|
|
583
622
|
}) });
|
|
584
623
|
}
|
|
585
|
-
function gapLabel(delta, measurement) {
|
|
624
|
+
function gapLabel(delta, measurement, entry = 0) {
|
|
586
625
|
if (delta === 0) return measurement === "sequence" ? "no drop-off" : "no difference";
|
|
587
|
-
if (delta > 0)
|
|
626
|
+
if (delta > 0) {
|
|
627
|
+
if (measurement !== "sequence") return `${formatCount(delta)} fewer`;
|
|
628
|
+
const share = entry > 0 ? ` \u2014 ${formatPercent(delta / entry, 0)} of everyone who landed` : "";
|
|
629
|
+
return `\u2212${formatCount(delta)} did not continue${share}`;
|
|
630
|
+
}
|
|
588
631
|
return `${formatCount(-delta)} more \u2014 not a subset of the step above`;
|
|
589
632
|
}
|
|
590
633
|
var funnelList = { listStyle: "none", margin: 0, padding: 0 };
|
|
@@ -596,19 +639,6 @@ var funnelGap = {
|
|
|
596
639
|
gap: 8,
|
|
597
640
|
padding: "4px 2px"
|
|
598
641
|
};
|
|
599
|
-
var lostTrack = {
|
|
600
|
-
width: "38%",
|
|
601
|
-
maxWidth: 200,
|
|
602
|
-
height: 6,
|
|
603
|
-
borderRadius: 2,
|
|
604
|
-
overflow: "hidden",
|
|
605
|
-
background: "currentColor",
|
|
606
|
-
opacity: 0.1,
|
|
607
|
-
// Right to left: the bar grows back toward the funnel it came out of, which reads as leaving
|
|
608
|
-
// rather than as another quantity accumulating alongside.
|
|
609
|
-
display: "flex",
|
|
610
|
-
justifyContent: "flex-end"
|
|
611
|
-
};
|
|
612
642
|
var lostFill = {
|
|
613
643
|
height: "100%",
|
|
614
644
|
borderRadius: 2,
|
|
@@ -629,18 +659,18 @@ function funnelStage(active) {
|
|
|
629
659
|
var barFill = {
|
|
630
660
|
height: "100%",
|
|
631
661
|
borderRadius: 2,
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
background:
|
|
636
|
-
opacity: 0.85
|
|
662
|
+
// Through the registry, not a hex plus an opacity chosen here. Drawn at 0.85 this sat at 2.77:1
|
|
663
|
+
// on the light theme while palette.test.ts asserted 3.42:1 against the bare constant — the test
|
|
664
|
+
// measured a colour that was never drawn. MARKS is what both now read.
|
|
665
|
+
background: mark("bar.fill")
|
|
637
666
|
};
|
|
638
667
|
var barTrack = {
|
|
639
668
|
height: 8,
|
|
640
669
|
borderRadius: 2,
|
|
641
670
|
overflow: "hidden",
|
|
642
|
-
|
|
643
|
-
|
|
671
|
+
// Furniture, not data: a rail is not a value, so 3:1 does not apply to it — but it is declared
|
|
672
|
+
// in MARKS so that exemption is stated somewhere a reader can check rather than assumed.
|
|
673
|
+
background: mark("bar.track")
|
|
644
674
|
};
|
|
645
675
|
var barHeader = {
|
|
646
676
|
display: "flex",
|
|
@@ -1431,9 +1461,8 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1431
1461
|
x2: at,
|
|
1432
1462
|
y1: TOP_PAD,
|
|
1433
1463
|
y2: height - AXIS_HEIGHT + 4,
|
|
1434
|
-
stroke: "
|
|
1435
|
-
strokeWidth: 1
|
|
1436
|
-
opacity: 0.18
|
|
1464
|
+
stroke: mark("chart.grid"),
|
|
1465
|
+
strokeWidth: 1
|
|
1437
1466
|
},
|
|
1438
1467
|
`grid-${date}`
|
|
1439
1468
|
);
|
|
@@ -1465,7 +1494,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1465
1494
|
const stems = row.mark === "events" ? row.points.filter((p) => p.value !== null && p.value !== 0) : [];
|
|
1466
1495
|
const zeroes = row.mark === "events" && pitch >= 6 ? row.points.filter((p) => p.value === 0) : [];
|
|
1467
1496
|
const gap = row.shortfall ? gapGen(row.points) ?? "" : "";
|
|
1468
|
-
const shortfallLine = row.shortfall
|
|
1497
|
+
const shortfallLine = row.shortfall ? lineGen(shortfallPoints) ?? "" : "";
|
|
1469
1498
|
const clipId = `${instanceId}-row-${rowIndex}`;
|
|
1470
1499
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
|
|
1471
1500
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("clipPath", { id: clipId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: GUTTER, y: plotTop, width: plotWidth, height: Math.max(1, bottom - plotTop) }) }) }),
|
|
@@ -1484,21 +1513,29 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1484
1513
|
{
|
|
1485
1514
|
d: ghost,
|
|
1486
1515
|
fill: "none",
|
|
1487
|
-
stroke: "
|
|
1516
|
+
stroke: mark("chart.ghost"),
|
|
1488
1517
|
strokeWidth: 1,
|
|
1489
|
-
strokeDasharray: "2 3"
|
|
1490
|
-
opacity: 0.35
|
|
1518
|
+
strokeDasharray: "2 3"
|
|
1491
1519
|
}
|
|
1492
1520
|
),
|
|
1493
1521
|
gap && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1494
1522
|
"path",
|
|
1495
1523
|
{
|
|
1496
1524
|
d: gap,
|
|
1497
|
-
fill: shortfallStroke ? seriesFill(colorFor(row.shortfall.source, row.unit, row.shortfall.color),
|
|
1525
|
+
fill: shortfallStroke ? seriesFill(colorFor(row.shortfall.source, row.unit, row.shortfall.color), MARKS["chart.region"].alpha) : "currentColor",
|
|
1498
1526
|
opacity: shortfallStroke ? 1 : 0.48
|
|
1499
1527
|
}
|
|
1500
1528
|
),
|
|
1501
|
-
shortfallLine && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1529
|
+
shortfallLine && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1530
|
+
"path",
|
|
1531
|
+
{
|
|
1532
|
+
d: shortfallLine,
|
|
1533
|
+
fill: "none",
|
|
1534
|
+
stroke: mark("chart.regionEdge"),
|
|
1535
|
+
strokeWidth: 1.5,
|
|
1536
|
+
strokeDasharray: "6 4"
|
|
1537
|
+
}
|
|
1538
|
+
),
|
|
1502
1539
|
zeroes.map((p) => {
|
|
1503
1540
|
const cx = at(p);
|
|
1504
1541
|
if (!Number.isFinite(cx)) return null;
|
|
@@ -1509,8 +1546,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1509
1546
|
y: zeroY - 3,
|
|
1510
1547
|
width: stemWidth,
|
|
1511
1548
|
height: 3,
|
|
1512
|
-
fill: stroke
|
|
1513
|
-
opacity: 0.7
|
|
1549
|
+
fill: stroke
|
|
1514
1550
|
},
|
|
1515
1551
|
`zero-${p.date}`
|
|
1516
1552
|
);
|
|
@@ -1526,8 +1562,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1526
1562
|
y: Math.min(headY, zeroY),
|
|
1527
1563
|
width: stemWidth,
|
|
1528
1564
|
height: Math.max(1.5, Math.abs(zeroY - headY)),
|
|
1529
|
-
fill: stroke
|
|
1530
|
-
opacity: 0.9
|
|
1565
|
+
fill: stroke
|
|
1531
1566
|
},
|
|
1532
1567
|
p.date
|
|
1533
1568
|
);
|
|
@@ -1539,8 +1574,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1539
1574
|
fill: "none",
|
|
1540
1575
|
stroke,
|
|
1541
1576
|
strokeWidth: 2,
|
|
1542
|
-
strokeDasharray: row.complete ? void 0 : "6 4"
|
|
1543
|
-
opacity: row.complete ? 0.95 : 0.7
|
|
1577
|
+
strokeDasharray: row.complete ? void 0 : "6 4"
|
|
1544
1578
|
}
|
|
1545
1579
|
)
|
|
1546
1580
|
] })
|
|
@@ -1897,9 +1931,9 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1897
1931
|
const average = averageOrderValue(data);
|
|
1898
1932
|
const value = metricSortValue(average);
|
|
1899
1933
|
if (value === null) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: average, label: "Average order value" });
|
|
1900
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1901
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 4, children: formatMoney(value, data.currency ?? null) }),
|
|
1902
|
-
average.status === "partial" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children:
|
|
1934
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
|
|
1935
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: figureRow, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 4, children: formatMoney(value, data.currency ?? null) }) }),
|
|
1936
|
+
average.status === "partial" && average.note && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: average.note })
|
|
1903
1937
|
] });
|
|
1904
1938
|
})(),
|
|
1905
1939
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "From your own orders, so it is exact \u2014 but only across the orders that carry an amount, which is what the revenue figure covers." })
|