@liiift-studio/sanity-visitor-insights 0.74.0 → 0.76.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 +107 -276
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +107 -276
- package/dist/index.mjs.map +1 -1
- package/dist/{ranges-DfjCK0XY.d.mts → ranges-B2W_3Z0t.d.mts} +2 -0
- package/dist/{ranges-DfjCK0XY.d.ts → ranges-B2W_3Z0t.d.ts} +2 -0
- package/dist/server.d.mts +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/server.js +6 -4
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +6 -4
- package/dist/server.mjs.map +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{vercel-CNoYeZgN.d.mts → vercel-D0tEOU9d.d.mts} +9 -0
- package/dist/{vercel-CNoYeZgN.d.ts → vercel-D0tEOU9d.d.ts} +9 -0
- package/package.json +1 -1
- package/src/reportData.ts +2 -0
- package/src/server/orders.ts +20 -5
- package/src/server/reports/reports.test.ts +53 -0
- package/src/studio/Figure.tsx +196 -425
- package/src/studio/palette.ts +0 -3
- package/src/studio/panels.test.tsx +223 -275
- package/src/studio/panels.tsx +37 -127
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-B2W_3Z0t.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-B2W_3Z0t.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-B2W_3Z0t.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-B2W_3Z0t.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
|
@@ -318,9 +318,6 @@ var MARKS = {
|
|
|
318
318
|
"estimate.dot": { key: "ga4Pageviews", alpha: 1, role: "data", note: "One independent estimate of how much GA4 sees" },
|
|
319
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" },
|
|
320
320
|
"estimate.rule": { key: "neutral", alpha: 0.45, role: "furniture", note: "The 100% reference a capture rate is read against" },
|
|
321
|
-
"shift.now": { key: "vercel", alpha: 1, role: "data", note: "A channel this period" },
|
|
322
|
-
"shift.before": { key: "comparison", alpha: 1, role: "data", note: "The same channel last period \u2014 context, not a second answer" },
|
|
323
|
-
"shift.link": { key: "comparison", alpha: 0.55, role: "fill", boundary: "shift.before", note: "The distance between the two, which is the finding" },
|
|
324
321
|
// A segment per colour rather than a dash pattern per segment. Dash was the ONLY channel
|
|
325
322
|
// separating these lines, and it is the one channel `preserveAspectRatio="none"` distorted —
|
|
326
323
|
// so two segments could be drawn identically on a wide pane. The keys are reused from the
|
|
@@ -662,6 +659,23 @@ function formatDay(iso) {
|
|
|
662
659
|
if (Number.isNaN(date.getTime())) return iso;
|
|
663
660
|
return `${date.getUTCDate()} ${date.toLocaleString("en-GB", { month: "short", timeZone: "UTC" })}`;
|
|
664
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
|
+
}
|
|
665
679
|
function rateLine(stage, index, entry, share, entryLabel) {
|
|
666
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" });
|
|
667
681
|
if (stage.partial) {
|
|
@@ -703,6 +717,8 @@ function FunnelChart({ stages, measurement }) {
|
|
|
703
717
|
const share = entry > 0 ? stage.value / entry : 0;
|
|
704
718
|
const width = Math.max(1.5, Math.min(1, share) * 100);
|
|
705
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;
|
|
706
722
|
const delta = previous ? previous.value - stage.value : 0;
|
|
707
723
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: funnelItem, children: [
|
|
708
724
|
previous && gapLabel(delta, measurement, entry) !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: funnelGapStyle(measurement === "sequence" && delta > 0 && entry > 0), children: [
|
|
@@ -739,13 +755,46 @@ function FunnelChart({ stages, measurement }) {
|
|
|
739
755
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: stage.label }),
|
|
740
756
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "semibold", children: formatCount(stage.value) })
|
|
741
757
|
] }),
|
|
742
|
-
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.
|
|
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)(
|
|
759
|
+
"div",
|
|
760
|
+
{
|
|
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
|
+
]
|
|
786
|
+
}
|
|
787
|
+
) }),
|
|
743
788
|
rateLine(stage, index, entry, share, stages[0]?.label ?? "")
|
|
744
789
|
]
|
|
745
790
|
}
|
|
746
791
|
)
|
|
747
792
|
] }, stage.key);
|
|
748
793
|
}) }),
|
|
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)) }),
|
|
749
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." }),
|
|
750
799
|
withheldAny && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
|
|
751
800
|
"Shares are shown only for steps at least ",
|
|
@@ -764,6 +813,22 @@ function gapLabel(delta, measurement, entry = 0) {
|
|
|
764
813
|
return `${formatCount(-delta)} more \u2014 not a subset of the step above`;
|
|
765
814
|
}
|
|
766
815
|
var partialSlot = { height: 10 };
|
|
816
|
+
var funnelSegmentKey = { display: "flex", gap: 14, flexWrap: "wrap", paddingTop: 4 };
|
|
817
|
+
var ladderRow = {
|
|
818
|
+
display: "grid",
|
|
819
|
+
gridTemplateColumns: "minmax(80px, 1.1fr) minmax(60px, 0.8fr) minmax(60px, 2fr) auto",
|
|
820
|
+
gap: 10,
|
|
821
|
+
alignItems: "center"
|
|
822
|
+
};
|
|
823
|
+
var ladderTrack = {
|
|
824
|
+
position: "relative",
|
|
825
|
+
height: 8,
|
|
826
|
+
borderRadius: 4,
|
|
827
|
+
overflow: "hidden",
|
|
828
|
+
background: mark("bar.track")
|
|
829
|
+
};
|
|
830
|
+
var ladderValue = { textAlign: "right", fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" };
|
|
831
|
+
var ladderMoney = { opacity: 0.7 };
|
|
767
832
|
var funnelList = { listStyle: "none", margin: 0, padding: 0 };
|
|
768
833
|
var funnelItem = { display: "grid", gap: 4 };
|
|
769
834
|
function funnelGapStyle(hasBar) {
|
|
@@ -989,169 +1054,33 @@ function EstimateDotPlot({
|
|
|
989
1054
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "The rule marks 100%." })
|
|
990
1055
|
] });
|
|
991
1056
|
}
|
|
992
|
-
function
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
const share = Math.min(1, value / entry);
|
|
1016
|
-
points.push([x(i), height - share * height]);
|
|
1017
|
-
shares.push(share);
|
|
1018
|
-
}
|
|
1019
|
-
if (points.length < 2) return null;
|
|
1020
|
-
return { segment, index, points, shares, colour: segmentColour(index) };
|
|
1021
|
-
});
|
|
1022
|
-
const lines = drawn.filter((d) => d !== null);
|
|
1023
|
-
if (lines.length === 0) return null;
|
|
1024
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 3, children: [
|
|
1025
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: survivalFrame, children: [
|
|
1026
|
-
[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)),
|
|
1027
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1028
|
-
"svg",
|
|
1029
|
-
{
|
|
1030
|
-
viewBox: `0 0 ${width} ${height}`,
|
|
1031
|
-
preserveAspectRatio: "none",
|
|
1032
|
-
style: { width: "100%", height: 150, overflow: "visible" },
|
|
1033
|
-
role: "img",
|
|
1034
|
-
"aria-label": survivalSummary(lines, stepLabels),
|
|
1035
|
-
children: [
|
|
1036
|
-
[1, 0.5, 0].map((level) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1037
|
-
"line",
|
|
1038
|
-
{
|
|
1039
|
-
x1: 0,
|
|
1040
|
-
x2: width,
|
|
1041
|
-
y1: height * (1 - level),
|
|
1042
|
-
y2: height * (1 - level),
|
|
1043
|
-
stroke: mark("survival.grid"),
|
|
1044
|
-
strokeWidth: 0.6,
|
|
1045
|
-
vectorEffect: "non-scaling-stroke"
|
|
1046
|
-
},
|
|
1047
|
-
level
|
|
1048
|
-
)),
|
|
1049
|
-
stepLabels.map((label, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1050
|
-
"rect",
|
|
1051
|
-
{
|
|
1052
|
-
x: i === 0 ? 0 : x(i) - width / (stepLabels.length - 1) / 2,
|
|
1053
|
-
y: 0,
|
|
1054
|
-
width: width / (stepLabels.length - 1) / (i === 0 || i === stepLabels.length - 1 ? 2 : 1),
|
|
1055
|
-
height,
|
|
1056
|
-
fill: "transparent",
|
|
1057
|
-
onMouseEnter: () => {
|
|
1058
|
-
setSteppedByKey(false);
|
|
1059
|
-
setActive(i);
|
|
1060
|
-
},
|
|
1061
|
-
onMouseLeave: () => setActive(null)
|
|
1062
|
-
},
|
|
1063
|
-
label
|
|
1064
|
-
)),
|
|
1065
|
-
active !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1066
|
-
"line",
|
|
1067
|
-
{
|
|
1068
|
-
x1: x(active),
|
|
1069
|
-
x2: x(active),
|
|
1070
|
-
y1: 0,
|
|
1071
|
-
y2: height,
|
|
1072
|
-
stroke: mark("survival.grid"),
|
|
1073
|
-
strokeWidth: 1
|
|
1074
|
-
}
|
|
1075
|
-
),
|
|
1076
|
-
lines.map(({ segment, points, colour }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1077
|
-
"polyline",
|
|
1078
|
-
{
|
|
1079
|
-
points: points.map(([px, py]) => `${px},${py}`).join(" "),
|
|
1080
|
-
fill: "none",
|
|
1081
|
-
stroke: colour,
|
|
1082
|
-
strokeWidth: 1.6,
|
|
1083
|
-
vectorEffect: "non-scaling-stroke",
|
|
1084
|
-
strokeLinejoin: "round"
|
|
1085
|
-
},
|
|
1086
|
-
segment.key
|
|
1087
|
-
)),
|
|
1088
|
-
active !== null && lines.map(({ segment, points, colour }) => {
|
|
1089
|
-
const point = points[active];
|
|
1090
|
-
if (!point) return null;
|
|
1091
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1092
|
-
"line",
|
|
1093
|
-
{
|
|
1094
|
-
x1: point[0],
|
|
1095
|
-
x2: point[0],
|
|
1096
|
-
y1: point[1],
|
|
1097
|
-
y2: point[1],
|
|
1098
|
-
stroke: colour,
|
|
1099
|
-
strokeWidth: 6,
|
|
1100
|
-
strokeLinecap: "round",
|
|
1101
|
-
vectorEffect: "non-scaling-stroke"
|
|
1102
|
-
},
|
|
1103
|
-
segment.key
|
|
1104
|
-
);
|
|
1105
|
-
})
|
|
1106
|
-
]
|
|
1107
|
-
}
|
|
1108
|
-
)
|
|
1109
|
-
] }),
|
|
1110
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: survivalAxis, children: stepLabels.map((label, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1111
|
-
"button",
|
|
1112
|
-
{
|
|
1113
|
-
type: "button",
|
|
1114
|
-
style: survivalTick(i === active, i, stepLabels.length),
|
|
1115
|
-
"aria-label": `Read ${label}`,
|
|
1116
|
-
onMouseEnter: () => {
|
|
1117
|
-
setSteppedByKey(false);
|
|
1118
|
-
setActive(i);
|
|
1119
|
-
},
|
|
1120
|
-
onMouseLeave: () => setActive(null),
|
|
1121
|
-
onFocus: () => {
|
|
1122
|
-
setSteppedByKey(true);
|
|
1123
|
-
setActive(i);
|
|
1124
|
-
},
|
|
1125
|
-
onBlur: () => setActive(null),
|
|
1126
|
-
children: label
|
|
1127
|
-
},
|
|
1128
|
-
label
|
|
1129
|
-
)) }),
|
|
1130
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: survivalLegend, "aria-live": steppedByKey ? "polite" : "off", children: lines.map(({ segment, shares, colour }) => {
|
|
1131
|
-
const share = active === null ? null : shares[active];
|
|
1132
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, children: [
|
|
1133
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...hoverSwatch, background: colour, display: "inline-block", marginRight: 6 } }),
|
|
1134
|
-
segment.label,
|
|
1135
|
-
active !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { style: survivalReadoutValue, children: [
|
|
1136
|
-
"\xA0",
|
|
1137
|
-
share === null || share === void 0 ? "not measured here" : formatPercent(share, 0)
|
|
1057
|
+
function LicenceLadder({ rows, currency }) {
|
|
1058
|
+
if (rows.length === 0) return null;
|
|
1059
|
+
const peak = Math.max(1, ...rows.map((row) => row.orders));
|
|
1060
|
+
const types = [];
|
|
1061
|
+
for (const row of rows) {
|
|
1062
|
+
const facet = types.find((t) => t.type === row.type);
|
|
1063
|
+
if (facet) facet.rungs.push(row);
|
|
1064
|
+
else types.push({ type: row.type, rungs: [row] });
|
|
1065
|
+
}
|
|
1066
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Stack, { space: 4, children: types.map((facet) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
|
|
1067
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "semibold", children: facet.type }),
|
|
1068
|
+
facet.rungs.map((row, index) => {
|
|
1069
|
+
const sameTierAsAbove = index > 0 && facet.rungs[index - 1]?.tier === row.tier;
|
|
1070
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: ladderRow, children: [
|
|
1071
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: sameTierAsAbove ? "" : row.tier }),
|
|
1072
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: row.term }),
|
|
1073
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-hidden": "true", style: ladderTrack, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...barFill, width: `${Math.max(2, row.orders / peak * 100)}%` } }) }),
|
|
1074
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, style: ladderValue, children: [
|
|
1075
|
+
formatCount(row.orders),
|
|
1076
|
+
row.revenue !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: ladderMoney, children: [
|
|
1077
|
+
" \xB7 ",
|
|
1078
|
+
formatMoney(row.revenue, currency)
|
|
1079
|
+
] })
|
|
1138
1080
|
] })
|
|
1139
|
-
] },
|
|
1140
|
-
})
|
|
1141
|
-
] });
|
|
1142
|
-
}
|
|
1143
|
-
function segmentColour(index) {
|
|
1144
|
-
const names = ["survival.line", "survival.line.2", "survival.line.3", "survival.line.4"];
|
|
1145
|
-
return mark(names[index % names.length]);
|
|
1146
|
-
}
|
|
1147
|
-
function survivalSummary(lines, stepLabels) {
|
|
1148
|
-
const parts = lines.map(({ segment, shares }) => {
|
|
1149
|
-
const last = shares.length - 1;
|
|
1150
|
-
const share = shares[last];
|
|
1151
|
-
const reached = stepLabels[last] ?? "the last measured step";
|
|
1152
|
-
return share === null || share === void 0 ? `${segment.label} could not be followed past ${reached}` : `${segment.label} ${formatPercent(share, 0)} by ${reached}`;
|
|
1153
|
-
});
|
|
1154
|
-
return `Share of each segment still present at each step, starting from ${stepLabels[0]}. ${parts.join(". ")}.`;
|
|
1081
|
+
] }, `${row.tier}\0${row.term}`);
|
|
1082
|
+
})
|
|
1083
|
+
] }, facet.type)) });
|
|
1155
1084
|
}
|
|
1156
1085
|
function RatioFigure({
|
|
1157
1086
|
value,
|
|
@@ -1428,82 +1357,6 @@ var estimateDot = {
|
|
|
1428
1357
|
borderRadius: "50%",
|
|
1429
1358
|
background: mark("estimate.dot")
|
|
1430
1359
|
};
|
|
1431
|
-
var shiftTrack = {
|
|
1432
|
-
position: "relative",
|
|
1433
|
-
height: 16,
|
|
1434
|
-
borderRadius: 2,
|
|
1435
|
-
background: mark("bar.track")
|
|
1436
|
-
};
|
|
1437
|
-
var shiftLink = {
|
|
1438
|
-
position: "absolute",
|
|
1439
|
-
top: 7,
|
|
1440
|
-
height: 2,
|
|
1441
|
-
background: mark("shift.link")
|
|
1442
|
-
};
|
|
1443
|
-
var shiftBefore = {
|
|
1444
|
-
position: "absolute",
|
|
1445
|
-
top: 3,
|
|
1446
|
-
width: 10,
|
|
1447
|
-
height: 10,
|
|
1448
|
-
marginLeft: -5,
|
|
1449
|
-
borderRadius: "50%",
|
|
1450
|
-
border: `2px solid ${mark("shift.before")}`,
|
|
1451
|
-
background: "transparent"
|
|
1452
|
-
};
|
|
1453
|
-
var shiftNow = {
|
|
1454
|
-
position: "absolute",
|
|
1455
|
-
top: 3,
|
|
1456
|
-
width: 10,
|
|
1457
|
-
height: 10,
|
|
1458
|
-
marginLeft: -5,
|
|
1459
|
-
borderRadius: "50%",
|
|
1460
|
-
background: mark("shift.now")
|
|
1461
|
-
};
|
|
1462
|
-
var survivalLegend = { display: "flex", gap: 14, flexWrap: "wrap" };
|
|
1463
|
-
var survivalFrame = { paddingLeft: 34, position: "relative" };
|
|
1464
|
-
var survivalAxis = {
|
|
1465
|
-
position: "relative",
|
|
1466
|
-
// Reserves the row's height, since the ticks inside it are positioned.
|
|
1467
|
-
height: 30,
|
|
1468
|
-
marginLeft: 34
|
|
1469
|
-
};
|
|
1470
|
-
function survivalTick(activeTick, index, count) {
|
|
1471
|
-
const at = count > 1 ? index / (count - 1) * 100 : 50;
|
|
1472
|
-
return {
|
|
1473
|
-
position: "absolute",
|
|
1474
|
-
left: `${at}%`,
|
|
1475
|
-
// The ends align to the plot edge; everything between is centred on its vertex.
|
|
1476
|
-
transform: index === 0 ? "none" : index === count - 1 ? "translateX(-100%)" : "translateX(-50%)",
|
|
1477
|
-
appearance: "none",
|
|
1478
|
-
background: "transparent",
|
|
1479
|
-
border: "none",
|
|
1480
|
-
borderTop: `2px solid ${activeTick ? "currentColor" : "transparent"}`,
|
|
1481
|
-
color: "inherit",
|
|
1482
|
-
opacity: activeTick ? 1 : 0.7,
|
|
1483
|
-
font: "inherit",
|
|
1484
|
-
fontSize: "0.72em",
|
|
1485
|
-
fontWeight: activeTick ? 600 : 400,
|
|
1486
|
-
padding: "6px 4px",
|
|
1487
|
-
minHeight: 24,
|
|
1488
|
-
cursor: "pointer",
|
|
1489
|
-
whiteSpace: "nowrap"
|
|
1490
|
-
};
|
|
1491
|
-
}
|
|
1492
|
-
function survivalYLabel(level) {
|
|
1493
|
-
return {
|
|
1494
|
-
position: "absolute",
|
|
1495
|
-
left: 0,
|
|
1496
|
-
// 150 is the plot's rendered height; the label is nudged up by half its own line so it sits
|
|
1497
|
-
// on the rule rather than under it.
|
|
1498
|
-
top: 150 * (1 - level) - 6,
|
|
1499
|
-
width: 28,
|
|
1500
|
-
textAlign: "right",
|
|
1501
|
-
fontSize: 10,
|
|
1502
|
-
opacity: 0.7,
|
|
1503
|
-
fontVariantNumeric: "tabular-nums"
|
|
1504
|
-
};
|
|
1505
|
-
}
|
|
1506
|
-
var survivalReadoutValue = { fontVariantNumeric: "tabular-nums" };
|
|
1507
1360
|
var ratioRow = {
|
|
1508
1361
|
display: "flex",
|
|
1509
1362
|
alignItems: "baseline",
|
|
@@ -3164,33 +3017,25 @@ function JourneyPanel({ data }) {
|
|
|
3164
3017
|
label: step.label,
|
|
3165
3018
|
value: step.count.value,
|
|
3166
3019
|
conversionFromPrevious: step.conversionFromPrevious,
|
|
3020
|
+
// The device split, subdividing this rung's bar rather than drawn as a second chart.
|
|
3021
|
+
// Only counts that are present and whole travel: a segment GA4 stopped reporting is
|
|
3022
|
+
// left out of the split rather than drawn as a shrunken share of one.
|
|
3023
|
+
...segments.length > 0 ? {
|
|
3024
|
+
segments: segments.flatMap((segment) => {
|
|
3025
|
+
const cell = segment.steps.find((s) => s.key === step.key);
|
|
3026
|
+
const value = cell ? metricSortValue(cell.count) : null;
|
|
3027
|
+
return value === null ? [] : [{ key: segment.key, label: segment.label, value }];
|
|
3028
|
+
})
|
|
3029
|
+
} : {},
|
|
3167
3030
|
...step.count.status === "partial" && step.count.coveredFrom ? { partial: { from: formatDay(step.count.coveredFrom) } } : {}
|
|
3168
3031
|
}]
|
|
3169
3032
|
);
|
|
3170
|
-
const wholeSpine = spine.filter((step) => {
|
|
3171
|
-
const cell = allSteps.find((s) => s.key === step.key);
|
|
3172
|
-
return cell?.count.status !== "partial";
|
|
3173
|
-
});
|
|
3174
3033
|
const tracked = data.measurement === "sequence";
|
|
3175
3034
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
|
|
3176
3035
|
/* @__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: [
|
|
3177
3036
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: tracked ? "Tracked path" : "Not a tracked path" }),
|
|
3178
3037
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: tracked, children: data.approximationNote })
|
|
3179
3038
|
] }) }),
|
|
3180
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3181
|
-
SurvivalLines,
|
|
3182
|
-
{
|
|
3183
|
-
segments: segments.map((segment) => ({
|
|
3184
|
-
key: segment.key,
|
|
3185
|
-
label: segment.label,
|
|
3186
|
-
values: wholeSpine.map((step) => {
|
|
3187
|
-
const cell = segment.steps.find((s) => s.key === step.key);
|
|
3188
|
-
return cell ? metricSortValue(cell.count) : null;
|
|
3189
|
-
})
|
|
3190
|
-
})),
|
|
3191
|
-
stepLabels: wholeSpine.map((step) => step.label)
|
|
3192
|
-
}
|
|
3193
|
-
),
|
|
3194
3039
|
segments.length >= 2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3195
3040
|
ChartData,
|
|
3196
3041
|
{
|
|
@@ -3489,22 +3334,8 @@ function TypefaceInterestPanel({ data }) {
|
|
|
3489
3334
|
] }),
|
|
3490
3335
|
(data.licences?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
3491
3336
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SectionTitle, { title: "How licences sell" }),
|
|
3492
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "From your orders, so exact.
|
|
3493
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3494
|
-
ProportionChart,
|
|
3495
|
-
{
|
|
3496
|
-
bars: (data.licences ?? []).map((row) => ({
|
|
3497
|
-
key: `${row.type}-${row.tier}-${row.term}`,
|
|
3498
|
-
label: `${row.type} \xB7 ${row.tier}`,
|
|
3499
|
-
sublabel: row.term,
|
|
3500
|
-
// Ranked by revenue where it is known, by orders where it is not — a
|
|
3501
|
-
// count would put a cheap tier above one worth ten times as much.
|
|
3502
|
-
value: row.revenue ?? row.orders
|
|
3503
|
-
})),
|
|
3504
|
-
format: (value) => (data.licences ?? []).some((r) => r.revenue !== null) ? formatMoney(value, data.currency ?? null) : `${formatCount(value)} orders`,
|
|
3505
|
-
totalLabel: (data.licences ?? []).some((r) => r.revenue !== null) ? "Total across licences" : "Total licence lines"
|
|
3506
|
-
}
|
|
3507
|
-
)
|
|
3337
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "From your orders, so exact. Bars are orders; the money beside them is that order total split evenly across the licences on it, so it is a share rather than a price." }),
|
|
3338
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(LicenceLadder, { rows: data.licences ?? [], currency: data.currency ?? null })
|
|
3508
3339
|
] }),
|
|
3509
3340
|
data.revenueIsApportioned && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "An order covering several families is split evenly between them, because the order documents carry no per-line value." }),
|
|
3510
3341
|
finiteOrNull(data.unattributedRevenue) !== null && data.unattributedRevenue > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: [
|