ui-v2: match tile / chart designs (#2633)

This commit is contained in:
Zane
2025-05-22 09:17:43 -07:00
committed by GitHub
parent 8cf8bbded2
commit f56b30fdba
5 changed files with 445 additions and 351 deletions
+78 -36
View File
@@ -1,7 +1,12 @@
import React from 'react';
import { useTimelineStyles } from '../../hooks/useTimelineStyles.ts';
import { ChartConfig, ChartContainer } from "@/components/ui/chart.tsx";
import { BarChart, Bar, LineChart, Line, ResponsiveContainer, Tooltip } from 'recharts';
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { BarChart, Bar, LineChart, Line, CartesianGrid, XAxis, ResponsiveContainer } from 'recharts';
interface ChartTileProps {
title: string;
@@ -27,17 +32,14 @@ export default function ChartTile({
// Convert the data array to the format expected by recharts
const chartData = data.map((value, index) => ({
value,
index: `Point ${index + 1}`
point: `P${index + 1}`
}));
// Chart configuration
// Chart configuration with proper color variables
const chartConfig = {
value: {
label: title,
theme: {
light: variant === 'line' ? '#0B54DE' : '#4CAF50',
dark: variant === 'line' ? '#00CAF7' : '#4CAF50'
}
color: variant === 'line' ? 'var(--chart-2)' : 'var(--chart-1)'
}
} satisfies ChartConfig;
@@ -45,54 +47,94 @@ export default function ChartTile({
<div
className={`
flex flex-col justify-between
w-[320px] h-[380px]
w-[320px] min-h-[380px]
${contentCardStyle}
rounded-[18px]
relative
overflow-hidden
transition-all duration-200
hover:scale-[1.02]
bg-background-default text-text-default
`}
>
{/* Header section with icon */}
<div className="p-4 space-y-4">
<div className="w-6 h-6">
<div className="w-6 h-6 text-text-default">
{icon}
</div>
<div>
<div className="text-gray-600 dark:text-white/40 text-sm mb-1">{title}</div>
<div className="text-gray-900 dark:text-white text-2xl font-semibold">
<div className="text-text-muted text-sm mb-1">{title}</div>
<div className="text-text-default text-2xl font-semibold">
{value}
{trend && <span className="ml-1 text-sm">{trend}</span>}
{trend && <span className="ml-1 text-sm text-text-muted">{trend}</span>}
</div>
</div>
</div>
{/* Chart Container */}
<div className="w-full h-[160px] px-4">
<ChartContainer config={chartConfig}>
{variant === 'line' ? (
<LineChart data={chartData}>
<Line
type="monotone"
dataKey="value"
stroke="var(--color-value)"
strokeWidth={2}
dot={{ fill: 'var(--color-value)', r: 4 }}
/>
<Tooltip />
</LineChart>
) : (
<BarChart data={chartData}>
<Bar
dataKey="value"
fill="var(--color-value)"
radius={[4, 4, 0, 0]}
/>
<Tooltip />
</BarChart>
)}
<div className="w-full h-[200px] px-4 pb-6">
<ChartContainer
config={chartConfig}
className="[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-tooltip-wrapper]:!pointer-events-none"
>
<ResponsiveContainer width="100%" height="100%">
{variant === 'line' ? (
<LineChart data={chartData} margin={{ top: 10, right: 10, bottom: 0, left: -20 }}>
<CartesianGrid vertical={false} className="stroke-border/50" />
<XAxis
dataKey="point"
tickLine={false}
tickMargin={10}
axisLine={false}
height={40}
tick={{ fill: 'var(--text-muted)' }}
/>
<ChartTooltip
content={
<ChartTooltipContent
className="border-border/50 bg-background-default text-text-default min-w-[180px] [&_.flex.flex-1]:gap-4 [&_.flex.flex-1>span]:whitespace-nowrap"
/>
}
/>
<Line
type="monotone"
dataKey="value"
stroke="var(--chart-2)"
strokeWidth={2}
dot={{ fill: 'var(--chart-2)', r: 4 }}
/>
</LineChart>
) : (
<BarChart data={chartData} margin={{ top: 10, right: 10, bottom: 0, left: 10 }}>
<CartesianGrid vertical={false} className="stroke-border/50" />
<XAxis
dataKey="point"
tickLine={false}
tickMargin={10}
axisLine={false}
height={40}
tick={{ fill: 'var(--text-muted)' }}
interval={0}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
indicator="dashed"
className="border-border/50 bg-background-default text-text-default min-w-[180px] [&_.flex.flex-1]:gap-4 [&_.flex.flex-1>span]:whitespace-nowrap"
/>
}
/>
<Bar
dataKey="value"
fill="var(--chart-1)"
radius={4}
maxBarSize={32}
/>
</BarChart>
)}
</ResponsiveContainer>
</ChartContainer>
</div>
</div>
+145 -67
View File
@@ -1,7 +1,7 @@
import React from 'react';
import React, { useState } from 'react';
import { useTimelineStyles } from '../../hooks/useTimelineStyles';
import { ChartConfig, ChartContainer } from "@/components/ui/chart";
import { PieChart, Pie, Cell, Tooltip, Legend } from 'recharts';
import { PieChart, Pie, Cell, Sector, ResponsiveContainer } from 'recharts';
interface PieChartSegment {
value: number;
@@ -16,6 +16,90 @@ interface PieChartTileProps {
date?: Date;
}
// Custom label renderer with connecting lines
const renderCustomizedLabel = ({
cx,
cy,
midAngle,
innerRadius,
outerRadius,
percent,
payload,
fill,
}: any) => {
const RADIAN = Math.PI / 180;
const sin = Math.sin(-RADIAN * midAngle);
const cos = Math.cos(-RADIAN * midAngle);
// Adjust these values to position labels closer to the pie
const labelOffset = 12;
const labelDistance = 18;
// Calculate positions with shorter distances
const mx = cx + (outerRadius + labelOffset) * cos;
const my = cy + (outerRadius + labelOffset) * sin;
const ex = mx + (cos >= 0 ? 1 : -1) * labelDistance;
const ey = my;
// Text anchor based on which side of the pie we're on
const textAnchor = cos >= 0 ? "start" : "end";
// Calculate percentage
const value = (percent * 100).toFixed(0);
// Determine if label should be on top or bottom half for potential y-offset
const isTopHalf = my < cy;
const yOffset = isTopHalf ? -2 : 2;
// Force specific adjustments for "In Progress" label if needed
const isInProgress = payload.name === "In Progress";
const adjustedEx = isInProgress ? ex - 5 : ex;
return (
<g>
{/* Label line - using absolute coordinates for reliability */}
<path
d={`M${cx + outerRadius * cos},${cy + outerRadius * sin}L${mx},${my}L${adjustedEx},${ey}`}
stroke={fill}
strokeWidth={1}
fill="none"
style={{ opacity: 1 }}
/>
{/* Label text with adjusted position */}
<text
x={adjustedEx + (cos >= 0 ? 5 : -5)}
y={ey + yOffset}
textAnchor={textAnchor}
fill="var(--text-default)"
className="text-[10px]"
style={{
pointerEvents: 'none',
}}
>
{payload.name} ({value}%)
</text>
</g>
);
};
// Active shape renderer for hover effect
const renderActiveShape = (props: any) => {
const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill } = props;
return (
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius}
outerRadius={outerRadius + 4}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
cornerRadius={4}
/>
);
};
export default function PieChartTile({
title,
icon,
@@ -23,102 +107,96 @@ export default function PieChartTile({
date
}: PieChartTileProps) {
const { contentCardStyle } = useTimelineStyles(date);
const [activeIndex, setActiveIndex] = useState<number>(0);
// Convert segments to the format expected by recharts
const chartData = segments.map(segment => ({
// Convert segments to the format expected by recharts and assign chart colors
const chartData = segments.map((segment, index) => ({
name: segment.label,
value: segment.value
value: segment.value,
chartColor: `var(--chart-${index + 1})` // Use chart-1, chart-2, chart-3, etc.
}));
// Create chart configuration with theme colors
const chartConfig = segments.reduce((config, segment) => {
config[segment.label] = {
label: segment.label,
color: segment.color
};
return config;
}, {} as ChartConfig);
// Create chart configuration using the chart color variables
const chartConfig = {
[segments[0].label.toLowerCase()]: {
label: segments[0].label,
color: 'var(--chart-1)'
},
[segments[1].label.toLowerCase()]: {
label: segments[1].label,
color: 'var(--chart-2)'
},
[segments[2].label.toLowerCase()]: {
label: segments[2].label,
color: 'var(--chart-3)'
}
} satisfies ChartConfig;
// Custom tooltip formatter
const tooltipFormatter = (value: number, name: string) => {
const total = segments.reduce((sum, segment) => sum + segment.value, 0);
const percentage = ((value / total) * 100).toFixed(1);
return [`${percentage}%`, name];
const onPieEnter = (_: any, index: number) => {
setActiveIndex(index);
};
return (
<div
className={`
flex flex-col
w-[320px] h-[380px]
w-[320px] min-h-[380px]
${contentCardStyle}
rounded-[18px]
relative
overflow-hidden
transition-all duration-200
hover:scale-[1.02]
bg-background-default text-text-default
`}
>
{/* Header */}
<div className="p-4">
<div className="w-6 h-6 mb-4">
<div className="w-6 h-6 mb-4 text-text-default">
{icon}
</div>
<div className="text-gray-600 dark:text-white/40 text-sm">
<div className="text-text-muted text-sm">
{title}
</div>
</div>
{/* Pie Chart */}
<div className="flex-1 flex flex-col items-center">
<div className="w-full h-[200px]">
<div className="flex-1 flex items-center justify-center p-4">
<div style={{ width: '100%', height: '260px', position: 'relative' }}>
<ChartContainer config={chartConfig}>
<PieChart>
<Pie
data={chartData}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
innerRadius={0}
outerRadius={70}
paddingAngle={2}
>
{segments.map((segment, index) => (
<Cell
key={`cell-${index}`}
fill={segment.color}
className="transition-all duration-200 hover:opacity-90"
/>
))}
</Pie>
<Tooltip formatter={tooltipFormatter} />
</PieChart>
<ResponsiveContainer>
<PieChart margin={{ top: 30, right: 40, bottom: 10, left: 40 }}>
<Pie
activeIndex={activeIndex}
activeShape={renderActiveShape}
data={chartData}
cx="50%"
cy="50%"
innerRadius={45}
outerRadius={65}
paddingAngle={5}
dataKey="value"
onMouseEnter={onPieEnter}
cornerRadius={4}
label={renderCustomizedLabel}
labelLine={false}
startAngle={90}
endAngle={-270}
isAnimationActive={false}
>
{chartData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.chartColor}
stroke="var(--background-default)"
strokeWidth={2}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</ChartContainer>
</div>
{/* Legend */}
<div className="mt-2 px-4 w-full space-y-2">
{segments.map((segment, index) => {
const percentage = ((segment.value / segments.reduce((sum, s) => sum + s.value, 0)) * 100).toFixed(1);
return (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center">
<div
className="w-3 h-3 rounded-full mr-2"
style={{ backgroundColor: segment.color }}
/>
<span className="text-sm text-gray-600 dark:text-white/60">
{segment.label}
</span>
</div>
<span className="text-sm font-medium text-gray-900 dark:text-white">
{percentage}%
</span>
</div>
);
})}
</div>
</div>
</div>
);