@liiift-studio/sanity-visitor-insights 0.73.0 → 0.74.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 +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +23 -82
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +24 -83
- package/dist/index.mjs.map +1 -1
- package/dist/{ranges-DhfuNtgW.d.mts → ranges-DfjCK0XY.d.mts} +0 -2
- package/dist/{ranges-DhfuNtgW.d.ts → ranges-DfjCK0XY.d.ts} +0 -2
- package/dist/server.d.mts +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/server.js +1 -11
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +1 -11
- package/dist/server.mjs.map +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{vercel-pqyL1I1o.d.mts → vercel-CNoYeZgN.d.mts} +0 -2
- package/dist/{vercel-pqyL1I1o.d.ts → vercel-CNoYeZgN.d.ts} +0 -2
- package/package.json +1 -1
- package/src/reportData.ts +0 -2
- package/src/server/orders.ts +0 -2
- package/src/server/reports/measurementHealth.ts +0 -11
- package/src/server/reports/reports.test.ts +0 -2
- package/src/studio/CrossSourceTimeline.tsx +15 -40
- package/src/studio/Figure.tsx +50 -43
- package/src/studio/palette.test.ts +3 -1
- package/src/studio/palette.ts +5 -9
- package/src/studio/panels.test.tsx +102 -32
- package/src/studio/panels.tsx +4 -16
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as sanity from 'sanity';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-
|
|
4
|
-
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-
|
|
3
|
+
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-DfjCK0XY.mjs';
|
|
4
|
+
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-DfjCK0XY.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as sanity from 'sanity';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-
|
|
4
|
-
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-
|
|
3
|
+
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-DfjCK0XY.js';
|
|
4
|
+
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneySegment, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-DfjCK0XY.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
package/dist/index.js
CHANGED
|
@@ -279,10 +279,6 @@ var SERIES = {
|
|
|
279
279
|
/** Revenue, same source, different unit. */
|
|
280
280
|
revenue: "#BF5D9B"
|
|
281
281
|
};
|
|
282
|
-
var REGION_ALPHA = 0.18;
|
|
283
|
-
function seriesFill(key, alpha = REGION_ALPHA) {
|
|
284
|
-
return withAlpha(SERIES[key], alpha);
|
|
285
|
-
}
|
|
286
282
|
function withAlpha(hex, alpha) {
|
|
287
283
|
const r = parseInt(hex.slice(1, 3), 16);
|
|
288
284
|
const g = parseInt(hex.slice(3, 5), 16);
|
|
@@ -304,21 +300,14 @@ var MARKS = {
|
|
|
304
300
|
"chart.coverage": { key: "ga4Sessions", alpha: 1, role: "data", note: "Share one source saw of another" },
|
|
305
301
|
"chart.stem": { key: "orders", alpha: 1, role: "data", note: "One day that had orders" },
|
|
306
302
|
"chart.zeroTick": { key: "orders", alpha: 1, role: "data", note: "A day MEASURED at zero \u2014 the package's founding distinction" },
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
boundary: "chart.regionEdge",
|
|
313
|
-
note: "What the lossier source missed. Light on purpose \u2014 the line it covers must stay readable"
|
|
314
|
-
},
|
|
303
|
+
// Renamed from `chart.regionEdge`. It was the boundary of a filled region; the region is gone —
|
|
304
|
+
// its width was the absolute gap, which under a flat coverage rate is the traffic curve scaled
|
|
305
|
+
// down, so it widened on busy days with no change in the instrument. This is now simply the
|
|
306
|
+
// lossier source's own line, which is what the collapse detector and the spoken summary read.
|
|
307
|
+
"chart.lossy": { key: "ga4Pageviews", alpha: 1, role: "data", note: "The lossier source's own level, dashed beneath the complete one" },
|
|
315
308
|
"chart.ghost": { key: "comparison", alpha: 1, role: "data", note: "The same window last period, behind everything" },
|
|
316
309
|
"chart.grid": { key: "neutral", alpha: 0.18, role: "furniture", note: "Grid rules" },
|
|
317
310
|
"bar.fill": { key: "vercel", alpha: 1, role: "data", note: "Every proportion, comparison and funnel bar" },
|
|
318
|
-
// The hatch on a rung whose count covers fewer days than the rail it sits in. A `data` mark, not
|
|
319
|
-
// furniture: it is the only thing on that rung saying the figure is not comparable, so it has to
|
|
320
|
-
// be as visible as a bar would have been.
|
|
321
|
-
"bar.partial": { key: "vercel", alpha: 1, role: "data", note: "A funnel rung measured over part of the window" },
|
|
322
311
|
"bar.track": { key: "neutral", alpha: 0.12, role: "furniture", note: "The rail a bar sits in" },
|
|
323
312
|
// Neither a source nor money. It was drawn in the REVENUE hue, so a magenta bar between funnel
|
|
324
313
|
// rungs read as takings; GA4's hue was rejected for the opposite reason, since that hue means
|
|
@@ -750,27 +739,14 @@ function FunnelChart({ stages, measurement }) {
|
|
|
750
739
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: stage.label }),
|
|
751
740
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "semibold", children: formatCount(stage.value) })
|
|
752
741
|
] }),
|
|
753
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-hidden": "true", style: { ...barTrack, height: 10 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
754
|
-
"div",
|
|
755
|
-
{
|
|
756
|
-
style: stage.partial ? {
|
|
757
|
-
height: "100%",
|
|
758
|
-
width: "100%",
|
|
759
|
-
borderRadius: 3,
|
|
760
|
-
backgroundImage: `repeating-linear-gradient(135deg, ${mark("bar.partial")} 0 4px, transparent 4px 8px)`
|
|
761
|
-
} : { ...barFill, width: `${width}%` }
|
|
762
|
-
}
|
|
763
|
-
) }),
|
|
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.jsx)("div", { style: { ...barFill, width: `${width}%` } }) }),
|
|
764
743
|
rateLine(stage, index, entry, share, stages[0]?.label ?? "")
|
|
765
744
|
]
|
|
766
745
|
}
|
|
767
746
|
)
|
|
768
747
|
] }, stage.key);
|
|
769
748
|
}) }),
|
|
770
|
-
stages.some((stage) => stage.partial) && /* @__PURE__ */ (0, import_jsx_runtime.
|
|
771
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", style: funnelKeySwatch }),
|
|
772
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "Striped: tracking started partway through this period, so the step covers fewer days than the ones above it and cannot be compared with them." })
|
|
773
|
-
] }),
|
|
749
|
+
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." }),
|
|
774
750
|
withheldAny && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
|
|
775
751
|
"Shares are shown only for steps at least ",
|
|
776
752
|
MIN_RATE_DENOMINATOR,
|
|
@@ -787,15 +763,7 @@ function gapLabel(delta, measurement, entry = 0) {
|
|
|
787
763
|
}
|
|
788
764
|
return `${formatCount(-delta)} more \u2014 not a subset of the step above`;
|
|
789
765
|
}
|
|
790
|
-
var
|
|
791
|
-
var funnelKeySwatch = {
|
|
792
|
-
display: "inline-block",
|
|
793
|
-
width: 24,
|
|
794
|
-
height: 10,
|
|
795
|
-
flex: "0 0 auto",
|
|
796
|
-
borderRadius: 3,
|
|
797
|
-
backgroundImage: `repeating-linear-gradient(135deg, ${mark("bar.partial")} 0 4px, transparent 4px 8px)`
|
|
798
|
-
};
|
|
766
|
+
var partialSlot = { height: 10 };
|
|
799
767
|
var funnelList = { listStyle: "none", margin: 0, padding: 0 };
|
|
800
768
|
var funnelItem = { display: "grid", gap: 4 };
|
|
801
769
|
function funnelGapStyle(hasBar) {
|
|
@@ -1013,11 +981,12 @@ function EstimateDotPlot({
|
|
|
1013
981
|
" of ",
|
|
1014
982
|
formatCount(estimate.actual),
|
|
1015
983
|
wide && " \u2014 too small a sample to lean on",
|
|
984
|
+
estimate.note && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: estimateNote, children: estimate.note }),
|
|
1016
985
|
estimate.rate > 1.05 && " \u2014 over 100%, so GA4 is counting more than the source it is checked against. That is usually a tag firing twice, not extra traffic."
|
|
1017
986
|
] })
|
|
1018
987
|
] }, estimate.basis);
|
|
1019
988
|
}),
|
|
1020
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "
|
|
989
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "The rule marks 100%." })
|
|
1021
990
|
] });
|
|
1022
991
|
}
|
|
1023
992
|
function SurvivalLines({
|
|
@@ -1435,6 +1404,7 @@ var estimateTrack = {
|
|
|
1435
1404
|
borderRadius: 2,
|
|
1436
1405
|
background: mark("bar.track")
|
|
1437
1406
|
};
|
|
1407
|
+
var estimateNote = { display: "block", marginTop: 2, opacity: 0.85 };
|
|
1438
1408
|
var estimateRule = {
|
|
1439
1409
|
position: "absolute",
|
|
1440
1410
|
top: 0,
|
|
@@ -2008,8 +1978,6 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
2008
1978
|
const defined = (p) => p.value !== null;
|
|
2009
1979
|
const lineGen = (0, import_d3_shape.line)().defined(defined).x(at).y((p) => y(p.value)).curve(import_d3_shape.curveLinear);
|
|
2010
1980
|
const shortfallPoints = row.shortfall?.points ?? [];
|
|
2011
|
-
const byDate = new Map(shortfallPoints.map((p) => [p.date, p.value]));
|
|
2012
|
-
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.curveLinear);
|
|
2013
1981
|
const path = row.mark === "events" ? "" : lineGen(row.points) ?? "";
|
|
2014
1982
|
const ghost = row.comparison && row.comparison.length > 1 ? lineGen(row.comparison.slice(0, dates.length).map((point, i) => ({
|
|
2015
1983
|
date: dates[i] ?? point.date,
|
|
@@ -2019,7 +1987,6 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
2019
1987
|
const stemWidth = Math.max(1, Math.min(9, pitch > 2 ? pitch - 1 : pitch));
|
|
2020
1988
|
const stems = row.mark === "events" ? row.points.filter((p) => p.value !== null && p.value !== 0) : [];
|
|
2021
1989
|
const zeroes = row.mark === "events" && pitch >= 6 ? row.points.filter((p) => p.value === 0) : [];
|
|
2022
|
-
const gap = row.shortfall ? gapGen(row.points) ?? "" : "";
|
|
2023
1990
|
const shortfallLine = row.shortfall ? lineGen(shortfallPoints) ?? "" : "";
|
|
2024
1991
|
const clipId = `${instanceId}-row-${rowIndex}`;
|
|
2025
1992
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
|
|
@@ -2044,20 +2011,12 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
2044
2011
|
strokeDasharray: "2 3"
|
|
2045
2012
|
}
|
|
2046
2013
|
),
|
|
2047
|
-
gap && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2048
|
-
"path",
|
|
2049
|
-
{
|
|
2050
|
-
d: gap,
|
|
2051
|
-
fill: shortfallStroke ? seriesFill(colorFor(row.shortfall.source, row.unit, row.shortfall.color), MARKS["chart.region"].alpha) : "currentColor",
|
|
2052
|
-
opacity: shortfallStroke ? 1 : 0.48
|
|
2053
|
-
}
|
|
2054
|
-
),
|
|
2055
2014
|
shortfallLine && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2056
2015
|
"path",
|
|
2057
2016
|
{
|
|
2058
2017
|
d: shortfallLine,
|
|
2059
2018
|
fill: "none",
|
|
2060
|
-
stroke: mark("chart.
|
|
2019
|
+
stroke: mark("chart.lossy"),
|
|
2061
2020
|
strokeWidth: 1.5,
|
|
2062
2021
|
strokeDasharray: "6 4"
|
|
2063
2022
|
}
|
|
@@ -2229,22 +2188,13 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
2229
2188
|
] }, m.label))
|
|
2230
2189
|
] }),
|
|
2231
2190
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: legendRow, children: [
|
|
2232
|
-
series.some((row) => row.shortfall) && /* @__PURE__ */ (0, import_jsx_runtime2.
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
{
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
),
|
|
2240
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2241
|
-
LegendKey,
|
|
2242
|
-
{
|
|
2243
|
-
swatch: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: legendBlock(seriesFill(shortfallKey(series), MARKS["chart.region"].alpha), shortfallColour(series)) }),
|
|
2244
|
-
label: "Shaded: what your analytics did not see"
|
|
2245
|
-
}
|
|
2246
|
-
)
|
|
2247
|
-
] }),
|
|
2191
|
+
series.some((row) => row.shortfall) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2192
|
+
LegendKey,
|
|
2193
|
+
{
|
|
2194
|
+
swatch: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: legendDash(shortfallColour(series)) }),
|
|
2195
|
+
label: `${shortfallLabel(series)} \u2014 the dashed line`
|
|
2196
|
+
}
|
|
2197
|
+
) }),
|
|
2248
2198
|
series.some((row) => (row.comparison?.length ?? 0) > 1) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2249
2199
|
LegendKey,
|
|
2250
2200
|
{
|
|
@@ -2281,15 +2231,6 @@ function legendDash(colour) {
|
|
|
2281
2231
|
borderTop: `2px dashed ${colour}`
|
|
2282
2232
|
};
|
|
2283
2233
|
}
|
|
2284
|
-
function legendBlock(fill, edge) {
|
|
2285
|
-
return {
|
|
2286
|
-
display: "inline-block",
|
|
2287
|
-
width: 18,
|
|
2288
|
-
height: 10,
|
|
2289
|
-
background: fill,
|
|
2290
|
-
borderTop: `1.5px dashed ${edge}`
|
|
2291
|
-
};
|
|
2292
|
-
}
|
|
2293
2234
|
var legendMarker = {
|
|
2294
2235
|
display: "inline-block",
|
|
2295
2236
|
position: "relative",
|
|
@@ -2383,9 +2324,6 @@ function metricOr(metric, detail) {
|
|
|
2383
2324
|
return { status: "unavailable", reason: "route_outdated", detail };
|
|
2384
2325
|
}
|
|
2385
2326
|
var OLDER_ROUTE = "This site\u2019s analytics route is older than this figure \u2014 it will appear after the site is next deployed.";
|
|
2386
|
-
function maxOf(metrics) {
|
|
2387
|
-
return metrics.reduce((max, metric) => !metric || metric.status === "unavailable" ? max : Math.max(max, metric.value), 0);
|
|
2388
|
-
}
|
|
2389
2327
|
function finiteOrNull(value) {
|
|
2390
2328
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
2391
2329
|
}
|
|
@@ -2847,7 +2785,6 @@ function Verdict({ data, previous }) {
|
|
|
2847
2785
|
);
|
|
2848
2786
|
}
|
|
2849
2787
|
function DataHealthPanel({ data, diagnostics }) {
|
|
2850
|
-
const pageviewMax = maxOf([data.ga4Pageviews, data.vercelPageviews]);
|
|
2851
2788
|
const contained = isContainment(data.vercelPageviews, data.ga4Pageviews);
|
|
2852
2789
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
|
|
2853
2790
|
/* @__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.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
|
|
@@ -3465,6 +3402,8 @@ function TypefaceInterestPanel({ data }) {
|
|
|
3465
3402
|
},
|
|
3466
3403
|
{
|
|
3467
3404
|
key: "bought",
|
|
3405
|
+
// Never folded. Zero sales is the finding, not an empty column.
|
|
3406
|
+
alwaysShow: true,
|
|
3468
3407
|
label: "Bought (orders)",
|
|
3469
3408
|
numeric: true,
|
|
3470
3409
|
sortValue: (row) => metricSortValue(row.bought),
|
|
@@ -3472,6 +3411,8 @@ function TypefaceInterestPanel({ data }) {
|
|
|
3472
3411
|
},
|
|
3473
3412
|
{
|
|
3474
3413
|
key: "revenue",
|
|
3414
|
+
// Same reason as Bought: an exact zero from the order book is a measurement.
|
|
3415
|
+
alwaysShow: true,
|
|
3475
3416
|
label: "Revenue (orders)",
|
|
3476
3417
|
numeric: true,
|
|
3477
3418
|
sortValue: (row) => metricSortValue(row.revenue),
|