diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 3cfdd8ba..d6158564 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { GoShield, GoShieldCheck } from 'react-icons/go'; import { Button } from '@/components/ui/button'; import { IconSwitch } from '@/components/ui/icon-switch'; +import { PaletteSelector } from '@/components/ui/palette-preview'; import Header from '@/components/layout/header/Header'; import { AdvancedRpcSettings } from '@/modals/settings/custom-rpc-settings'; import { VaultIdentity } from '@/features/autovault/components/vault-identity'; @@ -12,11 +13,13 @@ import { useModal } from '@/hooks/useModal'; import { useTrustedVaults } from '@/stores/useTrustedVaults'; import { useAppSettings } from '@/stores/useAppSettings'; import { useMarketPreferences } from '@/stores/useMarketPreferences'; +import { useChartPalette } from '@/stores/useChartPalette'; export default function SettingsPage() { const { usePermit2, setUsePermit2, showUnwhitelistedMarkets, setShowUnwhitelistedMarkets, isAprDisplay, setIsAprDisplay } = useAppSettings(); const { includeUnknownTokens, setIncludeUnknownTokens, showUnknownOracle, setShowUnknownOracle } = useMarketPreferences(); + const { palette: selectedPalette, setPalette } = useChartPalette(); const { vaults: userTrustedVaults } = useTrustedVaults(); @@ -101,6 +104,27 @@ export default function SettingsPage() { +
+

Chart Appearance

+ +
+
+
+

Color Palette

+

+ Choose a color scheme for charts and graphs. Changes apply to all chart visualizations across the application. +

+
+ {mounted && ( + + )} +
+
+
+

Filter Settings

diff --git a/src/components/ui/palette-preview.tsx b/src/components/ui/palette-preview.tsx new file mode 100644 index 00000000..f6d4e76f --- /dev/null +++ b/src/components/ui/palette-preview.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { cn } from '@/utils/components'; +import { CHART_PALETTES, PALETTE_META } from '@/constants/chartColors'; +import { CHART_PALETTE_NAMES, type ChartPaletteName } from '@/stores/useChartPalette'; + +type PaletteOptionProps = { + paletteId: ChartPaletteName; + selected: boolean; + onSelect: () => void; +}; + +function ColorSwatches({ paletteId }: { paletteId: ChartPaletteName }) { + const palette = CHART_PALETTES[paletteId]; + const colors = [ + palette.supply.stroke, + palette.borrow.stroke, + palette.apyAtTarget.stroke, + palette.risk.stroke, + ]; + + return ( +
+ {colors.map((color, index) => ( +
+ ))} +
+ ); +} + +export function PaletteOption({ paletteId, selected, onSelect }: PaletteOptionProps) { + const meta = PALETTE_META[paletteId]; + if (!meta) return null; + + return ( + + ); +} + +export function PaletteSelector({ + selectedPalette, + onSelect, +}: { + selectedPalette: ChartPaletteName; + onSelect: (palette: ChartPaletteName) => void; +}) { + return ( +
+ {CHART_PALETTE_NAMES.map((id) => ( + onSelect(id)} + /> + ))} +
+ ); +} diff --git a/src/constants/chartColors.ts b/src/constants/chartColors.ts index 3d0bcc54..52318467 100644 --- a/src/constants/chartColors.ts +++ b/src/constants/chartColors.ts @@ -1,50 +1,138 @@ +import { useChartPalette, type ChartPaletteName } from '@/stores/useChartPalette'; + export const MONARCH_PRIMARY = '#f45f2d'; -export const CHART_COLORS = { - supply: { - stroke: '#3B82F6', - gradient: { - start: '#3B82F6', - startOpacity: 0.3, - endOpacity: 0, - }, - }, - borrow: { - stroke: '#10B981', - gradient: { - start: '#10B981', - startOpacity: 0.3, - endOpacity: 0, - }, - }, - apyAtTarget: { - stroke: '#F59E0B', - gradient: { - start: '#F59E0B', - startOpacity: 0.3, - endOpacity: 0, - }, - }, -} as const; +type ChartColorConfig = { + stroke: string; + gradient: { + start: string; + startOpacity: number; + endOpacity: number; + }; +}; + +type ChartPaletteConfig = { + supply: ChartColorConfig; + borrow: ChartColorConfig; + apyAtTarget: ChartColorConfig; + risk: ChartColorConfig; + pie: readonly string[]; +}; -export const PIE_COLORS = [ - '#3B82F6', // Blue - '#10B981', // Green - '#F59E0B', // Amber - '#8B5CF6', // Violet - '#EC4899', // Pink - '#06B6D4', // Cyan - '#84CC16', // Lime - '#F97316', // Orange - '#6366F1', // Indigo - '#64748B', // Slate (for "Other") -] as const; - -export const RISK_COLORS = { - stroke: '#EF4444', +const createColorConfig = (color: string): ChartColorConfig => ({ + stroke: color, gradient: { - start: '#EF4444', + start: color, startOpacity: 0.3, endOpacity: 0, }, +}); + +export const CHART_PALETTES: Record = { + // Classic: Industry standard (Tableau 10) + classic: { + supply: createColorConfig('#4E79A7'), // Blue + borrow: createColorConfig('#59A14F'), // Green + apyAtTarget: createColorConfig('#EDC948'), // Yellow + risk: createColorConfig('#E15759'), // Red + pie: [ + '#4E79A7', // Blue + '#59A14F', // Green + '#EDC948', // Yellow + '#B07AA1', // Purple + '#76B7B2', // Teal + '#FF9DA7', // Pink + '#F28E2B', // Orange + '#9C755F', // Brown + '#BAB0AC', // Gray + '#64748B', // Slate (for "Other") + ], + }, + + // Earth: Warm terracotta and earth tones + earth: { + supply: createColorConfig('#B26333'), // Burnt sienna + borrow: createColorConfig('#89392D'), // Rust/terracotta + apyAtTarget: createColorConfig('#A48A7A'), // Taupe + risk: createColorConfig('#411E1D'), // Dark maroon + pie: [ + '#B26333', // Burnt sienna + '#89392D', // Rust + '#A48A7A', // Taupe + '#411E1D', // Dark maroon + '#EEEBEA', // Light cream + '#8B5A2B', // Saddle brown + '#CD853F', // Peru + '#D2691E', // Chocolate + '#A0522D', // Sienna + '#64748B', // Slate (for "Other") + ], + }, + + // Forest: Sage and olive tones + forest: { + supply: createColorConfig('#223A30'), // Dark forest green + borrow: createColorConfig('#8DA99D'), // Sage green + apyAtTarget: createColorConfig('#727472'), // Medium gray + risk: createColorConfig('#7A7C7B'), // Gray + pie: [ + '#223A30', // Dark forest + '#8DA99D', // Sage + '#727472', // Medium gray + '#7A7C7B', // Gray + '#DFDDDA', // Light cream + '#2F4F4F', // Dark slate gray + '#556B2F', // Dark olive + '#6B8E23', // Olive drab + '#4A5D23', // Army green + '#64748B', // Slate (for "Other") + ], + }, + + // Simple: Pure primary colors + simple: { + supply: createColorConfig('#2563EB'), // Blue-600 + borrow: createColorConfig('#16A34A'), // Green-600 + apyAtTarget: createColorConfig('#EAB308'), // Yellow-500 + risk: createColorConfig('#DC2626'), // Red-600 + pie: [ + '#2563EB', // Blue + '#16A34A', // Green + '#EAB308', // Yellow + '#DC2626', // Red + '#8B5CF6', // Purple + '#EC4899', // Pink + '#F97316', // Orange + '#6B7280', // Gray + '#14B8A6', // Teal + '#64748B', // Slate (for "Other") + ], + }, } as const; + +export const PALETTE_META: Record = { + classic: { name: 'Classic', description: 'Industry standard (Tableau)' }, + earth: { name: 'Earth', description: 'Warm terracotta tones' }, + forest: { name: 'Forest', description: 'Sage and olive tones' }, + simple: { name: 'Simple', description: 'Pure primary colors' }, +}; + +// Backwards compatibility exports +export const CHART_COLORS = CHART_PALETTES.classic; +export const PIE_COLORS = CHART_PALETTES.classic.pie; +export const RISK_COLORS = CHART_PALETTES.classic.risk; + +/** + * Hook to get chart colors based on user's palette preference. + */ +export function useChartColors(): ChartPaletteConfig { + const { palette } = useChartPalette(); + return CHART_PALETTES[palette]; +} + +/** + * Get chart colors for a specific palette (non-reactive). + */ +export function getChartColors(palette: ChartPaletteName): ChartPaletteConfig { + return CHART_PALETTES[palette]; +} diff --git a/src/features/market-detail/components/charts/borrowers-pie-chart.tsx b/src/features/market-detail/components/charts/borrowers-pie-chart.tsx index 6d3f174f..6544df1b 100644 --- a/src/features/market-detail/components/charts/borrowers-pie-chart.tsx +++ b/src/features/market-detail/components/charts/borrowers-pie-chart.tsx @@ -7,7 +7,7 @@ import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recha import { Card } from '@/components/ui/card'; import { Spinner } from '@/components/ui/spinner'; import { TokenIcon } from '@/components/shared/token-icon'; -import { PIE_COLORS } from '@/constants/chartColors'; +import { useChartColors } from '@/constants/chartColors'; import { useVaultRegistry } from '@/contexts/VaultRegistryContext'; import { useAllMarketBorrowers } from '@/hooks/useAllMarketPositions'; import { formatSimple } from '@/utils/balance'; @@ -39,6 +39,7 @@ export function BorrowersPieChart({ chainId, market, oraclePrice }: BorrowersPie const { data: borrowers, isLoading, totalCount } = useAllMarketBorrowers(market.uniqueKey, chainId); const { getVaultByAddress } = useVaultRegistry(); const [expandedOther, setExpandedOther] = useState(false); + const chartColors = useChartColors(); // Helper to get display name for an address (vault name or shortened address) const getDisplayName = (address: string): string => { @@ -246,7 +247,7 @@ export function BorrowersPieChart({ chainId, market, oraclePrice }: BorrowersPie {pieData.map((entry, index) => ( diff --git a/src/features/market-detail/components/charts/chart-utils.tsx b/src/features/market-detail/components/charts/chart-utils.tsx index 62cfb507..e6235cdb 100644 --- a/src/features/market-detail/components/charts/chart-utils.tsx +++ b/src/features/market-detail/components/charts/chart-utils.tsx @@ -1,5 +1,5 @@ import type { Dispatch, SetStateAction } from 'react'; -import { CHART_COLORS } from '@/constants/chartColors'; +import { CHART_COLORS, useChartColors } from '@/constants/chartColors'; export const TIMEFRAME_LABELS: Record = { '1d': '1D', @@ -42,6 +42,7 @@ export function ChartGradients({ prefix, gradients }: { prefix: string; gradient ); } +// Static gradient configs for backwards compatibility export const RATE_CHART_GRADIENTS: GradientConfig[] = [ { id: 'supplyGradient', color: CHART_COLORS.supply.stroke }, { id: 'borrowGradient', color: CHART_COLORS.borrow.stroke }, @@ -54,6 +55,31 @@ export const VOLUME_CHART_GRADIENTS: GradientConfig[] = [ { id: 'liquidityGradient', color: CHART_COLORS.apyAtTarget.stroke }, ]; +// Dynamic gradient config builders +export function createRateChartGradients(colors: ReturnType): GradientConfig[] { + return [ + { id: 'supplyGradient', color: colors.supply.stroke }, + { id: 'borrowGradient', color: colors.borrow.stroke }, + { id: 'targetGradient', color: colors.apyAtTarget.stroke }, + ]; +} + +export function createVolumeChartGradients(colors: ReturnType): GradientConfig[] { + return [ + { id: 'supplyGradient', color: colors.supply.stroke }, + { id: 'borrowGradient', color: colors.borrow.stroke }, + { id: 'liquidityGradient', color: colors.apyAtTarget.stroke }, + ]; +} + +export function createRiskChartGradients(colors: ReturnType): GradientConfig[] { + return [{ id: 'riskGradient', color: colors.apyAtTarget.stroke }]; +} + +export function createConcentrationGradient(color: string): GradientConfig[] { + return [{ id: 'concentrationGradient', color }]; +} + type ChartTooltipContentProps = { active?: boolean; payload?: any[]; diff --git a/src/features/market-detail/components/charts/collateral-at-risk-chart.tsx b/src/features/market-detail/components/charts/debt-at-risk-chart.tsx similarity index 94% rename from src/features/market-detail/components/charts/collateral-at-risk-chart.tsx rename to src/features/market-detail/components/charts/debt-at-risk-chart.tsx index b70e6b3d..8837bfce 100644 --- a/src/features/market-detail/components/charts/collateral-at-risk-chart.tsx +++ b/src/features/market-detail/components/charts/debt-at-risk-chart.tsx @@ -5,14 +5,14 @@ import { formatUnits } from 'viem'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import { Card } from '@/components/ui/card'; import { Spinner } from '@/components/ui/spinner'; -import { RISK_COLORS } from '@/constants/chartColors'; +import { useChartColors } from '@/constants/chartColors'; import { useAllMarketBorrowers } from '@/hooks/useAllMarketPositions'; import { formatReadable } from '@/utils/balance'; import type { SupportedNetworks } from '@/utils/networks'; import type { Market } from '@/utils/types'; -import { ChartGradients, chartTooltipCursor } from './chart-utils'; +import { ChartGradients, chartTooltipCursor, createRiskChartGradients } from './chart-utils'; -type CollateralAtRiskChartProps = { +type DebtAtRiskChartProps = { chainId: SupportedNetworks; market: Market; oraclePrice: bigint; @@ -24,11 +24,9 @@ type RiskDataPoint = { cumulativeDebtPercent: number; // Percentage of total debt }; -// Gradient config for the risk chart -const RISK_GRADIENTS = [{ id: 'riskGradient', color: RISK_COLORS.stroke }]; - -export function CollateralAtRiskChart({ chainId, market, oraclePrice }: CollateralAtRiskChartProps) { +export function DebtAtRiskChart({ chainId, market, oraclePrice }: DebtAtRiskChartProps) { const { data: borrowers, isLoading } = useAllMarketBorrowers(market.uniqueKey, chainId); + const chartColors = useChartColors(); const lltv = useMemo(() => { const lltvBigInt = BigInt(market.lltv); @@ -151,14 +149,14 @@ export function CollateralAtRiskChart({ chainId, market, oraclePrice }: Collater
-

Collateral at Risk

+

Debt at Risk

{riskMetrics && (
@-10%: 0 ? RISK_COLORS.stroke : 'inherit' }} + style={{ color: riskMetrics.percentAt10 > 0 ? chartColors.apyAtTarget.stroke : 'inherit' }} > {riskMetrics.percentAt10.toFixed(1)}% @@ -187,7 +185,7 @@ export function CollateralAtRiskChart({ chainId, market, oraclePrice }: Collater > s.setTimeframe); const { isAprDisplay } = useAppSettings(); const { short: rateLabel } = useRateLabel(); + const chartColors = useChartColors(); const { data: historicalData, isLoading } = useMarketHistoricalData(marketId, chainId, selectedTimeRange); @@ -163,7 +164,7 @@ function RateChart({ marketId, chainId, market }: RateChartProps) { > { @@ -200,7 +201,7 @@ export function SuppliersPieChart({ chainId, market }: SuppliersPieChartProps) { {pieData.map((entry, index) => ( diff --git a/src/features/market-detail/components/charts/volume-chart.tsx b/src/features/market-detail/components/charts/volume-chart.tsx index 42640913..b7fc12f5 100644 --- a/src/features/market-detail/components/charts/volume-chart.tsx +++ b/src/features/market-detail/components/charts/volume-chart.tsx @@ -7,7 +7,7 @@ import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Responsi import { formatUnits } from 'viem'; import { Spinner } from '@/components/ui/spinner'; import { TooltipContent } from '@/components/shared/tooltip-content'; -import { CHART_COLORS } from '@/constants/chartColors'; +import { useChartColors } from '@/constants/chartColors'; import { formatReadable } from '@/utils/balance'; import { formatChartTime } from '@/utils/chart'; import { useMarketHistoricalData } from '@/hooks/useMarketHistoricalData'; @@ -16,7 +16,7 @@ import { TIMEFRAME_LABELS, ChartGradients, ChartTooltipContent, - VOLUME_CHART_GRADIENTS, + createVolumeChartGradients, createLegendClickHandler, chartTooltipCursor, chartLegendStyle, @@ -36,6 +36,7 @@ function VolumeChart({ marketId, chainId, market }: VolumeChartProps) { const volumeView = useMarketDetailChartState((s) => s.volumeView); const setTimeframe = useMarketDetailChartState((s) => s.setTimeframe); const setVolumeView = useMarketDetailChartState((s) => s.setVolumeView); + const chartColors = useChartColors(); const { data: historicalData, isLoading } = useMarketHistoricalData(marketId, chainId, selectedTimeRange); @@ -267,7 +268,7 @@ function VolumeChart({ marketId, chainId, market }: VolumeChartProps) { >
@@ -382,13 +385,13 @@ function MarketContent() { totalCount={borrowersTotalCount} isLoading={borrowersLoading} title="Borrower Concentration" - color={CHART_COLORS.borrow.stroke} + color={chartColors.borrow.stroke} />
{/* Collateral at Risk chart */}
- void; +}; + +/** + * Zustand store for chart color palette preferences. + * Automatically persisted to localStorage. + */ +export const useChartPalette = create()( + persist( + (set) => ({ + palette: DEFAULT_PALETTE, + setPalette: (palette) => set({ palette }), + }), + { + name: 'monarch_store_chartPalette', + migrate: (persistedState) => { + const state = persistedState as { palette?: ChartPaletteName }; + if (!state?.palette || !CHART_PALETTE_NAMES.includes(state.palette)) { + return { palette: DEFAULT_PALETTE }; + } + return state; + }, + version: 1, + }, + ), +);