@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 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 = WIDTH - GUTTER - RIGHT_PAD;
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 ${WIDTH} ${height}`,
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: "auto", display: "block", cursor: onBrush ? "col-resize" : "default" },
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
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
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,