@liiift-studio/sanity-visitor-insights 0.66.0 → 0.68.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
@@ -214,30 +214,6 @@ interface MetricFigureProps {
214
214
  * Screen readers get the reason text rather than an unexplained dash.
215
215
  */
216
216
  declare function MetricFigure({ metric, label, size, unit }: MetricFigureProps): React.ReactElement;
217
- /** Props for ComparisonBar. */
218
- interface ComparisonBarProps {
219
- label: string;
220
- metric: MetricValue;
221
- /** Largest value across the sibling bars, used to scale width. */
222
- max: number;
223
- /**
224
- * What full width means, named.
225
- *
226
- * A ComparisonBar scales to the largest sibling, so a full bar means only "biggest of these" —
227
- * and unlike the other two bar idioms, which print their share and their total, this one said
228
- * nothing at all. There was a `tone` prop instead, which both call sites passed different
229
- * values to and which the component destructured and never used.
230
- */
231
- outOf?: string;
232
- }
233
- /**
234
- * A horizontal bar with its value printed alongside.
235
- *
236
- * Deliberately CSS rather than a charting library: these panels compare and rank a handful of
237
- * values, which a labelled bar does as well as a chart while avoiding a large dependency in the
238
- * Studio bundle and the theme-token bridging that a chart library would need for light and dark.
239
- */
240
- declare function ComparisonBar({ label, metric, max, outOf }: ComparisonBarProps): React.ReactElement;
241
217
  /** Props for ChartData. */
242
218
  interface ChartDataProps<Row> {
243
219
  /** What the disclosure is called. Names the chart it belongs to. */
@@ -655,4 +631,4 @@ interface VisitorInsightsPluginOptions {
655
631
  */
656
632
  declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
657
633
 
658
- export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type ViewState, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, decodeView, visitorInsights as default, encodeView, formatCount, formatMoney, formatPercent, knownShortfall, mergeIntoHash, useReport, visitorInsights };
634
+ export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type ViewState, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, decodeView, visitorInsights as default, encodeView, formatCount, formatMoney, formatPercent, knownShortfall, mergeIntoHash, useReport, visitorInsights };
package/dist/index.d.ts CHANGED
@@ -214,30 +214,6 @@ interface MetricFigureProps {
214
214
  * Screen readers get the reason text rather than an unexplained dash.
215
215
  */
216
216
  declare function MetricFigure({ metric, label, size, unit }: MetricFigureProps): React.ReactElement;
217
- /** Props for ComparisonBar. */
218
- interface ComparisonBarProps {
219
- label: string;
220
- metric: MetricValue;
221
- /** Largest value across the sibling bars, used to scale width. */
222
- max: number;
223
- /**
224
- * What full width means, named.
225
- *
226
- * A ComparisonBar scales to the largest sibling, so a full bar means only "biggest of these" —
227
- * and unlike the other two bar idioms, which print their share and their total, this one said
228
- * nothing at all. There was a `tone` prop instead, which both call sites passed different
229
- * values to and which the component destructured and never used.
230
- */
231
- outOf?: string;
232
- }
233
- /**
234
- * A horizontal bar with its value printed alongside.
235
- *
236
- * Deliberately CSS rather than a charting library: these panels compare and rank a handful of
237
- * values, which a labelled bar does as well as a chart while avoiding a large dependency in the
238
- * Studio bundle and the theme-token bridging that a chart library would need for light and dark.
239
- */
240
- declare function ComparisonBar({ label, metric, max, outOf }: ComparisonBarProps): React.ReactElement;
241
217
  /** Props for ChartData. */
242
218
  interface ChartDataProps<Row> {
243
219
  /** What the disclosure is called. Names the chart it belongs to. */
@@ -655,4 +631,4 @@ interface VisitorInsightsPluginOptions {
655
631
  */
656
632
  declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
657
633
 
658
- export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type ViewState, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, decodeView, visitorInsights as default, encodeView, formatCount, formatMoney, formatPercent, knownShortfall, mergeIntoHash, useReport, visitorInsights };
634
+ export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type ViewState, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, decodeView, visitorInsights as default, encodeView, formatCount, formatMoney, formatPercent, knownShortfall, mergeIntoHash, useReport, visitorInsights };
package/dist/index.js CHANGED
@@ -32,7 +32,6 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AcquisitionPanel: () => AcquisitionPanel,
34
34
  ChartData: () => ChartData,
35
- ComparisonBar: () => ComparisonBar,
36
35
  CrossSourceTimeline: () => CrossSourceTimeline,
37
36
  DataHealthPanel: () => DataHealthPanel,
38
37
  Delta: () => Delta,
@@ -313,7 +312,9 @@ var MARKS = {
313
312
  "estimate.rule": { key: "neutral", alpha: 0.45, role: "furniture", note: "The 100% reference a capture rate is read against" },
314
313
  "shift.now": { key: "vercel", alpha: 1, role: "data", note: "A channel this period" },
315
314
  "shift.before": { key: "neutral", alpha: 0.55, role: "furniture", note: "The same channel last period \u2014 context, not a second answer" },
316
- "shift.link": { key: "neutral", alpha: 0.35, role: "furniture", note: "The distance between the two, which is the finding" }
315
+ "shift.link": { key: "neutral", alpha: 0.35, role: "furniture", note: "The distance between the two, which is the finding" },
316
+ "survival.line": { key: "vercel", alpha: 1, role: "data", note: "One audience segment falling through a funnel" },
317
+ "survival.grid": { key: "neutral", alpha: 0.18, role: "furniture", note: "The 50% and 100% references a survival line is read against" }
317
318
  };
318
319
  function mark(name) {
319
320
  const m = MARKS[name];
@@ -499,30 +500,6 @@ function MetricFigure({ metric, label, size = 4, unit = "count" }) {
499
500
  }
500
501
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size, children: formatted });
501
502
  }
502
- function ComparisonBar({ label, metric, max, outOf }) {
503
- const value = metric.status === "unavailable" ? null : metric.value;
504
- const width = value !== null && max > 0 ? Math.max(2, value / max * 100) : 0;
505
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
506
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: barHeader, children: [
507
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: label }),
508
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MetricFigure, { metric, label, size: 1 })
509
- ] }),
510
- value === null ? (
511
- // A dashed rail, not a zero-width bar: absence must not read as a measured zero.
512
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
513
- import_sanity_ui_compat.Card,
514
- {
515
- "aria-hidden": "true",
516
- radius: 2,
517
- tone: "transparent",
518
- border: true,
519
- style: { height: 8, borderStyle: "dashed" }
520
- }
521
- )
522
- ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-hidden": "true", style: barTrack, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...barFill, width: `${width}%` } }) }),
523
- outOf && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: outOf })
524
- ] });
525
- }
526
503
  function ChartData({ label, rows, columns, rowKey, exportName, note, filterOn }) {
527
504
  if (rows.length === 0) return null;
528
505
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { style: disclosureBlock, children: [
@@ -896,6 +873,101 @@ function EstimateDotPlot({
896
873
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "Each is a separate way of asking the same question. Where they disagree, the spread is the answer\u2019s uncertainty \u2014 the rule marks 100%." })
897
874
  ] });
898
875
  }
876
+ function SurvivalLines({
877
+ segments,
878
+ stepLabels,
879
+ minDenominator = MIN_RATE_DENOMINATOR
880
+ }) {
881
+ if (segments.length < 2 || stepLabels.length < 2) return null;
882
+ const width = 100;
883
+ const height = 46;
884
+ const x = (i) => i / (stepLabels.length - 1) * width;
885
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 3, children: [
886
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
887
+ "svg",
888
+ {
889
+ viewBox: `0 0 ${width} ${height}`,
890
+ preserveAspectRatio: "none",
891
+ style: { width: "100%", height: 150, overflow: "visible" },
892
+ role: "img",
893
+ "aria-label": `Share of each segment surviving to each step, ${stepLabels.join(", then ")}`,
894
+ children: [
895
+ [0, 0.5].map((level) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
896
+ "line",
897
+ {
898
+ x1: 0,
899
+ x2: width,
900
+ y1: height * level,
901
+ y2: height * level,
902
+ stroke: mark("survival.grid"),
903
+ strokeWidth: 0.4
904
+ },
905
+ level
906
+ )),
907
+ segments.map((segment, index) => {
908
+ const entry = segment.values[0];
909
+ if (entry === null || entry === void 0 || entry <= 0) return null;
910
+ const points = [];
911
+ for (let i = 0; i < segment.values.length && i < stepLabels.length; i++) {
912
+ const value = segment.values[i];
913
+ const previous = i === 0 ? entry : segment.values[i - 1];
914
+ if (value === null || value === void 0) break;
915
+ if (i > 0 && (previous === null || previous === void 0 || previous < minDenominator)) break;
916
+ points.push([x(i), height - Math.min(1, value / entry) * height]);
917
+ }
918
+ if (points.length < 2) return null;
919
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
920
+ "polyline",
921
+ {
922
+ points: points.map(([px, py]) => `${px},${py}`).join(" "),
923
+ fill: "none",
924
+ stroke: mark("survival.line"),
925
+ strokeWidth: 0.8,
926
+ vectorEffect: "non-scaling-stroke",
927
+ strokeDasharray: index === 0 ? void 0 : index === 1 ? "4 3" : "1 2",
928
+ opacity: index === 0 ? 1 : 0.75
929
+ },
930
+ segment.key
931
+ );
932
+ })
933
+ ]
934
+ }
935
+ ),
936
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: survivalLegend, children: segments.map((segment, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
937
+ index === 0 ? "\u2014\u2014" : index === 1 ? "\u2013 \u2013" : "\xB7 \xB7",
938
+ " ",
939
+ segment.label
940
+ ] }, segment.key)) })
941
+ ] });
942
+ }
943
+ function RatioFigure({
944
+ value,
945
+ format,
946
+ parts,
947
+ size = 4,
948
+ unavailable: unavailable2
949
+ }) {
950
+ const [showParts, setShowParts] = import_react2.default.useState(false);
951
+ const canToggle = value !== null && parts.every((p) => p.value !== null);
952
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Stack, { space: 2, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: ratioRow, children: [
953
+ showParts && canToggle ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Stack, { space: 1, children: parts.map((part) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: size > 1 ? 2 : size, children: [
954
+ (part.format ?? formatCount)(part.value),
955
+ " ",
956
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { as: "span", size: 0, muted: true, children: part.label })
957
+ ] }, part.label)) }) : value === null ? unavailable2 : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size, children: format(value) }),
958
+ canToggle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
959
+ "button",
960
+ {
961
+ type: "button",
962
+ style: ratioToggle,
963
+ "aria-pressed": showParts,
964
+ "aria-label": showParts ? "Show the rate" : "Show the counts behind this rate",
965
+ onClick: () => setShowParts((was) => !was),
966
+ children: showParts ? "rate" : "counts"
967
+ }
968
+ )
969
+ ] }) });
970
+ }
899
971
  function SortableTable({
900
972
  caption,
901
973
  columns,
@@ -1173,6 +1245,25 @@ var shiftNow = {
1173
1245
  borderRadius: "50%",
1174
1246
  background: mark("shift.now")
1175
1247
  };
1248
+ var survivalLegend = { display: "flex", gap: 14, flexWrap: "wrap" };
1249
+ var ratioRow = {
1250
+ display: "flex",
1251
+ alignItems: "baseline",
1252
+ gap: 10,
1253
+ flexWrap: "wrap"
1254
+ };
1255
+ var ratioToggle = {
1256
+ appearance: "none",
1257
+ background: "transparent",
1258
+ border: "1px solid var(--card-border-color, rgba(128,128,128,0.3))",
1259
+ borderRadius: 3,
1260
+ color: "inherit",
1261
+ font: "inherit",
1262
+ fontSize: "0.72em",
1263
+ padding: "1px 7px",
1264
+ cursor: "pointer",
1265
+ opacity: 0.75
1266
+ };
1176
1267
  var filterInput = {
1177
1268
  font: "inherit",
1178
1269
  fontSize: "0.85em",
@@ -2029,24 +2120,6 @@ var figureRow = {
2029
2120
  gap: 10,
2030
2121
  flexWrap: "wrap"
2031
2122
  };
2032
- var segmentHeadCell = {
2033
- textAlign: "left",
2034
- fontSize: "0.78em",
2035
- letterSpacing: "0.06em",
2036
- textTransform: "uppercase",
2037
- fontWeight: 500,
2038
- opacity: 0.7,
2039
- padding: "8px 12px",
2040
- borderBottom: "1px solid var(--card-border-color, rgba(128,128,128,0.3))",
2041
- whiteSpace: "nowrap"
2042
- };
2043
- var segmentRowCell = {
2044
- textAlign: "left",
2045
- fontWeight: 400,
2046
- padding: "8px 12px",
2047
- borderBottom: "1px solid var(--card-border-color, rgba(128,128,128,0.18))",
2048
- verticalAlign: "baseline"
2049
- };
2050
2123
  var sourceLink = {
2051
2124
  color: "inherit",
2052
2125
  textDecoration: "underline",
@@ -2120,21 +2193,36 @@ function OverviewPanel({ data, previous, onBrush }) {
2120
2193
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: cardGrid, children: [
2121
2194
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2122
2195
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Average order" }),
2123
- (() => {
2124
- const average = averageOrderValue(data);
2125
- const value = metricSortValue(average);
2126
- if (value === null) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: average, label: "Average order value" });
2127
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
2128
- /* @__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) }) }),
2129
- average.status === "partial" && average.note && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: average.note })
2130
- ] });
2131
- })(),
2196
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2197
+ RatioFigure,
2198
+ {
2199
+ value: metricSortValue(averageOrderValue(data)),
2200
+ format: (v) => formatMoney(v, data.currency ?? null),
2201
+ parts: [
2202
+ { label: "taken", value: metricSortValue(data.revenue), format: (v) => formatMoney(v, data.currency ?? null) },
2203
+ { label: "orders with an amount", value: finiteOrNull(data.ordersWithTotal) }
2204
+ ],
2205
+ unavailable: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: averageOrderValue(data), label: "Average order value" })
2206
+ }
2207
+ ),
2208
+ averageOrderValue(data).status === "partial" && averageOrderValue(data).note && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: averageOrderValue(data).note }),
2132
2209
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "From your orders, so exact." })
2133
2210
  ] }) }),
2134
2211
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2135
2212
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Visitors per order" }),
2136
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: visitorsPerOrder(data), label: "Visitors per order" }),
2137
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Against Vercel\u2019s visitor count, not Google Analytics\u2019 \u2014 dividing by GA4 would flatter this by whatever share it is missing. Read it as a ceiling: some of those visitors are crawlers or repeat devices." })
2213
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2214
+ RatioFigure,
2215
+ {
2216
+ value: metricSortValue(visitorsPerOrder(data)),
2217
+ format: (v) => formatCount(v),
2218
+ parts: [
2219
+ { label: "visitors", value: metricSortValue(data.vercelVisitors) },
2220
+ { label: "orders", value: metricSortValue(data.orders) }
2221
+ ],
2222
+ unavailable: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: visitorsPerOrder(data), label: "Visitors per order" })
2223
+ }
2224
+ ),
2225
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Vercel\u2019s visitor count. A ceiling \u2014 some are bots or the same person twice." })
2138
2226
  ] }) })
2139
2227
  ] }),
2140
2228
  (data.crossSource?.length ?? 0) >= 3 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
@@ -2408,7 +2496,14 @@ function OverviewPanel({ data, previous, onBrush }) {
2408
2496
  exportValue: (c) => revenuePerThousandSent(c),
2409
2497
  render: (c) => {
2410
2498
  const value = revenuePerThousandSent(c);
2411
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: value === null ? "\u2014" : formatMoney(value, data.currency ?? null) });
2499
+ if (value === null) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: "\u2014" });
2500
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 1, children: [
2501
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatMoney(value, data.currency ?? null) }),
2502
+ c.revenueAfter !== null && c.revenueAfter !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
2503
+ "from ",
2504
+ formatMoney(c.revenueAfter, data.currency ?? null)
2505
+ ] })
2506
+ ] });
2412
2507
  }
2413
2508
  }
2414
2509
  ]
@@ -2484,10 +2579,45 @@ function DataHealthPanel({ data, diagnostics }) {
2484
2579
  part: data.ga4Pageviews,
2485
2580
  missingLabel: "it missed"
2486
2581
  }
2487
- ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
2488
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComparisonBar, { label: "Vercel pageviews", metric: data.vercelPageviews, max: pageviewMax }),
2489
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComparisonBar, { label: "GA4 pageviews", metric: data.ga4Pageviews, max: pageviewMax })
2490
- ] })
2582
+ ) : isContainment(data.ga4Pageviews, data.vercelPageviews) ? (
2583
+ /* Still a containment the operands are simply the other way round.
2584
+
2585
+ When GA4 counts MORE than Vercel, it is because Vercel started collecting after
2586
+ the range began, so Vercel's count is the subset. Falling back to two peer bars
2587
+ scaled to the larger put GA4 at full width saying nothing, which is verbatim the
2588
+ defect ContainmentBar was built to kill. */
2589
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
2590
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2591
+ ContainmentBar,
2592
+ {
2593
+ wholeLabel: "Pageviews Google Analytics counted",
2594
+ whole: data.ga4Pageviews,
2595
+ partLabel: "Also counted by Vercel",
2596
+ part: data.vercelPageviews,
2597
+ missingLabel: "Vercel did not see"
2598
+ }
2599
+ ),
2600
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Vercel counted fewer because its collection started after this range began, not because the traffic was not there." })
2601
+ ] })
2602
+ ) : (
2603
+ /* Neither containment holds, because only one source answered.
2604
+
2605
+ Deleting ComparisonBar took this case with it: with GA4 down, both containments
2606
+ return null and the section rendered NOTHING — not even the figure Vercel did
2607
+ report. That is the state where the reader most needs to see what survived, and
2608
+ it is the one the card below spends a comment explaining. */
2609
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2610
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: figureRow, children: [
2611
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Pageviews Vercel counted" }),
2612
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.vercelPageviews, label: "Vercel pageviews", size: 3 })
2613
+ ] }),
2614
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: figureRow, children: [
2615
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Seen by Google Analytics" }),
2616
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.ga4Pageviews, label: "GA4 pageviews", size: 3 })
2617
+ ] }),
2618
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Only one source answered for this range, so there is nothing to compare." })
2619
+ ] })
2620
+ )
2491
2621
  ] }),
2492
2622
  diagnostics && /* Open, and primary. This is the longest block on the tab and the obvious candidate
2493
2623
  for folding away — but the tab it sits on exists to answer "what should I fix
@@ -2526,10 +2656,18 @@ function DataHealthPanel({ data, diagnostics }) {
2526
2656
  Object.keys(data.orderStatuses ?? {}).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2527
2657
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Order statuses in this range", tone: "secondary" }),
2528
2658
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "What the orders actually say, before any filtering. Use these values to set which statuses count as a sale." }),
2529
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: cardGrid, children: Object.entries(data.orderStatuses ?? {}).sort((a, b) => b[1] - a[1]).map(([status, count]) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2530
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: status }),
2531
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 3, children: formatCount(count) })
2532
- ] }) }, status)) })
2659
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2660
+ ProportionChart,
2661
+ {
2662
+ bars: Object.entries(data.orderStatuses ?? {}).map(([status, count]) => ({
2663
+ key: status,
2664
+ label: status,
2665
+ value: count
2666
+ })),
2667
+ format: (n) => formatCount(n),
2668
+ totalLabel: "Orders in this range"
2669
+ }
2670
+ )
2533
2671
  ] }),
2534
2672
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2535
2673
  Section,
@@ -2776,48 +2914,6 @@ function rankLine(series, pick) {
2776
2914
  const ranked = weeklyRank(series.map((d) => ({ date: d.date, value: pick(d) })));
2777
2915
  return ranked ? describeRank(ranked) : null;
2778
2916
  }
2779
- function SegmentTable({ segments, dimension }) {
2780
- if (segments.length < 2) return null;
2781
- const spine = [...segments].sort((a, b) => b.steps.length - a.steps.length)[0]?.steps ?? [];
2782
- if (spine.length === 0) return null;
2783
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2784
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: [
2785
- "Every ",
2786
- dimension,
2787
- " at once. A rate needs ",
2788
- MIN_RATE_DENOMINATOR,
2789
- " people at the step before it; below that only the count is shown."
2790
- ] }),
2791
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { radius: 2, tone: "transparent", border: true, style: { overflowX: "auto", width: "100%" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("table", { style: { width: "100%", borderCollapse: "collapse", minWidth: 360 }, children: [
2792
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("caption", { style: visuallyHidden, children: [
2793
- "Funnel steps by ",
2794
- dimension
2795
- ] }),
2796
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { children: [
2797
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "col", style: segmentHeadCell, children: "Step" }),
2798
- segments.map((segment) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "col", style: { ...segmentHeadCell, textAlign: "right" }, children: segment.label }, segment.key))
2799
- ] }) }),
2800
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("tbody", { children: spine.map((step, index) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { children: [
2801
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "row", style: segmentRowCell, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: step.label }) }),
2802
- segments.map((segment) => {
2803
- const cell = segment.steps.find((s) => s.key === step.key);
2804
- const count = cell ? metricSortValue(cell.count) : null;
2805
- const before = index > 0 ? metricSortValue(segment.steps.find((s) => s.key === spine[index - 1].key)?.count) : null;
2806
- const rate = cell?.conversionFromPrevious ?? null;
2807
- const showRate = index > 0 && rate !== null && before !== null && before >= MIN_RATE_DENOMINATOR;
2808
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { style: { ...segmentRowCell, textAlign: "right", fontVariantNumeric: "tabular-nums" }, children: count === null ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "\u2014" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 1, children: [
2809
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatCount(count) }),
2810
- showRate && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
2811
- formatPercent(rate, 1),
2812
- " of ",
2813
- formatCount(before)
2814
- ] })
2815
- ] }) }, segment.key);
2816
- })
2817
- ] }, step.key)) })
2818
- ] }) })
2819
- ] });
2820
- }
2821
2917
  function revenueCoversEveryOrder(data) {
2822
2918
  const counted = metricSortValue(data.orders);
2823
2919
  const withTotal = finiteOrNull(data.ordersWithTotal);
@@ -2826,6 +2922,7 @@ function revenueCoversEveryOrder(data) {
2826
2922
  }
2827
2923
  function JourneyPanel({ data }) {
2828
2924
  const segments = data.segments ?? [];
2925
+ const spine = [...segments].sort((a, b) => b.steps.length - a.steps.length)[0]?.steps ?? [];
2829
2926
  const allSteps = data.steps ?? [];
2830
2927
  const shown = allSteps.filter((step) => step.count.status !== "unavailable");
2831
2928
  const hiddenSteps = allSteps.filter((step) => step.count.status === "unavailable");
@@ -2838,7 +2935,52 @@ function JourneyPanel({ data }) {
2838
2935
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: tracked ? "Tracked path" : "Not a tracked path" }),
2839
2936
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: tracked, children: data.approximationNote })
2840
2937
  ] }) }),
2841
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SegmentTable, { segments, dimension: data.segmentDimension ?? "segment" }),
2938
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2939
+ SurvivalLines,
2940
+ {
2941
+ segments: segments.map((segment) => ({
2942
+ key: segment.key,
2943
+ label: segment.label,
2944
+ values: spine.map((step) => {
2945
+ const cell = segment.steps.find((s) => s.key === step.key);
2946
+ return cell ? metricSortValue(cell.count) : null;
2947
+ })
2948
+ })),
2949
+ stepLabels: spine.map((step) => step.label)
2950
+ }
2951
+ ),
2952
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2953
+ ChartData,
2954
+ {
2955
+ label: "Show the figures behind this",
2956
+ rows: segments,
2957
+ rowKey: (segment) => segment.key,
2958
+ columns: [
2959
+ {
2960
+ key: "segment",
2961
+ label: data.segmentDimension ?? "Segment",
2962
+ sortValue: (segment) => segment.label,
2963
+ render: (segment) => segment.label
2964
+ },
2965
+ ...spine.map((step) => ({
2966
+ key: step.key,
2967
+ label: step.label,
2968
+ numeric: true,
2969
+ sortValue: (segment) => metricSortValue(segment.steps.find((s) => s.key === step.key)?.count),
2970
+ render: (segment) => {
2971
+ const cell = segment.steps.find((s) => s.key === step.key);
2972
+ const count = metricSortValue(cell?.count);
2973
+ if (count === null) return "\u2014";
2974
+ const index = spine.findIndex((sp) => sp.key === step.key);
2975
+ const before = index > 0 ? metricSortValue(segment.steps.find((s) => s.key === spine[index - 1].key)?.count) : null;
2976
+ const rate = cell?.conversionFromPrevious ?? null;
2977
+ const showRate = index > 0 && rate !== null && before !== null && before >= MIN_RATE_DENOMINATOR;
2978
+ return showRate ? `${formatCount(count)} \xB7 ${formatPercent(rate, 1)} of ${formatCount(before)}` : formatCount(count);
2979
+ }
2980
+ }))
2981
+ ]
2982
+ }
2983
+ ),
2842
2984
  spread(segments) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: spread(segments) }),
2843
2985
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FunnelChart, { stages, measurement: data.measurement ?? "independent-totals" }),
2844
2986
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: splitGrid, children: [
@@ -3895,7 +4037,6 @@ var index_default = visitorInsights;
3895
4037
  0 && (module.exports = {
3896
4038
  AcquisitionPanel,
3897
4039
  ChartData,
3898
- ComparisonBar,
3899
4040
  CrossSourceTimeline,
3900
4041
  DataHealthPanel,
3901
4042
  Delta,