@liiift-studio/sanity-visitor-insights 0.66.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,
@@ -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,73 @@ 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
+ }
899
943
  function SortableTable({
900
944
  caption,
901
945
  columns,
@@ -1173,6 +1217,7 @@ var shiftNow = {
1173
1217
  borderRadius: "50%",
1174
1218
  background: mark("shift.now")
1175
1219
  };
1220
+ var survivalLegend = { display: "flex", gap: 14, flexWrap: "wrap" };
1176
1221
  var filterInput = {
1177
1222
  font: "inherit",
1178
1223
  fontSize: "0.85em",
@@ -2029,24 +2074,6 @@ var figureRow = {
2029
2074
  gap: 10,
2030
2075
  flexWrap: "wrap"
2031
2076
  };
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
2077
  var sourceLink = {
2051
2078
  color: "inherit",
2052
2079
  textDecoration: "underline",
@@ -2484,10 +2511,45 @@ function DataHealthPanel({ data, diagnostics }) {
2484
2511
  part: data.ga4Pageviews,
2485
2512
  missingLabel: "it missed"
2486
2513
  }
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
- ] })
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
+ )
2491
2553
  ] }),
2492
2554
  diagnostics && /* Open, and primary. This is the longest block on the tab and the obvious candidate
2493
2555
  for folding away — but the tab it sits on exists to answer "what should I fix
@@ -2526,10 +2588,18 @@ function DataHealthPanel({ data, diagnostics }) {
2526
2588
  Object.keys(data.orderStatuses ?? {}).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2527
2589
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "Order statuses in this range", tone: "secondary" }),
2528
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." }),
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)) })
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
+ )
2533
2603
  ] }),
2534
2604
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2535
2605
  Section,
@@ -2776,48 +2846,6 @@ function rankLine(series, pick) {
2776
2846
  const ranked = weeklyRank(series.map((d) => ({ date: d.date, value: pick(d) })));
2777
2847
  return ranked ? describeRank(ranked) : null;
2778
2848
  }
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
2849
  function revenueCoversEveryOrder(data) {
2822
2850
  const counted = metricSortValue(data.orders);
2823
2851
  const withTotal = finiteOrNull(data.ordersWithTotal);
@@ -2826,6 +2854,7 @@ function revenueCoversEveryOrder(data) {
2826
2854
  }
2827
2855
  function JourneyPanel({ data }) {
2828
2856
  const segments = data.segments ?? [];
2857
+ const spine = [...segments].sort((a, b) => b.steps.length - a.steps.length)[0]?.steps ?? [];
2829
2858
  const allSteps = data.steps ?? [];
2830
2859
  const shown = allSteps.filter((step) => step.count.status !== "unavailable");
2831
2860
  const hiddenSteps = allSteps.filter((step) => step.count.status === "unavailable");
@@ -2838,7 +2867,52 @@ function JourneyPanel({ data }) {
2838
2867
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: tracked ? "Tracked path" : "Not a tracked path" }),
2839
2868
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: tracked, children: data.approximationNote })
2840
2869
  ] }) }),
2841
- /* @__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
+ ),
2842
2916
  spread(segments) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, children: spread(segments) }),
2843
2917
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FunnelChart, { stages, measurement: data.measurement ?? "independent-totals" }),
2844
2918
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: splitGrid, children: [
@@ -3895,7 +3969,6 @@ var index_default = visitorInsights;
3895
3969
  0 && (module.exports = {
3896
3970
  AcquisitionPanel,
3897
3971
  ChartData,
3898
- ComparisonBar,
3899
3972
  CrossSourceTimeline,
3900
3973
  DataHealthPanel,
3901
3974
  Delta,