fix(charts): mobile ui, and desktop tooltips
This commit is contained in:
parent
14a9343c1d
commit
818a49e799
|
|
@ -25,7 +25,7 @@ import {
|
|||
} from '@/hooks/use-chart-views';
|
||||
import { Account } from '@/types/account';
|
||||
import { format, subMonths } from 'date-fns';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Bar, BarChart, XAxis } from 'recharts';
|
||||
|
||||
interface BalanceDataPoint {
|
||||
|
|
@ -189,6 +189,17 @@ export function AccountBalanceChart({
|
|||
return formatCurrency(value, account.currency_code);
|
||||
};
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const minBarWidth = 50;
|
||||
const minChartWidth = chartData.length * minBarWidth;
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollLeft =
|
||||
scrollContainerRef.current.scrollWidth;
|
||||
}
|
||||
}, [chartData]);
|
||||
|
||||
if (initialLoading || isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
|
|
@ -264,33 +275,42 @@ export function AccountBalanceChart({
|
|||
</CardHeader>
|
||||
<CardContent className="relative">
|
||||
{chartViews.currentView === 'stacked' && (
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-[300px] w-full"
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="h-[300px] w-full overflow-x-auto"
|
||||
>
|
||||
<BarChart accessibilityLayer data={chartData.slice(1)}>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={formatXAxisLabel}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
hideLabel
|
||||
valueFormatter={valueFormatter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="var(--color-chart-2)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-full w-full"
|
||||
style={{ minWidth: `${minChartWidth}px` }}
|
||||
>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData.slice(1)}
|
||||
>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={formatXAxisLabel}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
hideLabel
|
||||
valueFormatter={valueFormatter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="var(--color-chart-2)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)}
|
||||
{chartViews.currentView === 'mom' && (
|
||||
<MoMChart
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { ChartViewType } from '@/hooks/use-chart-views';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { BarChart3, Percent, TrendingUp } from 'lucide-react';
|
||||
|
|
@ -12,11 +17,19 @@ interface ChartViewToggleProps {
|
|||
|
||||
const viewConfig: Record<
|
||||
ChartViewType,
|
||||
{ icon: React.ElementType; label: string }
|
||||
{ icon: React.ElementType; label: string; tooltip: string }
|
||||
> = {
|
||||
stacked: { icon: BarChart3, label: 'Accounts' },
|
||||
mom: { icon: TrendingUp, label: 'MoM' },
|
||||
mom_percent: { icon: Percent, label: 'MoM%' },
|
||||
stacked: {
|
||||
icon: BarChart3,
|
||||
label: 'Aggregate',
|
||||
tooltip: 'Monthly balance',
|
||||
},
|
||||
mom: { icon: TrendingUp, label: 'MoM', tooltip: 'Month over month change' },
|
||||
mom_percent: {
|
||||
icon: Percent,
|
||||
label: 'MoM%',
|
||||
tooltip: 'Month over month change (%)',
|
||||
},
|
||||
};
|
||||
|
||||
export function ChartViewToggle({
|
||||
|
|
@ -40,15 +53,20 @@ export function ChartViewToggle({
|
|||
const config = viewConfig[view];
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<ToggleGroupItem
|
||||
key={view}
|
||||
value={view}
|
||||
aria-label={config.label}
|
||||
className="gap-1.5 px-2"
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
<span className="hidden sm:inline">{config.label}</span>
|
||||
</ToggleGroupItem>
|
||||
<Tooltip key={view}>
|
||||
<TooltipTrigger asChild>
|
||||
<ToggleGroupItem
|
||||
value={view}
|
||||
aria-label={config.label}
|
||||
className="px-2"
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
</ToggleGroupItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{config.tooltip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</ToggleGroup>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { AmountDisplay } from '@/components/ui/amount-display';
|
|||
import { ChartConfig, ChartContainer } from '@/components/ui/chart';
|
||||
import { ChangeDataPoint } from '@/lib/chart-calculations';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
|
|
@ -17,6 +18,7 @@ interface MoMChartProps {
|
|||
currencyCode: string;
|
||||
xAxisFormatter?: (value: string) => string;
|
||||
className?: string;
|
||||
minBarWidth?: number;
|
||||
}
|
||||
|
||||
const chartConfig: ChartConfig = {
|
||||
|
|
@ -77,56 +79,76 @@ export function MoMChart({
|
|||
currencyCode,
|
||||
xAxisFormatter,
|
||||
className,
|
||||
minBarWidth = 50,
|
||||
}: MoMChartProps) {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const minChartWidth = data.length * minBarWidth;
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollLeft =
|
||||
scrollContainerRef.current.scrollWidth;
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const chartData = data.map((point) => ({
|
||||
...point,
|
||||
displayValue: point.change,
|
||||
}));
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className={className}>
|
||||
<BarChart accessibilityLayer data={chartData}>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={xAxisFormatter}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
}).format(value / 100);
|
||||
}}
|
||||
width={50}
|
||||
/>
|
||||
<ReferenceLine
|
||||
y={0}
|
||||
stroke="var(--color-border)"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<Tooltip
|
||||
content={<CustomTooltip currencyCode={currencyCode} />}
|
||||
cursor={{ fill: 'var(--color-muted)', opacity: 0.3 }}
|
||||
/>
|
||||
<Bar dataKey="displayValue" radius={[4, 4, 0, 0]}>
|
||||
{chartData.map((entry, index) => {
|
||||
const value = entry.displayValue;
|
||||
let fill = 'var(--color-muted)';
|
||||
if (value !== null) {
|
||||
fill =
|
||||
value >= 0
|
||||
? 'var(--color-chart-2)'
|
||||
: 'var(--color-chart-5)';
|
||||
}
|
||||
return <Cell key={`cell-${index}`} fill={fill} />;
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className={cn('overflow-x-auto', className)}
|
||||
>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-full w-full"
|
||||
style={{ minWidth: `${minChartWidth}px` }}
|
||||
>
|
||||
<BarChart accessibilityLayer data={chartData}>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={xAxisFormatter}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
}).format(value / 100);
|
||||
}}
|
||||
width={50}
|
||||
/>
|
||||
<ReferenceLine
|
||||
y={0}
|
||||
stroke="var(--color-border)"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<Tooltip
|
||||
content={<CustomTooltip currencyCode={currencyCode} />}
|
||||
cursor={{ fill: 'var(--color-muted)', opacity: 0.3 }}
|
||||
/>
|
||||
<Bar dataKey="displayValue" radius={[4, 4, 0, 0]}>
|
||||
{chartData.map((entry, index) => {
|
||||
const value = entry.displayValue;
|
||||
let fill = 'var(--color-muted)';
|
||||
if (value !== null) {
|
||||
fill =
|
||||
value >= 0
|
||||
? 'var(--color-chart-2)'
|
||||
: 'var(--color-chart-5)';
|
||||
}
|
||||
return <Cell key={`cell-${index}`} fill={fill} />;
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { ChartConfig, ChartContainer } from '@/components/ui/chart';
|
||||
import { formatPercentValue, PercentDataPoint } from '@/lib/chart-calculations';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
|
|
@ -15,6 +16,7 @@ interface MoMPercentChartProps {
|
|||
data: PercentDataPoint[];
|
||||
xAxisFormatter?: (value: string) => string;
|
||||
className?: string;
|
||||
minBarWidth?: number;
|
||||
}
|
||||
|
||||
const chartConfig: ChartConfig = {
|
||||
|
|
@ -68,51 +70,73 @@ export function MoMPercentChart({
|
|||
data,
|
||||
xAxisFormatter,
|
||||
className,
|
||||
minBarWidth = 50,
|
||||
}: MoMPercentChartProps) {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const minChartWidth = data.length * minBarWidth;
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollLeft =
|
||||
scrollContainerRef.current.scrollWidth;
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const chartData = data.map((point) => ({
|
||||
...point,
|
||||
displayValue: point.percent,
|
||||
}));
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className={className}>
|
||||
<BarChart accessibilityLayer data={chartData}>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={xAxisFormatter}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value: number) => `${value.toFixed(0)}%`}
|
||||
width={50}
|
||||
/>
|
||||
<ReferenceLine
|
||||
y={0}
|
||||
stroke="var(--color-border)"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<Tooltip
|
||||
content={<CustomTooltip />}
|
||||
cursor={{ fill: 'var(--color-muted)', opacity: 0.3 }}
|
||||
/>
|
||||
<Bar dataKey="displayValue" radius={[4, 4, 0, 0]}>
|
||||
{chartData.map((entry, index) => {
|
||||
const value = entry.displayValue;
|
||||
let fill = 'var(--color-muted)';
|
||||
if (value !== null) {
|
||||
fill =
|
||||
value >= 0
|
||||
? 'var(--color-chart-2)'
|
||||
: 'var(--color-chart-5)';
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className={cn('overflow-x-auto', className)}
|
||||
>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-full w-full"
|
||||
style={{ minWidth: `${minChartWidth}px` }}
|
||||
>
|
||||
<BarChart accessibilityLayer data={chartData}>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={xAxisFormatter}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value: number) =>
|
||||
`${value.toFixed(0)}%`
|
||||
}
|
||||
return <Cell key={`cell-${index}`} fill={fill} />;
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
width={50}
|
||||
/>
|
||||
<ReferenceLine
|
||||
y={0}
|
||||
stroke="var(--color-border)"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<Tooltip
|
||||
content={<CustomTooltip />}
|
||||
cursor={{ fill: 'var(--color-muted)', opacity: 0.3 }}
|
||||
/>
|
||||
<Bar dataKey="displayValue" radius={[4, 4, 0, 0]}>
|
||||
{chartData.map((entry, index) => {
|
||||
const value = entry.displayValue;
|
||||
let fill = 'var(--color-muted)';
|
||||
if (value !== null) {
|
||||
fill =
|
||||
value >= 0
|
||||
? 'var(--color-chart-2)'
|
||||
: 'var(--color-chart-5)';
|
||||
}
|
||||
return <Cell key={`cell-${index}`} fill={fill} />;
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue