@liiift-studio/sanity-visitor-insights 0.28.0 → 0.29.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.js +122 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +122 -47
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/studio/CrossSourceTimeline.tsx +150 -1
- package/src/studio/Figure.tsx +10 -1
- package/src/studio/panels.test.tsx +91 -4
package/dist/index.js
CHANGED
|
@@ -248,7 +248,8 @@ function Delta({ current, previous, riseIsGood = true, unit = "count" }) {
|
|
|
248
248
|
const now = current;
|
|
249
249
|
const before = previous;
|
|
250
250
|
const absolute = now - before;
|
|
251
|
-
|
|
251
|
+
const roundsToNothing = unit === "percent" ? Math.abs(absolute) < 0.05 : before !== 0 && Math.abs(absolute / Math.abs(before)) < 5e-3;
|
|
252
|
+
if (absolute === 0 || roundsToNothing) {
|
|
252
253
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: deltaStyle("flat"), children: "no change" });
|
|
253
254
|
}
|
|
254
255
|
const change = before === 0 ? null : absolute / Math.abs(before);
|
|
@@ -1087,11 +1088,49 @@ function dayIndexAt(clientX, left, boxWidth, viewBoxWidth, plotWidth, count) {
|
|
|
1087
1088
|
const index = Math.round((clamped - GUTTER) / plotWidth * (count - 1));
|
|
1088
1089
|
return Math.max(0, Math.min(count - 1, index));
|
|
1089
1090
|
}
|
|
1091
|
+
function findCoverageIncident(days, dates) {
|
|
1092
|
+
const measured = days.filter((d) => d !== null);
|
|
1093
|
+
if (measured.length < 21) return null;
|
|
1094
|
+
const sorted = [...measured].sort((a, b) => a - b);
|
|
1095
|
+
const normal = sorted[Math.floor(sorted.length / 2)];
|
|
1096
|
+
if (normal <= 0.1) return null;
|
|
1097
|
+
const threshold = normal * 0.5;
|
|
1098
|
+
const MIN_RUN = 3;
|
|
1099
|
+
let best = null;
|
|
1100
|
+
let runStart = null;
|
|
1101
|
+
for (let i = 0; i <= days.length; i++) {
|
|
1102
|
+
const value = days[i];
|
|
1103
|
+
const low = i < days.length && value !== null && value !== void 0 && value < threshold;
|
|
1104
|
+
if (low && runStart === null) runStart = i;
|
|
1105
|
+
if (!low && runStart !== null) {
|
|
1106
|
+
const length = i - runStart;
|
|
1107
|
+
if (length >= MIN_RUN && (!best || length > best.end - best.start)) best = { start: runStart, end: i };
|
|
1108
|
+
runStart = null;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (!best) return null;
|
|
1112
|
+
const during = days.slice(best.start, best.end).filter((d) => d !== null);
|
|
1113
|
+
const before = days.slice(0, best.start).filter((d) => d !== null);
|
|
1114
|
+
if (before.length < 3 || during.length === 0) return null;
|
|
1115
|
+
const mean = (values) => values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
1116
|
+
const onset = dates[best.start];
|
|
1117
|
+
if (!onset) return null;
|
|
1118
|
+
return {
|
|
1119
|
+
onset,
|
|
1120
|
+
days: best.end - best.start,
|
|
1121
|
+
before: mean(before),
|
|
1122
|
+
during: mean(during),
|
|
1123
|
+
// The run reaching the end of the series is the difference between "this happened" and "this
|
|
1124
|
+
// is happening", which is the difference between a note and a job for today.
|
|
1125
|
+
ongoing: best.end >= days.length
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1090
1128
|
function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
1091
1129
|
const [hoverIndex, setHoverIndex] = (0, import_react3.useState)(null);
|
|
1092
1130
|
const [pinned, setPinned] = (0, import_react3.useState)(false);
|
|
1093
1131
|
const [brushAnchor, setBrushAnchor] = (0, import_react3.useState)(null);
|
|
1094
1132
|
const [brushed, setBrushed] = (0, import_react3.useState)(null);
|
|
1133
|
+
const [steppedByKeyboard, setSteppedByKeyboard] = (0, import_react3.useState)(false);
|
|
1095
1134
|
const [tooShort, setTooShort] = (0, import_react3.useState)(false);
|
|
1096
1135
|
const [measured, setMeasured] = (0, import_react3.useState)(WIDTH);
|
|
1097
1136
|
const frameRef = (0, import_react3.useRef)(null);
|
|
@@ -1155,6 +1194,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1155
1194
|
(event) => {
|
|
1156
1195
|
const index = indexAt(event.clientX);
|
|
1157
1196
|
if (index === null) return;
|
|
1197
|
+
setSteppedByKeyboard(false);
|
|
1158
1198
|
setHoverIndex(index);
|
|
1159
1199
|
if (brushAnchor !== null) {
|
|
1160
1200
|
const from = Math.min(brushAnchor, index);
|
|
@@ -1212,6 +1252,17 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1212
1252
|
sentences.push(`${row.shortfall.source} counted ${formatValue(Math.round(excess), row.unit, currency)} more ${row.label.toLowerCase()} than ${row.source} on some days, which usually means a tag firing twice rather than extra traffic.`);
|
|
1213
1253
|
}
|
|
1214
1254
|
if (gap <= 0) return sentences;
|
|
1255
|
+
const coverageByDay = row.points.map((point) => {
|
|
1256
|
+
if (point.value === null || point.value <= 0) return null;
|
|
1257
|
+
const saw = seen.get(point.date);
|
|
1258
|
+
return saw == null ? null : saw / point.value;
|
|
1259
|
+
});
|
|
1260
|
+
const incident = findCoverageIncident(coverageByDay, row.points.map((p) => p.date));
|
|
1261
|
+
if (incident) {
|
|
1262
|
+
sentences.push(
|
|
1263
|
+
`${row.shortfall.source} coverage fell from about ${formatPercent(incident.before, 0)} to ${formatPercent(incident.during, 0)} on ${tickLabel(/* @__PURE__ */ new Date(`${incident.onset}T00:00:00Z`))}` + (incident.ongoing ? `, and has stayed there for ${incident.days} days. That is a change on a date, not a standing shortfall \u2014 something altered on or around then.` : ` for ${incident.days} days, then recovered.`)
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1215
1266
|
const total = `${row.shortfall.source} missed ${formatValue(Math.round(gap), row.unit, currency)} ${row.label.toLowerCase()} over this period`;
|
|
1216
1267
|
sentences.push(worst && worst.ratio > 0 ? `${total}; the gap was widest on ${tickLabel(/* @__PURE__ */ new Date(`${worst.date}T00:00:00Z`))} at ${formatPercent(worst.ratio, 0)}.` : `${total}.`);
|
|
1217
1268
|
return sentences;
|
|
@@ -1277,6 +1328,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1277
1328
|
}
|
|
1278
1329
|
if (next === null) return;
|
|
1279
1330
|
event.preventDefault();
|
|
1331
|
+
setSteppedByKeyboard(true);
|
|
1280
1332
|
setHoverIndex(next);
|
|
1281
1333
|
if (event.shiftKey && onBrush) {
|
|
1282
1334
|
const anchor = brushAnchor ?? current;
|
|
@@ -1305,7 +1357,9 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1305
1357
|
const zeroes = row.mark === "events" && pitch >= 6 ? row.points.filter((p) => p.value === 0) : [];
|
|
1306
1358
|
const gap = row.shortfall ? gapGen(row.points) ?? "" : "";
|
|
1307
1359
|
const shortfallLine = row.shortfall && revealed ? lineGen(shortfallPoints) ?? "" : "";
|
|
1360
|
+
const clipId = `row-clip-${rowIndex}`;
|
|
1308
1361
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
|
|
1362
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("clipPath", { id: clipId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: GUTTER, y: plotTop, width: plotWidth, height: Math.max(1, bottom - plotTop) }) }) }),
|
|
1309
1363
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: GUTTER, x2: GUTTER + plotWidth, y1: bottom, y2: bottom, stroke: "currentColor", strokeWidth: 1, opacity: 0.45 }),
|
|
1310
1364
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER + 4, y: top + 11, fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.85, fontWeight: 500, children: row.label }),
|
|
1311
1365
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("text", { x: GUTTER + plotWidth, y: top + 11, textAnchor: "end", fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.72, children: [
|
|
@@ -1314,52 +1368,54 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1314
1368
|
] }),
|
|
1315
1369
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER - 6, y: y(y.domain()[1]) + 4, textAnchor: "end", fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.7, children: formatValue(y.domain()[1], row.unit, currency) }),
|
|
1316
1370
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER - 6, y: bottom + 4, textAnchor: "end", fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.7, children: formatValue(0, row.unit, currency) }),
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1371
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { clipPath: `url(#${clipId})`, children: [
|
|
1372
|
+
gap && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: gap, fill: "currentColor", opacity: 0.48 }),
|
|
1373
|
+
shortfallLine && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: shortfallLine, fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeDasharray: "6 4", opacity: 0.8 }),
|
|
1374
|
+
zeroes.map((p) => {
|
|
1375
|
+
const cx = at(p);
|
|
1376
|
+
if (!Number.isFinite(cx)) return null;
|
|
1377
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1378
|
+
"rect",
|
|
1379
|
+
{
|
|
1380
|
+
x: Math.max(GUTTER, Math.min(GUTTER + plotWidth - stemWidth, cx - stemWidth / 2)),
|
|
1381
|
+
y: bottom - 3,
|
|
1382
|
+
width: stemWidth,
|
|
1383
|
+
height: 3,
|
|
1384
|
+
fill: "currentColor",
|
|
1385
|
+
opacity: 0.55
|
|
1386
|
+
},
|
|
1387
|
+
`zero-${p.date}`
|
|
1388
|
+
);
|
|
1389
|
+
}),
|
|
1390
|
+
stems.map((p) => {
|
|
1391
|
+
const cx = at(p);
|
|
1392
|
+
const headY = y(p.value);
|
|
1393
|
+
if (!Number.isFinite(cx) || !Number.isFinite(headY)) return null;
|
|
1394
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1395
|
+
"rect",
|
|
1396
|
+
{
|
|
1397
|
+
x: Math.max(GUTTER, Math.min(GUTTER + plotWidth - stemWidth, cx - stemWidth / 2)),
|
|
1398
|
+
y: Math.min(headY, bottom),
|
|
1399
|
+
width: stemWidth,
|
|
1400
|
+
height: Math.max(1.5, Math.abs(bottom - headY)),
|
|
1401
|
+
fill: "currentColor",
|
|
1402
|
+
opacity: 0.75
|
|
1403
|
+
},
|
|
1404
|
+
p.date
|
|
1405
|
+
);
|
|
1406
|
+
}),
|
|
1407
|
+
path && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1408
|
+
"path",
|
|
1341
1409
|
{
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
opacity: 0.
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
}),
|
|
1352
|
-
path && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1353
|
-
"path",
|
|
1354
|
-
{
|
|
1355
|
-
d: path,
|
|
1356
|
-
fill: "none",
|
|
1357
|
-
stroke: "currentColor",
|
|
1358
|
-
strokeWidth: 2,
|
|
1359
|
-
strokeDasharray: row.complete ? void 0 : "6 4",
|
|
1360
|
-
opacity: row.complete ? 0.95 : 0.7
|
|
1361
|
-
}
|
|
1362
|
-
)
|
|
1410
|
+
d: path,
|
|
1411
|
+
fill: "none",
|
|
1412
|
+
stroke: "currentColor",
|
|
1413
|
+
strokeWidth: 2,
|
|
1414
|
+
strokeDasharray: row.complete ? void 0 : "6 4",
|
|
1415
|
+
opacity: row.complete ? 0.95 : 0.7
|
|
1416
|
+
}
|
|
1417
|
+
)
|
|
1418
|
+
] })
|
|
1363
1419
|
] }, row.key);
|
|
1364
1420
|
}),
|
|
1365
1421
|
markers.map((marker) => {
|
|
@@ -1418,6 +1474,25 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1418
1474
|
opacity: 0.8
|
|
1419
1475
|
}
|
|
1420
1476
|
),
|
|
1477
|
+
tickIndexes.map((index) => {
|
|
1478
|
+
const date = dates[index];
|
|
1479
|
+
if (!date) return null;
|
|
1480
|
+
const at = x(/* @__PURE__ */ new Date(`${date}T00:00:00Z`));
|
|
1481
|
+
if (!Number.isFinite(at)) return null;
|
|
1482
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1483
|
+
"line",
|
|
1484
|
+
{
|
|
1485
|
+
x1: at,
|
|
1486
|
+
x2: at,
|
|
1487
|
+
y1: TOP_PAD,
|
|
1488
|
+
y2: height - AXIS_HEIGHT,
|
|
1489
|
+
stroke: "currentColor",
|
|
1490
|
+
strokeWidth: 1,
|
|
1491
|
+
opacity: 0.08
|
|
1492
|
+
},
|
|
1493
|
+
`grid-${date}`
|
|
1494
|
+
);
|
|
1495
|
+
}),
|
|
1421
1496
|
tickIndexes.map((index, position) => {
|
|
1422
1497
|
const date = dates[index];
|
|
1423
1498
|
if (!date) return null;
|
|
@@ -1438,7 +1513,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1438
1513
|
]
|
|
1439
1514
|
}
|
|
1440
1515
|
) }),
|
|
1441
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: readoutRow, "aria-live": "polite", children: [
|
|
1516
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: readoutRow, "aria-live": steppedByKeyboard ? "polite" : "off", children: [
|
|
1442
1517
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 0, weight: "medium", children: brushed && brushed[1] > brushed[0] && brushed[1] < dates.length ? `${tickLabel(/* @__PURE__ */ new Date(`${dates[brushed[0]]}T00:00:00Z`))} \u2013 ${tickLabel(/* @__PURE__ */ new Date(`${dates[brushed[1]]}T00:00:00Z`))} \xB7 ${brushed[1] - brushed[0] + 1} days` : hoveredDate ? tickLabel(/* @__PURE__ */ new Date(`${hoveredDate}T00:00:00Z`)) : "Hover the chart to read a day" }),
|
|
1443
1518
|
hoveredDate && series.map((row) => {
|
|
1444
1519
|
const point = row.points.find((p) => p.date === hoveredDate);
|