@liiift-studio/sanity-visitor-insights 0.73.0 → 0.75.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 +19 -2
- package/dist/index.d.ts +19 -2
- package/dist/index.js +82 -339
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +83 -340
- package/dist/index.mjs.map +1 -1
- package/dist/{ranges-DhfuNtgW.d.mts → ranges-DfjCK0XY.d.mts} +0 -2
- package/dist/{ranges-DhfuNtgW.d.ts → ranges-DfjCK0XY.d.ts} +0 -2
- package/dist/server.d.mts +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/server.js +1 -11
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +1 -11
- package/dist/server.mjs.map +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{vercel-pqyL1I1o.d.mts → vercel-CNoYeZgN.d.mts} +0 -2
- package/dist/{vercel-pqyL1I1o.d.ts → vercel-CNoYeZgN.d.ts} +0 -2
- package/package.json +1 -1
- package/src/reportData.ts +0 -2
- package/src/server/orders.ts +0 -2
- package/src/server/reports/measurementHealth.ts +0 -11
- package/src/server/reports/reports.test.ts +0 -2
- package/src/studio/CrossSourceTimeline.tsx +15 -40
- package/src/studio/Figure.tsx +161 -481
- package/src/studio/palette.test.ts +3 -1
- package/src/studio/palette.ts +5 -12
- package/src/studio/panels.test.tsx +260 -304
- package/src/studio/panels.tsx +24 -125
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as sanity from 'sanity';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-
|
|
4
|
-
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-
|
|
3
|
+
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-DfjCK0XY.mjs';
|
|
4
|
+
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-DfjCK0XY.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
|
@@ -297,6 +297,23 @@ interface FunnelStage {
|
|
|
297
297
|
partial?: {
|
|
298
298
|
from: string;
|
|
299
299
|
};
|
|
300
|
+
/**
|
|
301
|
+
* This rung split by audience segment, in a stable order across rungs.
|
|
302
|
+
*
|
|
303
|
+
* The bar's WIDTH is unchanged — still this rung's share of the entry step — and the split
|
|
304
|
+
* subdivides that width. So a segment collapsing reads as its portion shrinking down the column,
|
|
305
|
+
* against a total that stays honestly anchored to entry.
|
|
306
|
+
*
|
|
307
|
+
* This replaces a second chart. `SurvivalLines` drew the same segments as normalised shares on
|
|
308
|
+
* its own axis, above a funnel drawing the pooled counts: two pictures of one fact, and the
|
|
309
|
+
* share-only one could not print a count, which at rungs of 23 and 12 and 7 is the only fact
|
|
310
|
+
* that survives. Its denominator floor also silenced it at exactly those rungs.
|
|
311
|
+
*/
|
|
312
|
+
segments?: ReadonlyArray<{
|
|
313
|
+
key: string;
|
|
314
|
+
label: string;
|
|
315
|
+
value: number;
|
|
316
|
+
}>;
|
|
300
317
|
}
|
|
301
318
|
/** Props for FunnelChart. */
|
|
302
319
|
interface FunnelChartProps {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as sanity from 'sanity';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-
|
|
4
|
-
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-
|
|
3
|
+
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-DfjCK0XY.js';
|
|
4
|
+
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-DfjCK0XY.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
|
@@ -297,6 +297,23 @@ interface FunnelStage {
|
|
|
297
297
|
partial?: {
|
|
298
298
|
from: string;
|
|
299
299
|
};
|
|
300
|
+
/**
|
|
301
|
+
* This rung split by audience segment, in a stable order across rungs.
|
|
302
|
+
*
|
|
303
|
+
* The bar's WIDTH is unchanged — still this rung's share of the entry step — and the split
|
|
304
|
+
* subdivides that width. So a segment collapsing reads as its portion shrinking down the column,
|
|
305
|
+
* against a total that stays honestly anchored to entry.
|
|
306
|
+
*
|
|
307
|
+
* This replaces a second chart. `SurvivalLines` drew the same segments as normalised shares on
|
|
308
|
+
* its own axis, above a funnel drawing the pooled counts: two pictures of one fact, and the
|
|
309
|
+
* share-only one could not print a count, which at rungs of 23 and 12 and 7 is the only fact
|
|
310
|
+
* that survives. Its denominator floor also silenced it at exactly those rungs.
|
|
311
|
+
*/
|
|
312
|
+
segments?: ReadonlyArray<{
|
|
313
|
+
key: string;
|
|
314
|
+
label: string;
|
|
315
|
+
value: number;
|
|
316
|
+
}>;
|
|
300
317
|
}
|
|
301
318
|
/** Props for FunnelChart. */
|
|
302
319
|
interface FunnelChartProps {
|
package/dist/index.js
CHANGED
|
@@ -279,10 +279,6 @@ var SERIES = {
|
|
|
279
279
|
/** Revenue, same source, different unit. */
|
|
280
280
|
revenue: "#BF5D9B"
|
|
281
281
|
};
|
|
282
|
-
var REGION_ALPHA = 0.18;
|
|
283
|
-
function seriesFill(key, alpha = REGION_ALPHA) {
|
|
284
|
-
return withAlpha(SERIES[key], alpha);
|
|
285
|
-
}
|
|
286
282
|
function withAlpha(hex, alpha) {
|
|
287
283
|
const r = parseInt(hex.slice(1, 3), 16);
|
|
288
284
|
const g = parseInt(hex.slice(3, 5), 16);
|
|
@@ -304,21 +300,14 @@ var MARKS = {
|
|
|
304
300
|
"chart.coverage": { key: "ga4Sessions", alpha: 1, role: "data", note: "Share one source saw of another" },
|
|
305
301
|
"chart.stem": { key: "orders", alpha: 1, role: "data", note: "One day that had orders" },
|
|
306
302
|
"chart.zeroTick": { key: "orders", alpha: 1, role: "data", note: "A day MEASURED at zero \u2014 the package's founding distinction" },
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
boundary: "chart.regionEdge",
|
|
313
|
-
note: "What the lossier source missed. Light on purpose \u2014 the line it covers must stay readable"
|
|
314
|
-
},
|
|
303
|
+
// Renamed from `chart.regionEdge`. It was the boundary of a filled region; the region is gone —
|
|
304
|
+
// its width was the absolute gap, which under a flat coverage rate is the traffic curve scaled
|
|
305
|
+
// down, so it widened on busy days with no change in the instrument. This is now simply the
|
|
306
|
+
// lossier source's own line, which is what the collapse detector and the spoken summary read.
|
|
307
|
+
"chart.lossy": { key: "ga4Pageviews", alpha: 1, role: "data", note: "The lossier source's own level, dashed beneath the complete one" },
|
|
315
308
|
"chart.ghost": { key: "comparison", alpha: 1, role: "data", note: "The same window last period, behind everything" },
|
|
316
309
|
"chart.grid": { key: "neutral", alpha: 0.18, role: "furniture", note: "Grid rules" },
|
|
317
310
|
"bar.fill": { key: "vercel", alpha: 1, role: "data", note: "Every proportion, comparison and funnel bar" },
|
|
318
|
-
// The hatch on a rung whose count covers fewer days than the rail it sits in. A `data` mark, not
|
|
319
|
-
// furniture: it is the only thing on that rung saying the figure is not comparable, so it has to
|
|
320
|
-
// be as visible as a bar would have been.
|
|
321
|
-
"bar.partial": { key: "vercel", alpha: 1, role: "data", note: "A funnel rung measured over part of the window" },
|
|
322
311
|
"bar.track": { key: "neutral", alpha: 0.12, role: "furniture", note: "The rail a bar sits in" },
|
|
323
312
|
// Neither a source nor money. It was drawn in the REVENUE hue, so a magenta bar between funnel
|
|
324
313
|
// rungs read as takings; GA4's hue was rejected for the opposite reason, since that hue means
|
|
@@ -329,9 +318,6 @@ var MARKS = {
|
|
|
329
318
|
"estimate.dot": { key: "ga4Pageviews", alpha: 1, role: "data", note: "One independent estimate of how much GA4 sees" },
|
|
330
319
|
"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" },
|
|
331
320
|
"estimate.rule": { key: "neutral", alpha: 0.45, role: "furniture", note: "The 100% reference a capture rate is read against" },
|
|
332
|
-
"shift.now": { key: "vercel", alpha: 1, role: "data", note: "A channel this period" },
|
|
333
|
-
"shift.before": { key: "comparison", alpha: 1, role: "data", note: "The same channel last period \u2014 context, not a second answer" },
|
|
334
|
-
"shift.link": { key: "comparison", alpha: 0.55, role: "fill", boundary: "shift.before", note: "The distance between the two, which is the finding" },
|
|
335
321
|
// A segment per colour rather than a dash pattern per segment. Dash was the ONLY channel
|
|
336
322
|
// separating these lines, and it is the one channel `preserveAspectRatio="none"` distorted —
|
|
337
323
|
// so two segments could be drawn identically on a wide pane. The keys are reused from the
|
|
@@ -673,6 +659,23 @@ function formatDay(iso) {
|
|
|
673
659
|
if (Number.isNaN(date.getTime())) return iso;
|
|
674
660
|
return `${date.getUTCDate()} ${date.toLocaleString("en-GB", { month: "short", timeZone: "UTC" })}`;
|
|
675
661
|
}
|
|
662
|
+
function segmentColour(index) {
|
|
663
|
+
const names = ["survival.line", "survival.line.2", "survival.line.3", "survival.line.4"];
|
|
664
|
+
return mark(names[index % names.length]);
|
|
665
|
+
}
|
|
666
|
+
function segmentKeys(stages) {
|
|
667
|
+
const seen = /* @__PURE__ */ new Map();
|
|
668
|
+
for (const stage of stages) {
|
|
669
|
+
for (const segment of stage.segments ?? []) {
|
|
670
|
+
if (segment.value > 0 && !seen.has(segment.key)) seen.set(segment.key, segment.label);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return [...seen].map(([key, label]) => ({ key, label }));
|
|
674
|
+
}
|
|
675
|
+
function segmentShare(value, whole) {
|
|
676
|
+
if (!(whole > 0) || !(value > 0)) return 0;
|
|
677
|
+
return Math.min(1, value / whole);
|
|
678
|
+
}
|
|
676
679
|
function rateLine(stage, index, entry, share, entryLabel) {
|
|
677
680
|
if (index === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "100% \u2014 everyone who arrived" });
|
|
678
681
|
if (stage.partial) {
|
|
@@ -714,6 +717,8 @@ function FunnelChart({ stages, measurement }) {
|
|
|
714
717
|
const share = entry > 0 ? stage.value / entry : 0;
|
|
715
718
|
const width = Math.max(1.5, Math.min(1, share) * 100);
|
|
716
719
|
const active = activeKey === stage.key;
|
|
720
|
+
const hasSplit = (stage.segments?.length ?? 0) > 0;
|
|
721
|
+
const remainder = hasSplit ? Math.max(0, 1 - (stage.segments ?? []).reduce((sum, s) => sum + segmentShare(s.value, stage.value), 0)) : 0;
|
|
717
722
|
const delta = previous ? previous.value - stage.value : 0;
|
|
718
723
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: funnelItem, children: [
|
|
719
724
|
previous && gapLabel(delta, measurement, entry) !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: funnelGapStyle(measurement === "sequence" && delta > 0 && entry > 0), children: [
|
|
@@ -750,15 +755,34 @@ function FunnelChart({ stages, measurement }) {
|
|
|
750
755
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: stage.label }),
|
|
751
756
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "semibold", children: formatCount(stage.value) })
|
|
752
757
|
] }),
|
|
753
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-hidden": "true", style: { ...barTrack, height: 10 }, children: /* @__PURE__ */ (0, import_jsx_runtime.
|
|
758
|
+
stage.partial ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: partialSlot }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-hidden": "true", style: { ...barTrack, height: 10 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
754
759
|
"div",
|
|
755
760
|
{
|
|
756
|
-
style:
|
|
757
|
-
|
|
758
|
-
width:
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
761
|
+
style: {
|
|
762
|
+
...barFill,
|
|
763
|
+
width: `${width}%`,
|
|
764
|
+
display: "flex",
|
|
765
|
+
overflow: "hidden",
|
|
766
|
+
// Transparent when split. Left as the ordinary fill, the parent's own
|
|
767
|
+
// colour showed through wherever the segments did not reach — and on a
|
|
768
|
+
// rung where they sum to 451 of 475 that painted the missing 24 in the
|
|
769
|
+
// FIRST segment's colour, reading as a third segment of it.
|
|
770
|
+
...hasSplit ? { background: "transparent" } : {}
|
|
771
|
+
},
|
|
772
|
+
children: [
|
|
773
|
+
(stage.segments ?? []).map((segment, segmentIndex) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
774
|
+
"div",
|
|
775
|
+
{
|
|
776
|
+
style: {
|
|
777
|
+
width: `${segmentShare(segment.value, stage.value) * 100}%`,
|
|
778
|
+
background: segmentColour(segmentIndex),
|
|
779
|
+
height: "100%"
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
segment.key
|
|
783
|
+
)),
|
|
784
|
+
hasSplit && remainder > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { width: `${remainder * 100}%`, background: mark("bar.track"), height: "100%" } })
|
|
785
|
+
]
|
|
762
786
|
}
|
|
763
787
|
) }),
|
|
764
788
|
rateLine(stage, index, entry, share, stages[0]?.label ?? "")
|
|
@@ -767,10 +791,11 @@ function FunnelChart({ stages, measurement }) {
|
|
|
767
791
|
)
|
|
768
792
|
] }, stage.key);
|
|
769
793
|
}) }),
|
|
770
|
-
stages.
|
|
771
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", style:
|
|
772
|
-
|
|
773
|
-
] }),
|
|
794
|
+
segmentKeys(stages).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: funnelSegmentKey, children: segmentKeys(stages).map((segment, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
|
|
795
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", style: { ...hoverSwatch, background: segmentColour(index), display: "inline-block", marginRight: 6 } }),
|
|
796
|
+
segment.label
|
|
797
|
+
] }, segment.key)) }),
|
|
798
|
+
stages.some((stage) => stage.partial) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "Steps with no bar were only tracked for part of this period, so they cover fewer days than the ones above and cannot be drawn against them. Their counts are exact for the days they do cover." }),
|
|
774
799
|
withheldAny && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
|
|
775
800
|
"Shares are shown only for steps at least ",
|
|
776
801
|
MIN_RATE_DENOMINATOR,
|
|
@@ -787,15 +812,8 @@ function gapLabel(delta, measurement, entry = 0) {
|
|
|
787
812
|
}
|
|
788
813
|
return `${formatCount(-delta)} more \u2014 not a subset of the step above`;
|
|
789
814
|
}
|
|
790
|
-
var
|
|
791
|
-
var
|
|
792
|
-
display: "inline-block",
|
|
793
|
-
width: 24,
|
|
794
|
-
height: 10,
|
|
795
|
-
flex: "0 0 auto",
|
|
796
|
-
borderRadius: 3,
|
|
797
|
-
backgroundImage: `repeating-linear-gradient(135deg, ${mark("bar.partial")} 0 4px, transparent 4px 8px)`
|
|
798
|
-
};
|
|
815
|
+
var partialSlot = { height: 10 };
|
|
816
|
+
var funnelSegmentKey = { display: "flex", gap: 14, flexWrap: "wrap", paddingTop: 4 };
|
|
799
817
|
var funnelList = { listStyle: "none", margin: 0, padding: 0 };
|
|
800
818
|
var funnelItem = { display: "grid", gap: 4 };
|
|
801
819
|
function funnelGapStyle(hasBar) {
|
|
@@ -1013,177 +1031,14 @@ function EstimateDotPlot({
|
|
|
1013
1031
|
" of ",
|
|
1014
1032
|
formatCount(estimate.actual),
|
|
1015
1033
|
wide && " \u2014 too small a sample to lean on",
|
|
1034
|
+
estimate.note && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: estimateNote, children: estimate.note }),
|
|
1016
1035
|
estimate.rate > 1.05 && " \u2014 over 100%, so GA4 is counting more than the source it is checked against. That is usually a tag firing twice, not extra traffic."
|
|
1017
1036
|
] })
|
|
1018
1037
|
] }, estimate.basis);
|
|
1019
1038
|
}),
|
|
1020
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "
|
|
1021
|
-
] });
|
|
1022
|
-
}
|
|
1023
|
-
function SurvivalLines({
|
|
1024
|
-
segments,
|
|
1025
|
-
stepLabels,
|
|
1026
|
-
partialSteps = [],
|
|
1027
|
-
minDenominator = MIN_RATE_DENOMINATOR
|
|
1028
|
-
}) {
|
|
1029
|
-
const [active, setActive] = import_react2.default.useState(null);
|
|
1030
|
-
const [steppedByKey, setSteppedByKey] = import_react2.default.useState(false);
|
|
1031
|
-
if (segments.length < 2 || stepLabels.length < 2) return null;
|
|
1032
|
-
const width = 320;
|
|
1033
|
-
const height = 132;
|
|
1034
|
-
const x = (i) => i / (stepLabels.length - 1) * width;
|
|
1035
|
-
const drawn = segments.map((segment, index) => {
|
|
1036
|
-
const entry = segment.values[0];
|
|
1037
|
-
if (entry === null || entry === void 0 || entry <= 0) return null;
|
|
1038
|
-
const points = [];
|
|
1039
|
-
const shares = [];
|
|
1040
|
-
for (let i = 0; i < segment.values.length && i < stepLabels.length; i++) {
|
|
1041
|
-
const value = segment.values[i];
|
|
1042
|
-
const previous = i === 0 ? entry : segment.values[i - 1];
|
|
1043
|
-
if (value === null || value === void 0) break;
|
|
1044
|
-
if (i > 0 && (previous === null || previous === void 0 || previous < minDenominator)) break;
|
|
1045
|
-
if (partialSteps.includes(i)) break;
|
|
1046
|
-
const share = Math.min(1, value / entry);
|
|
1047
|
-
points.push([x(i), height - share * height]);
|
|
1048
|
-
shares.push(share);
|
|
1049
|
-
}
|
|
1050
|
-
if (points.length < 2) return null;
|
|
1051
|
-
return { segment, index, points, shares, colour: segmentColour(index) };
|
|
1052
|
-
});
|
|
1053
|
-
const lines = drawn.filter((d) => d !== null);
|
|
1054
|
-
if (lines.length === 0) return null;
|
|
1055
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 3, children: [
|
|
1056
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: survivalFrame, children: [
|
|
1057
|
-
[1, 0.5, 0].map((level) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: survivalYLabel(level), children: level === 1 ? "100%" : level === 0.5 ? "50%" : "0%" }, level)),
|
|
1058
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1059
|
-
"svg",
|
|
1060
|
-
{
|
|
1061
|
-
viewBox: `0 0 ${width} ${height}`,
|
|
1062
|
-
preserveAspectRatio: "none",
|
|
1063
|
-
style: { width: "100%", height: 150, overflow: "visible" },
|
|
1064
|
-
role: "img",
|
|
1065
|
-
"aria-label": survivalSummary(lines, stepLabels),
|
|
1066
|
-
children: [
|
|
1067
|
-
[1, 0.5, 0].map((level) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1068
|
-
"line",
|
|
1069
|
-
{
|
|
1070
|
-
x1: 0,
|
|
1071
|
-
x2: width,
|
|
1072
|
-
y1: height * (1 - level),
|
|
1073
|
-
y2: height * (1 - level),
|
|
1074
|
-
stroke: mark("survival.grid"),
|
|
1075
|
-
strokeWidth: 0.6,
|
|
1076
|
-
vectorEffect: "non-scaling-stroke"
|
|
1077
|
-
},
|
|
1078
|
-
level
|
|
1079
|
-
)),
|
|
1080
|
-
stepLabels.map((label, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1081
|
-
"rect",
|
|
1082
|
-
{
|
|
1083
|
-
x: i === 0 ? 0 : x(i) - width / (stepLabels.length - 1) / 2,
|
|
1084
|
-
y: 0,
|
|
1085
|
-
width: width / (stepLabels.length - 1) / (i === 0 || i === stepLabels.length - 1 ? 2 : 1),
|
|
1086
|
-
height,
|
|
1087
|
-
fill: "transparent",
|
|
1088
|
-
onMouseEnter: () => {
|
|
1089
|
-
setSteppedByKey(false);
|
|
1090
|
-
setActive(i);
|
|
1091
|
-
},
|
|
1092
|
-
onMouseLeave: () => setActive(null)
|
|
1093
|
-
},
|
|
1094
|
-
label
|
|
1095
|
-
)),
|
|
1096
|
-
active !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1097
|
-
"line",
|
|
1098
|
-
{
|
|
1099
|
-
x1: x(active),
|
|
1100
|
-
x2: x(active),
|
|
1101
|
-
y1: 0,
|
|
1102
|
-
y2: height,
|
|
1103
|
-
stroke: mark("survival.grid"),
|
|
1104
|
-
strokeWidth: 1
|
|
1105
|
-
}
|
|
1106
|
-
),
|
|
1107
|
-
lines.map(({ segment, points, colour }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1108
|
-
"polyline",
|
|
1109
|
-
{
|
|
1110
|
-
points: points.map(([px, py]) => `${px},${py}`).join(" "),
|
|
1111
|
-
fill: "none",
|
|
1112
|
-
stroke: colour,
|
|
1113
|
-
strokeWidth: 1.6,
|
|
1114
|
-
vectorEffect: "non-scaling-stroke",
|
|
1115
|
-
strokeLinejoin: "round"
|
|
1116
|
-
},
|
|
1117
|
-
segment.key
|
|
1118
|
-
)),
|
|
1119
|
-
active !== null && lines.map(({ segment, points, colour }) => {
|
|
1120
|
-
const point = points[active];
|
|
1121
|
-
if (!point) return null;
|
|
1122
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1123
|
-
"line",
|
|
1124
|
-
{
|
|
1125
|
-
x1: point[0],
|
|
1126
|
-
x2: point[0],
|
|
1127
|
-
y1: point[1],
|
|
1128
|
-
y2: point[1],
|
|
1129
|
-
stroke: colour,
|
|
1130
|
-
strokeWidth: 6,
|
|
1131
|
-
strokeLinecap: "round",
|
|
1132
|
-
vectorEffect: "non-scaling-stroke"
|
|
1133
|
-
},
|
|
1134
|
-
segment.key
|
|
1135
|
-
);
|
|
1136
|
-
})
|
|
1137
|
-
]
|
|
1138
|
-
}
|
|
1139
|
-
)
|
|
1140
|
-
] }),
|
|
1141
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: survivalAxis, children: stepLabels.map((label, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1142
|
-
"button",
|
|
1143
|
-
{
|
|
1144
|
-
type: "button",
|
|
1145
|
-
style: survivalTick(i === active, i, stepLabels.length),
|
|
1146
|
-
"aria-label": `Read ${label}`,
|
|
1147
|
-
onMouseEnter: () => {
|
|
1148
|
-
setSteppedByKey(false);
|
|
1149
|
-
setActive(i);
|
|
1150
|
-
},
|
|
1151
|
-
onMouseLeave: () => setActive(null),
|
|
1152
|
-
onFocus: () => {
|
|
1153
|
-
setSteppedByKey(true);
|
|
1154
|
-
setActive(i);
|
|
1155
|
-
},
|
|
1156
|
-
onBlur: () => setActive(null),
|
|
1157
|
-
children: label
|
|
1158
|
-
},
|
|
1159
|
-
label
|
|
1160
|
-
)) }),
|
|
1161
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: survivalLegend, "aria-live": steppedByKey ? "polite" : "off", children: lines.map(({ segment, shares, colour }) => {
|
|
1162
|
-
const share = active === null ? null : shares[active];
|
|
1163
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, children: [
|
|
1164
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...hoverSwatch, background: colour, display: "inline-block", marginRight: 6 } }),
|
|
1165
|
-
segment.label,
|
|
1166
|
-
active !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { style: survivalReadoutValue, children: [
|
|
1167
|
-
"\xA0",
|
|
1168
|
-
share === null || share === void 0 ? "not measured here" : formatPercent(share, 0)
|
|
1169
|
-
] })
|
|
1170
|
-
] }, segment.key);
|
|
1171
|
-
}) })
|
|
1039
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "The rule marks 100%." })
|
|
1172
1040
|
] });
|
|
1173
1041
|
}
|
|
1174
|
-
function segmentColour(index) {
|
|
1175
|
-
const names = ["survival.line", "survival.line.2", "survival.line.3", "survival.line.4"];
|
|
1176
|
-
return mark(names[index % names.length]);
|
|
1177
|
-
}
|
|
1178
|
-
function survivalSummary(lines, stepLabels) {
|
|
1179
|
-
const parts = lines.map(({ segment, shares }) => {
|
|
1180
|
-
const last = shares.length - 1;
|
|
1181
|
-
const share = shares[last];
|
|
1182
|
-
const reached = stepLabels[last] ?? "the last measured step";
|
|
1183
|
-
return share === null || share === void 0 ? `${segment.label} could not be followed past ${reached}` : `${segment.label} ${formatPercent(share, 0)} by ${reached}`;
|
|
1184
|
-
});
|
|
1185
|
-
return `Share of each segment still present at each step, starting from ${stepLabels[0]}. ${parts.join(". ")}.`;
|
|
1186
|
-
}
|
|
1187
1042
|
function RatioFigure({
|
|
1188
1043
|
value,
|
|
1189
1044
|
format,
|
|
@@ -1435,6 +1290,7 @@ var estimateTrack = {
|
|
|
1435
1290
|
borderRadius: 2,
|
|
1436
1291
|
background: mark("bar.track")
|
|
1437
1292
|
};
|
|
1293
|
+
var estimateNote = { display: "block", marginTop: 2, opacity: 0.85 };
|
|
1438
1294
|
var estimateRule = {
|
|
1439
1295
|
position: "absolute",
|
|
1440
1296
|
top: 0,
|
|
@@ -1458,82 +1314,6 @@ var estimateDot = {
|
|
|
1458
1314
|
borderRadius: "50%",
|
|
1459
1315
|
background: mark("estimate.dot")
|
|
1460
1316
|
};
|
|
1461
|
-
var shiftTrack = {
|
|
1462
|
-
position: "relative",
|
|
1463
|
-
height: 16,
|
|
1464
|
-
borderRadius: 2,
|
|
1465
|
-
background: mark("bar.track")
|
|
1466
|
-
};
|
|
1467
|
-
var shiftLink = {
|
|
1468
|
-
position: "absolute",
|
|
1469
|
-
top: 7,
|
|
1470
|
-
height: 2,
|
|
1471
|
-
background: mark("shift.link")
|
|
1472
|
-
};
|
|
1473
|
-
var shiftBefore = {
|
|
1474
|
-
position: "absolute",
|
|
1475
|
-
top: 3,
|
|
1476
|
-
width: 10,
|
|
1477
|
-
height: 10,
|
|
1478
|
-
marginLeft: -5,
|
|
1479
|
-
borderRadius: "50%",
|
|
1480
|
-
border: `2px solid ${mark("shift.before")}`,
|
|
1481
|
-
background: "transparent"
|
|
1482
|
-
};
|
|
1483
|
-
var shiftNow = {
|
|
1484
|
-
position: "absolute",
|
|
1485
|
-
top: 3,
|
|
1486
|
-
width: 10,
|
|
1487
|
-
height: 10,
|
|
1488
|
-
marginLeft: -5,
|
|
1489
|
-
borderRadius: "50%",
|
|
1490
|
-
background: mark("shift.now")
|
|
1491
|
-
};
|
|
1492
|
-
var survivalLegend = { display: "flex", gap: 14, flexWrap: "wrap" };
|
|
1493
|
-
var survivalFrame = { paddingLeft: 34, position: "relative" };
|
|
1494
|
-
var survivalAxis = {
|
|
1495
|
-
position: "relative",
|
|
1496
|
-
// Reserves the row's height, since the ticks inside it are positioned.
|
|
1497
|
-
height: 30,
|
|
1498
|
-
marginLeft: 34
|
|
1499
|
-
};
|
|
1500
|
-
function survivalTick(activeTick, index, count) {
|
|
1501
|
-
const at = count > 1 ? index / (count - 1) * 100 : 50;
|
|
1502
|
-
return {
|
|
1503
|
-
position: "absolute",
|
|
1504
|
-
left: `${at}%`,
|
|
1505
|
-
// The ends align to the plot edge; everything between is centred on its vertex.
|
|
1506
|
-
transform: index === 0 ? "none" : index === count - 1 ? "translateX(-100%)" : "translateX(-50%)",
|
|
1507
|
-
appearance: "none",
|
|
1508
|
-
background: "transparent",
|
|
1509
|
-
border: "none",
|
|
1510
|
-
borderTop: `2px solid ${activeTick ? "currentColor" : "transparent"}`,
|
|
1511
|
-
color: "inherit",
|
|
1512
|
-
opacity: activeTick ? 1 : 0.7,
|
|
1513
|
-
font: "inherit",
|
|
1514
|
-
fontSize: "0.72em",
|
|
1515
|
-
fontWeight: activeTick ? 600 : 400,
|
|
1516
|
-
padding: "6px 4px",
|
|
1517
|
-
minHeight: 24,
|
|
1518
|
-
cursor: "pointer",
|
|
1519
|
-
whiteSpace: "nowrap"
|
|
1520
|
-
};
|
|
1521
|
-
}
|
|
1522
|
-
function survivalYLabel(level) {
|
|
1523
|
-
return {
|
|
1524
|
-
position: "absolute",
|
|
1525
|
-
left: 0,
|
|
1526
|
-
// 150 is the plot's rendered height; the label is nudged up by half its own line so it sits
|
|
1527
|
-
// on the rule rather than under it.
|
|
1528
|
-
top: 150 * (1 - level) - 6,
|
|
1529
|
-
width: 28,
|
|
1530
|
-
textAlign: "right",
|
|
1531
|
-
fontSize: 10,
|
|
1532
|
-
opacity: 0.7,
|
|
1533
|
-
fontVariantNumeric: "tabular-nums"
|
|
1534
|
-
};
|
|
1535
|
-
}
|
|
1536
|
-
var survivalReadoutValue = { fontVariantNumeric: "tabular-nums" };
|
|
1537
1317
|
var ratioRow = {
|
|
1538
1318
|
display: "flex",
|
|
1539
1319
|
alignItems: "baseline",
|
|
@@ -2008,8 +1788,6 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
2008
1788
|
const defined = (p) => p.value !== null;
|
|
2009
1789
|
const lineGen = (0, import_d3_shape.line)().defined(defined).x(at).y((p) => y(p.value)).curve(import_d3_shape.curveLinear);
|
|
2010
1790
|
const shortfallPoints = row.shortfall?.points ?? [];
|
|
2011
|
-
const byDate = new Map(shortfallPoints.map((p) => [p.date, p.value]));
|
|
2012
|
-
const gapGen = (0, import_d3_shape.area)().defined((p) => p.value !== null && byDate.get(p.date) != null).x(at).y0((p) => y(byDate.get(p.date))).y1((p) => y(p.value)).curve(import_d3_shape.curveLinear);
|
|
2013
1791
|
const path = row.mark === "events" ? "" : lineGen(row.points) ?? "";
|
|
2014
1792
|
const ghost = row.comparison && row.comparison.length > 1 ? lineGen(row.comparison.slice(0, dates.length).map((point, i) => ({
|
|
2015
1793
|
date: dates[i] ?? point.date,
|
|
@@ -2019,7 +1797,6 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
2019
1797
|
const stemWidth = Math.max(1, Math.min(9, pitch > 2 ? pitch - 1 : pitch));
|
|
2020
1798
|
const stems = row.mark === "events" ? row.points.filter((p) => p.value !== null && p.value !== 0) : [];
|
|
2021
1799
|
const zeroes = row.mark === "events" && pitch >= 6 ? row.points.filter((p) => p.value === 0) : [];
|
|
2022
|
-
const gap = row.shortfall ? gapGen(row.points) ?? "" : "";
|
|
2023
1800
|
const shortfallLine = row.shortfall ? lineGen(shortfallPoints) ?? "" : "";
|
|
2024
1801
|
const clipId = `${instanceId}-row-${rowIndex}`;
|
|
2025
1802
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
|
|
@@ -2044,20 +1821,12 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
2044
1821
|
strokeDasharray: "2 3"
|
|
2045
1822
|
}
|
|
2046
1823
|
),
|
|
2047
|
-
gap && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2048
|
-
"path",
|
|
2049
|
-
{
|
|
2050
|
-
d: gap,
|
|
2051
|
-
fill: shortfallStroke ? seriesFill(colorFor(row.shortfall.source, row.unit, row.shortfall.color), MARKS["chart.region"].alpha) : "currentColor",
|
|
2052
|
-
opacity: shortfallStroke ? 1 : 0.48
|
|
2053
|
-
}
|
|
2054
|
-
),
|
|
2055
1824
|
shortfallLine && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2056
1825
|
"path",
|
|
2057
1826
|
{
|
|
2058
1827
|
d: shortfallLine,
|
|
2059
1828
|
fill: "none",
|
|
2060
|
-
stroke: mark("chart.
|
|
1829
|
+
stroke: mark("chart.lossy"),
|
|
2061
1830
|
strokeWidth: 1.5,
|
|
2062
1831
|
strokeDasharray: "6 4"
|
|
2063
1832
|
}
|
|
@@ -2229,22 +1998,13 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
2229
1998
|
] }, m.label))
|
|
2230
1999
|
] }),
|
|
2231
2000
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: legendRow, children: [
|
|
2232
|
-
series.some((row) => row.shortfall) && /* @__PURE__ */ (0, import_jsx_runtime2.
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
{
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
),
|
|
2240
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2241
|
-
LegendKey,
|
|
2242
|
-
{
|
|
2243
|
-
swatch: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: legendBlock(seriesFill(shortfallKey(series), MARKS["chart.region"].alpha), shortfallColour(series)) }),
|
|
2244
|
-
label: "Shaded: what your analytics did not see"
|
|
2245
|
-
}
|
|
2246
|
-
)
|
|
2247
|
-
] }),
|
|
2001
|
+
series.some((row) => row.shortfall) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2002
|
+
LegendKey,
|
|
2003
|
+
{
|
|
2004
|
+
swatch: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: legendDash(shortfallColour(series)) }),
|
|
2005
|
+
label: `${shortfallLabel(series)} \u2014 the dashed line`
|
|
2006
|
+
}
|
|
2007
|
+
) }),
|
|
2248
2008
|
series.some((row) => (row.comparison?.length ?? 0) > 1) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2249
2009
|
LegendKey,
|
|
2250
2010
|
{
|
|
@@ -2281,15 +2041,6 @@ function legendDash(colour) {
|
|
|
2281
2041
|
borderTop: `2px dashed ${colour}`
|
|
2282
2042
|
};
|
|
2283
2043
|
}
|
|
2284
|
-
function legendBlock(fill, edge) {
|
|
2285
|
-
return {
|
|
2286
|
-
display: "inline-block",
|
|
2287
|
-
width: 18,
|
|
2288
|
-
height: 10,
|
|
2289
|
-
background: fill,
|
|
2290
|
-
borderTop: `1.5px dashed ${edge}`
|
|
2291
|
-
};
|
|
2292
|
-
}
|
|
2293
2044
|
var legendMarker = {
|
|
2294
2045
|
display: "inline-block",
|
|
2295
2046
|
position: "relative",
|
|
@@ -2383,9 +2134,6 @@ function metricOr(metric, detail) {
|
|
|
2383
2134
|
return { status: "unavailable", reason: "route_outdated", detail };
|
|
2384
2135
|
}
|
|
2385
2136
|
var OLDER_ROUTE = "This site\u2019s analytics route is older than this figure \u2014 it will appear after the site is next deployed.";
|
|
2386
|
-
function maxOf(metrics) {
|
|
2387
|
-
return metrics.reduce((max, metric) => !metric || metric.status === "unavailable" ? max : Math.max(max, metric.value), 0);
|
|
2388
|
-
}
|
|
2389
2137
|
function finiteOrNull(value) {
|
|
2390
2138
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
2391
2139
|
}
|
|
@@ -2847,7 +2595,6 @@ function Verdict({ data, previous }) {
|
|
|
2847
2595
|
);
|
|
2848
2596
|
}
|
|
2849
2597
|
function DataHealthPanel({ data, diagnostics }) {
|
|
2850
|
-
const pageviewMax = maxOf([data.ga4Pageviews, data.vercelPageviews]);
|
|
2851
2598
|
const contained = isContainment(data.vercelPageviews, data.ga4Pageviews);
|
|
2852
2599
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
|
|
2853
2600
|
/* @__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: 2, children: [
|
|
@@ -3227,33 +2974,25 @@ function JourneyPanel({ data }) {
|
|
|
3227
2974
|
label: step.label,
|
|
3228
2975
|
value: step.count.value,
|
|
3229
2976
|
conversionFromPrevious: step.conversionFromPrevious,
|
|
2977
|
+
// The device split, subdividing this rung's bar rather than drawn as a second chart.
|
|
2978
|
+
// Only counts that are present and whole travel: a segment GA4 stopped reporting is
|
|
2979
|
+
// left out of the split rather than drawn as a shrunken share of one.
|
|
2980
|
+
...segments.length > 0 ? {
|
|
2981
|
+
segments: segments.flatMap((segment) => {
|
|
2982
|
+
const cell = segment.steps.find((s) => s.key === step.key);
|
|
2983
|
+
const value = cell ? metricSortValue(cell.count) : null;
|
|
2984
|
+
return value === null ? [] : [{ key: segment.key, label: segment.label, value }];
|
|
2985
|
+
})
|
|
2986
|
+
} : {},
|
|
3230
2987
|
...step.count.status === "partial" && step.count.coveredFrom ? { partial: { from: formatDay(step.count.coveredFrom) } } : {}
|
|
3231
2988
|
}]
|
|
3232
2989
|
);
|
|
3233
|
-
const wholeSpine = spine.filter((step) => {
|
|
3234
|
-
const cell = allSteps.find((s) => s.key === step.key);
|
|
3235
|
-
return cell?.count.status !== "partial";
|
|
3236
|
-
});
|
|
3237
2990
|
const tracked = data.measurement === "sequence";
|
|
3238
2991
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
|
|
3239
2992
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: tracked ? "transparent" : "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
|
|
3240
2993
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: tracked ? "Tracked path" : "Not a tracked path" }),
|
|
3241
2994
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: tracked, children: data.approximationNote })
|
|
3242
2995
|
] }) }),
|
|
3243
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3244
|
-
SurvivalLines,
|
|
3245
|
-
{
|
|
3246
|
-
segments: segments.map((segment) => ({
|
|
3247
|
-
key: segment.key,
|
|
3248
|
-
label: segment.label,
|
|
3249
|
-
values: wholeSpine.map((step) => {
|
|
3250
|
-
const cell = segment.steps.find((s) => s.key === step.key);
|
|
3251
|
-
return cell ? metricSortValue(cell.count) : null;
|
|
3252
|
-
})
|
|
3253
|
-
})),
|
|
3254
|
-
stepLabels: wholeSpine.map((step) => step.label)
|
|
3255
|
-
}
|
|
3256
|
-
),
|
|
3257
2996
|
segments.length >= 2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3258
2997
|
ChartData,
|
|
3259
2998
|
{
|
|
@@ -3465,6 +3204,8 @@ function TypefaceInterestPanel({ data }) {
|
|
|
3465
3204
|
},
|
|
3466
3205
|
{
|
|
3467
3206
|
key: "bought",
|
|
3207
|
+
// Never folded. Zero sales is the finding, not an empty column.
|
|
3208
|
+
alwaysShow: true,
|
|
3468
3209
|
label: "Bought (orders)",
|
|
3469
3210
|
numeric: true,
|
|
3470
3211
|
sortValue: (row) => metricSortValue(row.bought),
|
|
@@ -3472,6 +3213,8 @@ function TypefaceInterestPanel({ data }) {
|
|
|
3472
3213
|
},
|
|
3473
3214
|
{
|
|
3474
3215
|
key: "revenue",
|
|
3216
|
+
// Same reason as Bought: an exact zero from the order book is a measurement.
|
|
3217
|
+
alwaysShow: true,
|
|
3475
3218
|
label: "Revenue (orders)",
|
|
3476
3219
|
numeric: true,
|
|
3477
3220
|
sortValue: (row) => metricSortValue(row.revenue),
|