@liiift-studio/sanity-visitor-insights 0.40.0 → 0.41.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 CHANGED
@@ -943,7 +943,15 @@ function dayIndexAt(clientX, left, boxWidth, viewBoxWidth, plotWidth, count) {
943
943
  const index = Math.round((clamped - GUTTER) / plotWidth * (count - 1));
944
944
  return Math.max(0, Math.min(count - 1, index));
945
945
  }
946
- function findCoverageIncident(days, dates) {
946
+ function findCoverageIncident(days, dates, volumes = []) {
947
+ const busiest = volumes.reduce((best, v) => Math.max(best, v ?? 0), 0);
948
+ const floor = busiest > 0 ? Math.max(1, busiest * 0.1) : 0;
949
+ const usable = days.map((value, i) => {
950
+ const volume = volumes[i];
951
+ if (floor > 0 && (volume === null || volume === void 0 || volume < floor)) return null;
952
+ return value;
953
+ });
954
+ days = usable;
947
955
  const measured = days.filter((d) => d !== null);
948
956
  if (measured.length < 21) return null;
949
957
  const sorted = [...measured].sort((a, b) => a - b);
@@ -1127,7 +1135,11 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
1127
1135
  const saw = seen.get(point.date);
1128
1136
  return saw == null ? null : saw / point.value;
1129
1137
  });
1130
- const incident = findCoverageIncident(coverageByDay, row.points.map((p) => p.date));
1138
+ const incident = findCoverageIncident(
1139
+ coverageByDay,
1140
+ row.points.map((p) => p.date),
1141
+ row.points.map((p) => p.value)
1142
+ );
1131
1143
  if (incident) {
1132
1144
  sentences.push(
1133
1145
  `${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.`)
@@ -1810,6 +1822,8 @@ function Verdict({ data, previous }) {
1810
1822
  const change = (a - b) / Math.abs(b);
1811
1823
  if (Math.abs(change) < 0.05) {
1812
1824
  parts.push(Math.abs(change) < 5e-3 ? `${label} flat` : `${label} little changed (${change > 0 ? "+" : "\u2212"}${formatPercent(Math.abs(change), 0)})`);
1825
+ } else if (unit === "money") {
1826
+ parts.push(`${label} ${change > 0 ? "up" : "down"} to ${formatMoney(a, data.currency ?? null)} from ${formatMoney(b, data.currency ?? null)}`);
1813
1827
  } else parts.push(`${label} ${change > 0 ? "up" : "down"} ${formatPercent(Math.abs(change), 0)}`);
1814
1828
  };
1815
1829
  say("Revenue", data.revenue, previous?.revenue, "money");
@@ -2191,7 +2205,11 @@ function TypefaceInterestPanel({ data }) {
2191
2205
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: data.interpretationNote }) }),
2192
2206
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
2193
2207
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Engagement by typeface" }),
2194
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "Sort by any column. \u201CSells vs catalogue\u201D compares each family\u2019s sales-per-view against the catalogue as a whole \u2014 1.0\xD7 is average, and sorting up finds the families that are looked at and do not sell. It is a comparison between your families, not the share of visitors who buy: Google Analytics sees only a fraction of the views, which moves every family together and so cancels out here. With one or two sales a family will still swing a long way, so read it alongside the order counts beside it." }),
2208
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: [
2209
+ "Sort by any column. \u201CSells vs catalogue\u201D compares each family\u2019s sales-per-view against the catalogue as a whole \u2014 1.0\xD7 is average, and sorting up finds the families that are looked at and do not sell. It is a comparison between your families, not the share of visitors who buy: Google Analytics sees only a fraction of the views, which moves every family together and so cancels out here. With one or two sales a family will still swing a long way, so read it alongside the order counts beside it.",
2210
+ " ",
2211
+ "Bought and Revenue come from your own orders and are exact \u2014 do not scale those up."
2212
+ ] }),
2195
2213
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2196
2214
  SortableTable,
2197
2215
  {
@@ -2212,28 +2230,34 @@ function TypefaceInterestPanel({ data }) {
2212
2230
  },
2213
2231
  {
2214
2232
  key: "viewed",
2215
- label: "Viewed",
2233
+ // The source is in the HEADER, because the ribbon at the top of this tab
2234
+ // instructs the reader to multiply its figures by about five — and two of
2235
+ // these four columns come from the order book and are exact. Following that
2236
+ // instruction across the whole table multiplies real revenue fivefold. The
2237
+ // sentence that prevents it already exists on the licence block below; it was
2238
+ // simply never applied where the mistake is made.
2239
+ label: "Viewed (GA4)",
2216
2240
  numeric: true,
2217
2241
  sortValue: (row) => metricSortValue(row.viewed),
2218
2242
  render: (row) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: row.viewed, label: `${row.typeface} viewed`, size: 1 })
2219
2243
  },
2220
2244
  {
2221
2245
  key: "tested",
2222
- label: "Tested",
2246
+ label: "Tested (GA4)",
2223
2247
  numeric: true,
2224
2248
  sortValue: (row) => metricSortValue(row.tested),
2225
2249
  render: (row) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: row.tested, label: `${row.typeface} tested`, size: 1 })
2226
2250
  },
2227
2251
  {
2228
2252
  key: "bought",
2229
- label: "Bought",
2253
+ label: "Bought (orders)",
2230
2254
  numeric: true,
2231
2255
  sortValue: (row) => metricSortValue(row.bought),
2232
2256
  render: (row) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: row.bought, label: `${row.typeface} bought`, size: 1 })
2233
2257
  },
2234
2258
  {
2235
2259
  key: "revenue",
2236
- label: "Revenue",
2260
+ label: "Revenue (orders)",
2237
2261
  numeric: true,
2238
2262
  sortValue: (row) => metricSortValue(row.revenue),
2239
2263
  exportValue: (row) => {
@@ -2946,7 +2970,7 @@ function toHistory(cutover) {
2946
2970
  return cutover;
2947
2971
  }
2948
2972
  function overlaps(aStart, aEnd, bStart, bEnd) {
2949
- return aStart <= bEnd && (aEnd === null || aEnd >= bStart);
2973
+ return aStart <= bEnd && (aEnd === null || aEnd > bStart);
2950
2974
  }
2951
2975
  function coverageForRange(cutovers, eventName, range) {
2952
2976
  if (!(eventName in cutovers)) return { status: "none", reason: "unknown_event" };