improve(dashboard): stacked area chart and better dashed stroke (minor fix to bar chart)

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-11-26 21:27:20 +01:00
parent 955832c59a
commit 82d438164e
4 changed files with 119 additions and 182 deletions

View File

@@ -2,8 +2,8 @@
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
import { api } from '@/trpc/client';
import type { IChartData } from '@/trpc/client';
import { api } from '@/trpc/client';
import { cn } from '@/utils/cn';
import { getChartColor } from '@/utils/theme';
import { isSameDay, isSameHour, isSameMonth } from 'date-fns';
@@ -14,7 +14,6 @@ import {
CartesianGrid,
ComposedChart,
Legend,
Line,
ReferenceLine,
ResponsiveContainer,
Tooltip,
@@ -23,6 +22,7 @@ import {
} from 'recharts';
import { useXAxisProps, useYAxisProps } from '../common/axis';
import { SolidToDashedGradient } from '../common/linear-gradient';
import { ReportChartTooltip } from '../common/report-chart-tooltip';
import { ReportTable } from '../common/report-table';
import { SerieIcon } from '../common/serie-icon';
@@ -47,7 +47,6 @@ export function Chart({ data }: Props) {
isEditMode,
options: { hideXAxis, hideYAxis },
} = useReportChartContext();
const dataLength = data.series[0]?.data?.length || 0;
const references = api.reference.getChartReferences.useQuery(
{
projectId,
@@ -69,20 +68,6 @@ export function Chart({ data }: Props) {
const lastIntervalPercent =
((rechartData.length - 2) * 100) / (rechartData.length - 1);
const gradientTwoColors = (
id: string,
col1: string,
col2: string,
percentChange: number,
) => (
<linearGradient id={id} x1="0" y1="0" x2="100%" y2="0">
<stop offset="0%" stopColor={col1} />
<stop offset={`${percentChange}%`} stopColor={col1} />
<stop offset={`${percentChange}%`} stopColor={`${col2}`} />
<stop offset="100%" stopColor={col2} />
</linearGradient>
);
const lastSerieDataItem = last(series[0]?.data || [])?.date || new Date();
const useDashedLastLine = (() => {
if (interval === 'hour') {
@@ -102,7 +87,7 @@ export function Chart({ data }: Props) {
const CustomLegend = useCallback(() => {
return (
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1">
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1 text-xs mt-4 -mb-2">
{series.map((serie) => (
<div
className="flex items-center gap-1"
@@ -128,7 +113,6 @@ export function Chart({ data }: Props) {
interval,
});
const isAreaStyle = series.length === 1;
return (
<>
<div className={cn('h-full w-full', isEditMode && 'card p-4')}>
@@ -157,104 +141,56 @@ export function Chart({ data }: Props) {
))}
<YAxis {...yAxisProps} />
<XAxis {...xAxisProps} />
{series.length > 1 && (
<Legend
wrapperStyle={{ fontSize: '10px' }}
content={<CustomLegend />}
/>
)}
<Legend content={<CustomLegend />} />
<Tooltip content={<ReportChartTooltip />} />
{series.map((serie) => {
const color = getChartColor(serie.index);
return (
<React.Fragment key={serie.id}>
<defs>
{isAreaStyle && (
<linearGradient
id={`color${color}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="0%" stopColor={color} stopOpacity={0.8} />
<stop
offset="100%"
stopColor={color}
stopOpacity={0.1}
/>
</linearGradient>
)}
{gradientTwoColors(
`hideAllButLastInterval_${serie.id}`,
'rgba(0,0,0,0)',
color,
lastIntervalPercent,
)}
{gradientTwoColors(
`hideJustLastInterval_${serie.id}`,
color,
'rgba(0,0,0,0)',
lastIntervalPercent,
<linearGradient
id={`color${color}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="0%" stopColor={color} stopOpacity={0.8} />
<stop
offset={'100%'}
stopColor={color}
stopOpacity={0.1}
/>
</linearGradient>
{useDashedLastLine && (
<SolidToDashedGradient
percentage={lastIntervalPercent}
baseColor={color}
id={`stroke${color}`}
/>
)}
</defs>
<Line
dot={isAreaStyle && dataLength <= 8}
<Area
stackId="1"
type={lineType}
name={serie.id}
isAnimationActive={false}
strokeWidth={2}
dataKey={`${serie.id}:count`}
stroke={useDashedLastLine ? 'transparent' : color}
// Use for legend
fill={color}
stroke={useDashedLastLine ? `url(#stroke${color})` : color}
fill={`url(#color${color})`}
isAnimationActive={false}
fillOpacity={0.7}
/>
{isAreaStyle && (
<Area
dot={false}
name={`${serie.id}:area:noTooltip`}
dataKey={`${serie.id}:count`}
fill={`url(#color${color})`}
type={lineType}
isAnimationActive={false}
fillOpacity={0.1}
/>
)}
{useDashedLastLine && (
<>
<Line
dot={false}
type={lineType}
name={`${serie.id}:dashed:noTooltip`}
isAnimationActive={false}
strokeWidth={2}
dataKey={`${serie.id}:count`}
stroke={`url('#hideAllButLastInterval_${serie.id}')`}
strokeDasharray="4 2"
strokeOpacity={0.7}
/>
<Line
dot={false}
type={lineType}
name={`${serie.id}:solid:noTooltip`}
isAnimationActive={false}
strokeWidth={2}
dataKey={`${serie.id}:count`}
stroke={`url('#hideJustLastInterval_${serie.id}')`}
/>
</>
)}
{previous && (
<Line
<Area
stackId="2"
type={lineType}
name={`${serie.id}:prev`}
isAnimationActive
dot={false}
strokeOpacity={0.3}
dataKey={`${serie.id}:prev:count`}
stroke={color}
// Use for legend
fill={color}
fillOpacity={0.3}
strokeOpacity={0.3}
isAnimationActive={false}
/>
)}
</React.Fragment>

View File

@@ -0,0 +1,69 @@
interface GradientProps {
percentage: number;
baseColor: string;
id: string;
}
export const SolidToDashedGradient: React.FC<GradientProps> = ({
percentage,
baseColor,
id,
}) => {
const stops = generateSolidToDashedLinearGradient(percentage, baseColor);
return (
<linearGradient id={id} x1="0" y1="0" x2="1" y2="0">
{stops.map((stop, index) => (
<stop
key={index as any}
offset={stop.offset}
stopColor={stop.color}
stopOpacity={stop.opacity}
/>
))}
</linearGradient>
);
};
// Helper function moved to the same file
const generateSolidToDashedLinearGradient = (
percentage: number,
baseColor: string,
) => {
// Start with solid baseColor up to percentage
const stops = [
{ offset: '0%', color: baseColor, opacity: 1 },
{ offset: `${percentage}%`, color: baseColor, opacity: 1 },
];
// Calculate the remaining space for dashes
const remainingSpace = 100 - percentage;
const dashWidth = remainingSpace / 20; // 10 dashes = 20 segments (dash + gap)
// Generate 10 dashes
for (let i = 0; i < 10; i++) {
const startOffset = percentage + i * 2 * dashWidth;
// Add dash and gap with sharp transitions
stops.push(
// Start of dash
{ offset: `${startOffset}%`, color: baseColor, opacity: 1 },
// End of dash
{ offset: `${startOffset + dashWidth}%`, color: baseColor, opacity: 1 },
// Start of gap (immediate transition)
{
offset: `${startOffset + dashWidth}%`,
color: 'transparent',
opacity: 0,
},
// End of gap
{
offset: `${startOffset + 2 * dashWidth}%`,
color: 'transparent',
opacity: 0,
},
);
}
return stops;
};

View File

@@ -60,7 +60,7 @@ export function Chart({ data }: Props) {
<>
<div className={cn('h-full w-full', isEditMode && 'card p-4')}>
<ResponsiveContainer>
<BarChart data={rechartData} barCategoryGap={10}>
<BarChart data={rechartData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
@@ -68,30 +68,10 @@ export function Chart({ data }: Props) {
/>
<Tooltip content={<ReportChartTooltip />} cursor={<BarHover />} />
<YAxis {...yAxisProps} />
<XAxis {...xAxisProps} />
<XAxis {...xAxisProps} scale={'auto'} type="category" />
{series.map((serie) => {
return (
<React.Fragment key={serie.id}>
<defs>
<linearGradient
id="colorGradient"
x1="0"
y1="1"
x2="0"
y2="0"
>
<stop
offset="0%"
stopColor={getChartColor(serie.index)}
stopOpacity={0.7}
/>
<stop
offset="100%"
stopColor={getChartColor(serie.index)}
stopOpacity={1}
/>
</linearGradient>
</defs>
{previous && (
<Bar
key={`${serie.id}:prev`}
@@ -100,17 +80,17 @@ export function Chart({ data }: Props) {
fill={getChartColor(serie.index)}
fillOpacity={0.1}
radius={3}
barSize={20} // Adjust the bar width here
barSize={5} // Adjust the bar width here
/>
)}
<Bar
key={serie.id}
name={serie.id}
dataKey={`${serie.id}:count`}
fill="url(#colorGradient)"
fill={getChartColor(serie.index)}
radius={3}
fillOpacity={1}
barSize={20} // Adjust the bar width here
barSize={5} // Adjust the bar width here
/>
</React.Fragment>
);

View File

@@ -23,6 +23,7 @@ import {
} from 'recharts';
import { useXAxisProps, useYAxisProps } from '../common/axis';
import { SolidToDashedGradient } from '../common/linear-gradient';
import { ReportChartTooltip } from '../common/report-chart-tooltip';
import { ReportTable } from '../common/report-table';
import { SerieIcon } from '../common/serie-icon';
@@ -69,20 +70,6 @@ export function Chart({ data }: Props) {
const lastIntervalPercent =
((rechartData.length - 2) * 100) / (rechartData.length - 1);
const gradientTwoColors = (
id: string,
col1: string,
col2: string,
percentChange: number,
) => (
<linearGradient id={id} x1="0" y1="0" x2="100%" y2="0">
<stop offset="0%" stopColor={col1} />
<stop offset={`${percentChange}%`} stopColor={col1} />
<stop offset={`${percentChange}%`} stopColor={`${col2}`} />
<stop offset="100%" stopColor={col2} />
</linearGradient>
);
const lastSerieDataItem = last(series[0]?.data || [])?.date || new Date();
const useDashedLastLine = (() => {
if (interval === 'hour') {
@@ -102,7 +89,7 @@ export function Chart({ data }: Props) {
const CustomLegend = useCallback(() => {
return (
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1">
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1 text-xs mt-4 -mb-2">
{series.map((serie) => (
<div
className="flex items-center gap-1"
@@ -157,12 +144,7 @@ export function Chart({ data }: Props) {
domain={maxDomain ? [0, maxDomain] : undefined}
/>
<XAxis {...xAxisProps} />
{series.length > 1 && (
<Legend
wrapperStyle={{ fontSize: '10px' }}
content={<CustomLegend />}
/>
)}
{series.length > 1 && <Legend content={<CustomLegend />} />}
<Tooltip content={<ReportChartTooltip />} />
{series.map((serie) => {
const color = getChartColor(serie.index);
@@ -185,17 +167,12 @@ export function Chart({ data }: Props) {
/>
</linearGradient>
)}
{gradientTwoColors(
`hideAllButLastInterval_${serie.id}`,
'rgba(0,0,0,0)',
color,
lastIntervalPercent,
)}
{gradientTwoColors(
`hideJustLastInterval_${serie.id}`,
color,
'rgba(0,0,0,0)',
lastIntervalPercent,
{useDashedLastLine && (
<SolidToDashedGradient
percentage={lastIntervalPercent}
baseColor={color}
id={`stroke${color}`}
/>
)}
</defs>
<Line
@@ -205,7 +182,7 @@ export function Chart({ data }: Props) {
isAnimationActive={false}
strokeWidth={2}
dataKey={`${serie.id}:count`}
stroke={useDashedLastLine ? 'transparent' : color}
stroke={useDashedLastLine ? `url(#stroke${color})` : color}
// Use for legend
fill={color}
/>
@@ -221,31 +198,6 @@ export function Chart({ data }: Props) {
fillOpacity={0.1}
/>
)}
{useDashedLastLine && (
<>
<Line
dot={false}
type={lineType}
name={`${serie.id}:dashed:noTooltip`}
isAnimationActive={false}
strokeWidth={2}
dataKey={`${serie.id}:count`}
stroke={`url('#hideAllButLastInterval_${serie.id}')`}
strokeDasharray="2 4"
strokeLinecap="round"
strokeOpacity={0.7}
/>
<Line
dot={false}
type={lineType}
name={`${serie.id}:solid:noTooltip`}
isAnimationActive={false}
strokeWidth={2}
dataKey={`${serie.id}:count`}
stroke={`url('#hideJustLastInterval_${serie.id}')`}
/>
</>
)}
{previous && (
<Line
type={lineType}