205 lines
4.5 KiB
Vue
205 lines
4.5 KiB
Vue
<script setup>
|
||
import { ref, watch } from 'vue'
|
||
import {
|
||
Chart as ChartJS,
|
||
Title,
|
||
Tooltip,
|
||
Legend,
|
||
LineElement,
|
||
PointElement,
|
||
CategoryScale,
|
||
TimeScale,
|
||
LinearScale,
|
||
} from 'chart.js'
|
||
import { Line } from 'vue-chartjs'
|
||
import 'chartjs-adapter-date-fns'
|
||
|
||
ChartJS.register(
|
||
Title,
|
||
Tooltip,
|
||
Legend,
|
||
LineElement,
|
||
PointElement,
|
||
CategoryScale,
|
||
TimeScale,
|
||
LinearScale,
|
||
)
|
||
|
||
const props = defineProps({
|
||
chartsData: {
|
||
type: [Object, null],
|
||
required: true,
|
||
default: null,
|
||
},
|
||
})
|
||
|
||
const originColors = {
|
||
surugaya: '#2d3081',
|
||
mandarake: '#924646',
|
||
}
|
||
|
||
const chartData = ref({
|
||
datasets: [],
|
||
})
|
||
|
||
const showPlaceholder = ref(false)
|
||
const placeholderMessage = ref('')
|
||
|
||
function transformToChartJSSeries(payload) {
|
||
const datasets = []
|
||
for (const origin of payload.origins) {
|
||
if (!origin.Prices || origin.Prices.length === 0) continue
|
||
|
||
const data = origin.Prices.filter((p) => p.value != null)
|
||
.map((p) => {
|
||
let y = p.value
|
||
if (typeof y === 'string') {
|
||
y = parseFloat(y)
|
||
}
|
||
const x =
|
||
typeof p.created_at === 'number' ? p.created_at * 1000 : new Date(p.created_at).getTime()
|
||
|
||
return {
|
||
x,
|
||
y: typeof y === 'number' && !isNaN(y) ? y : null,
|
||
}
|
||
})
|
||
.filter((p) => p.y !== null)
|
||
.sort((a, b) => a.x - b.x)
|
||
|
||
if (data.length === 0) continue
|
||
|
||
const color = originColors[origin.origin] || '#000000'
|
||
|
||
datasets.push({
|
||
label: origin.origin,
|
||
data,
|
||
borderColor: color,
|
||
backgroundColor: color + '20',
|
||
fill: false,
|
||
pointRadius: 4,
|
||
pointHoverRadius: 6,
|
||
type: 'line',
|
||
})
|
||
}
|
||
return datasets
|
||
}
|
||
|
||
const chartOptions = {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
animation: false,
|
||
plugins: {
|
||
legend: { display: true },
|
||
tooltip: {
|
||
mode: 'index',
|
||
intersect: false,
|
||
callbacks: {
|
||
title: (context) => {
|
||
const date = new Date(context[0].parsed.x)
|
||
return date.toLocaleString('ru-RU', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
})
|
||
},
|
||
},
|
||
},
|
||
},
|
||
scales: {
|
||
x: {
|
||
type: 'time',
|
||
time: {
|
||
tooltipFormat: 'dd.MM.yyyy HH:mm',
|
||
unit: 'day',
|
||
displayFormats: { day: 'dd MMM' },
|
||
},
|
||
title: { display: true, text: 'Date' },
|
||
},
|
||
y: {
|
||
title: { display: true, text: 'Price' },
|
||
ticks: { precision: 0 },
|
||
},
|
||
},
|
||
elements: {
|
||
line: { borderWidth: 2, tension: 0, spanGaps: true },
|
||
point: { radius: 4, hoverRadius: 6 },
|
||
},
|
||
interaction: {
|
||
mode: 'nearest',
|
||
axis: 'x',
|
||
intersect: false,
|
||
},
|
||
}
|
||
|
||
watch(
|
||
() => props.chartsData,
|
||
(newData) => {
|
||
// Сброс состояния
|
||
showPlaceholder.value = false
|
||
placeholderMessage.value = ''
|
||
|
||
// Случай 1: null или undefined
|
||
if (newData === null || newData === undefined) {
|
||
showPlaceholder.value = true
|
||
placeholderMessage.value = 'No prices found'
|
||
chartData.value = { datasets: [] }
|
||
return
|
||
}
|
||
|
||
// Случай 2: явная ошибка в объекте
|
||
if (typeof newData === 'object' && newData.error) {
|
||
showPlaceholder.value = true
|
||
placeholderMessage.value = newData.error
|
||
chartData.value = { datasets: [] }
|
||
return
|
||
}
|
||
|
||
// Случай 3: нормальный объект — пробуем преобразовать
|
||
let datasets = []
|
||
if (newData.origins && Array.isArray(newData.origins)) {
|
||
datasets = transformToChartJSSeries(newData)
|
||
}
|
||
|
||
// 🔥 Ключевое изменение: проверяем, есть ли хоть один датасет
|
||
if (datasets.length === 0) {
|
||
showPlaceholder.value = true
|
||
placeholderMessage.value = 'No prices found'
|
||
chartData.value = { datasets: [] }
|
||
} else {
|
||
showPlaceholder.value = false
|
||
chartData.value = { datasets }
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
</script>
|
||
|
||
<template>
|
||
<div class="chart-container" style="height: 100%; width: 100%">
|
||
<div v-if="showPlaceholder" class="error-placeholder">
|
||
{{ placeholderMessage }}
|
||
</div>
|
||
<Line v-else :data="chartData" :options="chartOptions" style="height: 100%; width: 100%" />
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.chart-container {
|
||
position: relative;
|
||
min-width: 80%;
|
||
}
|
||
|
||
.error-placeholder {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
width: 100%;
|
||
color: #666;
|
||
font-size: 1.2rem;
|
||
text-align: center;
|
||
}
|
||
</style>
|