|
|
@@ -18,33 +18,111 @@ const { t } = useI18n();
|
|
|
|
|
|
const chartRef = ref(null);
|
|
|
const chartInstance = shallowRef(null);
|
|
|
+let resizeObserver = null;
|
|
|
+
|
|
|
+const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
|
+const YEAR_DAY_LABELS = (() => {
|
|
|
+ const labels = [];
|
|
|
+ for (let month = 1; month <= 12; month++) {
|
|
|
+ for (let day = 1; day <= DAYS_IN_MONTH[month - 1]; day++) {
|
|
|
+ labels.push(`${month}月${day}日`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return labels;
|
|
|
+})();
|
|
|
+
|
|
|
+const VISIBLE_X_LABEL_COUNT = 30;
|
|
|
+
|
|
|
+const getCurrentYearDayIndex = () => {
|
|
|
+ const now = new Date();
|
|
|
+ let index = 0;
|
|
|
+ for (let m = 0; m < now.getMonth(); m++) {
|
|
|
+ index += DAYS_IN_MONTH[m];
|
|
|
+ }
|
|
|
+ return index + now.getDate() - 1;
|
|
|
+};
|
|
|
+
|
|
|
+/** 以 centerIndex 为中心计算 dataZoom 百分比区间 */
|
|
|
+const calcDataZoomPercent = (totalCount, visibleCount = VISIBLE_X_LABEL_COUNT) => {
|
|
|
+ const span = Math.min(visibleCount, totalCount);
|
|
|
+ const centerIndex = Math.min(getCurrentYearDayIndex(), totalCount - 1);
|
|
|
+ const halfSpan = Math.floor(span / 2);
|
|
|
+ let startIndex = centerIndex - halfSpan;
|
|
|
+ let endIndex = startIndex + span;
|
|
|
+
|
|
|
+ if (startIndex < 0) {
|
|
|
+ startIndex = 0;
|
|
|
+ endIndex = span;
|
|
|
+ }
|
|
|
+ if (endIndex > totalCount) {
|
|
|
+ endIndex = totalCount;
|
|
|
+ startIndex = Math.max(0, totalCount - span);
|
|
|
+ }
|
|
|
+
|
|
|
+ const spanPercent = (span / totalCount) * 100;
|
|
|
+ return {
|
|
|
+ start: (startIndex / totalCount) * 100,
|
|
|
+ end: (endIndex / totalCount) * 100,
|
|
|
+ spanPercent,
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const padSeriesToLength = (series, length) => {
|
|
|
+ const padded = series.slice(0, length).map((item) => {
|
|
|
+ const num = Number(item);
|
|
|
+ return Number.isNaN(num) ? null : num;
|
|
|
+ });
|
|
|
+ while (padded.length < length) {
|
|
|
+ padded.push(null);
|
|
|
+ }
|
|
|
+ return padded;
|
|
|
+};
|
|
|
+
|
|
|
+const mapSeriesValues = (series) =>
|
|
|
+ series.map((item) => {
|
|
|
+ const num = Number(item);
|
|
|
+ return Number.isNaN(num) ? null : num;
|
|
|
+ });
|
|
|
|
|
|
const normalizedData = computed(() => {
|
|
|
const data = props.chartData;
|
|
|
- if (!data?.zoneSeries?.length || !data?.standardSeries?.length) {
|
|
|
+ const zoneOnly = Boolean(data?.zoneOnly);
|
|
|
+ if (!data?.zoneSeries?.length) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (!zoneOnly && !data?.standardSeries?.length) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- const zoneSeries = data.zoneSeries.map((item) => Number(item));
|
|
|
- const standardSeries = data.standardSeries.map((item) => Number(item));
|
|
|
- const length = Math.min(zoneSeries.length, standardSeries.length);
|
|
|
- const timeLabels =
|
|
|
- data.timeLabels?.length >= length
|
|
|
- ? data.timeLabels.slice(0, length)
|
|
|
- : Array.from({ length }, () => t("agriFile.timeAxisLabel"));
|
|
|
+ const usePartialAxis = Boolean(data.usePartialAxis);
|
|
|
+ const length = usePartialAxis ? data.zoneSeries.length : YEAR_DAY_LABELS.length;
|
|
|
+ const timeLabels = YEAR_DAY_LABELS.slice(0, length);
|
|
|
+ const zoneSeries = usePartialAxis
|
|
|
+ ? mapSeriesValues(data.zoneSeries)
|
|
|
+ : padSeriesToLength(mapSeriesValues(data.zoneSeries), length);
|
|
|
+ const standardSeries = zoneOnly
|
|
|
+ ? []
|
|
|
+ : padSeriesToLength(mapSeriesValues(data.standardSeries), length);
|
|
|
|
|
|
let highlightIndex = data.highlightIndex;
|
|
|
+ const validValues = zoneSeries
|
|
|
+ .map((value, index) => ({ value, index }))
|
|
|
+ .filter((item) => item.value != null);
|
|
|
if (highlightIndex == null || highlightIndex < 0 || highlightIndex >= length) {
|
|
|
- highlightIndex = zoneSeries.reduce(
|
|
|
- (maxIndex, value, index, list) => (value > list[maxIndex] ? index : maxIndex),
|
|
|
- 0
|
|
|
- );
|
|
|
+ highlightIndex =
|
|
|
+ validValues.length > 0
|
|
|
+ ? validValues.reduce((max, item) =>
|
|
|
+ item.value > max.value ? item : max
|
|
|
+ ).index
|
|
|
+ : 0;
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
timeLabels,
|
|
|
- zoneSeries: zoneSeries.slice(0, length),
|
|
|
- standardSeries: standardSeries.slice(0, length),
|
|
|
+ zoneSeries,
|
|
|
+ standardSeries,
|
|
|
+ zoneOnly,
|
|
|
+ zoneAreaFillDown: Boolean(data.zoneAreaFillDown),
|
|
|
highlightIndex,
|
|
|
};
|
|
|
});
|
|
|
@@ -52,9 +130,26 @@ const normalizedData = computed(() => {
|
|
|
const formatAxisValue = (value) => {
|
|
|
const num = Number(value);
|
|
|
if (Number.isNaN(num)) return value;
|
|
|
- if (num === 0) return "0";
|
|
|
- if (num === 1.2) return "1.2";
|
|
|
- return Number.isInteger(num) ? String(num) : num.toFixed(1);
|
|
|
+ return num.toFixed(2);
|
|
|
+};
|
|
|
+
|
|
|
+const calcYAxisRange = (zoneSeries, standardSeries) => {
|
|
|
+ const values = [...zoneSeries, ...standardSeries].filter(
|
|
|
+ (item) => item != null && !Number.isNaN(item)
|
|
|
+ );
|
|
|
+ if (!values.length) {
|
|
|
+ return { min: 0, max: 1, interval: 0.2 };
|
|
|
+ }
|
|
|
+
|
|
|
+ const min = Math.min(...values);
|
|
|
+ const max = Math.max(...values);
|
|
|
+ const span = max - min || 0.1;
|
|
|
+ const padding = span * 0.05;
|
|
|
+ const axisMin = Math.floor((min - padding) * 100) / 100;
|
|
|
+ const axisMax = Math.ceil((max + padding) * 100) / 100;
|
|
|
+ const interval = Math.max(0.01, Math.ceil(((axisMax - axisMin) / 5) * 100) / 100);
|
|
|
+
|
|
|
+ return { min: axisMin, max: axisMax, interval };
|
|
|
};
|
|
|
|
|
|
const formatHighlightValue = (value) => {
|
|
|
@@ -65,16 +160,127 @@ const formatHighlightValue = (value) => {
|
|
|
|
|
|
const buildOption = (data) => {
|
|
|
const highlightValue = data.zoneSeries[data.highlightIndex];
|
|
|
+ const totalCount = data.timeLabels.length;
|
|
|
+ const dataZoomRange = calcDataZoomPercent(totalCount);
|
|
|
+ const yAxisRange = calcYAxisRange(
|
|
|
+ data.zoneSeries,
|
|
|
+ data.zoneOnly ? [] : data.standardSeries
|
|
|
+ );
|
|
|
+ const zoneAreaOrigin = data.zoneAreaFillDown ? "start" : "auto";
|
|
|
+
|
|
|
+ const series = [
|
|
|
+ {
|
|
|
+ name: "zone",
|
|
|
+ type: "line",
|
|
|
+ smooth: true,
|
|
|
+ symbol: "none",
|
|
|
+ lineStyle: {
|
|
|
+ width: 2,
|
|
|
+ color: "#2199F8",
|
|
|
+ },
|
|
|
+ areaStyle: {
|
|
|
+ origin: zoneAreaOrigin,
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ { offset: 0, color: "rgba(33, 153, 248, 0.7)" },
|
|
|
+ { offset: 1, color: "rgba(33, 153, 248, 0)" },
|
|
|
+ ]),
|
|
|
+ },
|
|
|
+ data: data.zoneSeries,
|
|
|
+ markPoint:
|
|
|
+ highlightValue != null
|
|
|
+ ? {
|
|
|
+ symbol: "circle",
|
|
|
+ symbolSize: 7,
|
|
|
+ itemStyle: {
|
|
|
+ color: "#ffffff",
|
|
|
+ borderColor: "#2199F8",
|
|
|
+ borderWidth: 2,
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ position: "top",
|
|
|
+ distance: 6,
|
|
|
+ color: "#2199F8",
|
|
|
+ fontSize: 12,
|
|
|
+ formatter: () => `[${formatHighlightValue(highlightValue)}]`,
|
|
|
+ },
|
|
|
+ data: [
|
|
|
+ {
|
|
|
+ coord: [data.highlightIndex, highlightValue],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ : undefined,
|
|
|
+ z: 3,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (!data.zoneOnly && data.standardSeries?.length) {
|
|
|
+ series.push({
|
|
|
+ name: "standard",
|
|
|
+ type: "line",
|
|
|
+ smooth: true,
|
|
|
+ symbol: "none",
|
|
|
+ lineStyle: {
|
|
|
+ width: 2,
|
|
|
+ color: "#9FD2FA",
|
|
|
+ },
|
|
|
+ data: data.standardSeries,
|
|
|
+ z: 2,
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
return {
|
|
|
animation: false,
|
|
|
grid: {
|
|
|
left: 4,
|
|
|
right: 8,
|
|
|
- top: 14,
|
|
|
- bottom: 5,
|
|
|
+ top: 20,
|
|
|
+ bottom: 32,
|
|
|
containLabel: true,
|
|
|
},
|
|
|
+ dataZoom: [
|
|
|
+ {
|
|
|
+ type: "inside",
|
|
|
+ xAxisIndex: 0,
|
|
|
+ start: dataZoomRange.start,
|
|
|
+ end: dataZoomRange.end,
|
|
|
+ zoomLock: true,
|
|
|
+ minSpan: dataZoomRange.spanPercent,
|
|
|
+ maxSpan: dataZoomRange.spanPercent,
|
|
|
+ zoomOnMouseWheel: false,
|
|
|
+ moveOnMouseMove: true,
|
|
|
+ preventDefaultMouseMove: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: "slider",
|
|
|
+ xAxisIndex: 0,
|
|
|
+ start: dataZoomRange.start,
|
|
|
+ end: dataZoomRange.end,
|
|
|
+ zoomLock: true,
|
|
|
+ minSpan: dataZoomRange.spanPercent,
|
|
|
+ maxSpan: dataZoomRange.spanPercent,
|
|
|
+ height: 18,
|
|
|
+ bottom: 4,
|
|
|
+ brushSelect: false,
|
|
|
+ showDetail: false,
|
|
|
+ borderColor: "transparent",
|
|
|
+ backgroundColor: "#F5F5F5",
|
|
|
+ fillerColor: "rgba(33, 153, 248, 0.15)",
|
|
|
+ handleStyle: {
|
|
|
+ color: "#2199F8",
|
|
|
+ borderColor: "#2199F8",
|
|
|
+ },
|
|
|
+ dataBackground: {
|
|
|
+ lineStyle: { color: "#E0E0E0" },
|
|
|
+ areaStyle: { color: "#F0F0F0" },
|
|
|
+ },
|
|
|
+ selectedDataBackground: {
|
|
|
+ lineStyle: { color: "#2199F8" },
|
|
|
+ areaStyle: { color: "rgba(33, 153, 248, 0.1)" },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
xAxis: {
|
|
|
type: "category",
|
|
|
boundaryGap: false,
|
|
|
@@ -90,9 +296,9 @@ const buildOption = (data) => {
|
|
|
},
|
|
|
yAxis: {
|
|
|
type: "value",
|
|
|
- min: 0,
|
|
|
- max: 1.2,
|
|
|
- interval: 0.2,
|
|
|
+ min: yAxisRange.min,
|
|
|
+ max: yAxisRange.max,
|
|
|
+ interval: yAxisRange.interval,
|
|
|
axisLine: { show: false },
|
|
|
axisTick: { show: false },
|
|
|
axisLabel: {
|
|
|
@@ -106,85 +312,54 @@ const buildOption = (data) => {
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
- series: [
|
|
|
- {
|
|
|
- name: "zone",
|
|
|
- type: "line",
|
|
|
- smooth: true,
|
|
|
- symbol: "none",
|
|
|
- lineStyle: {
|
|
|
- width: 2,
|
|
|
- color: "#2199F8",
|
|
|
- },
|
|
|
- areaStyle: {
|
|
|
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
- { offset: 0, color: "rgba(33, 153, 248, 0.7)" },
|
|
|
- { offset: 1, color: "rgba(33, 153, 248, 0)" },
|
|
|
- ]),
|
|
|
- },
|
|
|
- data: data.zoneSeries,
|
|
|
- markPoint: {
|
|
|
- symbol: "circle",
|
|
|
- symbolSize: 7,
|
|
|
- itemStyle: {
|
|
|
- color: "#ffffff",
|
|
|
- borderColor: "#2199F8",
|
|
|
- borderWidth: 2,
|
|
|
- },
|
|
|
- label: {
|
|
|
- show: true,
|
|
|
- position: "top",
|
|
|
- distance: 6,
|
|
|
- color: "#2199F8",
|
|
|
- fontSize: 12,
|
|
|
- formatter: () => `[${formatHighlightValue(highlightValue)}]`,
|
|
|
- },
|
|
|
- data: [
|
|
|
- {
|
|
|
- coord: [data.highlightIndex, highlightValue],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- z: 3,
|
|
|
- },
|
|
|
- {
|
|
|
- name: "standard",
|
|
|
- type: "line",
|
|
|
- smooth: true,
|
|
|
- symbol: "none",
|
|
|
- lineStyle: {
|
|
|
- width: 2,
|
|
|
- color: "#9FD2FA",
|
|
|
- },
|
|
|
- data: data.standardSeries,
|
|
|
- z: 2,
|
|
|
- },
|
|
|
- ],
|
|
|
+ series,
|
|
|
};
|
|
|
};
|
|
|
|
|
|
+const resizeChart = () => {
|
|
|
+ chartInstance.value?.resize();
|
|
|
+};
|
|
|
+
|
|
|
const renderChart = () => {
|
|
|
if (!chartRef.value || !normalizedData.value) return;
|
|
|
|
|
|
+ const { clientWidth, clientHeight } = chartRef.value;
|
|
|
+ if (!clientWidth || !clientHeight) return;
|
|
|
+
|
|
|
if (!chartInstance.value) {
|
|
|
chartInstance.value = echarts.init(chartRef.value);
|
|
|
}
|
|
|
|
|
|
chartInstance.value.setOption(buildOption(normalizedData.value), true);
|
|
|
+ nextTick(resizeChart);
|
|
|
};
|
|
|
|
|
|
-const resizeChart = () => {
|
|
|
- chartInstance.value?.resize();
|
|
|
+const setupResizeObserver = () => {
|
|
|
+ if (!chartRef.value || typeof ResizeObserver === "undefined") return;
|
|
|
+
|
|
|
+ resizeObserver?.disconnect();
|
|
|
+ resizeObserver = new ResizeObserver(() => {
|
|
|
+ if (!normalizedData.value) return;
|
|
|
+ if (!chartInstance.value) {
|
|
|
+ renderChart();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ resizeChart();
|
|
|
+ });
|
|
|
+ resizeObserver.observe(chartRef.value);
|
|
|
};
|
|
|
|
|
|
onMounted(() => {
|
|
|
nextTick(() => {
|
|
|
renderChart();
|
|
|
+ setupResizeObserver();
|
|
|
});
|
|
|
window.addEventListener("resize", resizeChart);
|
|
|
});
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
+ resizeObserver?.disconnect();
|
|
|
+ resizeObserver = null;
|
|
|
window.removeEventListener("resize", resizeChart);
|
|
|
chartInstance.value?.dispose();
|
|
|
chartInstance.value = null;
|
|
|
@@ -204,7 +379,8 @@ watch(
|
|
|
<style lang="scss" scoped>
|
|
|
.remote-sensing-line-chart {
|
|
|
width: 100%;
|
|
|
- height: 180px;
|
|
|
+ min-width: 0;
|
|
|
+ height: 194px;
|
|
|
margin-top: 10px;
|
|
|
}
|
|
|
</style>
|