diff --git a/apps/web/src/app/(staticPages)/creator-economy/_components/charts.tsx b/apps/web/src/app/(staticPages)/creator-economy/_components/charts.tsx
index f3277b7f89..55b478c635 100644
--- a/apps/web/src/app/(staticPages)/creator-economy/_components/charts.tsx
+++ b/apps/web/src/app/(staticPages)/creator-economy/_components/charts.tsx
@@ -1,12 +1,14 @@
import React from "react";
-import { columnPath, formatCompact, hbarPath, scaleMax } from "./chart-utils";
+import { columnPath, formatCompact, formatFull, hbarPath, scaleMax } from "./chart-utils";
/**
* Server-rendered SVG charts (zero client JS). Colors come from CSS custom
* properties defined on .ce-viz (light + .dark overrides in the page), so the
* marks respond to the site theme without hydration. Text wears text tokens,
- * never the series color; every mark carries a native
tooltip and the
- * page ships full tables as the accessible view.
+ * never the series color. No svg tooltips anywhere (the document
+ * title streams late, so first-title parsers would read a tooltip as the page
+ * title); values live in direct labels, CSS hover readouts and the page's
+ * full tables, which are the accessible view.
*/
// Two validated series slots; charts on this page use at most two series.
@@ -23,12 +25,15 @@ export function ColumnChart({
labels,
series,
ariaLabel,
- valueFormatter = formatCompact
+ valueFormatter = formatCompact,
+ readoutFormatter
}: {
labels: string[];
series: ColumnSeries[]; // 1-2 series (grouped)
ariaLabel: string;
valueFormatter?: (n: number) => string;
+ /** unused here: columns carry direct cap labels; kept for Trend prop parity */
+ readoutFormatter?: (n: number) => string;
}) {
const W = 560;
const H = 230;
@@ -129,17 +134,25 @@ export function ColumnChart({
* Mark specs: 2px round-join line, >=8px end dot with a 2px surface ring,
* ~10% area wash for a single series. Labels are selective (first / peak /
* last values; year ticks on the x-axis) - the table views carry every value.
+ *
+ * Hover: pure-CSS per-point bands (see .ce-hband/.ce-hlbl in the page's style
+ * block) reveal a hairline, point dots and a fixed readout with the exact
+ * values. No client JS, and no svg tooltips - a streamed-late document
+ * means naive parsers would read a tooltip as the page title.
*/
export function LineChart({
labels,
series,
ariaLabel,
- valueFormatter = formatCompact
+ valueFormatter = formatCompact,
+ readoutFormatter = formatFull
}: {
labels: string[]; // one per point, e.g. "Q2 2020"
series: ColumnSeries[]; // 1-2 series
ariaLabel: string;
valueFormatter?: (n: number) => string;
+ /** hover readout shows EXACT values; sparse on-chart labels stay compact */
+ readoutFormatter?: (n: number) => string;
}) {
const W = 560;
const H = 230;
@@ -252,6 +265,57 @@ export function LineChart({
);
})}
+ {/* hover layer: one hit band per point; CSS reveals the readout */}
+
+ {labels.map((label, i) => {
+ const left = i === 0 ? 0 : (px(i - 1) + px(i)) / 2;
+ const right = i === n - 1 ? W : (px(i) + px(i + 1)) / 2;
+ return (
+
+
+
+
+ {series.map((s, si) => (
+
+ ))}
+
+ {label}
+ {series.map((s) =>
+ series.length > 1
+ ? ` · ${s.name} ${readoutFormatter(s.values[i] ?? 0)}`
+ : ` · ${readoutFormatter(s.values[i] ?? 0)}`
+ )}
+
+
+
+ );
+ })}
+
);
diff --git a/apps/web/src/app/(staticPages)/creator-economy/page.tsx b/apps/web/src/app/(staticPages)/creator-economy/page.tsx
index 87f36db491..ae447287ff 100644
--- a/apps/web/src/app/(staticPages)/creator-economy/page.tsx
+++ b/apps/web/src/app/(staticPages)/creator-economy/page.tsx
@@ -31,6 +31,8 @@ export async function generateMetadata(
const VIZ_VARS = `
.ce-viz{--ce-s1:#357ce6;--ce-s2:#1baf7a;--ce-grid:#e5e7eb;--ce-text2:#6b7280;--ce-surface:#ffffff}
.dark .ce-viz{--ce-s1:#3987e5;--ce-s2:#199e70;--ce-grid:#2f2f2f;--ce-text2:#9ca3af;--ce-surface:#131111}
+.ce-viz .ce-hlbl{opacity:0;pointer-events:none;transition:opacity .1s}
+.ce-viz .ce-hband:hover .ce-hlbl{opacity:1}
`;
export default async function CreatorEconomyPage() {
@@ -114,6 +116,7 @@ export default async function CreatorEconomyPage() {
series={[{ name: "USD", values: quarters.map((q) => q.rewards.usd ?? 0) }]}
ariaLabel={t("chart-usd")}
valueFormatter={(n) => `$${formatCompact(n)}`}
+ readoutFormatter={(n) => `$${formatFull(n)}`}
/>
diff --git a/apps/web/src/specs/features/creator-economy/line-chart.spec.tsx b/apps/web/src/specs/features/creator-economy/line-chart.spec.tsx
new file mode 100644
index 0000000000..fdb148b74d
--- /dev/null
+++ b/apps/web/src/specs/features/creator-economy/line-chart.spec.tsx
@@ -0,0 +1,46 @@
+import { render } from "@testing-library/react";
+import { LineChart } from "@/app/(staticPages)/creator-economy/_components/charts";
+
+const labels = ["Q1 2023", "Q2 2023", "Q3 2023", "Q4 2023"];
+
+describe("LineChart", () => {
+ it("renders one hover band per point with an exact-value readout", () => {
+ const { container } = render(
+
+ );
+ const bands = container.querySelectorAll(".ce-hband");
+ expect(bands).toHaveLength(labels.length);
+ // readout carries the quarter and the formatted value
+ expect(bands[1].textContent).toContain("Q2 2023");
+ expect(bands[1].textContent).toContain("250");
+ });
+
+ it("names each series in multi-series readouts", () => {
+ const { container } = render(
+
+ );
+ const band = container.querySelectorAll(".ce-hband")[2];
+ expect(band.textContent).toContain("Posts");
+ expect(band.textContent).toContain("Comments");
+ // readouts are full-precision by design (review: compact defeated the purpose)
+ expect(band.textContent).toContain("3,000");
+ });
+
+ it("never emits svg title elements (document-title streaming hazard)", () => {
+ const { container } = render(
+
+ );
+ expect(container.querySelectorAll("svg title")).toHaveLength(0);
+ });
+});