@liiift-studio/sanity-visitor-insights 0.74.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 +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +64 -262
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -262
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/studio/Figure.tsx +113 -440
- package/src/studio/palette.ts +0 -3
- package/src/studio/panels.test.tsx +161 -275
- package/src/studio/panels.tsx +20 -109
package/dist/index.d.mts
CHANGED
|
@@ -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
|
@@ -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,7 @@ 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 };
|
|
767
817
|
var funnelList = { listStyle: "none", margin: 0, padding: 0 };
|
|
768
818
|
var funnelItem = { display: "grid", gap: 4 };
|
|
769
819
|
function funnelGapStyle(hasBar) {
|
|
@@ -989,170 +1039,6 @@ function EstimateDotPlot({
|
|
|
989
1039
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "The rule marks 100%." })
|
|
990
1040
|
] });
|
|
991
1041
|
}
|
|
992
|
-
function SurvivalLines({
|
|
993
|
-
segments,
|
|
994
|
-
stepLabels,
|
|
995
|
-
partialSteps = [],
|
|
996
|
-
minDenominator = MIN_RATE_DENOMINATOR
|
|
997
|
-
}) {
|
|
998
|
-
const [active, setActive] = import_react2.default.useState(null);
|
|
999
|
-
const [steppedByKey, setSteppedByKey] = import_react2.default.useState(false);
|
|
1000
|
-
if (segments.length < 2 || stepLabels.length < 2) return null;
|
|
1001
|
-
const width = 320;
|
|
1002
|
-
const height = 132;
|
|
1003
|
-
const x = (i) => i / (stepLabels.length - 1) * width;
|
|
1004
|
-
const drawn = segments.map((segment, index) => {
|
|
1005
|
-
const entry = segment.values[0];
|
|
1006
|
-
if (entry === null || entry === void 0 || entry <= 0) return null;
|
|
1007
|
-
const points = [];
|
|
1008
|
-
const shares = [];
|
|
1009
|
-
for (let i = 0; i < segment.values.length && i < stepLabels.length; i++) {
|
|
1010
|
-
const value = segment.values[i];
|
|
1011
|
-
const previous = i === 0 ? entry : segment.values[i - 1];
|
|
1012
|
-
if (value === null || value === void 0) break;
|
|
1013
|
-
if (i > 0 && (previous === null || previous === void 0 || previous < minDenominator)) break;
|
|
1014
|
-
if (partialSteps.includes(i)) break;
|
|
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)
|
|
1138
|
-
] })
|
|
1139
|
-
] }, segment.key);
|
|
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(". ")}.`;
|
|
1155
|
-
}
|
|
1156
1042
|
function RatioFigure({
|
|
1157
1043
|
value,
|
|
1158
1044
|
format,
|
|
@@ -1428,82 +1314,6 @@ var estimateDot = {
|
|
|
1428
1314
|
borderRadius: "50%",
|
|
1429
1315
|
background: mark("estimate.dot")
|
|
1430
1316
|
};
|
|
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
1317
|
var ratioRow = {
|
|
1508
1318
|
display: "flex",
|
|
1509
1319
|
alignItems: "baseline",
|
|
@@ -3164,33 +2974,25 @@ function JourneyPanel({ data }) {
|
|
|
3164
2974
|
label: step.label,
|
|
3165
2975
|
value: step.count.value,
|
|
3166
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
|
+
} : {},
|
|
3167
2987
|
...step.count.status === "partial" && step.count.coveredFrom ? { partial: { from: formatDay(step.count.coveredFrom) } } : {}
|
|
3168
2988
|
}]
|
|
3169
2989
|
);
|
|
3170
|
-
const wholeSpine = spine.filter((step) => {
|
|
3171
|
-
const cell = allSteps.find((s) => s.key === step.key);
|
|
3172
|
-
return cell?.count.status !== "partial";
|
|
3173
|
-
});
|
|
3174
2990
|
const tracked = data.measurement === "sequence";
|
|
3175
2991
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
|
|
3176
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: [
|
|
3177
2993
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, weight: "medium", children: tracked ? "Tracked path" : "Not a tracked path" }),
|
|
3178
2994
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: tracked, children: data.approximationNote })
|
|
3179
2995
|
] }) }),
|
|
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
2996
|
segments.length >= 2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3195
2997
|
ChartData,
|
|
3196
2998
|
{
|