From fc99bac4498b7b283e24714a265676c8df9cc558 Mon Sep 17 00:00:00 2001 From: Cindy Chang Date: Fri, 7 Jun 2024 10:37:28 +0800 Subject: [PATCH] WIP customizing x tick according to time difference --- src/module/timeLabel.js | 11 ++ src/views/Compare/Dashboard/index.vue | 160 +++++++++++++++-------- src/views/Discover/Performance/index.vue | 62 ++++----- 3 files changed, 151 insertions(+), 82 deletions(-) diff --git a/src/module/timeLabel.js b/src/module/timeLabel.js index f911a49..b436c9f 100644 --- a/src/module/timeLabel.js +++ b/src/module/timeLabel.js @@ -215,3 +215,14 @@ export const setTimeStringFormatBaseOnTimeDifference = (minTimeStamp, maxTimeSta return dateFormat; }; + +/** + * Converts an array of Unix timestamps to formatted date strings based on the + * specified format for use as axis ticks. + * 根據指定的格式將 Unix 時間戳數組轉換為軸標籤的格式化日期字符串。 + * @param {Array} timeStampArr + * @param {string} timeFormat For example, 'MM/DD' + */ +export const mapTimestampToAxisTicksByFormat = (timeStampArr, timeFormat) => { + return timeStampArr.map(ts => moment(ts).format(timeFormat)); +}; \ No newline at end of file diff --git a/src/views/Compare/Dashboard/index.vue b/src/views/Compare/Dashboard/index.vue index dff2b63..4c0149d 100644 --- a/src/views/Compare/Dashboard/index.vue +++ b/src/views/Compare/Dashboard/index.vue @@ -9,7 +9,7 @@

{{ i18next.t("Compare.timeUsage") }}

@@ -68,8 +70,9 @@ {{ contentData.avgProcessTimeByTask.title }} - + @@ -152,7 +155,10 @@ import LoadingStore from '@/stores/loading.js'; import CompareStore from '@/stores/compare.js'; import SidebarStates from '@/components/Compare/SidebarStates.vue'; import { setLineChartData } from '@/module/setChartData.js'; -import { simpleTimeLabel, followTimeLabel, dateLabel } from '@/module/timeLabel.js'; +import { simpleTimeLabel, followTimeLabel, dateLabel, + setTimeStringFormatBaseOnTimeDifference, + mapTimestampToAxisTicksByFormat, + } from '@/module/timeLabel.js'; import i18next from '@/i18n/i18n'; const knownLayoutChartOption = { @@ -403,7 +409,7 @@ export default { * 手刻折線圖 x label 時間刻度 * @param { object } valueData {min: '2022-02-20T19:54:12', max: '2023-11-27T07:21:53'} */ - xLabelsData(valueData) { + setXLabelsData(valueData) { let min = new Date(valueData.min).getTime(); let max = new Date(valueData.max).getTime(); let numPoints = 12; @@ -434,7 +440,7 @@ export default { * @param { object } chartData chart data * @param { object } content titels 標題文字 * @param { string } yUnit y 軸單位 'date' | 'count',可傳入以上任一。 - * @returns { [setData, setOption] } 這兩者為符合 PriveVue 圖表格式的數據資料 + * @returns { [primeVueSetData, primeVueSetOption] } 這兩者為符合 primeVue 圖表格式的數據資料 */ getLineChart(chartData, content, yUnit) { let datasetsPrimary; @@ -442,33 +448,45 @@ export default { let minX = chartData.x_axis.min; let maxX = chartData.x_axis.max; let maxY = chartData.y_axis.max; - let xData; + let xLabelData; let labelPrimary = chartData.data[0].label; let labelSecondary = chartData.data[1].label; - let setData = {}; - let setOption = {}; + let primeVueSetData = {}; + let primeVueSetOption = {}; const getMoment = (time)=> { return this.$moment(time).format('YYYY/M/D hh:mm:ss') }; const getSimpleTimeLabel = simpleTimeLabel; const getFollowTimeLabel = followTimeLabel; + // fabricate x label according to y unit switch (yUnit) { case 'date': datasetsPrimary = setLineChartData(chartData.data[0].data, chartData.x_axis.max, chartData.x_axis.min, false, chartData.y_axis.max, chartData.y_axis.min); datasetsSecondary = setLineChartData(chartData.data[1].data, chartData.x_axis.max, chartData.x_axis.min, false, chartData.y_axis.max, chartData.y_axis.min); - xData = this.xLabelsData(chartData.x_axis); + xLabelData = this.setXLabelsData(chartData.x_axis); break; case 'count': // 次數 10 個點 datasetsPrimary = chartData.data[0].data; datasetsSecondary = chartData.data[1].data; - xData = chartData.data[0].data.map(item => new Date(item.x).getTime()); + xLabelData = chartData.data[0].data.map(item => new Date(item.x).getTime()); break; } - setData = { - labels: xData, + + // Customize X axis ticks due to different differences between min and max of data group + const formatToSet = setTimeStringFormatBaseOnTimeDifference(minX, maxX); + const ticksOfXAxis = mapTimestampToAxisTicksByFormat(xLabelData, formatToSet); + const customizedScaleOption = this.getCustomizedScaleOption( + knownScaleLineChartOptions, { + customizeOptions: { + content, ticksOfXAxis, + } + }); + + primeVueSetData = { + labels: xLabelData, datasets: [ { label: labelPrimary, @@ -488,7 +506,7 @@ export default { } ] }; - setOption = { + primeVueSetOption = { responsive: true, maintainAspectRatio: false, layout: { @@ -514,11 +532,11 @@ export default { display: false, }, }, - scales: this.customizeScaleChartOptionTitleByContent(knownScaleLineChartOptions, content), + scales: customizedScaleOption, }; switch (yUnit) { case 'date': - setOption.plugins.tooltip.callbacks.label = function(context) { + primeVueSetOption.plugins.tooltip.callbacks.label = function(context) { let value = getSimpleTimeLabel(context.parsed.y, 2); switch (context.datasetIndex) { @@ -528,15 +546,15 @@ export default { return `${labelSecondary}: ${value}`; } }; - setOption.scales.x.min = minX; - setOption.scales.x.max = maxX; - setOption.scales.y.ticks.callback = function (value, index, ticks) { + primeVueSetOption.scales.x.min = minX; + primeVueSetOption.scales.x.max = maxX; + primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) { return getFollowTimeLabel(value, maxY, 1) } break; case 'count': - setOption.scales.y.ticks.precision = 0; // y 軸顯示小數點後 0 位 - setOption.plugins.tooltip.callbacks.label = function(context) { + primeVueSetOption.scales.y.ticks.precision = 0; // y 軸顯示小數點後 0 位 + primeVueSetOption.plugins.tooltip.callbacks.label = function(context) { let value = context.parsed.y; switch (context.datasetIndex) { @@ -549,7 +567,7 @@ export default { break; } - return [setData, setOption] + return [primeVueSetData, primeVueSetOption] }, /** * 建立長條圖 @@ -569,8 +587,8 @@ export default { let xDataSecondary; let yDataSecondary; let labelSecondary = chartData.data[1].label; - let setData = {}; - let setOption = {}; + let primeVueSetData = {}; + let primeVueSetOption = {}; // 轉為百分比 datasetsPrimary = datasetsPrimary.map(value => { @@ -590,7 +608,7 @@ export default { xDataSecondary = datasetsSecondary.map(i => i.x); yDataSecondary = datasetsSecondary.map(i => i.y); - setData = { + primeVueSetData = { labels: xDataPrimary, datasets: [ { @@ -605,7 +623,7 @@ export default { }, ] }; - setOption = { + primeVueSetOption = { responsive: true, maintainAspectRatio: false, layout: knownLayoutChartOption, @@ -644,7 +662,7 @@ export default { scales: this.customizeScaleChartOptionTitleByContent(knownScaleBarChartOptions, content), }; - return [setData, setOption] + return [primeVueSetData, primeVueSetOption] }, /** * 建立水平長條圖 @@ -659,8 +677,8 @@ export default { const getFollowTimeLabel = followTimeLabel; const labelPrimary = chartData.data[0].label; const labelSecondary = chartData.data[1].label; - let setData = {}; - let setOption = {}; + let primeVueSetData = {}; + let primeVueSetOption = {}; // 大到小排序: Primary 為主 const datasetsPrimary = chartData.data[0].data; @@ -677,12 +695,12 @@ export default { }); // 找出排序後,分別拉出 x, y Data - const xData = datasetsPrimary.map(item => item.x); + const xLabelData = datasetsPrimary.map(item => item.x); const yDataPrimary = datasetsPrimary.map(item => item.y); const yDataSecondary = datasetsSecondary.map(item => item.y); - setData = { - labels: xData, + primeVueSetData = { + labels: xLabelData, datasets: [ { label: labelPrimary, @@ -696,7 +714,7 @@ export default { }, ] }; - setOption = { + primeVueSetOption = { indexAxis: 'y', responsive: true, maintainAspectRatio: false, @@ -723,7 +741,7 @@ export default { }; switch (xUnit) { case 'date': - setOption.plugins.tooltip.callbacks.label = function(context) { + primeVueSetOption.plugins.tooltip.callbacks.label = function(context) { let value = context.parsed.y; value = context.parsed.x === null ? "n/a" : getSimpleTimeLabel(context.parsed.x, 2); @@ -734,13 +752,13 @@ export default { return `${labelSecondary}: ${value}`; } }; - setOption.scales.x.ticks.callback = function (value, index, ticks) { + primeVueSetOption.scales.x.ticks.callback = function (value, index, ticks) { return getFollowTimeLabel(value, maxY, 1) }; break; case 'count': - setOption.scales.x.ticks.precision = 0; // x 軸顯示小數點後 0 位 - setOption.plugins.tooltip.callbacks.label = function(context) { + primeVueSetOption.scales.x.ticks.precision = 0; // x 軸顯示小數點後 0 位 + primeVueSetOption.plugins.tooltip.callbacks.label = function(context) { let value = context.parsed.y; value = context.parsed.x === null ? "n/a" : context.parsed.x; @@ -754,22 +772,22 @@ export default { break; } if(isSingle) { // 設定一個活動的 y label、提示框文字 - setOption.plugins.tooltip.callbacks.title = function(context) { + primeVueSetOption.plugins.tooltip.callbacks.title = function(context) { return `${content.y}: ${context[0].label}`; }; - setOption.scales.y.ticks.callback = function (value, index, ticks) { - let label = xData[index]; + primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) { + let label = xLabelData[index]; if(label) { return label.length > 21 ? `${label.substring(0, 18)}...` : label }; } }else { // 設定「活動」到「活動」的 y label、提示框文字 - setOption.plugins.tooltip.callbacks.title = function(context) { + primeVueSetOption.plugins.tooltip.callbacks.title = function(context) { return `${content.y}: ${context[0].label.replace(',', ' - ')}` }; - setOption.scales.y.ticks.callback = function (value, index, ticks) { - let label = xData[index]; + primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) { + let label = xLabelData[index]; let labelStart = label[0]; let labelEnd = label[1]; @@ -779,9 +797,28 @@ export default { }; } - return [setData, setOption] + return [primeVueSetData, primeVueSetOption] }, /** + * + * @param whichScaleObj PrimeVue scale option object to reference to + * @param customizeOptions + * @param customizeOptions.content + * @param customizeOptions.ticksOfXAxis + */ + getCustomizedScaleOption(whichScaleObj, {customizeOptions: { + content, + ticksOfXAxis, + }, + }) { + let resultScaleObj; + resultScaleObj = this.customizeScaleChartOptionTitleByContent(whichScaleObj, content); + resultScaleObj = this.customizeScaleChartOptionTicks(resultScaleObj, ticksOfXAxis); + return resultScaleObj; + }, + /** + * 在一個基本的物件上加以客製化這個物件,客製化的參照來源是 content 的內容 + * 之所以有辦法這樣撰寫,是因為我們知道物件的順序是先 x 再 title 再 text * This function alters the title property of known scales object of Chart option * This is based on the fact that we know the order must be x -> title -> text. * @param {object} whichScaleObj PrimeVue scale option object to reference to @@ -812,7 +849,28 @@ export default { } } }; - } + }, + /** + * + * @param {object} scaleObjectToAlter this object follows the format of prive vue chart + * @param {Array} ticksOfXAxis For example, ['05/06', '05,07', '05/08'] + * or ['08:03:01', '08:11:18', '09:03:41', ], and so on. + */ + customizeScaleChartOptionTicks(scaleObjectToAlter, ticksOfXAxis) { + return { + ...scaleObjectToAlter, + x: { + ...scaleObjectToAlter.x, + ticks: { + ...scaleObjectToAlter.x.ticks, + callback: function(value, index) { + // console.log('根據不同的級距客製化 x 軸的時間刻度'); + return ticksOfXAxis[index]; + }, + }, + }, + }; + }, }, async created() { this.isLoading = true; // moubeted 才停止 loading diff --git a/src/views/Discover/Performance/index.vue b/src/views/Discover/Performance/index.vue index 625cec4..3223e2b 100644 --- a/src/views/Discover/Performance/index.vue +++ b/src/views/Discover/Performance/index.vue @@ -224,7 +224,7 @@ export default { * 手刻折線圖 x label 時間刻度 * @param { object } valueData {min: '2022-02-20T19:54:12', max: '2023-11-27T07:21:53'} */ - xLabelsData(valueData) { + setXLabelsData(valueData) { let min = new Date(valueData.min).getTime(); let max = new Date(valueData.max).getTime(); let numPoints = 12; @@ -260,8 +260,8 @@ export default { let maxX = chartData.x_axis.max; let maxY = chartData.y_axis.max; let xData; - let setData = {}; - let setOption = {}; + let primeVueSetData = {}; + let primeVueSetOption = {}; const getMoment = (time)=> { return this.$moment(time).format('YYYY/M/D hh:mm:ss') }; @@ -272,14 +272,14 @@ export default { case 'date': datasets = setLineChartData(chartData.data, chartData.x_axis.max, chartData.x_axis.min, false, chartData.y_axis.max, chartData.y_axis.min); - xData = this.xLabelsData(chartData.x_axis); + xData = this.setXLabelsData(chartData.x_axis); break; case 'count': // 次數 10 個點 datasets = chartData.data; xData = chartData.data.map(item => new Date(item.x).getTime()); break; } - setData = { + primeVueSetData = { labels: xData, datasets: [ { @@ -291,7 +291,7 @@ export default { } ] }; - setOption = { + primeVueSetOption = { responsive: true, maintainAspectRatio: false, layout: { @@ -374,24 +374,24 @@ export default { }; switch (yUnit) { case 'date': - setOption.plugins.tooltip.callbacks.label = function(context) { + primeVueSetOption.plugins.tooltip.callbacks.label = function(context) { return `${content.y}: ${getSimpleTimeLabel(context.parsed.y, 2)}`; }; - setOption.scales.x.min = minX; - setOption.scales.x.max = maxX; - setOption.scales.y.ticks.callback = function (value, index, ticks) { + primeVueSetOption.scales.x.min = minX; + primeVueSetOption.scales.x.max = maxX; + primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) { return getFollowTimeLabel(value, maxY, 1) }; break; case 'count': - setOption.scales.y.ticks.precision = 0; // y 軸顯示小數點後 0 位 - setOption.plugins.tooltip.callbacks.label = function(context) { + primeVueSetOption.scales.y.ticks.precision = 0; // y 軸顯示小數點後 0 位 + primeVueSetOption.plugins.tooltip.callbacks.label = function(context) { return `${content.y}: ${context.parsed.y}`; }; break; } - return [setData, setOption] + return [primeVueSetData, primeVueSetOption] }, /** * 建立長條圖 @@ -406,8 +406,8 @@ export default { let datasets = chartData.data; let xData; let yData; - let setData = {}; - let setOption = {}; + let primeVueSetData = {}; + let primeVueSetOption = {}; datasets = datasets.map(value => { return { @@ -418,7 +418,7 @@ export default { xData = datasets.map(i => i.x); yData = datasets.map(i => i.y) - setData = { + primeVueSetData = { labels: xData, datasets: [ { @@ -428,7 +428,7 @@ export default { } ] }; - setOption = { + primeVueSetOption = { responsive: true, maintainAspectRatio: false, layout: { @@ -505,7 +505,7 @@ export default { }, }; - return [setData, setOption] + return [primeVueSetData, primeVueSetOption] }, /** * 建立水平長條圖 @@ -518,15 +518,15 @@ export default { const maxY = chartData.y_axis.max; const getSimpleTimeLabel = simpleTimeLabel; const getFollowTimeLabel = followTimeLabel; - let setData = {}; - let setOption = {}; + let primeVueSetData = {}; + let primeVueSetOption = {}; // 大到小排序 chartData.data.sort((a, b) => b.y - a.y); const xData = chartData.data.map(item => item.x); const yData = chartData.data.map(item => item.y); - setData = { + primeVueSetData = { labels: xData, datasets: [ { @@ -536,7 +536,7 @@ export default { } ] }; - setOption = { + primeVueSetOption = { indexAxis: 'y', responsive: true, maintainAspectRatio: false, @@ -609,33 +609,33 @@ export default { }; switch (xUnit) { case 'date': - setOption.plugins.tooltip.callbacks.label = function(context) { + primeVueSetOption.plugins.tooltip.callbacks.label = function(context) { return `${content.x}: ${getSimpleTimeLabel(context.parsed.x, 2)}`; }; - setOption.scales.x.ticks.callback = function (value, index, ticks) { + primeVueSetOption.scales.x.ticks.callback = function (value, index, ticks) { return getFollowTimeLabel(value, maxY, 1) }; break; case 'count': - setOption.scales.x.ticks.precision = 0; // x 軸顯示小數點後 0 位 - setOption.plugins.tooltip.callbacks.label = function(context) { + primeVueSetOption.scales.x.ticks.precision = 0; // x 軸顯示小數點後 0 位 + primeVueSetOption.plugins.tooltip.callbacks.label = function(context) { return `${content.x}: ${context.parsed.x}`; } break; } if(isSingle) { // 設定一個活動的 y label、提示框文字 - setOption.plugins.tooltip.callbacks.title = function(context) { + primeVueSetOption.plugins.tooltip.callbacks.title = function(context) { return `${content.y}: ${context[0].label}`; }; - setOption.scales.y.ticks.callback = function (value, index, ticks) { + primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) { let label = xData[index]; return label.length > 21 ? `${label.substring(0, 18)}...` : label }; }else { // 設定「活動」到「活動」的 y label、提示框文字 - setOption.plugins.tooltip.callbacks.title = function(context) { + primeVueSetOption.plugins.tooltip.callbacks.title = function(context) { return `${content.y}: ${context[0].label.replace(',', ' - ')}` }; - setOption.scales.y.ticks.callback = function (value, index, ticks) { + primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) { let label = xData[index]; let labelStart = label[0]; let labelEnd = label[1]; @@ -646,7 +646,7 @@ export default { }; } - return [setData, setOption] + return [primeVueSetData, primeVueSetOption] }, }, async created() {