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() {
+
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,
+ },
+ ),
+);