ui-v2: match tile / chart designs (#2633)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user