mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-09 04:19:27 -07:00
feat(stats): unify chart theme and add gridlines for legibility
This commit is contained in:
@@ -1,7 +1,15 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
|
import {
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
CartesianGrid,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
} from 'recharts';
|
||||||
import { epochDayToDate } from '../../lib/formatters';
|
import { epochDayToDate } from '../../lib/formatters';
|
||||||
import { CHART_THEME } from '../../lib/chart-theme';
|
import { CHART_DEFAULTS, CHART_THEME, TOOLTIP_CONTENT_STYLE } from '../../lib/chart-theme';
|
||||||
import type { DailyRollup } from '../../types/stats';
|
import type { DailyRollup } from '../../types/stats';
|
||||||
|
|
||||||
interface WatchTimeChartProps {
|
interface WatchTimeChartProps {
|
||||||
@@ -52,28 +60,23 @@ export function WatchTimeChart({ rollups }: WatchTimeChartProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ResponsiveContainer width="100%" height={160}>
|
<ResponsiveContainer width="100%" height={CHART_DEFAULTS.height}>
|
||||||
<BarChart data={chartData}>
|
<BarChart data={chartData} margin={CHART_DEFAULTS.margin}>
|
||||||
|
<CartesianGrid stroke={CHART_THEME.grid} {...CHART_DEFAULTS.grid} />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="date"
|
dataKey="date"
|
||||||
tick={{ fontSize: 10, fill: CHART_THEME.tick }}
|
tick={{ fontSize: CHART_DEFAULTS.tickFontSize, fill: CHART_THEME.tick }}
|
||||||
axisLine={false}
|
axisLine={{ stroke: CHART_THEME.axisLine }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
tick={{ fontSize: 10, fill: CHART_THEME.tick }}
|
tick={{ fontSize: CHART_DEFAULTS.tickFontSize, fill: CHART_THEME.tick }}
|
||||||
axisLine={false}
|
axisLine={{ stroke: CHART_THEME.axisLine }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
width={30}
|
width={32}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={TOOLTIP_CONTENT_STYLE}
|
||||||
background: CHART_THEME.tooltipBg,
|
|
||||||
border: `1px solid ${CHART_THEME.tooltipBorder}`,
|
|
||||||
borderRadius: 6,
|
|
||||||
color: CHART_THEME.tooltipText,
|
|
||||||
fontSize: 12,
|
|
||||||
}}
|
|
||||||
labelStyle={{ color: CHART_THEME.tooltipLabel }}
|
labelStyle={{ color: CHART_THEME.tooltipLabel }}
|
||||||
formatter={formatActiveMinutes}
|
formatter={formatActiveMinutes}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
|
import {
|
||||||
|
AreaChart,
|
||||||
|
Area,
|
||||||
|
CartesianGrid,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
} from 'recharts';
|
||||||
|
import { CHART_DEFAULTS, CHART_THEME, TOOLTIP_CONTENT_STYLE } from '../../lib/chart-theme';
|
||||||
import { epochDayToDate } from '../../lib/formatters';
|
import { epochDayToDate } from '../../lib/formatters';
|
||||||
|
|
||||||
export interface PerAnimeDataPoint {
|
export interface PerAnimeDataPoint {
|
||||||
@@ -64,14 +73,6 @@ export function StackedTrendChart({ title, data, colorPalette }: StackedTrendCha
|
|||||||
const { points, seriesKeys } = buildLineData(data);
|
const { points, seriesKeys } = buildLineData(data);
|
||||||
const colors = colorPalette ?? DEFAULT_LINE_COLORS;
|
const colors = colorPalette ?? DEFAULT_LINE_COLORS;
|
||||||
|
|
||||||
const tooltipStyle = {
|
|
||||||
background: '#363a4f',
|
|
||||||
border: '1px solid #494d64',
|
|
||||||
borderRadius: 6,
|
|
||||||
color: '#cad3f5',
|
|
||||||
fontSize: 12,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (points.length === 0) {
|
if (points.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-ctp-surface0 border border-ctp-surface1 rounded-lg p-4">
|
<div className="bg-ctp-surface0 border border-ctp-surface1 rounded-lg p-4">
|
||||||
@@ -84,21 +85,22 @@ export function StackedTrendChart({ title, data, colorPalette }: StackedTrendCha
|
|||||||
return (
|
return (
|
||||||
<div className="bg-ctp-surface0 border border-ctp-surface1 rounded-lg p-4">
|
<div className="bg-ctp-surface0 border border-ctp-surface1 rounded-lg p-4">
|
||||||
<h3 className="text-xs font-semibold text-ctp-text mb-2">{title}</h3>
|
<h3 className="text-xs font-semibold text-ctp-text mb-2">{title}</h3>
|
||||||
<ResponsiveContainer width="100%" height={120}>
|
<ResponsiveContainer width="100%" height={CHART_DEFAULTS.height}>
|
||||||
<AreaChart data={points}>
|
<AreaChart data={points} margin={CHART_DEFAULTS.margin}>
|
||||||
|
<CartesianGrid stroke={CHART_THEME.grid} {...CHART_DEFAULTS.grid} />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="label"
|
dataKey="label"
|
||||||
tick={{ fontSize: 9, fill: '#a5adcb' }}
|
tick={{ fontSize: CHART_DEFAULTS.tickFontSize, fill: CHART_THEME.tick }}
|
||||||
axisLine={false}
|
axisLine={{ stroke: CHART_THEME.axisLine }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
tick={{ fontSize: 9, fill: '#a5adcb' }}
|
tick={{ fontSize: CHART_DEFAULTS.tickFontSize, fill: CHART_THEME.tick }}
|
||||||
axisLine={false}
|
axisLine={{ stroke: CHART_THEME.axisLine }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
width={28}
|
width={32}
|
||||||
/>
|
/>
|
||||||
<Tooltip contentStyle={tooltipStyle} />
|
<Tooltip contentStyle={TOOLTIP_CONTENT_STYLE} />
|
||||||
{seriesKeys.map((key, i) => (
|
{seriesKeys.map((key, i) => (
|
||||||
<Area
|
<Area
|
||||||
key={key}
|
key={key}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import {
|
|||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
CartesianGrid,
|
||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
import { CHART_DEFAULTS, CHART_THEME, TOOLTIP_CONTENT_STYLE } from '../../lib/chart-theme';
|
||||||
|
|
||||||
interface TrendChartProps {
|
interface TrendChartProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -19,35 +21,29 @@ interface TrendChartProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TrendChart({ title, data, color, type, formatter, onBarClick }: TrendChartProps) {
|
export function TrendChart({ title, data, color, type, formatter, onBarClick }: TrendChartProps) {
|
||||||
const tooltipStyle = {
|
|
||||||
background: '#363a4f',
|
|
||||||
border: '1px solid #494d64',
|
|
||||||
borderRadius: 6,
|
|
||||||
color: '#cad3f5',
|
|
||||||
fontSize: 12,
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatValue = (v: number) => (formatter ? [formatter(v), title] : [String(v), title]);
|
const formatValue = (v: number) => (formatter ? [formatter(v), title] : [String(v), title]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-ctp-surface0 border border-ctp-surface1 rounded-lg p-4">
|
<div className="bg-ctp-surface0 border border-ctp-surface1 rounded-lg p-4">
|
||||||
<h3 className="text-xs font-semibold text-ctp-text mb-2">{title}</h3>
|
<h3 className="text-xs font-semibold text-ctp-text mb-2">{title}</h3>
|
||||||
<ResponsiveContainer width="100%" height={120}>
|
<ResponsiveContainer width="100%" height={CHART_DEFAULTS.height}>
|
||||||
{type === 'bar' ? (
|
{type === 'bar' ? (
|
||||||
<BarChart data={data}>
|
<BarChart data={data} margin={CHART_DEFAULTS.margin}>
|
||||||
|
<CartesianGrid stroke={CHART_THEME.grid} {...CHART_DEFAULTS.grid} />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="label"
|
dataKey="label"
|
||||||
tick={{ fontSize: 9, fill: '#a5adcb' }}
|
tick={{ fontSize: CHART_DEFAULTS.tickFontSize, fill: CHART_THEME.tick }}
|
||||||
axisLine={false}
|
axisLine={{ stroke: CHART_THEME.axisLine }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
tick={{ fontSize: 9, fill: '#a5adcb' }}
|
tick={{ fontSize: CHART_DEFAULTS.tickFontSize, fill: CHART_THEME.tick }}
|
||||||
axisLine={false}
|
axisLine={{ stroke: CHART_THEME.axisLine }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
width={28}
|
width={32}
|
||||||
|
tickFormatter={formatter}
|
||||||
/>
|
/>
|
||||||
<Tooltip contentStyle={tooltipStyle} formatter={formatValue} />
|
<Tooltip contentStyle={TOOLTIP_CONTENT_STYLE} formatter={formatValue} />
|
||||||
<Bar
|
<Bar
|
||||||
dataKey="value"
|
dataKey="value"
|
||||||
fill={color}
|
fill={color}
|
||||||
@@ -59,20 +55,22 @@ export function TrendChart({ title, data, color, type, formatter, onBarClick }:
|
|||||||
/>
|
/>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
) : (
|
) : (
|
||||||
<LineChart data={data}>
|
<LineChart data={data} margin={CHART_DEFAULTS.margin}>
|
||||||
|
<CartesianGrid stroke={CHART_THEME.grid} {...CHART_DEFAULTS.grid} />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="label"
|
dataKey="label"
|
||||||
tick={{ fontSize: 9, fill: '#a5adcb' }}
|
tick={{ fontSize: CHART_DEFAULTS.tickFontSize, fill: CHART_THEME.tick }}
|
||||||
axisLine={false}
|
axisLine={{ stroke: CHART_THEME.axisLine }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
tick={{ fontSize: 9, fill: '#a5adcb' }}
|
tick={{ fontSize: CHART_DEFAULTS.tickFontSize, fill: CHART_THEME.tick }}
|
||||||
axisLine={false}
|
axisLine={{ stroke: CHART_THEME.axisLine }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
width={28}
|
width={32}
|
||||||
|
tickFormatter={formatter}
|
||||||
/>
|
/>
|
||||||
<Tooltip contentStyle={tooltipStyle} formatter={formatValue} />
|
<Tooltip contentStyle={TOOLTIP_CONTENT_STYLE} formatter={formatValue} />
|
||||||
<Line dataKey="value" stroke={color} strokeWidth={2} dot={false} />
|
<Line dataKey="value" stroke={color} strokeWidth={2} dot={false} />
|
||||||
</LineChart>
|
</LineChart>
|
||||||
)}
|
)}
|
||||||
|
|||||||
16
stats/src/lib/chart-theme.test.ts
Normal file
16
stats/src/lib/chart-theme.test.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { CHART_DEFAULTS, CHART_THEME, TOOLTIP_CONTENT_STYLE } from './chart-theme';
|
||||||
|
|
||||||
|
test('CHART_THEME exposes a grid color', () => {
|
||||||
|
assert.equal(CHART_THEME.grid, '#494d64');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CHART_DEFAULTS uses 11px ticks for legibility', () => {
|
||||||
|
assert.equal(CHART_DEFAULTS.tickFontSize, 11);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TOOLTIP_CONTENT_STYLE mirrors the shared tooltip colors', () => {
|
||||||
|
assert.equal(TOOLTIP_CONTENT_STYLE.background, CHART_THEME.tooltipBg);
|
||||||
|
assert.ok(String(TOOLTIP_CONTENT_STYLE.border).includes(CHART_THEME.tooltipBorder));
|
||||||
|
});
|
||||||
@@ -5,4 +5,21 @@ export const CHART_THEME = {
|
|||||||
tooltipText: '#cad3f5',
|
tooltipText: '#cad3f5',
|
||||||
tooltipLabel: '#b8c0e0',
|
tooltipLabel: '#b8c0e0',
|
||||||
barFill: '#8aadf4',
|
barFill: '#8aadf4',
|
||||||
|
grid: '#494d64',
|
||||||
|
axisLine: '#494d64',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const CHART_DEFAULTS = {
|
||||||
|
height: 160,
|
||||||
|
tickFontSize: 11,
|
||||||
|
margin: { top: 8, right: 8, bottom: 0, left: 0 },
|
||||||
|
grid: { strokeDasharray: '3 3', vertical: false },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const TOOLTIP_CONTENT_STYLE = {
|
||||||
|
background: CHART_THEME.tooltipBg,
|
||||||
|
border: `1px solid ${CHART_THEME.tooltipBorder}`,
|
||||||
|
borderRadius: 6,
|
||||||
|
color: CHART_THEME.tooltipText,
|
||||||
|
fontSize: 12,
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user