@liiift-studio/sanity-visitor-insights 0.22.0 → 0.23.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 +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +54 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +54 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/studio/CrossSourceTimeline.tsx +73 -9
- package/src/studio/VisitorInsightsTool.tsx +34 -28
- package/src/studio/panels.test.tsx +105 -0
- package/src/studio/panels.tsx +3 -0
package/dist/index.d.mts
CHANGED
|
@@ -399,6 +399,16 @@ interface Series {
|
|
|
399
399
|
/** Whether the line's source sees everything. Drives the stroke and the wording. */
|
|
400
400
|
complete: boolean;
|
|
401
401
|
unit: SeriesUnit;
|
|
402
|
+
/**
|
|
403
|
+
* How the row is drawn. Defaults to `line`.
|
|
404
|
+
*
|
|
405
|
+
* `events` is for a quantity that only exists on the days it happened — orders, revenue. Darden
|
|
406
|
+
* does seven orders a quarter, and a monotone curve through those seven points drew a smooth
|
|
407
|
+
* rise and fall across eighty-three days on which nothing occurred, with the curve's overshoot
|
|
408
|
+
* putting non-zero revenue on days that had none. A stem per day says what a sale is: a discrete
|
|
409
|
+
* event, on a date, of a size. Days with nothing draw nothing, which is the truth.
|
|
410
|
+
*/
|
|
411
|
+
mark?: 'line' | 'events';
|
|
402
412
|
points: SeriesPoint[];
|
|
403
413
|
/**
|
|
404
414
|
* What a lossier source saw of the same thing.
|
package/dist/index.d.ts
CHANGED
|
@@ -399,6 +399,16 @@ interface Series {
|
|
|
399
399
|
/** Whether the line's source sees everything. Drives the stroke and the wording. */
|
|
400
400
|
complete: boolean;
|
|
401
401
|
unit: SeriesUnit;
|
|
402
|
+
/**
|
|
403
|
+
* How the row is drawn. Defaults to `line`.
|
|
404
|
+
*
|
|
405
|
+
* `events` is for a quantity that only exists on the days it happened — orders, revenue. Darden
|
|
406
|
+
* does seven orders a quarter, and a monotone curve through those seven points drew a smooth
|
|
407
|
+
* rise and fall across eighty-three days on which nothing occurred, with the curve's overshoot
|
|
408
|
+
* putting non-zero revenue on days that had none. A stem per day says what a sale is: a discrete
|
|
409
|
+
* event, on a date, of a size. Days with nothing draw nothing, which is the truth.
|
|
410
|
+
*/
|
|
411
|
+
mark?: 'line' | 'events';
|
|
402
412
|
points: SeriesPoint[];
|
|
403
413
|
/**
|
|
404
414
|
* What a lossier source saw of the same thing.
|
package/dist/index.js
CHANGED
|
@@ -1031,7 +1031,7 @@ var import_sanity_ui_compat5 = require("@liiift-studio/sanity-ui-compat");
|
|
|
1031
1031
|
var import_sanity_ui_compat3 = require("@liiift-studio/sanity-ui-compat");
|
|
1032
1032
|
|
|
1033
1033
|
// src/studio/CrossSourceTimeline.tsx
|
|
1034
|
-
var import_react3 = require("react");
|
|
1034
|
+
var import_react3 = __toESM(require("react"));
|
|
1035
1035
|
var import_sanity_ui_compat2 = require("@liiift-studio/sanity-ui-compat");
|
|
1036
1036
|
var import_d3_scale = require("d3-scale");
|
|
1037
1037
|
var import_d3_shape = require("d3-shape");
|
|
@@ -1058,13 +1058,24 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1058
1058
|
const [pinned, setPinned] = (0, import_react3.useState)(false);
|
|
1059
1059
|
const [brushAnchor, setBrushAnchor] = (0, import_react3.useState)(null);
|
|
1060
1060
|
const [brushed, setBrushed] = (0, import_react3.useState)(null);
|
|
1061
|
+
const [measured, setMeasured] = (0, import_react3.useState)(WIDTH);
|
|
1061
1062
|
const frameRef = (0, import_react3.useRef)(null);
|
|
1062
1063
|
const dates = (0, import_react3.useMemo)(() => {
|
|
1063
1064
|
const all = /* @__PURE__ */ new Set();
|
|
1064
1065
|
for (const s of series) for (const p of s.points) all.add(p.date);
|
|
1065
1066
|
return [...all].sort();
|
|
1066
1067
|
}, [series]);
|
|
1067
|
-
const plotWidth =
|
|
1068
|
+
const plotWidth = Math.max(120, measured - GUTTER - RIGHT_PAD);
|
|
1069
|
+
import_react3.default.useEffect(() => {
|
|
1070
|
+
const frame = frameRef.current;
|
|
1071
|
+
if (!frame || typeof ResizeObserver === "undefined") return;
|
|
1072
|
+
const observer = new ResizeObserver((entries) => {
|
|
1073
|
+
const width = entries[0]?.contentRect.width;
|
|
1074
|
+
if (width && Math.abs(width - measured) > 1) setMeasured(Math.round(width));
|
|
1075
|
+
});
|
|
1076
|
+
observer.observe(frame);
|
|
1077
|
+
return () => observer.disconnect();
|
|
1078
|
+
}, [measured]);
|
|
1068
1079
|
const height = TOP_PAD + series.length * ROW_HEIGHT + AXIS_HEIGHT;
|
|
1069
1080
|
const x = (0, import_react3.useMemo)(() => {
|
|
1070
1081
|
if (dates.length === 0) return null;
|
|
@@ -1132,7 +1143,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1132
1143
|
"svg",
|
|
1133
1144
|
{
|
|
1134
1145
|
ref: frameRef,
|
|
1135
|
-
viewBox: `0 0 ${
|
|
1146
|
+
viewBox: `0 0 ${measured} ${height}`,
|
|
1136
1147
|
onMouseMove: onMove,
|
|
1137
1148
|
onMouseLeave: () => {
|
|
1138
1149
|
setHoverIndex(null);
|
|
@@ -1160,7 +1171,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1160
1171
|
setBrushed(null);
|
|
1161
1172
|
commit(from, to);
|
|
1162
1173
|
},
|
|
1163
|
-
style: { width: "100%", height
|
|
1174
|
+
style: { width: "100%", height, display: "block", cursor: onBrush ? "col-resize" : "default" },
|
|
1164
1175
|
tabIndex: 0,
|
|
1165
1176
|
role: "img",
|
|
1166
1177
|
"aria-label": summary,
|
|
@@ -1205,7 +1216,10 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1205
1216
|
const shortfallPoints = row.shortfall?.points ?? [];
|
|
1206
1217
|
const byDate = new Map(shortfallPoints.map((p) => [p.date, p.value]));
|
|
1207
1218
|
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.curveMonotoneX);
|
|
1208
|
-
const path = lineGen(row.points) ?? "";
|
|
1219
|
+
const path = row.mark === "events" ? "" : lineGen(row.points) ?? "";
|
|
1220
|
+
const slot = plotWidth / Math.max(1, dates.length);
|
|
1221
|
+
const stemWidth = Math.max(1.5, Math.min(9, slot - 1));
|
|
1222
|
+
const stems = row.mark === "events" ? row.points.filter((p) => p.value !== null && p.value > 0) : [];
|
|
1209
1223
|
const gap = row.shortfall ? gapGen(row.points) ?? "" : "";
|
|
1210
1224
|
const shortfallLine = row.shortfall && revealed ? lineGen(shortfallPoints.filter((p) => p.value !== null)) ?? "" : "";
|
|
1211
1225
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
|
|
@@ -1219,7 +1233,24 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1219
1233
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER - 6, y: bottom + 4, textAnchor: "end", fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.7, children: "0" }),
|
|
1220
1234
|
gap && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: gap, fill: "currentColor", opacity: 0.48 }),
|
|
1221
1235
|
shortfallLine && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: shortfallLine, fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeDasharray: "6 4", opacity: 0.8 }),
|
|
1222
|
-
|
|
1236
|
+
stems.map((p) => {
|
|
1237
|
+
const cx = at(p);
|
|
1238
|
+
const top2 = y(p.value);
|
|
1239
|
+
if (!Number.isFinite(cx) || !Number.isFinite(top2)) return null;
|
|
1240
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1241
|
+
"rect",
|
|
1242
|
+
{
|
|
1243
|
+
x: cx - stemWidth / 2,
|
|
1244
|
+
y: top2,
|
|
1245
|
+
width: stemWidth,
|
|
1246
|
+
height: Math.max(1.5, bottom - top2),
|
|
1247
|
+
fill: "currentColor",
|
|
1248
|
+
opacity: 0.75
|
|
1249
|
+
},
|
|
1250
|
+
p.date
|
|
1251
|
+
);
|
|
1252
|
+
}),
|
|
1253
|
+
path && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1223
1254
|
"path",
|
|
1224
1255
|
{
|
|
1225
1256
|
d: path,
|
|
@@ -1513,6 +1544,8 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1513
1544
|
source: "Sanity",
|
|
1514
1545
|
complete: true,
|
|
1515
1546
|
unit: "money",
|
|
1547
|
+
// A sale is an event on a date, not a level that varies day to day.
|
|
1548
|
+
mark: "events",
|
|
1516
1549
|
points: (data.crossSource ?? []).map((d) => ({ date: d.date, value: d.revenue }))
|
|
1517
1550
|
}] : [{
|
|
1518
1551
|
key: "orders",
|
|
@@ -1520,6 +1553,7 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1520
1553
|
source: "Sanity",
|
|
1521
1554
|
complete: true,
|
|
1522
1555
|
unit: "count",
|
|
1556
|
+
mark: "events",
|
|
1523
1557
|
points: (data.crossSource ?? []).map((d) => ({ date: d.date, value: d.orders }))
|
|
1524
1558
|
}]
|
|
1525
1559
|
]
|
|
@@ -2352,10 +2386,6 @@ function ReportPanel({
|
|
|
2352
2386
|
// felt state of the tool was "slightly unreadable", and muted text at 55% is under
|
|
2353
2387
|
// 3:1 in both themes. The line says it; the figures stay legible.
|
|
2354
2388
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: "Updating\u2026" }),
|
|
2355
|
-
state.status === "ready" && state.revalidationError && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Card, { padding: 3, radius: 2, tone: "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 1, children: [
|
|
2356
|
-
"These figures are the last ones that loaded. Refreshing them failed: ",
|
|
2357
|
-
state.revalidationError
|
|
2358
|
-
] }) }),
|
|
2359
2389
|
state.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Flex, { align: "center", gap: 3, padding: 4, children: [
|
|
2360
2390
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Spinner, { muted: true }),
|
|
2361
2391
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Text, { size: 1, muted: true, children: "Loading\u2026" })
|
|
@@ -2369,16 +2399,6 @@ function ReportPanel({
|
|
|
2369
2399
|
!state.disabled && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", style: rangeButton(false), onClick: reload, children: "Try again" }) })
|
|
2370
2400
|
] }) }),
|
|
2371
2401
|
state.status === "ready" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 4, children: [
|
|
2372
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SourceStatusRow, { sources: state.envelope.sources }),
|
|
2373
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NoticeList, { notices: state.envelope.notices }),
|
|
2374
|
-
state.envelope.comparison && COMPARED_TABS.includes(tabId) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: [
|
|
2375
|
-
"Changes are against ",
|
|
2376
|
-
state.envelope.comparison.range.start,
|
|
2377
|
-
" to ",
|
|
2378
|
-
state.envelope.comparison.range.end,
|
|
2379
|
-
", the equivalent window immediately before this one.",
|
|
2380
|
-
state.envelope.comparison.provisional && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: " This window\u2019s last days are still being processed by GA4, so changes read low." })
|
|
2381
|
-
] }),
|
|
2382
2402
|
tabId === "overview" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(OverviewPanel, { data: state.envelope.data, previous: state.envelope.comparison?.data, onBrush }),
|
|
2383
2403
|
tabId === "data-health" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Stack, { space: 4, children: [
|
|
2384
2404
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
@@ -2397,6 +2417,20 @@ function ReportPanel({
|
|
|
2397
2417
|
tabId === "acquisition" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AcquisitionPanel, { data: state.envelope.data, previous: state.envelope.comparison?.data }),
|
|
2398
2418
|
tabId === "journey" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(JourneyPanel, { data: state.envelope.data }),
|
|
2399
2419
|
tabId === "typeface-interest" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TypefaceInterestPanel, { data: state.envelope.data }),
|
|
2420
|
+
state.revalidationError && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sanity_ui_compat4.Card, { padding: 3, radius: 2, tone: "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 1, children: [
|
|
2421
|
+
"These figures are the last ones that loaded. Refreshing them failed: ",
|
|
2422
|
+
state.revalidationError
|
|
2423
|
+
] }) }),
|
|
2424
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SourceStatusRow, { sources: state.envelope.sources }),
|
|
2425
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NoticeList, { notices: state.envelope.notices }),
|
|
2426
|
+
state.envelope.comparison && COMPARED_TABS.includes(tabId) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: [
|
|
2427
|
+
"Changes are against ",
|
|
2428
|
+
state.envelope.comparison.range.start,
|
|
2429
|
+
" to ",
|
|
2430
|
+
state.envelope.comparison.range.end,
|
|
2431
|
+
", the equivalent window immediately before this one.",
|
|
2432
|
+
state.envelope.comparison.provisional && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: " This window\u2019s last days are still being processed by GA4, so changes read low." })
|
|
2433
|
+
] }),
|
|
2400
2434
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_sanity_ui_compat4.Text, { size: 0, muted: true, children: [
|
|
2401
2435
|
"Figures cover ",
|
|
2402
2436
|
state.envelope.range.start,
|