@liiift-studio/sanity-visitor-insights 0.65.0 → 0.67.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,
@@ -310,7 +309,12 @@ var MARKS = {
310
309
  "bar.seen": { key: "ga4Pageviews", alpha: 1, role: "data", note: "The share of a complete count that the lossy source saw" },
311
310
  "estimate.dot": { key: "ga4Pageviews", alpha: 1, role: "data", note: "One independent estimate of how much GA4 sees" },
312
311
  "estimate.interval": { key: "ga4Pageviews", alpha: 0.35, role: "fill", boundary: "estimate.dot", note: "How wide the sample leaves that estimate \u2014 the dot is its own boundary" },
313
- "estimate.rule": { key: "neutral", alpha: 0.45, role: "furniture", note: "The 100% reference a capture rate is read against" }
312
+ "estimate.rule": { key: "neutral", alpha: 0.45, role: "furniture", note: "The 100% reference a capture rate is read against" },
313
+ "shift.now": { key: "vercel", alpha: 1, role: "data", note: "A channel this period" },
314
+ "shift.before": { key: "neutral", alpha: 0.55, role: "furniture", note: "The same channel last period \u2014 context, not a second answer" },
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" }
314
318
  };
315
319
  function mark(name) {
316
320
  const m = MARKS[name];
@@ -496,30 +500,6 @@ function MetricFigure({ metric, label, size = 4, unit = "count" }) {
496
500
  }
497
501
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size, children: formatted });
498
502
  }
499
- function ComparisonBar({ label, metric, max, outOf }) {
500
- const value = metric.status === "unavailable" ? null : metric.value;
501
- const width = value !== null && max > 0 ? Math.max(2, value / max * 100) : 0;
502
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
503
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: barHeader, children: [
504
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: label }),
505
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MetricFigure, { metric, label, size: 1 })
506
- ] }),
507
- value === null ? (
508
- // A dashed rail, not a zero-width bar: absence must not read as a measured zero.
509
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
510
- import_sanity_ui_compat.Card,
511
- {
512
- "aria-hidden": "true",
513
- radius: 2,
514
- tone: "transparent",
515
- border: true,
516
- style: { height: 8, borderStyle: "dashed" }
517
- }
518
- )
519
- ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-hidden": "true", style: barTrack, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...barFill, width: `${width}%` } }) }),
520
- outOf && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: outOf })
521
- ] });
522
- }
523
503
  function ChartData({ label, rows, columns, rowKey, exportName, note, filterOn }) {
524
504
  if (rows.length === 0) return null;
525
505
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { style: disclosureBlock, children: [
@@ -893,6 +873,73 @@ function EstimateDotPlot({
893
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%." })
894
874
  ] });
895
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
+ }
896
943
  function SortableTable({
897
944
  caption,
898
945
  columns,
@@ -1139,6 +1186,38 @@ var estimateDot = {
1139
1186
  borderRadius: "50%",
1140
1187
  background: mark("estimate.dot")
1141
1188
  };
1189
+ var shiftTrack = {
1190
+ position: "relative",
1191
+ height: 16,
1192
+ borderRadius: 2,
1193
+ background: mark("bar.track")
1194
+ };
1195
+ var shiftLink = {
1196
+ position: "absolute",
1197
+ top: 7,
1198
+ height: 2,
1199
+ background: mark("shift.link")
1200
+ };
1201
+ var shiftBefore = {
1202
+ position: "absolute",
1203
+ top: 3,
1204
+ width: 10,
1205
+ height: 10,
1206
+ marginLeft: -5,
1207
+ borderRadius: "50%",
1208
+ border: `2px solid ${mark("shift.before")}`,
1209
+ background: "transparent"
1210
+ };
1211
+ var shiftNow = {
1212
+ position: "absolute",
1213
+ top: 3,
1214
+ width: 10,
1215
+ height: 10,
1216
+ marginLeft: -5,
1217
+ borderRadius: "50%",
1218
+ background: mark("shift.now")
1219
+ };
1220
+ var survivalLegend = { display: "flex", gap: 14, flexWrap: "wrap" };
1142
1221
  var filterInput = {
1143
1222
  font: "inherit",
1144
1223
  fontSize: "0.85em",
@@ -1995,24 +2074,6 @@ var figureRow = {
1995
2074
  gap: 10,
1996
2075
  flexWrap: "wrap"
1997
2076
  };
1998
- var segmentHeadCell = {
1999
- textAlign: "left",
2000
- fontSize: "0.78em",
2001
- letterSpacing: "0.06em",
2002
- textTransform: "uppercase",
2003
- fontWeight: 500,
2004
- opacity: 0.7,
2005
- padding: "8px 12px",
2006
- borderBottom: "1px solid var(--card-border-color, rgba(128,128,128,0.3))",
2007
- whiteSpace: "nowrap"
2008
- };
2009
- var segmentRowCell = {
2010
- textAlign: "left",
2011
- fontWeight: 400,
2012
- padding: "8px 12px",
2013
- borderBottom: "1px solid var(--card-border-color, rgba(128,128,128,0.18))",
2014
- verticalAlign: "baseline"
2015
- };
2016
2077
  var sourceLink = {
2017
2078
  color: "inherit",
2018
2079
  textDecoration: "underline",
@@ -2450,10 +2511,45 @@ function DataHealthPanel({ data, diagnostics }) {
2450
2511
  part: data.ga4Pageviews,
2451
2512
  missingLabel: "it missed"
2452
2513
  }
2453
- ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
2454
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComparisonBar, { label: "Vercel pageviews", metric: data.vercelPageviews, max: pageviewMax }),
2455
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComparisonBar, { label: "GA4 pageviews", metric: data.ga4Pageviews, max: pageviewMax })
2456
- ] })
2514
+ ) : isContainment(data.ga4Pageviews, data.vercelPageviews) ? (
2515
+ /* Still a containment the operands are simply the other way round.
2516
+
2517
+ When GA4 counts MORE than Vercel, it is because Vercel started collecting after
2518
+ the range began, so Vercel's count is the subset. Falling back to two peer bars
2519
+ scaled to the larger put GA4 at full width saying nothing, which is verbatim the
2520
+ defect ContainmentBar was built to kill. */
2521
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
2522
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2523
+ ContainmentBar,
2524
+ {
2525
+ wholeLabel: "Pageviews Google Analytics counted",
2526
+ whole: data.ga4Pageviews,
2527
+ partLabel: "Also counted by Vercel",
2528
+ part: data.vercelPageviews,
2529
+ missingLabel: "Vercel did not see"
2530
+ }
2531
+ ),
2532
+ /* @__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." })
2533
+ ] })
2534
+ ) : (
2535
+ /* Neither containment holds, because only one source answered.
2536
+
2537
+ Deleting ComparisonBar took this case with it: with GA4 down, both containments
2538
+ return null and the section rendered NOTHING — not even the figure Vercel did
2539
+ report. That is the state where the reader most needs to see what survived, and
2540
+ it is the one the card below spends a comment explaining. */
2541
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2542
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: figureRow, children: [
2543
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Pageviews Vercel counted" }),
2544
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.vercelPageviews, label: "Vercel pageviews", size: 3 })
2545
+ ] }),
2546
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: figureRow, children: [
2547
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Seen by Google Analytics" }),
2548
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: data.ga4Pageviews, label: "GA4 pageviews", size: 3 })
2549
+ ] }),
2550
+ /* @__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." })
2551
+ ] })
2552
+ )
2457
2553
  ] }),
2458
2554
  diagnostics && /* Open, and primary. This is the longest block on the tab and the obvious candidate
2459
2555
  for folding away — but the tab it sits on exists to answer "what should I fix
@@ -2492,10 +2588,18 @@ function DataHealthPanel({ data, diagnostics }) {
2492
2588
  Object.keys(data.orderStatuses ?? {}).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2493
2589
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Order statuses in this range", tone: "secondary" }),
2494
2590
  /* @__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." }),
2495
- /* @__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: [
2496
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: status }),
2497
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 3, children: formatCount(count) })
2498
- ] }) }, status)) })
2591
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2592
+ ProportionChart,
2593
+ {
2594
+ bars: Object.entries(data.orderStatuses ?? {}).map(([status, count]) => ({
2595
+ key: status,
2596
+ label: status,
2597
+ value: count
2598
+ })),
2599
+ format: (n) => formatCount(n),
2600
+ totalLabel: "Orders in this range"
2601
+ }
2602
+ )
2499
2603
  ] }),
2500
2604
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2501
2605
  Section,
@@ -2742,48 +2846,6 @@ function rankLine(series, pick) {
2742
2846
  const ranked = weeklyRank(series.map((d) => ({ date: d.date, value: pick(d) })));
2743
2847
  return ranked ? describeRank(ranked) : null;
2744
2848
  }
2745
- function SegmentTable({ segments, dimension }) {
2746
- if (segments.length < 2) return null;
2747
- const spine = [...segments].sort((a, b) => b.steps.length - a.steps.length)[0]?.steps ?? [];
2748
- if (spine.length === 0) return null;
2749
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2750
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: [
2751
- "Every ",
2752
- dimension,
2753
- " at once. A rate needs ",
2754
- MIN_RATE_DENOMINATOR,
2755
- " people at the step before it; below that only the count is shown."
2756
- ] }),
2757
- /* @__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: [
2758
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("caption", { style: visuallyHidden, children: [
2759
- "Funnel steps by ",
2760
- dimension
2761
- ] }),
2762
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { children: [
2763
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "col", style: segmentHeadCell, children: "Step" }),
2764
- segments.map((segment) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "col", style: { ...segmentHeadCell, textAlign: "right" }, children: segment.label }, segment.key))
2765
- ] }) }),
2766
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("tbody", { children: spine.map((step, index) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { children: [
2767
- /* @__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 }) }),
2768
- segments.map((segment) => {
2769
- const cell = segment.steps.find((s) => s.key === step.key);
2770
- const count = cell ? metricSortValue(cell.count) : null;
2771
- const before = index > 0 ? metricSortValue(segment.steps.find((s) => s.key === spine[index - 1].key)?.count) : null;
2772
- const rate = cell?.conversionFromPrevious ?? null;
2773
- const showRate = index > 0 && rate !== null && before !== null && before >= MIN_RATE_DENOMINATOR;
2774
- 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: [
2775
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: formatCount(count) }),
2776
- showRate && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
2777
- formatPercent(rate, 1),
2778
- " of ",
2779
- formatCount(before)
2780
- ] })
2781
- ] }) }, segment.key);
2782
- })
2783
- ] }, step.key)) })
2784
- ] }) })
2785
- ] });
2786
- }
2787
2849
  function revenueCoversEveryOrder(data) {
2788
2850
  const counted = metricSortValue(data.orders);
2789
2851
  const withTotal = finiteOrNull(data.ordersWithTotal);
@@ -2792,6 +2854,7 @@ function revenueCoversEveryOrder(data) {
2792
2854
  }
2793
2855
  function JourneyPanel({ data }) {
2794
2856
  const segments = data.segments ?? [];
2857
+ const spine = [...segments].sort((a, b) => b.steps.length - a.steps.length)[0]?.steps ?? [];
2795
2858
  const allSteps = data.steps ?? [];
2796
2859
  const shown = allSteps.filter((step) => step.count.status !== "unavailable");
2797
2860
  const hiddenSteps = allSteps.filter((step) => step.count.status === "unavailable");
@@ -2804,7 +2867,52 @@ function JourneyPanel({ data }) {
2804
2867
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: tracked ? "Tracked path" : "Not a tracked path" }),
2805
2868
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: tracked, children: data.approximationNote })
2806
2869
  ] }) }),
2807
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SegmentTable, { segments, dimension: data.segmentDimension ?? "segment" }),
2870
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2871
+ SurvivalLines,
2872
+ {
2873
+ segments: segments.map((segment) => ({
2874
+ key: segment.key,
2875
+ label: segment.label,
2876
+ values: spine.map((step) => {
2877
+ const cell = segment.steps.find((s) => s.key === step.key);
2878
+ return cell ? metricSortValue(cell.count) : null;
2879
+ })
2880
+ })),
2881
+ stepLabels: spine.map((step) => step.label)
2882
+ }
2883
+ ),
2884
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2885
+ ChartData,
2886
+ {
2887
+ label: "Show the figures behind this",
2888
+ rows: segments,
2889
+ rowKey: (segment) => segment.key,
2890
+ columns: [
2891
+ {
2892
+ key: "segment",
2893
+ label: data.segmentDimension ?? "Segment",
2894
+ sortValue: (segment) => segment.label,
2895
+ render: (segment) => segment.label
2896
+ },
2897
+ ...spine.map((step) => ({
2898
+ key: step.key,
2899
+ label: step.label,
2900
+ numeric: true,
2901
+ sortValue: (segment) => metricSortValue(segment.steps.find((s) => s.key === step.key)?.count),
2902
+ render: (segment) => {
2903
+ const cell = segment.steps.find((s) => s.key === step.key);
2904
+ const count = metricSortValue(cell?.count);
2905
+ if (count === null) return "\u2014";
2906
+ const index = spine.findIndex((sp) => sp.key === step.key);
2907
+ const before = index > 0 ? metricSortValue(segment.steps.find((s) => s.key === spine[index - 1].key)?.count) : null;
2908
+ const rate = cell?.conversionFromPrevious ?? null;
2909
+ const showRate = index > 0 && rate !== null && before !== null && before >= MIN_RATE_DENOMINATOR;
2910
+ return showRate ? `${formatCount(count)} \xB7 ${formatPercent(rate, 1)} of ${formatCount(before)}` : formatCount(count);
2911
+ }
2912
+ }))
2913
+ ]
2914
+ }
2915
+ ),
2808
2916
  spread(segments) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: spread(segments) }),
2809
2917
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FunnelChart, { stages, measurement: data.measurement ?? "independent-totals" }),
2810
2918
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: splitGrid, children: [
@@ -3861,7 +3969,6 @@ var index_default = visitorInsights;
3861
3969
  0 && (module.exports = {
3862
3970
  AcquisitionPanel,
3863
3971
  ChartData,
3864
- ComparisonBar,
3865
3972
  CrossSourceTimeline,
3866
3973
  DataHealthPanel,
3867
3974
  Delta,