@liiift-studio/sanity-visitor-insights 0.52.0 → 0.53.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 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 magnitude = change === null ? rising ? "new" : "gone" : unit === "percent" ? `${rising ? "+" : ""}${absolute.toFixed(1)} pts` : `${rising ? "+" : ""}${formatPercent(change, 0)}`;
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, "aria-hidden": "true", children: [
552
- measurement === "sequence" && delta > 0 && entry > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: lostTrack, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...lostFill, width: `${Math.max(1, Math.min(1, delta / entry) * 100)}%` } }) }),
553
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: gapLabel(delta, measurement) })
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) return measurement === "sequence" ? `\u2212${formatCount(delta)} did not continue` : `${formatCount(delta)} fewer`;
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
- // The reference blue, as everywhere else the tool draws "people who were here". Previously
633
- // currentColor at 0.55, which made the funnel a stack of grey bars whose only variable was
634
- // length legible, and giving the eye nothing to hold on to across five rungs.
635
- background: SERIES.vercel,
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
- background: "currentColor",
643
- opacity: 0.12
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",
@@ -1897,9 +1927,9 @@ function OverviewPanel({ data, previous, onBrush }) {
1897
1927
  const average = averageOrderValue(data);
1898
1928
  const value = metricSortValue(average);
1899
1929
  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)("div", { style: figureRow, children: [
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: "partial" })
1930
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
1931
+ /* @__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) }) }),
1932
+ average.status === "partial" && average.note && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: average.note })
1903
1933
  ] });
1904
1934
  })(),
1905
1935
  /* @__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." })