WIP same as the previoius commit. Tried to extract out an independent vue component to prevent shared primevue option.

X axis is with bug now.
This commit is contained in:
Cindy Chang
2024-06-13 13:35:58 +08:00
parent 310c416fd7
commit f6989c4759
6 changed files with 330 additions and 176 deletions

156
src/constants/constants.js Normal file
View File

@@ -0,0 +1,156 @@
export const knownLayoutChartOption = {
padding: {
top: 16,
left: 8,
right: 8,
}
};
export const knownScaleLineChartOptions = {
x: {
type: 'time',
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
}
},
time: {
displayFormats: {
second: 'h:mm:ss', // ex: 1:11:11
minute: 'M/d h:mm', // ex: 1/1 1:11
hour: 'M/d h:mm', // ex: 1/1 1:11
day: 'M/d h', // ex: 1/1 1
month: 'y/M/d', // ex: 1911/1/1
},
},
ticks: {
display: true,
maxRotation: 0, // 不旋轉 lable 0~50
color: '#64748b',
source: 'labels', // 依比例彈性顯示 label 數量
},
border: {
color: '#64748b',
},
grid: {
tickLength: 0, // 網格是否超過邊線
}
},
y: {
beginAtZero: true, // scale 包含 0
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
},
},
ticks:{
color: '#64748b',
padding: 8,
},
grid: {
color: '#64748b',
},
border: {
display: false, // 隱藏左側多出來的線
},
},
};
export const knownScaleHorizontalChartOptions = {
x: {
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
}
},
ticks: {
display: true,
maxRotation: 0, // 不旋轉 lable 0~50
color: '#64748b',
},
grid: {
color: '#64748b',
tickLength: 0, // 網格是否超過邊線
},
border: {
display:false,
},
},
y: {
beginAtZero: true, // scale 包含 0
type: 'category',
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
},
},
ticks:{
color: '#64748b',
padding: 8,
},
grid: {
display:false,
color: '#64748b',
},
border: {
display: false, // 隱藏左側多出來的線
},
},
};
export const knownScaleBarChartOptions = {
x: {
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
}
},
ticks: {
display: true,
maxRotation: 0, // 不旋轉 lable 0~50
color: '#64748b',
},
grid: {
color: '#64748b',
tickLength: 0, // 網格是否超過邊線
},
border: {
display:false,
},
},
y: {
beginAtZero: true, // scale 包含 0
type: 'category',
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
},
},
ticks:{
color: '#64748b',
padding: 8,
},
grid: {
display:false,
color: '#64748b',
},
border: {
display: false, // 隱藏左側多出來的線
},
},
};

View File

@@ -289,5 +289,7 @@ export const setTimeStringFormatBaseOnTimeDifference = (minTimeStamp, maxTimeSta
* @param {string} timeFormat For example, 'MM/DD' * @param {string} timeFormat For example, 'MM/DD'
*/ */
export const mapTimestampToAxisTicksByFormat = (timeStampArr, timeFormat) => { export const mapTimestampToAxisTicksByFormat = (timeStampArr, timeFormat) => {
return timeStampArr.map(ts => moment(ts).format(timeFormat)); if (timeStampArr) {
return timeStampArr.map(ts => moment(ts).format(timeFormat));
}
}; };

View File

@@ -6,6 +6,12 @@ export default defineStore('performanceStore', {
allPerformanceData: null, allPerformanceData: null,
freqChartData: null, freqChartData: null,
freqChartOptions: null, freqChartOptions: null,
freqChartXData: {
minX: null,
maxX: null,
xData: null,
content: null,
}
}), }),
getters: { getters: {
performanceData: state => { performanceData: state => {
@@ -50,5 +56,12 @@ export default defineStore('performanceStore', {
setFreqChartOptions(freqChartOptions){ setFreqChartOptions(freqChartOptions){
this.freqChartOptions = freqChartOptions; this.freqChartOptions = freqChartOptions;
}, },
/**
*
* @param {object} freqChartXData
*/
setFreqChartXData(freqChartXData) {
this.freqChartXData = freqChartXData;
}
}, },
}) })

View File

@@ -162,163 +162,13 @@ import { simpleTimeLabel, followTimeLabel, dateLabel,
getYTicksByIndex, getYTicksByIndex,
} from '@/module/timeLabel.js'; } from '@/module/timeLabel.js';
import i18next from '@/i18n/i18n'; import i18next from '@/i18n/i18n';
import {
knownLayoutChartOption,
knownScaleLineChartOptions,
knownScaleHorizontalChartOptions,
knownScaleBarChartOptions,
} from "@/constants/constants.js";
const knownLayoutChartOption = {
padding: {
top: 16,
left: 8,
right: 8,
}
};
const knownScaleLineChartOptions = {
x: {
type: 'time',
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
}
},
time: {
displayFormats: {
second: 'h:mm:ss', // ex: 1:11:11
minute: 'M/d h:mm', // ex: 1/1 1:11
hour: 'M/d h:mm', // ex: 1/1 1:11
day: 'M/d h', // ex: 1/1 1
month: 'y/M/d', // ex: 1911/1/1
},
},
ticks: {
display: true,
maxRotation: 0, // 不旋轉 lable 0~50
color: '#64748b',
source: 'labels', // 依比例彈性顯示 label 數量
},
border: {
color: '#64748b',
},
grid: {
tickLength: 0, // 網格是否超過邊線
}
},
y: {
beginAtZero: true, // scale 包含 0
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
},
},
ticks:{
color: '#64748b',
padding: 8,
},
grid: {
color: '#64748b',
},
border: {
display: false, // 隱藏左側多出來的線
},
},
};
const knownScaleHorizontalChartOptions = {
x: {
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
}
},
ticks: {
display: true,
maxRotation: 0, // 不旋轉 lable 0~50
color: '#64748b',
},
grid: {
color: '#64748b',
tickLength: 0, // 網格是否超過邊線
},
border: {
display:false,
},
},
y: {
beginAtZero: true, // scale 包含 0
type: 'category',
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
},
},
ticks:{
color: '#64748b',
padding: 8,
},
grid: {
display:false,
color: '#64748b',
},
border: {
display: false, // 隱藏左側多出來的線
},
},
};
const knownScaleBarChartOptions = {
x: {
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
}
},
ticks: {
display: true,
maxRotation: 0, // 不旋轉 lable 0~50
color: '#64748b',
},
grid: {
color: '#64748b',
tickLength: 0, // 網格是否超過邊線
},
border: {
display:false,
},
},
y: {
beginAtZero: true, // scale 包含 0
type: 'category',
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
},
},
ticks:{
color: '#64748b',
padding: 8,
},
grid: {
display:false,
color: '#64748b',
},
border: {
display: false, // 隱藏左側多出來的線
},
},
};
export default { export default {
setup() { setup() {

View File

@@ -3,22 +3,151 @@
</template> </template>
<script> <script>
import { computed } from 'vue'; import { computed, ref, watch, onMounted } from 'vue';
import { mapState, mapActions } from 'pinia';
import usePerformanceStore from "@/stores/performance.js"; import usePerformanceStore from "@/stores/performance.js";
import {
setTimeStringFormatBaseOnTimeDifference,
mapTimestampToAxisTicksByFormat,
} from '@/module/timeLabel.js';
import {
knownScaleLineChartOptions,
} from "@/constants/constants.js";
// 如果把 chart 獨立成一個 vue component
// 是否可以防止 PrimeVue 誤用其他圖表 option 值的 bug? const cloneDeep = (obj) => JSON.parse(JSON.stringify(obj));
// 試著把 chart 獨立成一個 vue component
// 企圖防止 PrimeVue 誤用其他圖表 option 值的 bug
export default { export default {
setup() { setup() {
const performanceStore = usePerformanceStore(); const performanceStore = usePerformanceStore();
const freqChartData = computed(() => performanceStore.freqChartData); const freqChartData = computed(() => performanceStore.freqChartData);
const freqChartOptions = computed(() => performanceStore.freqChartOptions); const originalFreqChartOptions = computed(() => performanceStore.freqChartOptions);
const minX = computed(() => performanceStore.freqChartXData.minX);
const maxX = computed(() => performanceStore.freqChartXData.maxX);
const xData = computed(() => performanceStore.freqChartXData.xData);
const content = computed(() => performanceStore.freqChartXData.content);
const customizedScaleOption = computed(() => {});
const updatedFreqChartOptions = ref(cloneDeep(originalFreqChartOptions.value));
const customizeOptionsFunc = () => {
// Customize X axis ticks due to different differences between min and max of data group
// Compare page and Performance page share the same logic
const formatToSet = setTimeStringFormatBaseOnTimeDifference(minX.value, maxX.value);
const ticksOfXAxis = mapTimestampToAxisTicksByFormat(xData.value, formatToSet);
customizedScaleOption.value = getCustomizedScaleOption(
knownScaleLineChartOptions, {
customizeOptions: {
content, ticksOfXAxis,
}
});
};
const newTicksCallback = (value, index, values) => {
return value; // 沒有單位,只有數值,因為是取計算數量
};
const updateTicksCallback = () => {
//在這裡額外宣告本圖表的 callback 區分跟其他圖表的 callback避免共用 option 造成不預期的共用與混淆
if (updatedFreqChartOptions.value.scales && updatedFreqChartOptions.value.scales.y) {
updatedFreqChartOptions.value = customizedScaleOption;
updatedFreqChartOptions.value.scales.y.ticks.callback = newTicksCallback;
}
};
watch(originalFreqChartOptions, (newOptions) => {
updatedFreqChartOptions.value = cloneDeep(newOptions);
});
/**
* Compare page and Performance have this same function.
* @param whichScaleObj PrimeVue scale option object to reference to
* @param customizeOptions
* @param customizeOptions.content
* @param customizeOptions.ticksOfXAxis
*/
const getCustomizedScaleOption = (whichScaleObj, {customizeOptions: {
content,
ticksOfXAxis,
},
}) => {
let resultScaleObj;
resultScaleObj = customizeScaleChartOptionTitleByContent(whichScaleObj, content);
resultScaleObj = customizeScaleChartOptionTicks(resultScaleObj, ticksOfXAxis);
return resultScaleObj;
};
/**
* Compare page and Performance have this same function.
* @param {object} scaleObjectToAlter this object follows the format of prive vue chart
* @param {Array<string>} ticksOfXAxis For example, ['05/06', '05,07', '05/08']
* or ['08:03:01', '08:11:18', '09:03:41', ], and so on.
*/
const customizeScaleChartOptionTicks = (scaleObjectToAlter, ticksOfXAxis) => {
return {
...scaleObjectToAlter,
x: {
...scaleObjectToAlter.x,
ticks: {
...scaleObjectToAlter.x.ticks,
callback: function(value, index) {
// console.log('根據不同的級距客製化 x 軸的時間刻度');
return ticksOfXAxis[index];
},
},
},
};
};
/** Compare page and Performance have this same function.
* 在一個基本的物件上加以客製化這個物件,客製化的參照來源是 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
* @param content whose property includes x and y and stand for titles
*
* @returns { object } an object modified with two titles
*/
const customizeScaleChartOptionTitleByContent = (whichScaleObj, content) => {
if (!content) {
// Early return
return whichScaleObj;
}
return {
...whichScaleObj,
x: {
...whichScaleObj.x,
title: {
...whichScaleObj.x.title,
text: content.x
}
},
y: {
...whichScaleObj.y,
title: {
...whichScaleObj.y.title,
text: content.y
}
}
};
};
onMounted(() => {
customizeOptionsFunc();
});
return { return {
freqChartData, freqChartData,
freqChartOptions, updatedFreqChartOptions,
updateTicksCallback,
}; };
} }
}; };

View File

@@ -118,6 +118,7 @@
<ul> <ul>
<li class="bg-neutral-10 mb-4 p-1 border border-neutral-300 rounded"> <li class="bg-neutral-10 mb-4 p-1 border border-neutral-300 rounded">
<span class="block font-bold text-sm leading-loose text-center my-2">{{ contentData.freq.title }}</span> <span class="block font-bold text-sm leading-loose text-center my-2">{{ contentData.freq.title }}</span>
<!-- <Chart type="line" :data="freqData" :options="freqOptions" class="h-96" /> -->
<FreqChart/> <FreqChart/>
</li> </li>
<li class="bg-neutral-10 p-1 border border-neutral-300 rounded"> <li class="bg-neutral-10 p-1 border border-neutral-300 rounded">
@@ -354,6 +355,11 @@ export default {
} }
}); });
// special case, freq chart case
if(whichCaller === "freqChartCaller") {
this.setFreqChartXData({ minX, maxX, xData, content});
}
primeVueSetData = { primeVueSetData = {
labels: xData, labels: xData,
datasets: [ datasets: [
@@ -393,6 +399,7 @@ export default {
}, },
scales: customizedScaleOption, scales: customizedScaleOption,
}; };
const {resultStepSize, unitToUse} = getStepSizeOfYTicks(maxY);
switch (yUnit) { switch (yUnit) {
case 'date': case 'date':
primeVueSetOption.plugins.tooltip.callbacks.label = function(context) { primeVueSetOption.plugins.tooltip.callbacks.label = function(context) {
@@ -400,19 +407,12 @@ export default {
}; };
primeVueSetOption.scales.x.min = minX; primeVueSetOption.scales.x.min = minX;
primeVueSetOption.scales.x.max = maxX; primeVueSetOption.scales.x.max = maxX;
const {resultStepSize: YTickStepSize, unitToUse} = getStepSizeOfYTicks(maxY); // Stepsize only needs to be calculated once
primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) { primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) {
switch(whichCaller){ return getYTicksByIndex(resultStepSize, index, unitToUse);
case "freqChartCaller":
console.log('freqChartCaller', freqChartCaller);
return value;
default:
return getYTicksByIndex(YTickStepSize, index, unitToUse);
}
return;
}; };
break; break;
case 'count': case 'count':
default:
primeVueSetOption.scales.y.ticks.precision = 0; // y 軸顯示小數點後 0 位 primeVueSetOption.scales.y.ticks.precision = 0; // y 軸顯示小數點後 0 位
primeVueSetOption.plugins.tooltip.callbacks.label = function(context) { primeVueSetOption.plugins.tooltip.callbacks.label = function(context) {
return `${content.y}: ${context.parsed.y}`; return `${content.y}: ${context.parsed.y}`;
@@ -420,6 +420,7 @@ export default {
break; break;
} }
return [primeVueSetData, primeVueSetOption] return [primeVueSetData, primeVueSetOption]
}, },
/** /**
@@ -749,8 +750,11 @@ export default {
}, },
}; };
}, },
...mapActions(PerformanceStore, ['setFreqChartData']), ...mapActions(PerformanceStore, [
...mapActions(PerformanceStore, ['setFreqChartOptions']), 'setFreqChartData',
'setFreqChartOptions',
'setFreqChartXData'
]),
}, },
async created() { async created() {
this.isLoading = true; // moubeted 才停止 loading this.isLoading = true; // moubeted 才停止 loading
@@ -792,8 +796,8 @@ export default {
} }
[this.freqData, this.freqOptions] = this.getLineChart(this.performanceData.freq.cases, this.contentData.freq, 'count', "freqChartCaller"); [this.freqData, this.freqOptions] = this.getLineChart(this.performanceData.freq.cases, this.contentData.freq, 'count', "freqChartCaller");
this.setFreqChartData(this.freqData); this.setFreqChartData(this.freqData);
this.setFreqChartOptions(this.freqOptions); this.setFreqChartOptions(this.freqOptions);
[this.casesByTaskData, this.casesByTaskOptions] = this.getHorizontalBarChart(this.performanceData.freq.cases_by_task, [this.casesByTaskData, this.casesByTaskOptions] = this.getHorizontalBarChart(this.performanceData.freq.cases_by_task,
this.contentData.casesByTask, true, 'count'); this.contentData.casesByTask, true, 'count');