我正在编写 vue 代码来渲染热图。 y 轴可以根据日历选择进行更改。它在加载时正确渲染。但如果我更改日历上的日期,则只会呈现 y 轴上的最后一个类别。
当我查看
this.chart
时,它拥有所有数据,但只是未渲染所有数据。
我的代码:
components/BaseElements.vue
<template>
<div id="filters" class="m-2">
<DateRangePicker
ref="picker"
v-model="dateRange"
opens="inline"
show-dropdowns="true"
auto-apply="true"
single-date-picker="range"
:date-range="dateRange"
:ranges="customRanges"
@select="handleDateRangeChange"
>
</DateRangePicker>
</div>
</template>
<script>
import "bootstrap/dist/css/bootstrap.min.css";
import DateRangePicker from "vue3-daterange-picker";
import moment from "moment";
import Multiselect from "vue-multiselect";
import axios from "axios";
export default {
components: { DateRangePicker, Multiselect },
props: {
startDate: {
type: String,
required: true,
},
endDate: {
type: String,
required: true,
},
},
data() {
return {
dateRange: {
startDate: this.startDate,
endDate: this.endDate,
},
};
},
mounted() {
this.loadData(this.dateRange.startDate, this.dateRange.endDate);
},
methods: {
handleDateRangeChange(value) {
this.dateRange.startDate = value.startDate;
this.dateRange.endDate = value.endDate;
this.$emit("date-range-changed", {
startDate: this.dateRange.startDate,
endDate: this.dateRange.endDate,
divSelected: this.selectedDivisions,
serviceSelected: this.selectedServices,
});
},
loadData(startDate, endDate) {
const baseURL ="http://localhost:3001/api";
if (!baseURL) {
console.error("VUE_APP_API_BASE_URL is not defined");
return;
}
const url = `${baseURL}/filters?startDate=${moment(startDate).format(
"YYYY-MM-DD"
)}&endDate=${moment(endDate).format("YYYY-MM-DD")}`;
},
},
};
</script>
<style>
/* Add your component-specific styles here */
</style>
components/MainTab.vue
<template>
<div id="webs" class="m-2 d-inline-flex">
<BaseElements
:start-date="startDate"
:end-date="endDate"
@date-range-changed="updateDateRange"
/>
<div style="width: 100%" class="d-flex flex-column">
<HeatMap id="heatMap" style="width: 100%" />
</div>
</div>
</template>
<script>
import "bootstrap/dist/css/bootstrap.min.css";
import HeatMap from "./WebTab/HeatMap.vue";
export default {
components: { HeatMap },
};
</script>
<style>
/* Add your component-specific styles here */
</style>
components/MainHeatMap.vue
<template>
<div>
<highcharts
:constructor-type="'chart'"
class="hc"
:options="chartOptions"
ref="chart"
></highcharts>
</div>
</template>
<script>
import moment from "moment";
export default {
props: {
chartData: {
type: Array,
default: () => [],
},
divisionProp: {
type: String,
default: "division",
},
serviceProp: {
type: String,
default: "service",
},
profileNameProp: {
type: String,
default: "profile_name",
},
reportDateProp: {
type: String,
default: "report_date",
},
videoViewsProp: {
type: String,
default: "insight_video_views",
},
},
data() {
const vm = this;
return {
chartOptions: {
chart: {
backgroundColor: "#fbfcf8",
type: "heatmap",
plotBorderWidth: 1,
height: "100%",
events: {
redraw: function () {
if (!vm.initialLoad) {
console.log(this.series[0].data);
console.log(vm.seriesData);
}
},
drilldown: function (e) {
if (e.seriesOptions !== undefined) {
const drilldowns = [
...new Set(e.seriesOptions.data.map((d) => d.name)),
];
this.xAxis[0].categories = drilldowns;
}
},
drillup: function (e) {
const drillups = [
...new Set(e.seriesOptions.data.map((d) => d.name)),
];
this.xAxis[0].categories = drillups;
},
},
},
title: {
text: "Views",
align: "center",
},
exporting: {
enabled: true,
buttons: {
contextButton: {
menuItems: [
"viewFullscreen",
"printChart",
"separator",
"downloadPNG",
"downloadJPEG",
"separator",
"downloadPDF",
"downloadCSV",
],
},
},
},
credits: {
enabled: false,
},
legend: {
enabled: true,
itemStyle: {
color: "#000",
},
},
rangeSelector: {
enabled: false,
},
navigator: {
enabled: false,
},
scrollbar: {
enabled: false,
},
xAxis: {
categories: [],
},
yAxis: {
title: "",
categories: [],
reversed: false,
},
accessibility: {
point: {
descriptionFormat:
"{(add index 1)}. " +
"{series.xAxis.categories.(x)} " +
"{series.yAxis.categories.(y)}, {value}.",
},
},
colorAxis: {
min: 0,
minColor: "#FFFFFF",
maxColor: "#DA70D6",
},
tooltip: {
style: { color: "#000" },
formatter() {
const addCommas = (value) => {
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
return (
`<b>${this.series.xAxis.categories[this.point.x]}</b> had<br>` +
`<b>${addCommas(
this.point.value
)}</b> video views in the week of <br>` +
`<b>${this.series.yAxis.categories[this.point.y]}</b>`
);
},
},
series: [
{
name: "Video Views",
borderWidth: 1,
data: [],
dataLabels: {
enabled: true,
color: "#000000",
formatter() {
const numericSymbols = ["K", "M", "G", "T", "P", "E"];
if (this.point.value >= 1000) {
const symbolIndex = Math.floor(
Math.log10(this.point.value) / 3
);
const scaledValue =
this.point.value / Math.pow(1000, symbolIndex);
const symbol = numericSymbols[symbolIndex - 1];
return `${Math.ceil(scaledValue)}${symbol}`;
}
return this.point.value;
},
},
},
],
drilldown: {
series: [],
},
responsive: {
rules: [
{
condition: {
maxWidth: 500,
},
chartOptions: {
yAxis: {
labels: {
format: "{substr value 0 1}",
},
},
},
},
],
},
},
fullData: [],
seriesData: [],
initialLoad: true,
};
},
watch: {
chartData: {
handler(newChartData, oldChartData) {
if (newChartData !== oldChartData) {
this.updateChartData(newChartData);
}
},
deep: true,
},
},
created() {
this.updateChartData(this.chartData);
},
methods: {
updateChartData(data) {
this.fullData = data;
const filteredData = this.fullData.map((item) => ({
division: item[this.divisionProp],
service: item[this.serviceProp],
profile_name: item[this.profileNameProp],
report_date: item[this.reportDateProp],
insight_video_views: item[this.videoViewsProp],
}));
const generateDateRanges = (fullData, groupBy) => {
const dates = [...new Set(fullData.map((dt) => dt.report_date))].sort();
const startDate = moment(dates[0]);
const endDate = moment(dates[dates.length - 1]);
const diffDays = endDate.diff(startDate, "days");
const diffMonths = endDate.diff(startDate, "months");
const sumInsightVideoViews = (start, end, groupValue) =>
fullData
.filter(
(dt) =>
dt[groupBy] === groupValue &&
moment(dt.report_date).isBetween(start, end, null, "[)")
)
.reduce((sum, dt) => sum + dt.insight_video_views, 0);
const uniqueGroupValues = [
...new Set(fullData.map((dt) => dt[groupBy])),
];
const ranges = [];
uniqueGroupValues.forEach((groupValue) => {
let current = startDate.clone();
while (current.isBefore(endDate) || current.isSame(endDate, "day")) {
let next;
let format;
if (diffDays <= 14) {
current.startOf("day");
next = current.clone().add(1, "day");
format = "ll";
} else if (diffMonths <= 6) {
current.startOf("week");
next = current.clone().add(1, "week");
format = "ll";
} else {
current.startOf("month");
next = current.clone().add(1, "month");
format = "MMMM, YYYY";
}
const division = fullData.find(
(dt) => dt[groupBy] === groupValue
).division;
ranges.push({
[groupBy]: groupValue,
division: division,
date: current.format(format),
insight_video_views: sumInsightVideoViews(
current,
next,
groupValue
),
});
current = next;
}
});
return ranges;
};
let yAxis = [
...new Set(
generateDateRanges(filteredData, "division").map((d) => d.date)
),
];
let xAxis = [
...new Set(
generateDateRanges(filteredData, "division").map((d) => d.division)
),
];
this.chartOptions.yAxis.categories = yAxis;
this.chartOptions.xAxis.categories = xAxis;
const groupBy = (array, property) => {
return array.reduce((acc, item) => {
const key = item[property];
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(item);
return acc;
}, {});
};
// Group by division
const groupedByDivision = groupBy(
generateDateRanges(filteredData, "division"),
"division"
);
// Group by service
const groupedByService = groupBy(
generateDateRanges(filteredData, "service"),
"service"
);
const seriesData = Object.keys(groupedByDivision).flatMap((division) => {
const divisionIndex = xAxis.indexOf(division);
return groupedByDivision[division].map((range, dateIndex) => ({
x: divisionIndex,
y: dateIndex,
value: range.insight_video_views,
name: division,
id: division,
drilldown: division,
}));
});
this.seriesData = seriesData;
this.chartOptions.series[0].data = this.seriesData;
const transformDrill = (data) => {
const result = {};
const serviceIndexMap = {};
for (const [service, items] of Object.entries(data)) {
for (let index = 0; index < items.length; index++) {
const { division, insight_video_views } = items[index];
if (!result[division]) result[division] = [];
if (!serviceIndexMap[division]) serviceIndexMap[division] = {};
if (serviceIndexMap[division][service] === undefined) {
serviceIndexMap[division][service] = Object.keys(
serviceIndexMap[division]
).length;
}
const x = serviceIndexMap[division][service];
result[division].push({
x,
y: index,
value: insight_video_views,
name: service,
});
}
}
return Object.entries(result).map(([id, data]) => ({
id,
borderWidth: 1,
dataLabels: {
enabled: true,
color: "#000000",
formatter() {
const numericSymbols = ["K", "M", "G", "T", "P", "E"];
if (this.point.value >= 1000) {
const symbolIndex = Math.floor(
Math.log10(this.point.value) / 3
);
const scaledValue =
this.point.value / Math.pow(1000, symbolIndex);
const symbol = numericSymbols[symbolIndex - 1];
return `${Math.ceil(scaledValue)}${symbol}`;
}
return this.point.value;
},
},
data,
}));
};
const drilldownData = transformDrill(groupedByService);
this.chartOptions.drilldown.series = drilldownData;
console.log(this.chartOptions);
console.log(this.initialLoad);
this.initialLoad = false;
},
},
};
</script>
<style>
.highcharts-heatmap-series > .highcharts-drilldown-point {
cursor: default !important;
}
.highcharts-heatmap-series > .highcharts-drilldown-data-label {
cursor: default !important;
}
.highcharts-heatmap-series > .highcharts-drilldown-data-label > text {
text-decoration: none !important;
color: #000000 !important;
fill: #000000 !important;
}
</style>
问题是
console.log(this.chartOptions);
返回更新后的yAxis
和更新后的this.chartOptions.series[0].data
。所以不确定为什么它不能正确渲染。 this.series[0].setData(vm.seriesData);
只是没有在页面上加载任何图表。
更新系列数据的最佳方法是使用图表引用更新整个图表对象。以下是包含数据更新的热图系列演示:https://codesandbox.io/p/sandbox/gifted-andras-d7gl3d