feat(lme): add color legend, dual Y-axis scaling for price vs stocks, and full Persian localization

This commit is contained in:
alireza 2026-08-26 10:56:10 +03:30
parent 380605daa4
commit 8ec51d92a8
2 changed files with 258 additions and 125 deletions

View File

@ -35,17 +35,17 @@ FIELD_MAP = {
}
FA_NAMES = {
"Copper": "مس (Copper)",
"Tin": "قلع (Tin)",
"Lead": "سرب (Lead)",
"Zinc": "روی (Zinc)",
"Aluminium": "آلومینیوم (Aluminium)",
"Nickel": "نیکل (Nickel)",
"WM_Cu_low": "مس کاتد آلمان (WM-Notiz کف)",
"WM_Cu_high": "مس کاتد آلمان (WM-Notiz سقف)",
"WI_Cu": "مس ویلند (Wieland Copper)",
"Copper": "مس",
"Tin": "قلع",
"Lead": "سرب",
"Zinc": "روی",
"Aluminium": "آلومینیوم",
"Nickel": "نیکل",
"WM_Cu_low": "مس کاتد آلمان (کف قیمت)",
"WM_Cu_high": "مس کاتد آلمان (سقف قیمت)",
"WI_Cu": "مس ویلند (Wieland)",
"ACI": "شاخص مس پیشرفته (ACI)",
"MB_bronze_94_6": "برنز 94/6",
"MB_bronze_94_6": "برنز ۹۴/۶",
"MB_MS_58_1": "برنج MS 58 (مرحله ۱)",
"MB_MS_58_2": "برنج MS 58 (مرحله ۲)",
"MB_MS_63_37": "برنج MS 63/37",
@ -57,6 +57,28 @@ FA_NAMES = {
"Ag_processed": "نقره ساخته‌شده در اروپا",
}
SERIES_FA_NAMES = {
"LME Copper Cash-Settlement": "قیمت تسویه نقدی مس",
"LME Copper stock": "موجودی انبار مس",
"LME Aluminium Cash-Settlement": "قیمت تسویه نقدی آلومینیوم",
"LME Aluminium stock": "موجودی انبار آلومینیوم",
"LME Zinc Cash-Settlement": "قیمت تسویه نقدی روی",
"LME Zinc stock": "موجودی انبار روی",
"LME Nickel Cash-Settlement": "قیمت تسویه نقدی نیکل",
"LME Nickel stock": "موجودی انبار نیکل",
"LME Lead Cash-Settlement": "قیمت تسویه نقدی سرب",
"LME Lead stock": "موجودی انبار سرب",
"LME Tin Cash-Settlement": "قیمت تسویه نقدی قلع",
"LME Tin stock": "موجودی انبار قلع",
"WM_Cu_low": "کف قیمت مس کاتد آلمان",
"WM_Cu_high": "سقف قیمت مس کاتد آلمان",
"Gold London Fixing": "انس طلای لندن",
"Fine Silver in Euro/kg": "نقره خالص در اروپا",
"Gold in Euro/kg": "طلای شمش در اروپا",
"Advanced Copper Index (ACI)": "شاخص مس پیشرفته (ACI)",
"Wieland-Copper": "مس ویلند",
}
def clean_num(val: str) -> float:
if not val:
return 0.0
@ -98,7 +120,7 @@ def fetch_raw():
"cash_str": cells[1],
"three_months": clean_num(cells[2]),
"three_months_str": cells[2],
"unit": "USD/mt"
"unit": "دلار بر تن"
})
# Table 1: LME Stocks
@ -118,7 +140,7 @@ def fetch_raw():
"stocks_str": cells[1],
"change": clean_num(cells[2]),
"change_str": cells[2],
"unit": "mt"
"unit": "تن"
})
# Combine Prices + Stocks by metal
@ -142,7 +164,7 @@ def fetch_raw():
"stocks_str": s_data.get("stocks_str", "-"),
"stocks_change": s_data.get("change", 0),
"stocks_change_str": s_data.get("change_str", "-"),
"unit": "USD/mt"
"unit": "دلار بر تن"
})
# Table 2: FX Rates
@ -171,7 +193,7 @@ def fetch_raw():
"field": field,
"price_str": cells[1],
"prev_str": cells[2],
"unit": "EUR/100kg"
"unit": "یورو بر ۱۰۰ کیلوگرم"
})
# Table 4: Precious Metals
@ -218,12 +240,7 @@ def get_lme_data(ttl_seconds: int = 180):
return _cache["data"]
def get_entire_diagram_data(field: str, ttl_seconds: int = 3600):
"""
Fetches the ENTIRE multi-year historical dataset for any symbol
from the official XML marketdata API.
"""
field_clean = field.strip()
# Resolve aliases (e.g. "copper" -> "LME_Cu_cash")
field_clean = FIELD_MAP.get(field_clean.lower(), field_clean)
now = time.time()
@ -245,17 +262,35 @@ def get_entire_diagram_data(field: str, ttl_seconds: int = 3600):
lines = []
for line in series_elem:
raw_name = line.attrib.get('name', field_clean)
fa_line_name = SERIES_FA_NAMES.get(raw_name, raw_name)
color = line.attrib.get('lineColor', '#D02E00')
unit = line.attrib.get('yUnit', '')
raw_unit = line.attrib.get('yUnit', '')
precision = int(line.attrib.get('yPrecision', '2'))
# Persian unit & type
is_stock = "stock" in raw_name.lower() or "bestände" in raw_name.lower() or "انبار" in fa_line_name
if is_stock:
unit = "تن"
series_type = "stocks"
color = "#f97316" # vibrant orange for stocks
elif "" in raw_unit or "euro" in raw_unit.lower():
unit = "یورو"
series_type = "price"
color = "#D02E00" # red for primary price
elif "$" in raw_unit or "usd" in raw_unit.lower():
unit = "دلار"
series_type = "price"
color = "#D02E00"
else:
unit = raw_unit
series_type = "price"
pts = []
for val in line:
raw_x = val.attrib.get('x', '') # YYYY/MM/DD
date_iso = raw_x.replace('/', '-')
# Also format DD/MM/YYYY for tooltips as shown in user screenshot
parts = date_iso.split('-')
date_display = f"{parts[2]}/{parts[1]}/{parts[0]}" if len(parts) == 3 else date_iso
date_display = f"{parts[0]}/{parts[1]}/{parts[2]}" if len(parts) == 3 else date_iso
y_str = val.attrib.get('y', '0').replace(',', '')
try:
@ -270,7 +305,9 @@ def get_entire_diagram_data(field: str, ttl_seconds: int = 3600):
})
lines.append({
"name": raw_name,
"name": fa_line_name,
"raw_name": raw_name,
"series_type": series_type,
"color": color,
"unit": unit,
"precision": precision,
@ -290,6 +327,5 @@ def get_entire_diagram_data(field: str, ttl_seconds: int = 3600):
return result
# Backward compatibility alias
def get_metal_history(metal_key: str, ttl_seconds: int = 300):
return get_entire_diagram_data(metal_key, ttl_seconds)

View File

@ -9,12 +9,11 @@ import {
Globe,
X,
LineChart as LineChartIcon,
ChevronLeft,
ChevronRight,
Maximize2,
Loader2,
Calendar,
Sparkles
Eye,
EyeOff
} from "lucide-react";
import {
ResponsiveContainer,
@ -25,7 +24,6 @@ import {
YAxis,
Tooltip,
CartesianGrid,
Legend
} from "recharts";
import { API_BASE } from "@/lib/api";
@ -83,12 +81,14 @@ export interface LmeData {
export interface DiagramPoint {
x: string; // YYYY-MM-DD
date_display: string; // DD/MM/YYYY
date_display: string;
y: number;
}
export interface DiagramLine {
name: string;
raw_name: string;
series_type: "price" | "stocks";
color: string;
unit: string;
precision: number;
@ -105,21 +105,6 @@ export interface DiagramResponse {
type Timeframe = "3M" | "6M" | "1Y" | "2Y" | "ENTIRE";
const METAL_COLORS: Record<string, string> = {
Copper: "#D02E00",
WM_Cu_low: "#D02E00",
WM_Cu_high: "#D02E00",
Aluminium: "#0ea5e9",
Zinc: "#10b981",
Nickel: "#8b5cf6",
Lead: "#64748b",
Tin: "#06b6d4",
ACI: "#f59e0b",
USD_ozt_London: "#eab308",
Au: "#eab308",
Ag: "#94a3b8",
};
export function LmeView() {
const [data, setData] = useState<LmeData | null>(null);
const [error, setError] = useState<string | null>(null);
@ -140,6 +125,10 @@ export function LmeView() {
const [timeframe, setTimeframe] = useState<Timeframe>("ENTIRE");
const [offsetIndex, setOffsetIndex] = useState<number>(0);
// Visibility toggles for dual-series charts
const [showPrice, setShowPrice] = useState<boolean>(true);
const [showStocks, setShowStocks] = useState<boolean>(true);
// Load overview catalog
const loadData = async () => {
try {
@ -169,6 +158,8 @@ export function LmeView() {
let alive = true;
setDiagramLoading(true);
setOffsetIndex(0);
setShowPrice(true);
setShowStocks(true);
fetch(`${API}/api/lme/diagram?field=${encodeURIComponent(selectedSymbol.field)}`, {
credentials: "include"
@ -199,11 +190,20 @@ export function LmeView() {
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
// Line separation: price line vs stock line
const priceLine = useMemo(() => {
return diagramData?.lines?.find((l) => l.series_type === "price") || diagramData?.lines?.[0];
}, [diagramData]);
const stockLine = useMemo(() => {
return diagramData?.lines?.find((l) => l.series_type === "stocks");
}, [diagramData]);
// Filter main series by timeframe & offset
const chartData = useMemo(() => {
if (!diagramData?.lines?.length) return [];
const mainLine = diagramData.lines[0];
const total = mainLine.points.length;
const baseLine = priceLine || diagramData.lines[0];
const total = baseLine.points.length;
if (!total) return [];
let sliceCount = total;
@ -213,38 +213,36 @@ export function LmeView() {
else if (timeframe === "2Y") sliceCount = 520;
else if (timeframe === "ENTIRE") sliceCount = total;
// Windowing with offsetIndex
const maxOffset = Math.max(0, total - sliceCount);
const effectiveOffset = Math.min(offsetIndex, maxOffset);
const end = total - effectiveOffset;
const start = Math.max(0, end - sliceCount);
const windowPoints = mainLine.points.slice(start, end);
const windowPoints = baseLine.points.slice(start, end);
// Merge secondary lines (e.g. stock line in LME_Cu_cash)
if (diagramData.lines.length > 1) {
const secondLine = diagramData.lines[1];
const secondMap = new Map(secondLine.points.map((p) => [p.x, p.y]));
if (stockLine) {
const stockMap = new Map(stockLine.points.map((p) => [p.x, p.y]));
return windowPoints.map((p) => ({
x: p.x,
date_display: p.date_display,
[mainLine.name]: p.y,
[secondLine.name]: secondMap.get(p.x) ?? null,
priceVal: p.y,
stockVal: stockMap.get(p.x) ?? null,
}));
}
return windowPoints.map((p) => ({
x: p.x,
date_display: p.date_display,
[mainLine.name]: p.y,
priceVal: p.y,
}));
}, [diagramData, timeframe, offsetIndex]);
}, [diagramData, priceLine, stockLine, timeframe, offsetIndex]);
// Statistics calculation for the active window
const stats = useMemo(() => {
if (!chartData.length || !diagramData?.lines?.[0]) return null;
const mainName = diagramData.lines[0].name;
const values = chartData.map((d: any) => d[mainName]).filter((v): v is number => typeof v === "number" && v > 0);
if (!chartData.length) return null;
const values = chartData
.map((d) => d.priceVal)
.filter((v): v is number => typeof v === "number" && v > 0);
if (!values.length) return null;
const max = Math.max(...values);
@ -256,14 +254,14 @@ export function LmeView() {
const pct = first > 0 ? (diff / first) * 100 : 0;
return { max, min, avg, diff, pct, first, last };
}, [chartData, diagramData]);
}, [chartData]);
const canStepPrev = useMemo(() => {
if (timeframe === "ENTIRE") return false;
const total = diagramData?.lines?.[0]?.points?.length || 0;
const total = (priceLine || diagramData?.lines?.[0])?.points?.length || 0;
const count = timeframe === "3M" ? 65 : timeframe === "6M" ? 130 : timeframe === "1Y" ? 260 : 520;
return offsetIndex + count < total;
}, [timeframe, diagramData, offsetIndex]);
}, [timeframe, priceLine, diagramData, offsetIndex]);
const canStepNext = useMemo(() => {
return offsetIndex > 0 && timeframe !== "ENTIRE";
@ -319,7 +317,7 @@ export function LmeView() {
{data?.date && (
<div className="self-start sm:self-auto rounded-xl border border-border/80 bg-card/60 px-3.5 py-1.5 text-xs text-muted-foreground shadow-sm">
تاریخ: <span className="font-semibold text-foreground">{data.date}</span>
تاریخ بازار: <span className="font-semibold text-foreground">{data.date}</span>
</div>
)}
</div>
@ -389,7 +387,7 @@ export function LmeView() {
field: m.field,
title: m.metal,
name_fa: m.name_fa,
unit: "USD/mt",
unit: "دلار بر تن",
currentPrice: m.cash_str,
})
}
@ -400,7 +398,7 @@ export function LmeView() {
<div className="text-base font-bold text-foreground group-hover:text-primary transition-colors">
{m.name_fa}
</div>
<div className="text-xs text-muted-foreground font-mono mt-0.5">{m.metal} USD/mt</div>
<div className="text-xs text-muted-foreground font-mono mt-0.5">{m.metal} دلار بر تن</div>
</div>
<span className="text-[11px] px-2 py-0.5 rounded-lg bg-primary/10 text-primary font-medium flex items-center gap-1 group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
<span>نمودار کامل</span>
@ -410,13 +408,13 @@ export function LmeView() {
<div className="mt-4 grid grid-cols-2 gap-3 pt-3 border-t border-border/40">
<div>
<div className="text-[11px] text-muted-foreground">تسویه نقدی (Cash)</div>
<div className="text-[11px] text-muted-foreground">تسویه نقدی</div>
<div className="text-lg font-bold font-mono text-foreground mt-0.5">
${m.cash_str}
</div>
</div>
<div>
<div className="text-[11px] text-muted-foreground">۳ ماهه (3-Month)</div>
<div className="text-[11px] text-muted-foreground">قرارداد ۳ ماهه</div>
<div className="text-lg font-bold font-mono text-foreground mt-0.5">
${m.three_months_str}
</div>
@ -479,7 +477,7 @@ export function LmeView() {
<div className="mt-4 flex items-center justify-between pt-3 border-t border-border/40">
<div>
<div className="text-[11px] text-muted-foreground">آخرین مظنه (EUR/100kg)</div>
<div className="text-[11px] text-muted-foreground">آخرین مظنه (یورو بر ۱۰۰ کیلو)</div>
<div className="text-xl font-bold font-mono text-foreground mt-0.5">
{s.price_str}
</div>
@ -565,15 +563,13 @@ export function LmeView() {
{selectedSymbol && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-6 bg-black/75 backdrop-blur-md animate-in fade-in duration-200">
<div
className="relative w-full max-w-5xl max-h-[92vh] overflow-y-auto rounded-3xl border border-border/80 bg-background/95 p-5 sm:p-7 shadow-2xl backdrop-blur-2xl space-y-6"
className="relative w-full max-w-5xl max-h-[92vh] overflow-y-auto rounded-3xl border border-border/80 bg-background/95 p-5 sm:p-7 shadow-2xl backdrop-blur-2xl space-y-5"
dir="rtl"
>
{/* Header */}
<div className="flex items-start justify-between border-b border-border/60 pb-4">
<div className="flex items-center gap-3">
<div
className="w-12 h-12 rounded-2xl grid place-items-center border shrink-0 bg-primary/10 border-primary/30"
>
<div className="w-12 h-12 rounded-2xl grid place-items-center border shrink-0 bg-primary/10 border-primary/30">
<LineChartIcon className="w-6 h-6 text-primary" />
</div>
<div>
@ -586,7 +582,7 @@ export function LmeView() {
</span>
</div>
<p className="text-xs text-muted-foreground mt-0.5">
نمودار کامل تاریخی {diagramData?.total_points ? `${diagramData.total_points.toLocaleString()} روز معاملاتی ثبت‌شده` : "در حال بارگذاری..."}
نمودار کامل تاریخی {diagramData?.total_points ? `${diagramData.total_points.toLocaleString()} روز کاری ثبت‌شده` : "در حال بارگذاری..."}
</p>
</div>
</div>
@ -600,29 +596,29 @@ export function LmeView() {
</button>
</div>
{/* Quick Stat Pill */}
{/* Quick Stat Pills (100% Persian) */}
{stats && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="rounded-2xl border border-border/60 bg-card/60 p-3.5">
<div className="text-[11px] text-muted-foreground">آخرین قیمت (Latest)</div>
<div className="text-[11px] text-muted-foreground">آخرین قیمت</div>
<div className="text-lg font-bold font-mono text-foreground mt-0.5">
{stats.last.toLocaleString()} {diagramData?.lines?.[0]?.unit || ""}
{stats.last.toLocaleString()} {priceLine?.unit || ""}
</div>
</div>
<div className="rounded-2xl border border-border/60 bg-card/60 p-3.5">
<div className="text-[11px] text-muted-foreground">بالاترین دوره (Max)</div>
<div className="text-[11px] text-muted-foreground">بالاترین قیمت دوره</div>
<div className="text-lg font-bold font-mono text-foreground mt-0.5">
{stats.max.toLocaleString()} {diagramData?.lines?.[0]?.unit || ""}
{stats.max.toLocaleString()} {priceLine?.unit || ""}
</div>
</div>
<div className="rounded-2xl border border-border/60 bg-card/60 p-3.5">
<div className="text-[11px] text-muted-foreground">پایینترین دوره (Min)</div>
<div className="text-[11px] text-muted-foreground">پایینترین قیمت دوره</div>
<div className="text-lg font-bold font-mono text-foreground mt-0.5">
{stats.min.toLocaleString()} {diagramData?.lines?.[0]?.unit || ""}
{stats.min.toLocaleString()} {priceLine?.unit || ""}
</div>
</div>
<div className="rounded-2xl border border-border/60 bg-card/60 p-3.5">
<div className="text-[11px] text-muted-foreground">تغییر بازه انتخابی</div>
<div className="text-[11px] text-muted-foreground">تغییر در بازه انتخابی</div>
<div
className={`text-lg font-bold font-mono mt-0.5 ${
stats.diff >= 0 ? "text-emerald-500" : "text-rose-500"
@ -634,12 +630,59 @@ export function LmeView() {
</div>
)}
{/* Chart Area */}
<div className="rounded-2xl border border-border/60 bg-card/40 p-4 sm:p-5">
{/* Chart Container */}
<div className="rounded-2xl border border-border/60 bg-card/40 p-4 sm:p-5 space-y-4">
{/* LEGEND & TOGGLE BAR (راهنمای خطوط و رنگ‌ها) */}
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-border/40 pb-3">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-foreground">راهنمای رنگها:</span>
{/* Price Line Toggle */}
{priceLine && (
<button
onClick={() => setShowPrice((v) => !v)}
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-xl text-xs font-medium border transition ${
showPrice
? "bg-[#D02E00]/15 border-[#D02E00]/40 text-[#D02E00]"
: "bg-muted/40 border-border/60 text-muted-foreground opacity-50"
}`}
title="کلیک برای نمایش/مخفی‌سازی خط قیمت"
>
<span className="w-2.5 h-2.5 rounded-full bg-[#D02E00] shrink-0" />
<span>{priceLine.name} ({priceLine.unit})</span>
{showPrice ? <Eye className="w-3 h-3" /> : <EyeOff className="w-3 h-3" />}
</button>
)}
{/* Stock Line Toggle (if exists) */}
{stockLine && (
<button
onClick={() => setShowStocks((v) => !v)}
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-xl text-xs font-medium border transition ${
showStocks
? "bg-[#f97316]/15 border-[#f97316]/40 text-[#f97316]"
: "bg-muted/40 border-border/60 text-muted-foreground opacity-50"
}`}
title="کلیک برای نمایش/مخفی‌سازی خط موجودی انبار"
>
<span className="w-2.5 h-2.5 rounded-full bg-[#f97316] shrink-0" />
<span>{stockLine.name} ({stockLine.unit})</span>
{showStocks ? <Eye className="w-3 h-3" /> : <EyeOff className="w-3 h-3" />}
</button>
)}
</div>
{stockLine && (
<span className="text-[11px] text-muted-foreground font-medium hidden sm:inline">
* محور راست: قیمت ({priceLine?.unit}) محور چپ: موجودی انبار ({stockLine?.unit})
</span>
)}
</div>
{diagramLoading ? (
<div className="h-80 flex flex-col items-center justify-center gap-2 text-muted-foreground text-sm">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
<span>در حال بارگذاری دیتای کامل تاریخی (Entire Data)...</span>
<span>در حال بارگذاری تمام دادههای تاریخی...</span>
</div>
) : chartData.length === 0 ? (
<div className="h-80 flex items-center justify-center text-muted-foreground text-sm">
@ -648,14 +691,20 @@ export function LmeView() {
) : (
<div className="h-80 sm:h-96 w-full" dir="ltr">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={chartData} margin={{ top: 10, right: 15, left: 10, bottom: 0 }}>
<ComposedChart data={chartData} margin={{ top: 10, right: 15, left: 15, bottom: 0 }}>
<defs>
<linearGradient id="chartGradient" x1="0" y1="0" x2="0" y2="1">
<linearGradient id="chartGradientPrice" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#D02E00" stopOpacity={0.25} />
<stop offset="95%" stopColor="#D02E00" stopOpacity={0.0} />
</linearGradient>
<linearGradient id="chartGradientStocks" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#f97316" stopOpacity={0.18} />
<stop offset="95%" stopColor="#f97316" stopOpacity={0.0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="2 2" opacity={0.15} vertical={true} />
{/* X Axis */}
<XAxis
dataKey="x"
tick={{ fontSize: 10, fill: "var(--muted-foreground)" }}
@ -666,64 +715,112 @@ export function LmeView() {
}}
minTickGap={40}
/>
{/* Right Y-Axis for Price */}
<YAxis
domain={["auto", "auto"]}
tick={{ fontSize: 10, fill: "var(--muted-foreground)" }}
tickFormatter={(v) => v.toLocaleString()}
yAxisId="price"
orientation="right"
domain={["auto", "auto"]}
tick={{ fontSize: 10, fill: "#D02E00" }}
tickFormatter={(v) => v.toLocaleString()}
hide={!showPrice}
/>
{/* Tooltip styled exactly like Westmetall screenshot */}
{/* Left Y-Axis for Warehouse Stocks (Scaled properly!) */}
{stockLine && (
<YAxis
yAxisId="stocks"
orientation="left"
domain={["auto", "auto"]}
tick={{ fontSize: 10, fill: "#f97316" }}
tickFormatter={(v) => `${(v / 1000).toFixed(0)}k`}
hide={!showStocks}
/>
)}
{/* Persian Tooltip */}
<Tooltip
content={({ active, payload, label }) => {
if (!active || !payload?.length) return null;
const pt = payload[0];
const ptData = pt.payload;
const lineName = pt.name;
const val = pt.value;
const unit = diagramData?.lines?.[0]?.unit || "";
return (
<div className="rounded-xl border border-amber-600/40 bg-[#D02E00] px-3.5 py-2.5 text-white shadow-xl">
<div className="text-[11px] font-semibold tracking-wider font-mono opacity-90">
{lineName}
</div>
<div className="text-[10px] font-mono opacity-80 mt-0.5">
{ptData.date_display || label}
</div>
<div className="text-sm font-bold font-mono mt-1">
{typeof val === "number" ? val.toLocaleString(undefined, { minimumFractionDigits: 2 }) : val} {unit}
<div className="rounded-xl border border-border/80 bg-card/95 p-3 text-foreground shadow-2xl backdrop-blur-md space-y-1.5 min-w-[170px]" dir="rtl">
<div className="text-[11px] font-medium text-muted-foreground border-b border-border/40 pb-1 font-mono">
تاریخ: {ptData.date_display || label}
</div>
{showPrice && ptData.priceVal !== undefined && (
<div className="flex items-center justify-between gap-3 text-xs">
<div className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-[#D02E00]" />
<span>{priceLine?.name || "قیمت"}:</span>
</div>
<span className="font-mono font-bold text-[#D02E00]">
{Number(ptData.priceVal).toLocaleString(undefined, { minimumFractionDigits: 2 })} {priceLine?.unit || ""}
</span>
</div>
)}
{showStocks && ptData.stockVal !== null && ptData.stockVal !== undefined && (
<div className="flex items-center justify-between gap-3 text-xs">
<div className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-[#f97316]" />
<span>{stockLine?.name || "موجودی انبار"}:</span>
</div>
<span className="font-mono font-bold text-[#f97316]">
{Number(ptData.stockVal).toLocaleString()} {stockLine?.unit || "تن"}
</span>
</div>
)}
</div>
);
}}
/>
{diagramData?.lines?.map((line, idx) => (
{/* Price Curve */}
{showPrice && (
<Area
key={line.name}
yAxisId="price"
type="monotone"
dataKey={line.name}
stroke={line.color || "#D02E00"}
strokeWidth={1.8}
dataKey="priceVal"
name={priceLine?.name || "قیمت"}
stroke="#D02E00"
strokeWidth={2}
fillOpacity={1}
fill={idx === 0 ? "url(#chartGradient)" : "transparent"}
fill="url(#chartGradientPrice)"
dot={false}
/>
))}
)}
{/* Stock Curve (Mapped to Left Y-Axis!) */}
{showStocks && stockLine && (
<Line
yAxisId="stocks"
type="monotone"
dataKey="stockVal"
name={stockLine.name}
stroke="#f97316"
strokeWidth={2}
dot={false}
/>
)}
</ComposedChart>
</ResponsiveContainer>
</div>
)}
{/* TIMEFRAME BAR — Exactly matching user's screenshot */}
<div className="mt-4 pt-4 border-t border-border/40 flex flex-wrap items-center justify-center gap-1.5" dir="ltr">
{/* TIMEFRAME BAR (100% Persian) */}
<div className="pt-3 border-t border-border/40 flex flex-wrap items-center justify-center gap-2" dir="rtl">
{/* Step Backwards */}
<button
onClick={() => stepWindow("prev")}
disabled={!canStepPrev}
className="rounded-lg border border-border/80 bg-background/80 px-2.5 py-1.5 text-xs text-foreground transition hover:bg-accent disabled:opacity-30 disabled:cursor-not-allowed"
className="rounded-xl border border-border/80 bg-background/80 px-3 py-1.5 text-xs text-foreground transition hover:bg-accent disabled:opacity-30 disabled:cursor-not-allowed"
title="نمایش داده‌های قدیمی‌تر"
>
&lt;
&gt; قبلی
</button>
{/* 3 months */}
@ -732,13 +829,13 @@ export function LmeView() {
setTimeframe("3M");
setOffsetIndex(0);
}}
className={`rounded-lg border px-3 py-1.5 text-xs font-medium transition ${
className={`rounded-xl border px-3.5 py-1.5 text-xs font-semibold transition ${
timeframe === "3M"
? "border-primary bg-primary text-primary-foreground shadow-sm"
: "border-border/80 bg-background/80 text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
3 months
۳ ماه
</button>
{/* 6 months */}
@ -747,13 +844,13 @@ export function LmeView() {
setTimeframe("6M");
setOffsetIndex(0);
}}
className={`rounded-lg border px-3 py-1.5 text-xs font-medium transition ${
className={`rounded-xl border px-3.5 py-1.5 text-xs font-semibold transition ${
timeframe === "6M"
? "border-primary bg-primary text-primary-foreground shadow-sm"
: "border-border/80 bg-background/80 text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
6 months
۶ ماه
</button>
{/* 1 year */}
@ -762,13 +859,13 @@ export function LmeView() {
setTimeframe("1Y");
setOffsetIndex(0);
}}
className={`rounded-lg border px-3 py-1.5 text-xs font-medium transition ${
className={`rounded-xl border px-3.5 py-1.5 text-xs font-semibold transition ${
timeframe === "1Y"
? "border-primary bg-primary text-primary-foreground shadow-sm"
: "border-border/80 bg-background/80 text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
1 year
۱ سال
</button>
{/* 2 year */}
@ -777,13 +874,13 @@ export function LmeView() {
setTimeframe("2Y");
setOffsetIndex(0);
}}
className={`rounded-lg border px-3 py-1.5 text-xs font-medium transition ${
className={`rounded-xl border px-3.5 py-1.5 text-xs font-semibold transition ${
timeframe === "2Y"
? "border-primary bg-primary text-primary-foreground shadow-sm"
: "border-border/80 bg-background/80 text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
2 year
۲ سال
</button>
{/* entire data (ACTIVE HIGHLIGHT) */}
@ -792,23 +889,23 @@ export function LmeView() {
setTimeframe("ENTIRE");
setOffsetIndex(0);
}}
className={`rounded-lg border px-3 py-1.5 text-xs font-bold transition ${
className={`rounded-xl border px-4 py-1.5 text-xs font-bold transition ${
timeframe === "ENTIRE"
? "border-primary bg-primary text-primary-foreground shadow-md ring-2 ring-primary/20"
: "border-border/80 bg-background/80 text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
>
entire data
کل دادهها (تاریخچه کامل)
</button>
{/* Step Forwards */}
<button
onClick={() => stepWindow("next")}
disabled={!canStepNext}
className="rounded-lg border border-border/80 bg-background/80 px-2.5 py-1.5 text-xs text-foreground transition hover:bg-accent disabled:opacity-30 disabled:cursor-not-allowed"
className="rounded-xl border border-border/80 bg-background/80 px-3 py-1.5 text-xs text-foreground transition hover:bg-accent disabled:opacity-30 disabled:cursor-not-allowed"
title="نمایش داده‌های جدیدتر"
>
&gt;
بعدی &lt;
</button>
</div>
</div>