edit chart for section hero and live in landing

This commit is contained in:
2026-08-09 12:19:43 +03:30
parent fa42e96fa0
commit 5d660783da
3 changed files with 512 additions and 141 deletions
+73
View File
@@ -0,0 +1,73 @@
/** تاریخچه قیمت عمومی بورس کالا (بدون توکن) — کاتد مس */
export const ZAANBOX_COPPER_CTHD_ID = '8c19e06a-446e-3d56-9cc0-750f7c6cd767'
export const ZAANBOX_HISTORY_URL = `https://price.zaarbox.ir/api/history/${ZAANBOX_COPPER_CTHD_ID}`
export type ZaanboxHistoryPoint = {
time: string
price: number
}
export type ZaanboxHistoryResponse = {
history?: ZaanboxHistoryPoint[]
data?: { history?: ZaanboxHistoryPoint[] }
}
export type LandingHistoryRange = 'DAILY' | 'WEEKLY' | 'MONTHLY'
/** نقطهٔ خام تاریخچه — قدیم → جدید */
export type LandingHistorySample = {
timeMs: number
price: number
}
const RANGE_MS: Record<LandingHistoryRange, number> = {
DAILY: 1 * 24 * 60 * 60 * 1000,
WEEKLY: 7 * 24 * 60 * 60 * 1000,
MONTHLY: 30 * 24 * 60 * 60 * 1000,
}
const HISTORY_LIMIT = 1_000_000_000
function resolveHistoryRows(payload: unknown): ZaanboxHistoryPoint[] {
if (!payload || typeof payload !== 'object') return []
const body = payload as ZaanboxHistoryResponse
if (Array.isArray(body.history)) return body.history
if (Array.isArray(body.data?.history)) return body.data!.history!
return []
}
/** نقاط خام بازه، مرتب از قدیم → جدید */
export async function fetchZaanboxPriceHistory(
range: LandingHistoryRange,
now = new Date(),
): Promise<LandingHistorySample[]> {
const end = now
const start = new Date(end.getTime() - RANGE_MS[range])
const url = new URL(ZAANBOX_HISTORY_URL)
url.searchParams.set('start', start.toISOString())
url.searchParams.set('end', end.toISOString())
url.searchParams.set('limit', String(HISTORY_LIMIT))
const response = await fetch(url.toString(), {
method: 'GET',
headers: { Accept: 'application/json' },
})
if (!response.ok) {
throw new Error(`Zaanbox history HTTP ${response.status}`)
}
const payload = (await response.json()) as unknown
const rows = resolveHistoryRows(payload)
return rows
.map((row) => {
const timeMs = Date.parse(String(row.time ?? ''))
const price = Number(row.price)
return { timeMs, price }
})
.filter((point) => Number.isFinite(point.timeMs) && Number.isFinite(point.price) && point.price > 0)
.sort((a, b) => a.timeMs - b.timeMs)
}
@@ -201,44 +201,71 @@
<span class="dlanding-price-card__change-label">نسبت به ابتدای روز</span>
</div>
<div class="dlanding-chart" aria-hidden="true">
<svg
v-if="landingChart.hasData"
class="dlanding-chart__svg"
viewBox="0 0 360 120"
preserveAspectRatio="none"
<div class="dlanding-chart">
<div
class="dlanding-chart__plot"
@pointermove="onHeroChartPointerMove"
@pointerleave="onHeroChartPointerLeave"
>
<defs>
<linearGradient id="landingChartFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#c98a5f" stop-opacity="0.28" />
<stop offset="100%" stop-color="#c98a5f" stop-opacity="0" />
</linearGradient>
</defs>
<path
class="dlanding-chart__area"
:d="landingChart.areaPath"
fill="url(#landingChartFill)"
/>
<path
class="dlanding-chart__path"
:d="landingChart.linePath"
fill="none"
stroke="#c98a5f"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle :cx="landingChart.lastX" :cy="landingChart.lastY" r="5" fill="#c98a5f" />
<circle
:cx="landingChart.lastX"
:cy="landingChart.lastY"
r="9"
fill="#c98a5f"
fill-opacity="0.2"
/>
</svg>
<div v-else class="dlanding-chart__empty">
{{ liveChartLoading ? 'در حال بارگذاری نمودار…' : 'نمودار در دسترس نیست' }}
<svg
v-if="landingChart.hasData"
class="dlanding-chart__svg"
viewBox="0 0 360 120"
preserveAspectRatio="none"
>
<defs>
<linearGradient id="landingChartFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#c98a5f" stop-opacity="0.28" />
<stop offset="100%" stop-color="#c98a5f" stop-opacity="0" />
</linearGradient>
</defs>
<path
class="dlanding-chart__area"
:d="landingChart.areaPath"
fill="url(#landingChartFill)"
/>
<path
class="dlanding-chart__path"
:d="landingChart.linePath"
fill="none"
stroke="#c98a5f"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle :cx="landingChart.lastX" :cy="landingChart.lastY" r="5" fill="#c98a5f" />
<circle
:cx="landingChart.lastX"
:cy="landingChart.lastY"
r="9"
fill="#c98a5f"
fill-opacity="0.2"
/>
</svg>
<div v-else class="dlanding-chart__empty">
{{ liveChartLoading ? 'در حال بارگذاری نمودار…' : 'نمودار در دسترس نیست' }}
</div>
<template v-if="heroChartHover">
<div
class="dlanding-chart__crosshair"
:style="{ left: `${heroChartHover.xPercent}%` }"
/>
<div
class="dlanding-chart__dot"
:style="{
left: `${heroChartHover.xPercent}%`,
top: `${heroChartHover.yPercent}%`,
}"
/>
<div
class="dlanding-chart__tooltip"
:class="{ 'is-near-end': heroChartHover.xPercent > 72 }"
:style="{ left: `${heroChartHover.xPercent}%` }"
>
<strong>{{ heroChartHover.priceLabel }}</strong>
<span>{{ heroChartHover.timeLabel }}</span>
</div>
</template>
</div>
<div class="dlanding-chart__labels">
<span>اکنون</span>
@@ -531,7 +558,11 @@
</button>
</div>
</div>
<div class="dlanding-live__chart" aria-hidden="true">
<div
class="dlanding-live__chart"
@pointermove="onLiveChartPointerMove"
@pointerleave="onLiveChartPointerLeave"
>
<svg
v-if="liveSectionChart.hasData"
class="dlanding-live__chart-svg"
@@ -572,6 +603,27 @@
: 'نمودار در دسترس نیست'
}}
</div>
<template v-if="liveChartHover">
<div
class="dlanding-live__chart-crosshair"
:style="{ left: `${liveChartHover.xPercent}%` }"
/>
<div
class="dlanding-live__chart-dot"
:style="{
left: `${liveChartHover.xPercent}%`,
top: `${liveChartHover.yPercent}%`,
}"
/>
<div
class="dlanding-live__chart-tooltip"
:class="{ 'is-near-end': liveChartHover.xPercent > 72 }"
:style="{ left: `${liveChartHover.xPercent}%` }"
>
<strong>{{ liveChartHover.priceLabel }}</strong>
<span>{{ liveChartHover.timeLabel }}</span>
</div>
</template>
</div>
</div>
</article>
@@ -859,9 +911,6 @@ import type { Price } from '@/models/home'
import { persianToEnglishDigits } from '@/utils/persianDigits'
import {
applyLastPricePayload,
getTradeBuyPrice,
lastPrice,
percentPriceChanged,
} from '@/utils/price'
import { getPriceDisplayState, hasLivePriceQuote, isStoreClosed } from '@/utils/storeHours'
import useAxios from '@/utils/useAxios'
@@ -872,10 +921,13 @@ import {
} from '@/utils/usePrice'
import {
CHART_METAL_MARKUP_RATIO,
resolveMetalDayRangePrices,
usePriceChart,
type ChartMetalType,
} from '@/views/base/home/composables/usePriceChart'
import {
fetchZaanboxPriceHistory,
type LandingHistoryRange,
type LandingHistorySample,
} from '@/api/zaarboxPriceHistory'
import {
PWA_MANIFEST_READY_EVENT,
getDeferredPwaInstallPrompt,
@@ -886,7 +938,7 @@ import DesktopPwaGuideModal from '@/views/login/desktop/components/DesktopPwaGui
import { useAboutUsPageContext } from '../composables/useAboutUsPage'
const MOBILE_NAV_MQ = '(max-width: 1023.98px)'
const PRICE_POLL_MS = 10_000
const PRICE_POLL_MS = 30_000
const CHART_WIDTH = 360
const CHART_HEIGHT = 120
const LIVE_CHART_WIDTH = 560
@@ -894,7 +946,14 @@ const LIVE_CHART_HEIGHT = 160
const CHART_PAD = 10
const MAX_CHART_POINTS = 48
type LiveChartRange = 'DAILY' | 'WEEKLY' | 'MONTHLY'
type LiveChartRange = LandingHistoryRange
type ChartHoverState = {
xPercent: number
yPercent: number
priceLabel: string
timeLabel: string
}
const LIVE_CHART_RANGE_OPTIONS: Array<{
value: LiveChartRange
@@ -908,22 +967,17 @@ const LIVE_CHART_RANGE_OPTIONS: Array<{
const { config } = useAboutUsPageContext()
/** همان سیستم نمودار Home — فقط مقادیر سری، بدون UI/استایل Home */
const {
priceChartSeries,
chartSelection,
chartLoading,
updateChartData,
fetchPrices,
setChartMetal,
loadMetalChartOffsets,
} = usePriceChart()
const activeProduct = ref<'wire' | 'cathode'>('wire')
const liveChartRange = ref<LiveChartRange>('DAILY')
const liveChartRangeOptions = LIVE_CHART_RANGE_OPTIONS
const investmentAmount = ref(0)
/** اسنپ‌شات DAILY برای اسپارک‌لاین هیرو وقتی بازه live عوض می‌شود */
const dailyHomeChartValues = ref<number[]>([])
/** سری خام ۲۴ ساعت — هیرو + قیمت لحظه‌ای + متریک‌های روز */
const dailyRawPoints = ref<LandingHistorySample[]>([])
/** سری خام بازهٔ فعلی سکشن live */
const liveRangeRawPoints = ref<LandingHistorySample[]>([])
const liveChartLoading = ref(false)
const heroChartHover = ref<ChartHoverState | null>(null)
const liveChartHover = ref<ChartHoverState | null>(null)
const openFaqIndex = ref(0)
const isMobileMenuOpen = ref(false)
const showPwaGuide = ref(false)
@@ -952,16 +1006,12 @@ const liveProductName = computed(() =>
activeProduct.value === 'wire' ? 'مفتول مس' : 'کاتد مس',
)
const liveChartRange = computed(() => chartSelection.value as LiveChartRange)
const liveChartRangeLabel = computed(
() =>
LIVE_CHART_RANGE_OPTIONS.find((option) => option.value === liveChartRange.value)?.label ??
'نمودار ۲۴ ساعت گذشته',
)
const liveChartLoading = chartLoading
function selectedMetalType(): ChartMetalType {
return activeProduct.value === 'wire' ? 'maftol' : 'katod'
}
@@ -973,48 +1023,35 @@ function applyProductMarkup(basePrice: number) {
return Math.round(basePrice + basePrice * ratio)
}
function applyMarkupToSeries(points: LandingHistorySample[]): LandingHistorySample[] {
return points
.map((point) => ({
timeMs: point.timeMs,
price: applyProductMarkup(point.price),
}))
.filter((point) => Number.isFinite(point.price) && point.price > 0)
}
function setLiveProduct(product: 'wire' | 'cathode') {
activeProduct.value = product
setChartMetal(product === 'wire' ? 'maftol' : 'katod')
heroChartHover.value = null
liveChartHover.value = null
}
function setLiveChartRange(range: LiveChartRange) {
updateChartData(range)
async function setLiveChartRange(range: LiveChartRange) {
if (liveChartRange.value === range && liveRangeRawPoints.value.length >= 2) return
liveChartRange.value = range
liveChartHover.value = null
await loadLiveRangeHistory(range)
}
/** مقادیر Y همان سری Home (بعد از مارک‌آپ فلز) */
const homeChartValues = computed(() =>
(priceChartSeries.value[0]?.data ?? [])
.map((point) => Number(point?.[1]))
.filter((value) => Number.isFinite(value) && value > 0),
)
watch(
[homeChartValues, chartSelection],
([values, selection]) => {
if (selection === 'DAILY' && values.length >= 2) {
dailyHomeChartValues.value = values
}
},
{ immediate: true },
)
/** پایه قیمت — buy / price_last مثل Home */
/** پایه قیمت = آخرین رکورد history روزانه */
const baseBuyPrice = computed(() => {
const buy = Number(getTradeBuyPrice(lastPrice.value) || 0)
if (Number.isFinite(buy) && buy > 0) return buy
const lastDaily = dailyRawPoints.value[dailyRawPoints.value.length - 1]?.price
if (Number.isFinite(lastDaily) && lastDaily > 0) return lastDaily
const fromDaily = dailyHomeChartValues.value[dailyHomeChartValues.value.length - 1]
if (Number.isFinite(fromDaily) && fromDaily > 0) {
const ratio = CHART_METAL_MARKUP_RATIO[selectedMetalType()] || 0
return ratio ? Math.round(fromDaily / (1 + ratio)) : fromDaily
}
const fromLive = homeChartValues.value[homeChartValues.value.length - 1]
if (Number.isFinite(fromLive) && fromLive > 0) {
const ratio = CHART_METAL_MARKUP_RATIO[selectedMetalType()] || 0
return ratio ? Math.round(fromLive / (1 + ratio)) : fromLive
}
const lastLive = liveRangeRawPoints.value[liveRangeRawPoints.value.length - 1]?.price
if (Number.isFinite(lastLive) && lastLive > 0) return lastLive
return 0
})
@@ -1028,9 +1065,10 @@ const formattedLivePrice = computed(() => {
/** open | closed | no_quote — برای badge وضعیت بازار */
const liveMarketState = computed(() => {
if (isStoreClosed()) return 'closed' as const
const state = getPriceDisplayState()
if (state === 'open') return 'open' as const
if (isStoreClosed()) return 'closed' as const
if (baseBuyPrice.value > 0) return 'open' as const
if (!hasLivePriceQuote()) return 'no_quote' as const
return 'closed' as const
})
@@ -1059,15 +1097,18 @@ const liveUpdateLabel = computed(() => {
return 'مظنه دریافت نشده'
})
/** اسپارک‌لاین هیرو: همیشه سری DAILY از Home */
const chartValues = computed(() =>
dailyHomeChartValues.value.length >= 2
? dailyHomeChartValues.value
: homeChartValues.value,
)
/** هیرو: همیشه ۲۴ ساعت */
const chartPoints = computed(() => applyMarkupToSeries(dailyRawPoints.value))
const chartValues = computed(() => chartPoints.value.map((point) => point.price))
/** سکشن live: دقیقاً همان سری فعلی Home برای بازه انتخاب‌شده */
const liveSectionChartValues = computed(() => homeChartValues.value)
/** live: بازه انتخاب‌شده — محور Y از min تا max همین سری */
const liveSectionChartPoints = computed(() => {
const raw =
liveChartRange.value === 'DAILY' && liveRangeRawPoints.value.length < 2
? dailyRawPoints.value
: liveRangeRawPoints.value
return applyMarkupToSeries(raw)
})
const dayChangePercent = computed(() => {
const values = chartValues.value
@@ -1076,8 +1117,7 @@ const dayChangePercent = computed(() => {
const last = values[values.length - 1]
return ((last - first) / first) * 100
}
if (!liveUnitPrice.value) return 0
return percentPriceChanged()
return 0
})
const dayChangeAmountLabel = computed(() => {
@@ -1090,12 +1130,12 @@ const dayChangeAmountLabel = computed(() => {
})
const dayRangePrices = computed(() => {
const metal = selectedMetalType()
return resolveMetalDayRangePrices({
price: lastPrice.value,
metal,
metalRatio: CHART_METAL_MARKUP_RATIO[metal],
})
const values = chartValues.value
if (values.length === 0) return { high: 0, low: 0 }
return {
high: Math.max(...values),
low: Math.min(...values),
}
})
const liveMetrics = computed(() => {
@@ -1151,57 +1191,146 @@ const forecastTrendText = computed(() => {
return `بر اساس داده‌های ۲۴ ساعت اخیر، قیمت ${product} نسبتاً پایدار بوده است.`
})
function downsampleValues(values: number[], maxPoints: number) {
if (values.length <= maxPoints) return values
const result: number[] = []
const lastIndex = values.length - 1
function downsampleSamples(points: LandingHistorySample[], maxPoints: number) {
if (points.length <= maxPoints) return points
const result: LandingHistorySample[] = []
const lastIndex = points.length - 1
for (let i = 0; i < maxPoints; i += 1) {
const index = Math.round((i / (maxPoints - 1)) * lastIndex)
result.push(values[index])
result.push(points[index])
}
return result
}
function formatChartTimeLabel(timeMs: number) {
try {
return new Intl.DateTimeFormat('fa-IR', {
timeZone: 'Asia/Tehran',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(new Date(timeMs))
} catch {
return new Date(timeMs).toLocaleString('fa-IR')
}
}
type ChartPlotSample = {
x: number
y: number
price: number
timeMs: number
}
function buildLandingChartPaths(
values: number[],
points: LandingHistorySample[],
width = CHART_WIDTH,
height = CHART_HEIGHT,
) {
const empty = { hasData: false, linePath: '', areaPath: '', lastX: 0, lastY: height / 2 }
if (values.length < 2) return empty
const empty = {
hasData: false,
linePath: '',
areaPath: '',
lastX: 0,
lastY: height / 2,
samples: [] as ChartPlotSample[],
}
if (points.length < 2) return empty
const sampled = downsampleValues(values, MAX_CHART_POINTS)
const min = Math.min(...sampled)
const max = Math.max(...sampled)
const sampled = downsampleSamples(points, MAX_CHART_POINTS)
// محور Y: از min تا max همان بازه — از صفر شروع نمی‌شود
const prices = sampled.map((point) => point.price)
const min = Math.min(...prices)
const max = Math.max(...prices)
const range = max - min || 1
const usableHeight = height - CHART_PAD * 2
const points = sampled.map((value, index) => {
const plotSamples = sampled.map((point, index) => {
const x = (index / (sampled.length - 1)) * width
const y = CHART_PAD + (1 - (value - min) / range) * usableHeight
return [x, y] as const
const y = CHART_PAD + (1 - (point.price - min) / range) * usableHeight
return {
x,
y,
price: point.price,
timeMs: point.timeMs,
}
})
const linePath = points
.map(([x, y], index) => `${index === 0 ? 'M' : 'L'}${x.toFixed(2)} ${y.toFixed(2)}`)
const linePath = plotSamples
.map((point, index) => `${index === 0 ? 'M' : 'L'}${point.x.toFixed(2)} ${point.y.toFixed(2)}`)
.join(' ')
const last = points[points.length - 1]
const last = plotSamples[plotSamples.length - 1]
const areaPath = `${linePath} L${width} ${height} L0 ${height} Z`
return {
hasData: true,
linePath,
areaPath,
lastX: last[0],
lastY: last[1],
lastX: last.x,
lastY: last.y,
samples: plotSamples,
}
}
const landingChart = computed(() => buildLandingChartPaths(chartValues.value))
const landingChart = computed(() => buildLandingChartPaths(chartPoints.value))
const liveSectionChart = computed(() =>
buildLandingChartPaths(liveSectionChartValues.value, LIVE_CHART_WIDTH, LIVE_CHART_HEIGHT),
buildLandingChartPaths(liveSectionChartPoints.value, LIVE_CHART_WIDTH, LIVE_CHART_HEIGHT),
)
function resolveHoverFromEvent(
event: PointerEvent,
chart: { samples: ChartPlotSample[]; hasData: boolean },
width: number,
height: number,
): ChartHoverState | null {
if (!chart.hasData || chart.samples.length < 2) return null
const target = event.currentTarget as HTMLElement | null
if (!target) return null
const rect = target.getBoundingClientRect()
if (rect.width <= 0 || rect.height <= 0) return null
const ratio = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width))
const index = Math.round(ratio * (chart.samples.length - 1))
const sample = chart.samples[index]
if (!sample) return null
return {
xPercent: (sample.x / width) * 100,
yPercent: (sample.y / height) * 100,
priceLabel: `${usePriceWithoutType(sample.price)} ریال`,
timeLabel: formatChartTimeLabel(sample.timeMs),
}
}
function onHeroChartPointerMove(event: PointerEvent) {
heroChartHover.value = resolveHoverFromEvent(
event,
landingChart.value,
CHART_WIDTH,
CHART_HEIGHT,
)
}
function onHeroChartPointerLeave() {
heroChartHover.value = null
}
function onLiveChartPointerMove(event: PointerEvent) {
liveChartHover.value = resolveHoverFromEvent(
event,
liveSectionChart.value,
LIVE_CHART_WIDTH,
LIVE_CHART_HEIGHT,
)
}
function onLiveChartPointerLeave() {
liveChartHover.value = null
}
const investmentDisplay = computed(() =>
investmentAmount.value > 0 ? usePriceWithoutType(investmentAmount.value) : '',
)
@@ -1228,7 +1357,8 @@ function onInvestmentInput(event: Event) {
investmentAmount.value = raw ? Number(raw) : 0
}
async function loadLandingPrice() {
/** وضعیت باز/بسته بازار از API داخلی — اختیاری */
async function loadLandingMarketStatus() {
try {
const data = await useAxios.get<Price>('/api/price/last', {
meta: { errorMessage: 'none' },
@@ -1237,14 +1367,52 @@ async function loadLandingPrice() {
applyLastPricePayload(data)
}
} catch (error) {
console.error('Landing price fetch failed:', error)
console.error('Landing market status fetch failed:', error)
}
}
async function loadDailyHistory() {
try {
const series = await fetchZaanboxPriceHistory('DAILY')
if (series.length >= 2) {
dailyRawPoints.value = series
if (liveChartRange.value === 'DAILY') {
liveRangeRawPoints.value = series
}
}
} catch (error) {
console.error('Landing daily history fetch failed:', error)
}
}
async function loadLiveRangeHistory(range: LiveChartRange) {
liveChartLoading.value = true
try {
const series = await fetchZaanboxPriceHistory(range)
liveRangeRawPoints.value = series.length >= 2 ? series : []
if (range === 'DAILY' && series.length >= 2) {
dailyRawPoints.value = series
}
} catch (error) {
console.error(`Landing ${range} history fetch failed:`, error)
liveRangeRawPoints.value = []
} finally {
liveChartLoading.value = false
}
}
/** همان init نمودار Home: fetchPrices + metal offsets */
async function loadLandingChart() {
setChartMetal(selectedMetalType())
await Promise.all([fetchPrices(true), loadMetalChartOffsets(true)])
liveChartLoading.value = true
try {
await loadDailyHistory()
if (liveChartRange.value !== 'DAILY') {
await loadLiveRangeHistory(liveChartRange.value)
} else if (liveRangeRawPoints.value.length < 2) {
liveRangeRawPoints.value = dailyRawPoints.value
}
} finally {
liveChartLoading.value = false
}
}
let mobileNavMq: MediaQueryList | null = null
@@ -1293,10 +1461,14 @@ onMounted(() => {
syncInstallAvailability()
setLiveProduct(activeProduct.value)
void loadLandingPrice()
void loadLandingMarketStatus()
void loadLandingChart()
pricePollTimer = setInterval(() => {
void loadLandingPrice()
void loadLandingMarketStatus()
void loadDailyHistory()
if (liveChartRange.value !== 'DAILY') {
void loadLiveRangeHistory(liveChartRange.value)
}
}, PRICE_POLL_MS)
void nextTick(() => {
@@ -584,6 +584,13 @@ body.dlanding-menu-open {
height: auto;
margin-top: 14px;
&__plot {
position: relative;
width: 100%;
cursor: crosshair;
touch-action: none;
}
&__svg {
display: block;
width: 100%;
@@ -600,6 +607,64 @@ body.dlanding-menu-open {
font-weight: 500;
}
&__crosshair {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background: rgba(201, 138, 95, 0.45);
pointer-events: none;
transform: translateX(-50%);
z-index: 2;
}
&__dot {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
background: #c98a5f;
box-shadow: 0 0 0 4px rgba(201, 138, 95, 0.2);
pointer-events: none;
transform: translate(-50%, -50%);
z-index: 3;
}
&__tooltip {
position: absolute;
top: 8px;
z-index: 4;
display: flex;
flex-direction: column;
gap: 2px;
min-width: 118px;
padding: 8px 10px;
border-radius: 10px;
background: rgba(26, 43, 71, 0.92);
color: #fff;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.18);
pointer-events: none;
transform: translateX(-50%);
white-space: nowrap;
&.is-near-end {
transform: translateX(-100%);
}
strong {
font-size: 12px;
font-weight: 700;
direction: ltr;
text-align: right;
}
span {
color: rgba(255, 255, 255, 0.78);
font-size: 11px;
font-weight: 500;
}
}
&__labels {
display: flex;
align-items: center;
@@ -1439,8 +1504,11 @@ body.dlanding-menu-open {
}
&__chart {
position: relative;
width: 100%;
height: clamp(120px, 14vw, 160px);
cursor: crosshair;
touch-action: none;
}
&__chart-svg {
@@ -1460,6 +1528,64 @@ body.dlanding-menu-open {
font-weight: 500;
}
&__chart-crosshair {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background: rgba(201, 138, 95, 0.45);
pointer-events: none;
transform: translateX(-50%);
z-index: 2;
}
&__chart-dot {
position: absolute;
width: 11px;
height: 11px;
border-radius: 50%;
background: #c98a5f;
box-shadow: 0 0 0 5px rgba(201, 138, 95, 0.2);
pointer-events: none;
transform: translate(-50%, -50%);
z-index: 3;
}
&__chart-tooltip {
position: absolute;
top: 10px;
z-index: 4;
display: flex;
flex-direction: column;
gap: 3px;
min-width: 132px;
padding: 9px 11px;
border-radius: 12px;
background: rgba(26, 43, 71, 0.94);
color: #fff;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.2);
pointer-events: none;
transform: translateX(-50%);
white-space: nowrap;
&.is-near-end {
transform: translateX(-100%);
}
strong {
font-size: 13px;
font-weight: 700;
direction: ltr;
text-align: right;
}
span {
color: rgba(255, 255, 255, 0.78);
font-size: 11px;
font-weight: 500;
}
}
&__aside {
display: flex;
flex-direction: column;