Add untracked chart and schema files

This commit is contained in:
2026-03-13 00:11:59 -04:00
parent 8a8c4f7177
commit 01199d489a
16 changed files with 1794 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
import { cn } from '@/lib/utils';
type ChartContainerProps = {
children: React.ReactNode;
height?: number;
loading?: boolean;
error?: string | null;
className?: string;
};
export function ChartContainer({
children,
height = 400,
loading = false,
error = null,
className
}: ChartContainerProps) {
return (
<div
className={cn(
'relative w-full rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)]',
className
)}
style={{ height: `${height}px` }}
>
{loading && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-sm text-[color:var(--terminal-muted)]">Loading chart...</div>
</div>
)}
{error && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-sm text-[color:var(--danger)]">{error}</div>
</div>
)}
{!loading && !error && (
<div className="h-full w-full p-4">
{children}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,76 @@
import { Download } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { ChartType, TimeRange } from '@/lib/types';
import { Button } from '@/components/ui/button';
type ChartToolbarProps = {
chartType: ChartType;
timeRange: TimeRange;
onChartTypeChange: (type: ChartType) => void;
onTimeRangeChange: (range: TimeRange) => void;
onExport: () => void;
};
const TIME_RANGES: TimeRange[] = ['1W', '1M', '3M', '1Y', '3Y', '5Y', '10Y', '20Y'];
const CHART_TYPES: { value: ChartType; label: string }[] = [
{ value: 'line', label: 'Line' },
{ value: 'combination', label: 'Compare' }
];
export function ChartToolbar({
chartType,
timeRange,
onChartTypeChange,
onTimeRangeChange,
onExport
}: ChartToolbarProps) {
return (
<div className="mb-4 flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-wrap gap-1.5">
{TIME_RANGES.map((range) => (
<button
key={range}
type="button"
onClick={() => onTimeRangeChange(range)}
className={cn(
'min-h-9 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors',
timeRange === range
? 'bg-[color:var(--accent)] text-[#16181c]'
: 'bg-[color:var(--panel-soft)] text-[color:var(--terminal-muted)] hover:bg-[color:var(--panel-bright)] hover:text-[color:var(--terminal-bright)]'
)}
>
{range}
</button>
))}
</div>
<div className="flex items-center gap-2">
<div className="flex gap-1">
{CHART_TYPES.map((type) => (
<button
key={type.value}
type="button"
onClick={() => onChartTypeChange(type.value)}
className={cn(
'rounded-lg px-3 py-1.5 text-xs font-medium transition-colors',
chartType === type.value
? 'bg-[color:var(--accent)] text-[#16181c]'
: 'bg-[color:var(--panel-soft)] text-[color:var(--terminal-muted)] hover:bg-[color:var(--panel-bright)] hover:text-[color:var(--terminal-bright)]'
)}
>
{type.label}
</button>
))}
</div>
<Button
variant="ghost"
onClick={onExport}
className="min-h-9 gap-1.5 px-2.5 py-1.5 text-xs"
>
<Download className="h-3.5 w-3.5" />
Export
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,79 @@
import type { TooltipContentProps, TooltipPayloadEntry } from 'recharts';
import { formatCurrency, formatCompactCurrency } from '@/lib/format';
import { getChartColors } from '../utils/chart-colors';
import { isOHLCVData } from '../utils/chart-data-transformers';
type ChartTooltipProps = TooltipContentProps & {
formatters?: {
price?: (value: number) => string;
date?: (value: string) => string;
volume?: (value: number) => string;
};
};
export function ChartTooltip(props: ChartTooltipProps) {
const { active, payload, label, formatters } = props;
if (!active || !payload || payload.length === 0) return null;
const colors = getChartColors();
const formatDate = formatters?.date || ((date: string) => new Date(date).toLocaleDateString());
const formatPrice = formatters?.price || formatCurrency;
const formatVolume = formatters?.volume || formatCompactCurrency;
const data = payload[0].payload;
return (
<div
className="min-w-[180px] rounded-xl border p-3 backdrop-blur-sm"
style={{
backgroundColor: colors.tooltipBg,
borderColor: colors.tooltipBorder
}}
>
<div className="mb-2 text-xs uppercase tracking-wider" style={{ color: colors.muted }}>
{formatDate(label as string)}
</div>
{isOHLCVData(data) ? (
<div className="space-y-1.5">
<TooltipRow label="Open" value={formatPrice(data.open)} color={colors.text} />
<TooltipRow label="High" value={formatPrice(data.high)} color={colors.positive} />
<TooltipRow label="Low" value={formatPrice(data.low)} color={colors.negative} />
<TooltipRow label="Close" value={formatPrice(data.close)} color={colors.text} />
<div className="mt-2 border-t pt-2" style={{ borderColor: colors.tooltipBorder }}>
<TooltipRow label="Volume" value={formatVolume(data.volume)} color={colors.muted} />
</div>
</div>
) : (
<div className="space-y-1.5">
{payload.map((entry: TooltipPayloadEntry, index: number) => (
<TooltipRow
key={index}
label={String(entry.name || 'Value')}
value={formatPrice(entry.value as number)}
color={entry.color || colors.text}
/>
))}
</div>
)}
</div>
);
}
type TooltipRowProps = {
label: string;
value: string;
color: string;
};
function TooltipRow({ label, value, color }: TooltipRowProps) {
return (
<div className="flex items-center justify-between gap-4 text-xs">
<span style={{ color: color }}>{label}:</span>
<span className="font-mono font-medium" style={{ color: color }}>
{value}
</span>
</div>
);
}

View File

@@ -0,0 +1,58 @@
import { BarChart, Bar, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
import type { OHLCVDataPoint } from '@/lib/types';
import { getChartColors } from '../utils/chart-colors';
import { formatCompactCurrency } from '@/lib/format';
type VolumeIndicatorProps = {
data: OHLCVDataPoint[];
height?: number;
formatters?: {
volume?: (value: number) => string;
};
};
export function VolumeIndicator({
data,
height = 80,
formatters
}: VolumeIndicatorProps) {
const colors = getChartColors();
if (data.length === 0) return null;
const formatVolume = formatters?.volume || formatCompactCurrency;
return (
<div style={{ height: `${height}px`, width: '100%' }} className="mt-2">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data}>
<YAxis
orientation="right"
tickFormatter={formatVolume}
stroke={colors.muted}
fontSize={10}
width={60}
/>
<Tooltip
formatter={(value) => [
formatVolume(typeof value === 'number' ? value : Number(value ?? 0)),
'Volume'
]}
labelFormatter={(label) => `Date: ${label}`}
contentStyle={{
backgroundColor: colors.tooltipBg,
border: `1px solid ${colors.tooltipBorder}`,
borderRadius: '0.75rem',
fontSize: '0.75rem'
}}
/>
<Bar
dataKey="volume"
fill={colors.volume}
opacity={0.3}
isAnimationActive={data.length <= 500}
/>
</BarChart>
</ResponsiveContainer>
</div>
);
}