Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <title> tooltip and the
* page ships full tables as the accessible view.
* never the series color. No svg <title> 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.
Expand All @@ -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;
Expand Down Expand Up @@ -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 <title> tooltips - a streamed-late document
* <title> 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;
Expand Down Expand Up @@ -252,6 +265,57 @@ export function LineChart({
</g>
);
})}
{/* hover layer: one hit band per point; CSS reveals the readout */}
<g aria-hidden="true">
{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 (
<g key={`h-${i}`} className="ce-hband">
<rect x={left} y={0} width={right - left} height={H} fill="transparent" />
<g className="ce-hlbl">
<line
x1={px(i)}
y1={padTop}
x2={px(i)}
y2={H - padBottom}
stroke="var(--ce-text2)"
strokeWidth={1}
opacity={0.4}
/>
{series.map((s, si) => (
<circle
key={s.name}
cx={px(i)}
cy={py(s.values[i] ?? 0)}
r={4}
fill={seriesColor(si)}
stroke="var(--ce-surface)"
strokeWidth={2}
/>
Comment thread
greptile-apps[bot] marked this conversation as resolved.
))}
<text
x={padX}
y={20}
textAnchor="start"
fontSize={11}
fill="var(--ce-text2)"
stroke="var(--ce-surface)"
strokeWidth={3}
paintOrder="stroke"
>
{label}
{series.map((s) =>
series.length > 1
? ` · ${s.name} ${readoutFormatter(s.values[i] ?? 0)}`
: ` · ${readoutFormatter(s.values[i] ?? 0)}`
)}
</text>
</g>
</g>
);
})}
</g>
</svg>
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/app/(staticPages)/creator-economy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)}`}
/>
</section>

Expand Down
46 changes: 46 additions & 0 deletions apps/web/src/specs/features/creator-economy/line-chart.spec.tsx
Original file line number Diff line number Diff line change
@@ -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(
<LineChart
labels={labels}
series={[{ name: "USD", values: [100, 250, 200, 150] }]}
ariaLabel="usd"
/>
);
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(
<LineChart
labels={labels}
series={[
{ name: "Posts", values: [10, 20, 30, 40] },
{ name: "Comments", values: [1000, 2000, 3000, 4000] }
]}
ariaLabel="content"
/>
);
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(
<LineChart labels={labels} series={[{ name: "USD", values: [1, 2, 3, 4] }]} ariaLabel="x" />
);
expect(container.querySelectorAll("svg title")).toHaveLength(0);
});
});
Loading