Router: change /Discover to /Discover/map/type/filterId

This commit is contained in:
chiayin
2023-06-16 17:13:59 +08:00
parent 07b35fcce0
commit af1f8f3016
20 changed files with 121 additions and 57 deletions

View File

@@ -0,0 +1,169 @@
<template>
<!-- Activity List -->
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full">
<div class="flex justify-between items-center my-2 flex-wrap">
<p class="h2">Activity List&nbsp({{ data.length }})</p>
</div>
<!-- Table -->
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]">
<table class="border-separate border-spacing-x-2 table-auto min-w-full text-sm" :class="data.length === 0? 'h-full': null">
<thead class="sticky top-0 left-0 z-10 bg-neutral-10">
<tr>
<th class="text-start font-semibold leading-10 px-2 border-b border-neutral-500">Activity</th>
<th class="font-semibold leading-10 px-2 border-b border-neutral-500 text-start" colspan="3">Occurrences</th>
</tr>
</thead>
<Draggable :list="data" group="activity" itemKey="name" tag="tbody" animation="300" @end="onEnd" :fallbackTolerance="5" :sort="false" :forceFallback="true" :ghostClass="'ghostSelected'" :dragClass="'dragSelected'">
<template #item="{ element, index }">
<tr @dblclick="moveActItem(index, element)">
<td class="px-4 py-2" :id="element.label">{{ element.label }}</td>
<td class="px-4 py-2 w-24">
<div class="h-4 min-w-[96px] bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(element.occ_value)"></div>
</div>
</td>
<td class="px-4 py-2 text-right">{{ element.occurrences }}</td>
<td class="px-4 py-2 text-right">{{ element.occurrence_ratio }}</td>
</tr>
</template>
</Draggable>
</table>
</div>
</div>
<!-- Sequence -->
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 pb-4 w-full h-full relative text-sm">
<p class="h2 border-b border-500 my-2">Sequence&nbsp({{ listSeq.length }})</p>
<!-- No Data -->
<div v-if="listSequence.length === 0" class="p-4 w-[calc(100%_-_32px)] h-5/6 flex justify-center items-center absolute">
<p class="text-neutral-500">Please drag and drop activity(s) here and sort.</p>
</div>
<!-- Have Data -->
<div class="py-4 m-auto w-full h-[calc(100%_-_56px)]">
<div class="w-full h-full overflow-y-auto overflow-x-auto scrollbar px-4 text-center listSequence">
<draggable class="h-full" :list="listSequence" group="activity" itemKey="name" animation="300" :forceFallback="true" :dragClass="'dragSelected'" :fallbackTolerance="5" @start="onStart" @end="onEnd" @choose="onChoose">
<template #item="{ element, index }">
<div>
<div class="w-full p-2 border border-primary rounded text-primary" @dblclick="moveSeqItem(index, element)">
<span>{{ element.label }}</span>
</div>
<span v-show="index !== listSeq.length - 1 && index !== lastItemIndex - 1" class="pi pi-chevron-down !text-lg inline-block py-2"></span>
</div>
</template>
</draggable>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
filterTaskData: {
type: Array,
required: true,
},
progressWidth: {
type: Function,
required: false,
},
listSeq: {
type: Array,
required: true,
}
},
data() {
return {
listSequence: this.listSeq,
filteredData: this.filterTaskData,
lastItemIndex: null,
isDragging: false,
}
},
computed: {
data: function() {
// TODO Activity List 的 dblclick, drag & drop 要改假刪除
// Activity List 要排序
return this.filteredData.sort((x, y) => y.occurrences - x.occurrences);
}
},
watch: {
listSeq(newval){
this.listSequence = newval;
},
filterTaskData(newval){
this.data = newval;
}
},
methods: {
/**
* double click Activity List
* @param {number} index data item index
* @param {object} element data item
*/
moveActItem(index, element){
this.data.splice(index, 1);
this.listSequence.push(element);
},
/**
* double click Sequence List
* @param {number} index data item index
* @param {object} element data item
*/
moveSeqItem(index, element){
this.listSequence.splice(index, 1);
this.data.push(element);
},
/**
* Element dragging started
*/
onStart(evt) {
this.isDragging = true;
// 隱藏拖曳元素原位置
const originalElement = evt.item;
originalElement.style.display = 'none';
// 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
const listIndex = this.listSequence.length - 1;
if(evt.oldIndex === listIndex) this.lastItemIndex = listIndex;
},
/**
* Element dragging ended
*/
onEnd(evt) {
this.isDragging = false;
// 顯示拖曳元素
const originalElement = evt.item;
originalElement.style.display = '';
// 拖曳結束要顯示箭頭,但最後一個不用
const lastChild = evt.item.lastChild;
const listIndex = this.listSequence.length - 1
evt.oldIndex !== listIndex ? lastChild.style.display = '' : null;
// reset: 拖曳最後一個元素時,倒數第二的元素的箭頭要隱藏
this.lastItemIndex = null;
this.$emit('update:listSeq', this.listSequence);
},
/**
* Element is chosen
*/
onChoose(evt) {
// 拖曳時要隱藏箭頭
// isDragging: 只有拖曳才要隱藏dblclick 不用
const lastChild = evt.item.lastChild;
if (this.isDragging) lastChild.style.display = 'none';
}
}
}
</script>
<style scoped>
.ghostSelected {
/* @apply shadow-inner bg-neutral-900 shadow-neutral-500 */
@apply shadow-[0px_0px_100px_-10px_inset] shadow-neutral-200
}
.dragSelected {
@apply shadow-[0px_0px_4px_2px] bg-neutral-10 shadow-neutral-300 !opacity-100
}
.dragged-item {
@apply !opacity-100
}
</style>

View File

@@ -0,0 +1,76 @@
<template>
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full">
<div class="flex justify-between items-center my-2 flex-wrap">
<p class="h2">{{ tableTitle }}&nbsp({{ tableData.length }})</p>
<!-- Search -->
<!-- <Search></Search> -->
</div>
<!-- Table -->
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]">
<DataTable v-model:selection="select" :value="tableData" dataKey="label" breakpoint="0" tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm" @row-select="onRowSelect">
<ColumnGroup type="header">
<Row>
<Column selectionMode="single" headerClass="w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10"></Column>
<Column field="label" header="Activity" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" sortable />
<Column field="occ_value" header="Occurrences" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" sortable :colspan="3" />
</Row>
</ColumnGroup>
<Column selectionMode="single" bodyClass="!p-2 !border-0"></Column>
<Column field="label" header="Activity" bodyClass="break-words !py-2 !border-0"></Column>
<Column header="進度條" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_value)"></div>
</div>
</template>
</Column>
<Column field="occurrences" header="Occurrences" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="occurrence_ratio" header="Occurrence Ratio" bodyClass="!text-right !py-2 !border-0"></Column>
</DataTable>
</div>
</div>
</template>
<script>
import Search from '@/components/Search.vue';
export default {
props: {
tableTitle: {
type: String,
required: true,
},
tableData: {
type: Array,
required: true,
},
tableSelect: {
type: [Object, Array],
default: null
},
progressWidth: {
type: Function,
required: false,
}
},
data() {
return {
select: this.tableSelect,
metaKey: true
}
},
components: {
Search,
},
watch: {
tableSelect(newval){
this.select = newval;
}
},
methods: {
onRowSelect(e) {
this.$emit('on-row-select', e)
}
},
}
</script>

View File

@@ -0,0 +1,80 @@
<template>
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 h-full">
<div class="flex justify-between items-center my-2">
<p class="h2">{{ tableTitle }}&nbsp({{ data.length }})</p>
<!-- Search -->
<!-- <Search></Search> -->
</div>
<!-- Table -->
<div class="overflow-y-auto overflow-x-auto scrollbar -mx-2 h-[calc(100%_-_64px)]">
<DataTable v-model:selection="select" :value="data" breakpoint="0" tableClass="w-full !border-separate !border-spacing-x-2 !table-auto text-sm" @row-select="onRowSelect" @row-unselect="onRowUnselect" @row-select-all="onRowSelectAll" @row-unselect-all="onRowUnelectAll">
<ColumnGroup type="header">
<Row>
<Column selectionMode="multiple" headerClass="w-8 !p-2 !bg-neutral-10 !border-neutral-500 sticky top-0 left-0 z-10 bg-neutral-10 allCheckboxAct"></Column>
<Column field="label" header="Activity" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" sortable />
<Column field="occ_value" header="Occurrences" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" sortable :colspan="3" />
<Column field="case_value" headerClass="!bg-neutral-10 !border-neutral-500 !py-2 sticky top-0 left-0 z-10 bg-neutral-10" header="Cases with Activity" sortable :colspan="3" />
</Row>
</ColumnGroup>
<Column selectionMode="multiple" bodyClass="!p-2 !border-0"></Column>
<Column field="label" header="Activity" bodyClass="break-words !py-2 !border-0"></Column>
<Column header="進度條" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.occ_value)"></div>
</div>
</template>
</Column>
<Column field="occurrences" header="Occurrences" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="occurrence_ratio" header="O2" bodyClass="!text-right !py-2 !border-0"></Column>
<Column header="進度條" bodyClass="!py-2 !border-0 min-w-[96px]">
<template #body="slotProps">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(slotProps.data.case_value)"></div>
</div>
</template>
</Column>
<Column field="cases" header="Cases with Activity" bodyClass="!text-right !py-2 !border-0"></Column>
<Column field="case_ratio" header="C2" bodyClass="!text-right !py-2 !border-0"></Column>
</DataTable>
</div>
</div>
</template>
<script>
import Search from '@/components/Search.vue';
export default {
props: ['tableTitle', 'tableData', 'tableSelect', 'progressWidth'],
data() {
return {
select: this.tableSelect,
data: this.tableData
}
},
components: {
Search,
},
watch: {
tableSelect(newval){
this.select = newval;
}
},
methods: {
onRowSelect() {
this.$emit('on-row-select', this.select);
},
onRowUnselect() {
this.$emit('on-row-select', this.select);
},
onRowSelectAll(e) {
this.select = e.data;
this.$emit('on-row-select', this.select);
},
onRowUnelectAll(e) {
this.select = null;
this.$emit('on-row-select', this.select)
}
}
}
</script>

View File

@@ -0,0 +1,130 @@
<template>
<div class=" w-full h-full">
<div class="h-[calc(100%_-_58px)] border-b border-neutral-400 mb-2 scrollbar overflow-x-hidden overflow-y-auto">
<div v-if="this.temporaryData.length === 0" class="h-full flex justify-center items-center">
<span class="text-neutral-500">No Filter.</span>
</div>
<div v-else>
<div class="text-primary h2 flex items-center justify-start my-4">
<span class="material-symbols-outlined m-2">info</span>
<p>Disabled filters will not be saved.</p>
</div>
<Timeline :value="ruleData">
<template #content="rule">
<div class="border-b border-neutral-300 flex justify-between items-center space-x-2">
<!-- content -->
<div class="pl-2 mb-2">
<p class="text-sm font-medium leading-5">{{ rule.item.type }}:&nbsp;<span class="text-neutral-500">{{ rule.item.label }}</span></p>
</div>
<!-- button -->
<div class="min-w-fit">
<InputSwitch v-model="rule.item.toggle" @input="isRule($event, rule.index)"/>
<button type="button" class="m-2 focus:ring focus:ring-danger/20 text-neutral-500 hover:text-danger" @click.stop="deleteRule(rule.index)">
<span class="material-symbols-outlined">delete</span>
</button>
</div>
</div>
</template>
</Timeline>
</div>
</div>
<!-- Button -->
<div>
<div class="float-right space-x-4 px-4 py-2">
<button type="button" class="btn btn-sm " :class="[ temporaryData.length === 0 ? 'btn-disable' : 'btn-neutral']" :disabled="temporaryData.length === 0" @click="deleteRule('all')">Delete All</button>
<button type="button" class="btn btn-sm" :class="[ temporaryData.length === 0 ? 'btn-disable' : 'btn-neutral']" :disabled="temporaryData.length === 0" @click="submitAll">Apply All</button>
</div>
</div>
</div>
</template>
<script>
import { storeToRefs } from 'pinia';
import LoadingStore from '@/stores/loading.js';
import AllMapDataStore from '@/stores/allMapData.js';
export default {
setup() {
const loadingStore = LoadingStore();
const allMapDataStore = AllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { hasResultRule, temporaryData, postRuleData, ruleData, isRuleData} = storeToRefs(allMapDataStore);
return { isLoading, hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, allMapDataStore }
},
methods: {
/**
* @param {boolean} e ture | false
* @param {numble} index rule's index
*/
isRule(e, index){
let rule = this.isRuleData[index];
// 先取得 rule object
// 為了讓 data 順序不亂掉,將值指向 0submitAll 時再刪掉
if(!e) this.temporaryData[index] = 0;
else this.temporaryData[index] = rule;
},
/**
* header:Funnel 刪除全部的 Funnel
*/
deleteRule(index) {
if(index === 'all') {
this.temporaryData = [];
this.isRuleData = [];
this.ruleData = [];
this.$toast.success('All deleted.');
}else{
this.$toast.success(`Delete ${this.ruleData[index].label}.`);
this.temporaryData.splice(index, 1);
this.isRuleData.splice(index, 1);
this.ruleData.splice(index, 1);
}
},
/**
* header:Funnel 發送暫存的選取資料
*/
async submitAll() {
this.postRuleData = this.temporaryData.filter(item => item !== 0); // 取得 submit 的資料,有 toggle button 的話,找出並刪除陣列中為 0 的項目
if(!this.postRuleData?.length) return this.$toast.error('Not selected');
await this.allMapDataStore.checkHasResult(); // 後端快速檢查有沒有結果
if(this.hasResultRule === null) return;
else if(this.hasResultRule) {
this.isLoading = true;
await this.allMapDataStore.addTempFilterId();
await this.allMapDataStore.getAllMapData();
await this.$emit('submit-all');
this.isLoading = false;
this.$toast.success('Filter Success. View the Map.');
}else {
this.isLoading = true;
await new Promise(resolve => setTimeout(resolve, 1000));
this.isLoading = false;
this.$toast.warning('No Data.');
};
},
},
}
</script>
<style scoped>
/* TimeLine */
:deep(.p-timeline) {
@apply leading-none my-4
}
:deep(.p-timeline-event-opposite) {
@apply hidden
}
:deep(.p-timeline-event-separator) {
@apply mx-4
}
:deep(.p-timeline-event-marker) {
@apply !bg-primary !border-primary !w-2 !h-2
}
:deep(.p-timeline-event-connector) {
@apply !bg-primary my-2 !w-[1px]
}
:deep(.p-timeline-event-content) {
@apply !px-0
}
</style>

View File

@@ -0,0 +1,316 @@
<template>
<div class="bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full">
<section class="py-2 space-y-2 text-sm min-w-[48%] h-full">
<p class="h2">Range Selection</p>
<div class="text-primary h2 flex items-center justify-start">
<span class="material-symbols-outlined mr-2 text-base">info</span>
<p>Select or fill in a time range.</p>
</div>
<div class="chartContainer h-3/5 relative">
<canvas id="chartCanvasId"></canvas>
<div id="chart-mask-left" class="absolute bg-neutral-10/50"></div>
<div id="chart-mask-right" class="absolute bg-neutral-10/50"></div>
</div>
<div class="px-2 py-3">
<Slider v-model="selectArea" :step="1" :min="0" :max="selectRange" range class="mx-2" @change="changeSelectArea($event)"/>
</div>
<!-- Calendar group -->
<div class="flex justify-center items-center space-x-2 w-full">
<div>
<span class="block mb-2">Start time</span>
<Calendar v-model="startTime" dateFormat="yy/mm/dd" :panelProps="panelProps" :minDate="startMinDate" :maxDate="startMaxDate" showTime showIcon hourFormat="24" @date-select="sliderTimeRange($event, 'start')" id="startCalendar"/>
</div>
<span class="block mt-4">~</span>
<div>
<span class="block mb-2">End time</span>
<Calendar v-model="endTime" dateFormat="yy/mm/dd" :panelProps="panelProps" :minDate="endMinDate" :maxDate="endMaxDate" showTime showIcon hourFormat="24" @date-select="sliderTimeRange($event, 'end')" id="endCalendar"/>
</div>
</div>
<!-- End calendar group -->
</section>
</div>
</template>
<script>
import { storeToRefs } from 'pinia';
import AllMapDataStore from '@/stores/allMapData.js';
import { Chart, registerables } from 'chart.js';
import 'chartjs-adapter-date-fns';
import getMoment from 'moment';
export default{
setup() {
const allMapDataStore = AllMapDataStore();
const { filterTimeframe, selectTimeFrame } = storeToRefs(allMapDataStore);
return {allMapDataStore, filterTimeframe, selectTimeFrame }
},
data() {
return {
selectRange: 1000, // 更改 select 的切分數
selectArea: null,
chart: null,
canvasId: null,
startTime: null,
endTime: null,
startMinDate: null,
startMaxDate: null,
endMinDate: null,
endMaxDate: null,
panelProps: {
onClick: (event) => {
event.stopPropagation();
},
},
}
},
computed: {
// user select time start and end
timeFrameStartEnd: function() {
let start = getMoment(this.startTime).format('YYYY-MM-DDTHH:mm:ss');
let end = getMoment(this.endTime).format('YYYY-MM-DDTHH:mm:ss');
this.selectTimeFrame = [start, end]; // 傳給後端的資料
return [start, end];
},
// 找出 slidrData時間格式:毫秒時間戳
sliderData: function() {
let xAxisMin = new Date(this.filterTimeframe.x_axis.min).getTime();
let xAxisMax = new Date(this.filterTimeframe.x_axis.max).getTime();
let range = xAxisMax - xAxisMin;
let step = range / this.selectRange;
let sliderData = []
for (let i = 0; i <= this.selectRange; i++) {
sliderData.push(xAxisMin + (step * i));
}
return sliderData;
},
// 加入最大、最小值
timeFrameData: function(){
let data = this.filterTimeframe.data.map(i=>({x:i.x,y:i.y}))
// y 軸斜率計算請參考 ./public/timeFrameSlope 的圖
// x 值為 0 ~ 11,
// 將三的座標(ax, ay), (bx, by), (cx, cy)命名為 (a, b), (c, d), (e, f)
// 最小值: (f - b)(c - a) = (e - a)(d - b),求 b = (ed - ad - fa - fc) / (e - c - a)
// 最大值: (f - b)(e - c) = (f - d)(e - a),求 f = (be - bc -de + da) / (a - c)
// y 軸最小值
let a = 0;
let b;
let c = 1;
let d = this.filterTimeframe.data[0].y;
let e = 2;
let f = this.filterTimeframe.data[1].y;
b = (e*d - a*d - f*a - f*c) / (e - c - a)
b < 0 ? b = 0 : b;
// y 軸最大值
let ma = 9;
let mb = this.filterTimeframe.data[8].y;
let mc = 10;
let md = this.filterTimeframe.data[9].y;
let me = 11;
let mf;
mf = (mb*me - mb*mc -md*me + md*ma) / (ma - mc);
mf < 0 ? mf = 0 : mf;
// 添加最小值
data.unshift({
x: this.filterTimeframe.x_axis.min,
y: b,
})
// 添加最大值
data.push({
x: this.filterTimeframe.x_axis.max,
y: mf,
})
return data;
},
},
watch:{
selectTimeFrame(newValue, oldValue) {
if(newValue.length === 0) {
this.startTime = new Date(getMoment(this.filterTimeframe.x_axis.min).format());
this.endTime = new Date(getMoment(this.filterTimeframe.x_axis.max).format());
this.selectArea = [0, this.selectRange];
this.resizeMask(this.chart);
}
},
},
methods: {
resizeMask(chart) {
let from = (this.selectArea[0] * 0.01) / (this.selectRange * 0.01);
let to = (this.selectArea[1] * 0.01) / (this.selectRange * 0.01);
this.resizeLeftMask(chart, from);
this.resizeRightMask(chart, to);
},
resizeLeftMask(chart, from) {
const canvas = document.getElementById("chartCanvasId");
const mask = document.getElementById("chart-mask-left");
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left}px`;
mask.style.width = `${chart.chartArea.width * from}px`;
mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`;
mask.style.height = `${chart.chartArea.height}px`;
},
resizeRightMask(chart, to) {
const canvas = document.getElementById("chartCanvasId");
const mask = document.getElementById("chart-mask-right");
mask.style.left = `${canvas.offsetLeft + chart.chartArea.left + chart.chartArea.width * to}px`;
mask.style.width = `${chart.chartArea.width * (1 - to)}px`;
mask.style.top = `${canvas.offsetTop + chart.chartArea.top}px`;
mask.style.height = `${chart.chartArea.height}px`;
},
createChart() {
const max = this.filterTimeframe.y_axis.max * 1.1;
const data = {
datasets: [
{
label: 'Case',
data: this.timeFrameData,
fill: 'start',
showLine: false,
tension: 0.4,
backgroundColor: 'rgba(0,153,255)',
pointRadius: 0,
x: 'x',
y: 'y',
}
]
};
const options = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
plugins: {
legend: false, // 圖例
filler: {
propagate: false
},
title: false
},
// animations: false, // 取消動畫
animation: {
onComplete: e => {
this.resizeMask(e.chart);
}
},
interaction: {
intersect: true,
},
scales: {
x: {
type: 'time',
ticks: {
autoSkip: false,
maxRotation: 0, // 不旋轉 lable 0~50
color: '#334155',
display: true,
},
grid: {
display: false, // 隱藏 x 軸網格
},
},
y: {
beginAtZero: true, // scale 包含 0
max: max,
ticks: { // 設定間隔數值
display: false, // 隱藏數值,只顯示格線
stepSize: max / 4,
},
grid: {
color: 'rgba(100,116,139)',
z: 1,
},
border: {
display: false, // 隱藏左側多出來的線
}
},
},
};
const config = {
type: 'line',
data: data,
options: options,
};
this.canvasId = document.getElementById("chartCanvasId");
this.chart = new Chart(this.canvasId, config);
},
/**
* 滑塊改變的時候
* @param {array} e [1, 100]
*/
changeSelectArea(e) {
// 日曆改變時,滑塊跟著改變
let sliderData = this.sliderData;
this.startTime = new Date(sliderData[e[0]]);
this.endTime = new Date(sliderData[e[1]]);
// 重新設定 start end 日曆選取範圍
this.endMinDate = new Date(sliderData[e[0]]);
this.startMaxDate = new Date(sliderData[e[1]]);
// 重新算圖
this.resizeMask(this.chart);
// 執行 timeFrameStartEnd 才會改變數據
this.timeFrameStartEnd;
},
/**
* 選取開始或結束時間時,要改變滑塊跟圖表
* @param {object} e Tue Jan 25 2022 00:00:00 GMT+0800 (台北標準時間)
* @param {string} direction start or end
*/
sliderTimeRange(e, direction) {
// 找到最鄰近的 index時間格式: 毫秒時間戳
let sliderData = this.sliderData;
const targetTime = [new Date(this.timeFrameStartEnd[0]).getTime(), new Date(this.timeFrameStartEnd[1]).getTime()];
const closestIndexes = targetTime.map(target => {
let closestIndex = 0;
closestIndex = ((target - sliderData[0])/(sliderData[sliderData.length-1]-sliderData[0])) * sliderData.length;
let result = Math.round(Math.abs(closestIndex));
result = result > this.selectRange ? this.selectRange : result;
return result
});
// 改變滑塊
this.selectArea = closestIndexes;
// 重新設定 start end 日曆選取範圍
if(direction === 'start') this.endMinDate = e;
else if(direction === 'end') this.startMaxDate = e;
// 重新算圖
if(!isNaN(closestIndexes[0]) && !isNaN(closestIndexes[1])) this.resizeMask(this.chart);
else return;
},
},
mounted() {
// Chart.js
Chart.register(...registerables);
this.createChart();
// Slider
this.selectArea = [0, this.selectRange];
// Calendar
this.startMinDate = new Date(getMoment(this.filterTimeframe.x_axis.min).format());
this.startMaxDate = new Date(getMoment(this.filterTimeframe.x_axis.max).format());
this.endMinDate = new Date(getMoment(this.filterTimeframe.x_axis.min).format());
this.endMaxDate = new Date(getMoment(this.filterTimeframe.x_axis.max).format());
// 讓日曆的範圍等於時間軸的範圍
this.startTime = this.startMinDate;
this.endTime = this.startMaxDate;
this.timeFrameStartEnd;
},
beforeUnmount() {
this.selectArea = [0, this.selectRange];
this.resizeMask(this.chart);
this.startTime = new Date(getMoment(this.filterTimeframe.x_axis.min).format());
this.endTime = new Date(getMoment(this.filterTimeframe.x_axis.max).format());
this.timeFrameStartEnd;
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,271 @@
<template>
<div class="flex justify-between items-start bg-neutral-10 border border-neutral-300 rounded-xl px-4 w-full h-full space-x-4 overflow-y-auto overflow-x-auto scrollbar">
<!-- Range Selection -->
<section class="py-2 space-y-2 text-sm min-w-[48%] h-full">
<p class="h2">Range Selection</p>
<div class="text-primary h2 flex items-center justify-start">
<span class="material-symbols-outlined mr-2 text-base">info</span>
<p>Select a percentage range.</p>
</div>
<Chart type="bar" :data="chartData" :options="chartOptions" class="h-2/5" />
<div class="px-2">
<p class="py-4">Select percentage of case <span class=" float-right">{{ caseTotalPercent }}%</span></p>
<Slider v-model="selectArea" :step="1" :min="0" :max="traceTotal" range class="mx-2" />
</div>
</section>
<!-- Trace List -->
<section class="h-full min-w-[48%] py-2 space-y-2">
<p class="h2">Trace List ({{ traceTotal }})</p>
<p class="text-primary h2 flex items-center justify-start">
<span class="material-symbols-outlined mr-2 text-base">info</span>Click trace number to see more.
</p>
<div class="overflow-y-scroll overflow-x-hidden scrollbar mx-[-8px] max-h-[calc(100%_-_96px)]" >
<table class="border-separate border-spacing-x-2 text-sm w-full">
<thead class="sticky top-0 z-10 bg-neutral-10">
<tr>
<th class="h2 px-2 border-b border-neutral-500">Trace</th>
<th class="h2 px-2 border-b border-neutral-500 text-start" colspan="3">Occurrences</th>
</tr>
</thead>
<tbody>
<tr v-for="(trace, key) in traceList" :key="key" class=" cursor-pointer hover:text-primary" @click="switchCaseData(trace.id)">
<td class="p-2 text-center">#{{ trace.id }}</td>
<td class="p-2 min-w-[96px]">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(trace.value)"></div>
</div>
</td>
<td class="py-2 text-right">{{ trace.count }}</td>
<td class="p-2 text-right">{{ trace.ratio }}%</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Trace item Table -->
<section v-show="showTraceId" class="pl-4 h-full min-w-full py-2 space-y-2">
<p class="h2 mb-2">Trace #{{ showTraceId }}</p>
<div class="h-52 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div id="cyTrace" ref="cyTrace" class="h-full min-w-full relative"></div>
</div>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_264px)]">
<DataTable :value="cases" showGridlines tableClass="text-sm" breakpoint="0">
<Column field="id" header="Case ID" sortable></Column>
<Column field="started_at" header="Start time" sortable></Column>
<Column field="completed_at" header="End time" sortable></Column>
</DataTable>
</div>
</section>
</div>
</template>
<script>
import { storeToRefs } from 'pinia';
import AllMapDataStore from '@/stores/allMapData.js';
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
export default {
expose: ['selectArea', 'showTraceId', 'traceTotal'],
setup() {
const allMapDataStore = AllMapDataStore();
const { traces, traceTaskSeq, cases } = storeToRefs(allMapDataStore);
return {allMapDataStore, traces, traceTaskSeq, cases}
},
data() {
return {
processMap:{
nodes:[],
edges:[],
},
showTraceId: null,
chartOptions: null,
selectArea: [0, 1]
}
},
computed: {
traceTotal: function() {
return this.traces.length;
},
traceCountTotal: function() {
return this.traces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
},
traceList: function() {
return this.traces.map(trace => {
return {
id: trace.id,
value: Number((trace.ratio * 100).toFixed(1)),
count: trace.count,
ratio: this.getPercentLabel(trace.count / this.traceCountTotal),
};
}).sort((x, y) => x.id - y.id)
.slice(this.selectArea[0], this.selectArea[1]);
},
caseTotalPercent: function() {
let ratioSum = this.traceList.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0) / this.traceCountTotal;
return this.getPercentLabel(ratioSum)
},
chartData: function() {
const start = this.selectArea[0];
const end = this.selectArea[1]-1;
const labels = this.traces.map(trace => `#${trace.id}`);
const data = this.traces.map(trace => this.getPercentLabel(trace.count / this.traceCountTotal));
const selectAreaData = this.traces.map((trace, index) => index >= start && index <= end ? 'rgba(0,153,255)' : 'rgba(203, 213, 225)');
return { // 要呈現的資料
labels,
datasets: [
{
label: 'Trace', // 資料的標題標籤
data,
backgroundColor: selectAreaData,
categoryPercentage: 1.0,
barPercentage: 1.0
},
]
};
}
},
methods: {
/**
* Set bar chart Options
*/
barOptions(){
return {
maintainAspectRatio: false,
aspectRatio: 0.8,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
plugins: {
legend: { // 圖例
display: false,
}
},
animations: false,
scales: {
x: {
display:false
},
y: {
ticks: { // 設定間隔數值
display: false, // 隱藏數值,只顯示格線
min: 0,
max: this.traceList[0].ratio,
stepSize: (this.traceList[0].ratio)/4,
},
grid: {
color: 'rgba(100,116,139)',
z: 1,
},
border: {
display: false, // 隱藏左側多出來的線
}
}
}
};
},
/**
* Number to percentage
* @param {number} val
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
return (val * 100 === 100) ? val * 100 : (val * 100).toFixed(1);
},
/**
* set progress bar width
* @param {number} value
* @returns {string} 樣式的寬度設定
*/
progressWidth(value){
return `width:${value}%;`
},
/**
* switch case data
* @param {number} id
*/
async switchCaseData(id) {
this.showTraceId = id;
this.allMapDataStore.traceId = id;
await this.allMapDataStore.getTraceDetail();
this.createCy();
},
/**
* 將 trace element nodes 資料彙整
*/
setNodesData(){
// 避免每次渲染都重複累加
this.processMap.nodes = [];
// 將 api call 回來的資料帶進 node
this.traceTaskSeq.forEach((node, index) => {
this.processMap.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
height: 80,
width: 100
}
});
})
},
/**
* 將 trace edge line 資料彙整
*/
setEdgesData(){
this.processMap.edges = [];
this.traceTaskSeq.forEach((edge, index) => {
this.processMap.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
});
});
// 關係線數量筆節點少一個
this.processMap.edges.pop();
},
/**
* create trace cytoscape's map
*/
createCy(){
let graphId = this.$refs.cyTrace;
this.setNodesData();
this.setEdgesData();
cytoscapeMapTrace(this.processMap.nodes, this.processMap.edges, graphId);
},
},
mounted() {
this.setNodesData();
this.setEdgesData();
this.createCy();
this.chartOptions = this.barOptions();
this.selectArea = [0, this.traceTotal]
},
}
</script>
<style scoped>
/* Table set */
:deep(.p-datatable-thead th) {
@apply sticky top-0 left-0 z-10 bg-neutral-10
}
:deep(.p-datatable .p-datatable-thead > tr > th) {
@apply !border-y-0 border-neutral-500 bg-neutral-100 after:absolute after:left-0 after:w-full after:h-full after:block after:top-0 after:border-b after:border-t after:border-neutral-500
}
:deep(.p-datatable .p-datatable-tbody > tr > td) {
@apply border-neutral-500 !border-t-0
}
</style>

View File

@@ -0,0 +1,532 @@
<template>
<Sidebar :visible="sidebarFilter" :closeIcon="'pi pi-chevron-left'" :modal="false" position="left" :dismissable="true" :baseZIndex="15" class="!w-11/12 !bg-neutral-100" @hide="hide()">
<template #header>
<ul class="flex space-x-4">
<li class="h1 border-r-2 border-neutral-300 pr-4 cursor-pointer hover:text-neutral-900 hover:duration-700" @click="switchTab('filter')" :class="tab === 'filter'? 'text-neutral-900': 'text-neutral-500'" id="tabFilter">Filter</li>
<li class="h1 border-r-2 border-neutral-300 pr-4 cursor-pointer hover:text-neutral-900 hover:duration-700" @click="switchTab('funnel')" :class="tab === 'funnel'? 'text-neutral-900': 'text-neutral-500'" id="tabFunnel">Funnel</li>
</ul>
</template>
<!-- header: filter -->
<div v-if="tab === 'filter'" class="pt-4 flex w-full h-full">
<!-- title: filter silect -->
<div class="space-y-2 mr-4 w-48 text-sm">
<div>
<p class="h2">Filter Type</p>
<div v-for="(item, index) in selectFilter['Filter Type']" :key="index">
<RadioButton v-model="selectValue[0]" :inputId="item + index" name="Filter Type" :value="item" class="mb-1 mr-2"/>
<label :for="item + index">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[0] === 'Sequence'">
<p class="h2">Activity Sequence</p>
<div v-for="(item, index) in selectFilter['Activity Sequence']" :key="index">
<RadioButton v-model="selectValue[1]" :inputId="item + index" name="Activity Sequence" :value="item" class="mb-1 mr-2"/>
<label :for="item + index">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[0] === 'Sequence' && selectValue[1] === 'Start activity / End activity'">
<p class="h2">Start & End</p>
<div v-for="(item, index) in selectFilter['Start & End']" :key="index">
<RadioButton v-model="selectValue[2]" :inputId="item + index" name="Start & End" :value="item" class="mb-1 mr-2"/>
<label :for="item + index">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[0] === 'Sequence' && selectValue[1] === 'Sequence'">
<p class="h2">Mode</p>
<div v-for="(item, index) in selectFilter['Mode']" :key="index">
<RadioButton v-model="selectValue[3]" :inputId="item + index" name="Mode" :value="item" class="mb-1 mr-2"/>
<label :for="item + index">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[0] === 'Attributes'">
<p class="h2">Mode</p>
<div v-for="(item, index) in selectFilter['ModeAtt']" :key="index">
<RadioButton v-model="selectValue[4]" :inputId="item + index" name="ModeAtt" :value="item" class="mb-1 mr-2"/>
<label :for="item + index">{{ item }}</label>
</div>
</div>
<div>
<p class="h2">Refine</p>
<div v-for="(item, index) in selectFilter['Refine']" :key="index">
<RadioButton v-model="selectValue[5]" :inputId="item + index" name="Refinee" :value="item" class="mb-1 mr-2"/>
<label :for="item + index">{{ item }}</label>
</div>
</div>
<div v-show="selectValue[0] === 'Timeframes'">
<p class="h2">Containment</p>
<div v-for="(item, index) in selectFilter['Containment']" :key="index">
<RadioButton v-model="selectValue[6]" :inputId="item + index" name="Containment" :value="item" class="mb-1 mr-2"/>
<label :for="item + index">{{ item }}</label>
</div>
</div>
</div>
<!-- Fiter content -->
<div class="w-[calc(100%_-_208px)] h-full">
<!-- Filter titles -->
<div class="space-y-2 w-full h-[calc(100%_-_48px)]">
<!-- title: Activity Select -->
<div class="w-full h-[calc(100%_-_40px)]" v-if="selectValue[0] === 'Sequence'">
<p class="h2 ml-1">Activity Select</p>
<!-- Filter task Data-->
<ActOccCase v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Have activity(s)'" :tableTitle="'Activity List'" :tableData="filterAllTaskData" :tableSelect="selectFilterTask" :progressWidth ="progressWidth" @on-row-select="onRowAct"></ActOccCase>
<!-- Filter Start Data -->
<ActOcc v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Start activity / End activity' && selectValue[2] === 'Start'" :tableTitle="'Start activity'" :tableData="filterStartData" :tableSelect="selectFilterStart" :progressWidth ="progressWidth" @on-row-select="onRowStart"></ActOcc>
<!-- Filter End Data -->
<ActOcc v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Start activity / End activity' && selectValue[2] === 'End'" :tableTitle="'End activity'" :tableData="filterEndData" :tableSelect="selectFilterEnd" :progressWidth ="progressWidth" @on-row-select="onRowEnd"></ActOcc>
<!-- Filter Start And End Data -->
<div v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Start activity / End activity' && selectValue[2] === 'Start & End'" class="flex justify-between items-center w-full h-full space-x-4 ">
<ActOcc :tableTitle="'Start activity'" :tableData="filterStartToEndData" :tableSelect="selectFilterStartToEnd" :progressWidth ="progressWidth" class="w-1/2" @on-row-select="startRow"></ActOcc>
<ActOcc :tableTitle="'End activity'" :tableData="filterEndToStartData" :tableSelect="selectFilterEndToStart" :progressWidth ="progressWidth" class="w-1/2" @on-row-select="endRow"></ActOcc>
</div>
<!-- Filter Sequence -->
<div v-if="selectValue[0] === 'Sequence' && selectValue[1] === 'Sequence'" class="flex justify-between items-center w-full h-full space-x-4">
<ActAndSeq :filterTaskData="filterTaskData" :progressWidth ="progressWidth" :listSeq="listSeq" @update:listSeq="onUpdateListSeq"></ActAndSeq>
</div>
</div>
<!-- title: Trace -->
<Trace v-if="selectValue[0] === 'Trace'" ref="filterTraceView"></Trace>
<!-- title: Timeframes -->
<Timeframes v-if="selectValue[0] === 'Timeframes'"></Timeframes>
</div>
<!-- Button -->
<div class="float-right space-x-4 px-4 py-2">
<button type="button" class="btn btn-sm btn-neutral" @click="reset">Clear</button>
<button type="button" class="btn btn-sm btn-neutral" @click="submit">Apply</button>
</div>
</div>
</div>
<!-- header: funnel -->
<Funnel v-if="tab === 'funnel'" @submit-all="sumbitAll"></Funnel>
</Sidebar>
</template>
<script>
import { storeToRefs } from 'pinia';
import LoadingStore from '@/stores/loading.js';
import AllMapDataStore from '@/stores/allMapData.js';
import ActOccCase from '@/components/Discover/Map/Filter/ActOccCase.vue';
import ActOcc from '@/components/Discover/Map/Filter/ActOcc.vue';
import ActAndSeq from '@/components/Discover/Map/Filter/ActAndSeq.vue';
import Funnel from '@/components/Discover/Map/Filter/Funnel.vue';
import Trace from '@/components/Discover/Map/Filter/Trace.vue';
import Timeframes from '@/components/Discover/Map/Filter/Timeframes.vue';
import getMoment from 'moment';
export default {
props: ['sidebarFilter', 'filterTasks', 'filterStartToEnd', 'filterEndToStart', 'filterTimeframe', 'filterTrace'],
setup() {
const loadingStore = LoadingStore();
const allMapDataStore = AllMapDataStore();
const { isLoading } = storeToRefs(loadingStore);
const { hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, selectTimeFrame } = storeToRefs(allMapDataStore);
return { isLoading, hasResultRule, temporaryData, postRuleData, ruleData, isRuleData, allMapDataStore, selectTimeFrame }
},
data() {
return {
selectFilter: {
// 'Filter Type': ['Sequence', 'Attributes', 'Trace', 'Timeframes'],
'Filter Type': ['Sequence', 'Trace', 'Timeframes'],
'Activity Sequence':['Have activity(s)', 'Start activity / End activity', 'Sequence'],
'Start & End': ['Start', 'End', 'Start & End'],
'Mode': ['Directly follows', 'Eventually follows'],
'ModeAtt': ['Case', 'Activity'],
'Refine': ['Include', 'Exclude'],
'Containment': ['Contained in', 'Started in', 'Ended in', 'Active in'],
},
tab: 'filter', // filter | funnel
selectValue: {
0: 'Sequence',
1: 'Have activity(s)',
2: 'Start',
3: 'Directly follows',
4: 'Case',
5: 'Include',
6: 'Contained in',
},
selectFilterTask: null,
selectFilterStart: null,
selectFilterEnd: null,
selectFilterStartToEnd: null,
selectFilterEndToStart: null,
listSeq: [],
//若第一次選擇 start, 則 end 連動改變,若第一次選擇 end, 則 start 連動改變
isStartSelected: null,
isEndSelected: null,
isActAllTask: true,
rowData: [],
}
},
components: {
ActOccCase,
ActOcc,
ActAndSeq,
Funnel,
Trace,
Timeframes,
},
computed: {
// All Task
filterAllTaskData: function() {
return this.setHaveAct([...this.filterTasks]);
},
// Act And Seq
filterTaskData: function() {
return this.isActAllTask? this.setHaveAct([...this.filterTasks]) : this.filterTaskData;
},
// Start and End Task
filterStartData: function() {
return this.setActData(this.filterStartToEnd);
},
filterEndData: function() {
return this.setActData(this.filterEndToStart);
},
filterStartToEndData: function() {
return this.isEndSelected ? this.setStartAndEndData(this.filterEndToStart, this.rowData, 'sources') : this.setActData(this.filterStartToEnd);
},
filterEndToStartData: function() {
return this.isStartSelected ? this.setStartAndEndData(this.filterStartToEnd, this.rowData, 'sinks') : this.setActData(this.filterEndToStart);
},
},
methods: {
/**
* @param {string} switch Summary or Insight
*/
switchTab(tab) {
this.tab = tab;
},
/**
* Number to percentage
* @param {number} val
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
return (val * 100 === 100) ? `${val * 100}%` : `${(val * 100).toFixed(1)}%`;
},
/**
* set progress bar width
* @param {number} value
* @returns {string} 樣式的寬度設定
*/
progressWidth(value){
return `width:${value}%;`
},
//設定 Have activity(s) 內容
/**
* @param {array} data filterTaskData
*/
setHaveAct(data){
return data.map(task => {
return {
label: task.label,
occ_value: Number(task.occurrence_ratio * 100),
occurrences: Number(task.occurrences).toLocaleString('en-US'),
occurrence_ratio: this.getPercentLabel(task.occurrence_ratio),
case_value: Number(task.case_ratio * 100),
cases: task.cases.toLocaleString('en-US'),
case_ratio: this.getPercentLabel(task.case_ratio),
};
}).sort((x, y) => y.occurrences - x.occurrences);
},
// 調整 filterStartData / filterEndData / filterStartToEndData / filterEndToStartData 的內容
/**
* @param {array} array filterStartToEnd / filterEndToStart
*/
setActData(array) {
let list = [];
array.forEach((task, index) => {
let data = {
label: task.label,
occ_value: Number(task.occurrence_ratio * 100),
occurrences: Number(task.occurrences).toLocaleString('en-US'),
occurrence_ratio: this.getPercentLabel(task.occurrence_ratio),
};
list.push(data);
});
return list;
},
/**
* @param {array} select select Have activity(s) rows
*/
onRowAct(select){
this.selectFilterTask = select;
},
/**
* @param {object} e select Start rows
*/
onRowStart(e){
this.selectFilterStart = e.data;
},
/**
* @param {object} e select End rows
*/
onRowEnd(e){
this.selectFilterEnd = e.data;
},
/**
* @param {array} e Update List Seq
*/
onUpdateListSeq(listSeq) {
this.listSeq = listSeq;
this.isActAllTask = false;
},
// 在 Start & End 若第一次選擇 start, 則 end 連動改變,若第一次選擇 end, 則 start 連動改變
/**
* @param {object} e object contains selected row's data
*/
startRow(e){
this.selectFilterStartToEnd = e.data;
if(this.isStartSelected === null || this.isStartSelected === true){
this.isStartSelected = true;
this.isEndSelected = false;
this.rowData = e.data;
}
},
endRow(e) {
this.selectFilterEndToStart = e.data;
if(this.isEndSelected === null || this.isEndSelected === true){
this.isEndSelected = true;
this.isStartSelected = false;
this.rowData = e.data;
}
},
// 重新設定連動的 filterStartToEndData / filterEndToStartData 內容
/**
* @param {array} eventData Start or End List
* @param {object} rowData 所選擇的 row's data
* @param {string} event sinks / sources
*/
setStartAndEndData(eventData, rowData, event){
const filterData = event === 'sinks' ? this.filterEndToStart : this.filterStartToEnd;
const relatedItems = eventData
.find(task => task.label === rowData.label)
?.[event]?.filter(item => filterData.some(ele => ele.label === item))
?.map(item => filterData.find(ele => ele.label === item));
if (!relatedItems) return [];
return relatedItems.map(item => ({
label: item.label,
occ_value: Number(item.occurrence_ratio * 100),
occurrences: Number(item.occurrences).toLocaleString('en-US'),
occurrence_ratio: this.getPercentLabel(item.occurrence_ratio),
}));
},
/**
* @param {object} e task's object
*/
setRule(e) {
let label, type;
const includeStr = e.is_exclude?" Exclude ":" Include ";
let containmentMap = {
'occurred-in' : 'Contained in',
'started-in' : 'Started in',
'completed-in' : 'Ended in',
'occurred-around' : 'Active in'
};
switch(e.type){
case "contains-task":
label = `${includeStr} ${e.task}`
type = "Sequence"
break
case "starts-with":
label = `Start with ${e.task} ${includeStr}`
type = "Sequence"
break
case "ends-with":
label = `End with ${e.task} ${includeStr}`
type = "Sequence"
break
case "start-end":
label = `Start with ${e.starts_with}, End with ${e.ends_with} ${includeStr}`
type = "Sequence"
break
case "directly-follows":
label = `Directly-follows ${e.task_seq.join(' -> ')}${includeStr}`
type = "Sequence"
break
case "eventually-follows":
label = `Eventually-follows ${e.task_seq.join(' -> ')}${includeStr}`
type = "Sequence"
break
case "occurred-in":
case "started-in":
case "completed-in":
case "occurred-around":
label = `${containmentMap[e.type]} from ${getMoment(e.start).format("YYYY-MM-DD HH:mm:ss")} to ${getMoment(e.end).format("YYYY-MM-DD HH:mm:ss")} ${includeStr}`
type = "Timeframe"
break
case "trace-freq":
label = `from #${e.lower} to #${e.upper} ${includeStr}`
type = "Trace"
break
};
return {
type,
label,
toggle: true,
};
},
/**
* @param {boolean} massage true | false 清空選項
*/
reset(massage) {
// Sequence
this.selectFilterTask = null;
this.selectFilterStart = null;
this.selectFilterEnd = null;
this.selectFilterStartToEnd = null;
this.selectFilterEndToStart = null;
this.listSeq = [];
this.isStartSelected = null;
this.isEndSelected = null;
this.isActAllTask = true;
// Timeframes
this.selectTimeFrame = [];
// Trace
if (this.$refs.filterTraceView) {
this.$refs.filterTraceView.showTraceId = null;
this.$refs.filterTraceView.selectArea = [0, this.$refs.filterTraceView.traceTotal];
};
// 成功訊息
massage ? this.$toast.success('Reset Success.') : null;
},
// header:Filter 發送選取的資料
async submit(){
let data;
let sele = this.selectValue;
let isExclude = sele[5] === 'Exclude' ? true : false;
let containmentMap = {
'Contained in': 'occurred-in',
'Started in': 'started-in',
'Ended in': 'completed-in',
'Active in': 'occurred-around'
};
// Filter Type 選 Sequence 的行為
// 若陣列為空,則跳出警告訊息
if(sele[0] === 'Sequence'){
if(sele[1] === 'Have activity(s)'){ // Activity Sequence 選 Have activity(s) 的行為
if(!this.selectFilterTask?.length) return this.$toast.error('Not selected');
else {
// 將多選的 task 拆成一包包 obj
data = this.selectFilterTask.map(task => {
return {
type : 'contains-task',
task : task.label,
is_exclude : isExclude,
}
})
};
}else if(sele[1] === 'Start activity / End activity') { // Activity Sequence 選 Start activity / End activity 的行為
if(sele[2] === 'Start') {
if(this.selectFilterStart === null || this.selectFilterStart.length === 0) return this.$toast.error('Not selected');
else {
data = {
type: 'starts-with',
task: this.selectFilterStart.label,
is_exclude: isExclude,
}
};
}else if(sele[2] === 'End') {
if(this.selectFilterEnd === null || this.selectFilterEnd.length === 0) return this.$toast.error('Not selected');
else {
data = {
type: 'ends-with',
task: this.selectFilterEnd.label,
is_exclude: isExclude,
}
};
}else if(sele[2] === 'Start & End') {
if(this.selectFilterStartToEnd === null || this.selectFilterStartToEnd.length === 0 || this.selectFilterEndToStart === null || this.selectFilterEndToStart.length === 0 ) return this.$toast.error('Both Start and End must be selected.');
else {
data = {
type: 'start-end',
starts_with: this.selectFilterStartToEnd.label,
ends_with: this.selectFilterEndToStart.label,
is_exclude: isExclude,
}
}
};
}else if(sele[1] === 'Sequence'){ // Activity Sequence 選 Sequence 的行為
if(this.listSeq.length < 2) return this.$toast.error('Select two or more.');
else {
data = {
type: sele[3] === 'Directly follows' ? 'directly-follows' : 'eventually-follows',
task_seq: this.listSeq.map(task => task.label),
is_exclude: isExclude,
}
};
}
} else if(sele[0] === 'Timeframes'){ // Filter Type 選 Timeframes 的行為
data = {
type: containmentMap[sele[6]],
start: this.selectTimeFrame[0],
end: this.selectTimeFrame[1],
is_exclude: isExclude,
}
} else if(sele[0] === 'Trace'){ // Filter Type 選 Trace 的行為
data = {
type: 'trace-freq',
lower: this.$refs.filterTraceView.selectArea[0]+1,
upper: this.$refs.filterTraceView.selectArea[1],
is_exclude: isExclude,
}
}
// 將資料指向 Vue data 雙向綁定
const postData = Array.isArray(data) ? data : [data];
// 快速檢查每一 filter 規則是否為空集合
this.postRuleData = postData;
await this.allMapDataStore.checkHasResult();
// 有 Data 就加進 Funnel沒有 Data 不加進 Funnel 和跳錯誤訊息
if(this.hasResultRule === null) return;
else if(this.hasResultRule) {
if(!this.temporaryData?.length){
this.temporaryData.push(...postData);
this.isRuleData = Array.from(this.temporaryData);
this.ruleData = this.isRuleData.map(e => this.setRule(e));
}else {
this.temporaryData.push(...postData);
this.isRuleData.push(...postData);
this.ruleData.push(...postData.map(e => this.setRule(e)))
}
this.reset(false);
this.isLoading = true;
await new Promise(resolve => setTimeout(resolve, 1000));
this.isLoading = false;
this.$toast.success('Filter Success. Click on Funnel to view the configuration result.');
}else {
this.reset(false);
this.isLoading = true;
await new Promise(resolve => setTimeout(resolve, 1000));
this.isLoading = false;
this.$toast.warning('No Data.');
};
},
/**
* create map
*/
sumbitAll() {
this.$emit('submit-all');
},
/**
* hide map
*/
hide() {
// 因 trace api 連動,所以關閉側邊欄時讓數值歸 1
this.allMapDataStore.traceId = 1;
this.allMapDataStore.getTraceDetail();
}
},
}
</script>
<style scoped>
#searchFiles::-webkit-search-cancel-button{
appearance: none;
}
</style>

View File

@@ -0,0 +1,266 @@
<template>
<Sidebar :visible="sidebarState" :closeIcon="'pi pi-angle-right'" :modal="false" position="right" :dismissable="true" class="!w-[360px]" @hide="hide" @show="show">
<template #header>
<ul class="flex space-x-4 pl-4">
<li class="h1 border-r-2 border-neutral-300 pr-4 cursor-pointer hover:text-neutral-900 hover:duration-700" @click="switchTab('summary')" :class="tab === 'summary'? 'text-neutral-900': ''">Summary</li>
<li class="h1 border-r-2 border-neutral-300 pr-4 cursor-pointer hover:text-neutral-900 hover:duration-700" @click="switchTab('insight')" :class="tab === 'insight'? 'text-neutral-900': ''">Insight</li>
</ul>
</template>
<!-- header: summary -->
<div v-if="tab === 'summary'">
<!-- Stats -->
<ul class="pb-4 border-b border-neutral-300">
<li>
<p class="h2">Cases</p>
<div class="flex justify-between items-center">
<div class="w-full mr-8">
<span class="block text-sm">{{ numberLabel(stats.cases.count) }} / {{ numberLabel(stats.cases.total) }}</span>
<ProgressBar :value="valueCases" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300"></ProgressBar>
</div>
<span class="block text-primary text-2xl font-medium">{{ getPercentLabel(stats.cases.ratio) }}</span>
</div>
</li>
<li>
<p class="h2">Traces</p>
<div class="flex justify-between items-center">
<div class="w-full mr-8">
<span class="block text-sm">{{ numberLabel(stats.traces.count) }} / {{ numberLabel(stats.traces.total) }}</span>
<ProgressBar :value="valueTraces" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300"></ProgressBar>
</div>
<span class="block text-primary text-2xl font-medium">{{ getPercentLabel(stats.traces.ratio) }}</span>
</div>
</li>
<li>
<p class="h2">Activity Instances</p>
<div class="flex justify-between items-center">
<div class="w-full mr-8">
<span class="block text-sm">{{ numberLabel(stats.task_instances.count) }} / {{ numberLabel(stats.task_instances.total) }}</span>
<ProgressBar :value="valueTaskInstances" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300"></ProgressBar>
</div>
<span class="block text-primary text-2xl font-medium">{{ getPercentLabel(stats.task_instances.ratio) }}</span>
</div>
</li>
<li>
<p class="h2">Activities</p>
<div class="flex justify-between items-center">
<div class="w-full mr-8">
<span class="block text-sm">{{ numberLabel(stats.tasks.count) }} / {{ numberLabel(stats.tasks.total) }}</span>
<ProgressBar :value="valueTasks" :showValue="false" class="!h-2 !rounded-full my-2 !bg-neutral-300"></ProgressBar>
</div>
<span class="block text-primary text-2xl font-medium">{{ getPercentLabel(stats.tasks.ratio) }}</span>
</div>
</li>
</ul>
<!-- Log Timeframe -->
<div class="pt-1 pb-4 border-b border-neutral-300">
<p class="h2">Log Timeframe</p>
<p><span class="px-4">{{ moment(stats.started_at
) }}</span>~<span class="px-4">{{ moment(stats.completed_at
) }}</span></p>
</div>
<!-- Case Duration -->
<div class="pt-1 pb-4">
<p class="h2">Case Duration</p>
<ul class="space-y-1">
<li><Tag value="MIN" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ timeLabel(stats.case_duration.min) }}</li>
<li><Tag value="AVG" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ timeLabel(stats.case_duration.average
) }}</li>
<li><Tag value="MED" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ timeLabel(stats.case_duration.median) }}</li>
<li><Tag value="MAX" class="!text-neutral-900 !bg-neutral-200 mr-2 !w-10"></Tag>{{ timeLabel(stats.case_duration.max) }}</li>
</ul>
</div>
</div>
<!-- header: insight -->
<div v-if="tab === 'insight'">
<div class="border-b-2 border-neutral-300 mb-4">
<p class="h2">Most frequent</p>
<ul class="list-disc ml-6">
<li>Activity:&nbsp;
<span class="text-primary break-words" v-for="(value, key) in insights.most_freq_tasks" :key="key">{{ value }}<span v-if="key !== insights.most_freq_tasks.length - 1" class="text-neutral-900">,&nbsp;</span>
</span>
</li>
<li>Inbound connections:&nbsp;
<span class="text-primary break-words" v-for="(value, key) in insights.most_freq_in" :key="key">{{ value }}<span v-if="key !== insights.most_freq_in.length - 1" class="text-neutral-900">,&nbsp;</span>
</span>
</li>
<li>Outbound connections:&nbsp;
<span class="text-primary break-words" v-for="(value, key) in insights.most_freq_out" :key="key">{{ value }}<span v-if="key !== insights.most_freq_out.length - 1" class="text-neutral-900">,&nbsp;</span>
</span>
</li>
</ul>
<p class="h2">Most time-consuming</p>
<ul class="list-disc ml-6 mb-4">
<li class="w-full">Activity:&nbsp;
<span class="text-primary break-words" v-for="(value, key) in insights.most_time_tasks" :key="key">{{ value }}<span v-if="key !== insights.most_time_tasks.length - 1" class="text-neutral-900">,&nbsp;</span>
</span>
</li>
<li>Connection:&nbsp;
<span class="text-primary break-words" v-for="(item, key) in insights.most_time_edges" :key="key">
<span v-for="(value, index) in item" :key="index">{{ value }}<span v-if="index !== item.length - 1">&nbsp;<span class="material-symbols-outlined text-lg align-sub ">arrow_forward</span>&nbsp;</span>
</span><span v-if="key !== insights.most_time_edges.length - 1" class="text-neutral-900">,&nbsp;</span>
</span>
</li>
</ul>
</div>
<div>
<ul class="text-neutral-500 grid grid-cols-2 gap-2 text-center text-sm font-medium mb-2">
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="active1 = 0" :class="active1 === 0? 'text-primary border-primary':''">Self-loop</li>
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="active1 = 1" :class="active1 === 1? 'text-primary border-primary':''">Short-loop</li>
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="active1 = 2" :class="active1 === 2? 'text-primary border-primary':''">Shortest Trace</li>
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="active1 = 3" :class="active1 === 3? 'text-primary border-primary':''">Longest Trace</li>
<li class="border border-neutral-500 rounded p-2 cursor-pointer hover:text-primary hover:border-primary hover:duration-500" @click="active1 = 4" :class="active1 === 4? 'text-primary border-primary':''">Most Frequent Trace</li>
</ul>
<div>
<TabView ref="tabview2" v-model:activeIndex="active1">
<TabPanel header="Self-loop">
<p v-if="insights.self_loops.length === 0">No data</p>
<ul v-else class="list-disc ml-6">
<li v-for="(value, key) in insights.self_loops" :key="key">
<span>{{ value }}</span>
</li>
</ul>
</TabPanel>
<TabPanel header="Short-loop">
<p v-if="insights.short_loops.length === 0">No data</p>
<ul v-else class="list-disc ml-6">
<li class="break-words" v-for="(item, key) in insights.short_loops" :key="key">
<span v-for="(value, index) in item" :key="index">{{ value }}<span v-if="index !== item.length - 1">&nbsp;<span class="material-symbols-outlined text-lg align-sub">sync_alt</span>&nbsp;</span>
</span>
</li>
</ul>
</TabPanel>
<TabPanel header="Shortest Trace">
<p v-if="insights.shortest_traces.length === 0">No data</p>
<ul v-else class="list-disc ml-6">
<li class="break-words" v-for="(item, key) in insights.shortest_traces" :key="key">
<span v-for="(value, index) in item" :key="index">{{ value }}<span v-if="index !== item.length - 1">&nbsp;<span class="material-symbols-outlined text-lg align-sub">arrow_forward</span>&nbsp;</span>
</span>
</li>
</ul>
</TabPanel>
<TabPanel header="Longest Trace">
<p v-if="insights.longest_traces.length === 0">No data</p>
<ul v-else class="list-disc ml-6">
<li class="break-words" v-for="(item, key) in insights.longest_traces" :key="key">
<span v-for="(value, index) in item" :key="index">{{ value }}<span v-if="index !== item.length - 1">&nbsp;<span class="material-symbols-outlined text-lg align-sub">arrow_forward</span>&nbsp;</span>
</span>
</li>
</ul>
</TabPanel>
<TabPanel header="Most Frequent Trace">
<li v-if="insights.most_freq_traces.length === 0">No data</li>
<ul v-else class="list-disc ml-6">
<li class="break-words" v-for="(item, key) in insights.most_freq_traces" :key="key">
<span v-for="(value, index) in item" :key="index">{{ value }}<span v-if="index !== item.length - 1">&nbsp;<span class="material-symbols-outlined text-lg align-sub">arrow_forward</span>&nbsp;</span>
</span>
</li>
</ul>
</TabPanel>
</TabView>
</div>
</div>
</div>
</Sidebar>
</template>
<script>
import getNumberLabel from '@/module/numberLabel.js';
import getTimeLabel from '@/module/timeLabel.js';
import getMoment from 'moment';
export default {
props:{
sidebarState: {
type: Boolean,
require: false,
},
stats: {
type: Object,
required: false,
},
insights: {
type: Object,
required: false,
}
},
data() {
return {
tab: 'summary',
valueCases: 0,
valueTraces: 0,
valueTaskInstances: 0,
valueTasks: 0,
active1: 0,
}
},
methods: {
/**
* @param {string} switch Summary or Insight
*/
switchTab(tab) {
this.tab = tab;
},
/**
* @param {number} time use timeLabel.js
*/
timeLabel(time){
return getTimeLabel(time);
},
/**
* @param {number} time use moment
*/
moment(time){
return getMoment(time).format('YYYY-MM-DD HH:mm');
},
/**
* @param {number} num use numberLabel.js
*/
numberLabel(num){
return getNumberLabel(num);
},
/**
* Number to percentage
* @param {number} val
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
if(val * 100 === 100) return `${val * 100}%`;
return `${(val * 100).toFixed(1)}%`;
},
/**
* Behavior when show
*/
show(){
this.valueCases = this.stats.cases.ratio * 100;
this.valueTraces= this.stats.traces.ratio * 100;
this.valueTaskInstances = this.stats.task_instances.ratio * 100;
this.valueTasks = this.stats.tasks.ratio * 100;
},
/**
* Behavior when hidden
*/
hide(){
this.valueCases = 0;
this.valueTraces= 0;
this.valueTaskInstances = 0;
this.valueTasks = 0;
},
},
}
</script>
<style scoped>
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-primary
}
:deep(.p-tabview-nav-container) {
@apply hidden
}
:deep(.p-tabview-panels) {
@apply !bg-neutral-100 p-2 rounded
}
:deep(.p-tabview-panel) {
@apply animate-fadein
}
</style>

View File

@@ -0,0 +1,207 @@
<template>
<Sidebar :visible="sidebarTraces" :closeIcon="'pi pi-chevron-left'" :modal="false" position="left" :dismissable="true" class="!w-11/12" @show="show()" @hide="hide()">
<template #header>
<p class="h1">Traces</p>
</template>
<div class="pt-4 h-full flex items-center justify-start">
<!-- Trace List -->
<section class="w-80 h-full pr-4 border-r border-neutral-300">
<p class="h2 px-2 mb-2">Trace List ({{ traceTotal }})</p>
<p class="text-primary h2 px-2 mb-2">
<span class="material-symbols-outlined text-sm align-[-10%] mr-2">info</span>Click trace number to see more.
</p>
<div class="overflow-y-scroll overflow-x-hidden scrollbar mx-[-8px] max-h-[calc(100%_-_96px)]" >
<table class="border-separate border-spacing-x-2 text-sm">
<thead class="sticky top-0 z-10 bg-neutral-10">
<tr>
<th class="h2 px-2 border-b border-neutral-500">Trace</th>
<th class="h2 px-2 border-b border-neutral-500 text-start" colspan="3">Occurrences</th>
</tr>
</thead>
<tbody>
<tr v-for="(trace, key) in traceList" :key="key" class=" cursor-pointer hover:text-primary" @click="switchCaseData(trace.id)">
<td class="p-2">#{{ trace.id }}</td>
<td class="p-2 w-24">
<div class="h-4 w-full bg-neutral-300 rounded-sm overflow-hidden">
<div class="h-full bg-primary" :style="progressWidth(trace.value)"></div>
</div>
</td>
<td class="py-2 text-right">{{ trace.count }}</td>
<td class="p-2">{{ trace.ratio }}</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Trace item Table -->
<section class="pl-4 h-full w-[calc(100%_-_320px)]">
<p class="h2 mb-2">Trace #{{ showTraceId }}</p>
<div class="h-52 w-full px-2 mb-2 border border-neutral-300 rounded">
<div class="h-full w-full">
<div id="cyTrace" ref="cyTrace" class="h-full min-w-full relative"></div>
</div>
</div>
<div class="overflow-y-auto overflow-x-auto scrollbar h-[calc(100%_-_264px)]">
<DataTable :value="cases" showGridlines tableClass="text-sm" breakpoint="0">
<Column field="id" header="Case ID" sortable></Column>
<Column field="started_at" header="Start time" sortable></Column>
<Column field="completed_at" header="End time" sortable></Column>
</DataTable>
</div>
</section>
</div>
</Sidebar>
</template>
<script>
import cytoscapeMapTrace from '@/module/cytoscapeMapTrace.js';
import { storeToRefs } from 'pinia';
import AllMapDataStore from '@/stores/allMapData.js';
export default {
props: {
sidebarTraces: Boolean,
traces: Array,
traceTaskSeq: Array,
cases: Array,
},
setup() {
const allMapDataStore = AllMapDataStore();
return {allMapDataStore}
},
data() {
return {
processMap:{
nodes:[],
edges:[],
},
showTraceId: 1,
}
},
computed: {
traceTotal: function() {
return this.traces.length;
},
traceList: function() {
let sum = this.traces.map(trace => trace.count).reduce((acc, cur) => acc + cur, 0);
return this.traces.map(trace => {
return {
id: trace.id,
value: Number((trace.ratio * 100).toFixed(1)),
count: trace.count,
ratio: this.getPercentLabel(trace.count / sum),
};
}).sort((x, y) => x.id - y.id);
},
},
methods: {
/**
* Number to percentage
* @param {number} val
* @returns {string} 轉換完成的百分比字串
*/
getPercentLabel(val){
return (val * 100 === 100) ? `${val * 100}%` : `${(val * 100).toFixed(1)}%`;
},
/**
* set progress bar width
* @param {number} value
* @returns {string} 樣式的寬度設定
*/
progressWidth(value){
return `width:${value}%;`
},
/**
* switch case data
* @param {number} id
*/
async switchCaseData(id) {
this.showTraceId = id;
this.$emit('switch-Trace-Id', this.showTraceId);
},
/**
* 將 trace element nodes 資料彙整
*/
setNodesData(){
// 避免每次渲染都重複累加
this.processMap.nodes = [];
// 將 api call 回來的資料帶進 node
this.traceTaskSeq.forEach((node, index) => {
this.processMap.nodes.push({
data: {
id: index,
label: node,
backgroundColor: '#CCE5FF',
bordercolor: '#003366',
shape: 'round-rectangle',
height: 80,
width: 100
}
});
})
},
/**
* 將 trace edge line 資料彙整
*/
setEdgesData(){
this.processMap.edges = [];
this.traceTaskSeq.forEach((edge, index) => {
this.processMap.edges.push({
data: {
source: `${index}`,
target: `${index + 1}`,
lineWidth: 1,
style: 'solid'
}
});
});
// 關係線數量筆節點少一個
this.processMap.edges.pop();
},
/**
* create trace cytoscape's map
*/
createCy(){
let graphId = this.$refs.cyTrace;
this.setNodesData();
this.setEdgesData();
cytoscapeMapTrace(this.processMap.nodes, this.processMap.edges, graphId);
},
/**
* create map
*/
show() {
this.setNodesData();
this.setEdgesData();
this.createCy();
},
/**
* hide map
*/
hide() {
// 因 trace api 連動,所以關閉側邊欄時讓數值歸 1
this.showTraceId = 1;
this.allMapDataStore.getTraceDetail();
}
},
}
</script>
<style scoped>
/* 進度條顏色 */
:deep(.p-progressbar .p-progressbar-value) {
@apply bg-primary
}
/* Table set */
:deep(.p-datatable-thead th) {
@apply sticky top-0 left-0 z-10 bg-neutral-10
}
:deep(.p-datatable .p-datatable-thead > tr > th) {
@apply !border-y-0 border-neutral-500 bg-neutral-100 after:absolute after:left-0 after:w-full after:h-full after:block after:top-0 after:border-b after:border-t after:border-neutral-500
}
:deep(.p-datatable .p-datatable-tbody > tr > td) {
@apply border-neutral-500 !border-t-0
}
</style>

View File

@@ -0,0 +1,167 @@
<template>
<Sidebar :visible="sidebarView" :closeIcon="'pi pi-chevron-left'" :modal="false" position="left" :dismissable="true" >
<template #header>
<p class="h1">Visualization Setting</p>
</template>
<div>
<!-- View -->
<div class="my-4 border-b border-neutral-200">
<p class="h2">View</p>
<ul class="space-y-3 mb-4">
<!-- 選擇 bpmn / processmap button -->
<li class="btn-toggle-content">
<span class="btn-toggle-item" :class="mapType === 'processMap'?'btn-toggle-show ':''" @click="switchMapType('processMap')">
Process map
</span>
<span class="btn-toggle-item" :class="mapType === 'bpmn'?'btn-toggle-show':''" @click="switchMapType('bpmn')">
BPMN Model
</span>
</li>
<!-- 選擇繪畫樣式 bezier / unbundled-bezier button-->
<li class="btn-toggle-content">
<span class="btn-toggle-item" :class="curveStyle === 'unbundled-bezier'?'btn-toggle-show ':''" @click="switchCurveStyles('unbundled-bezier')">
Curved
</span>
<span class="btn-toggle-item" :class="curveStyle === 'taxi'?'btn-toggle-show':''" @click="switchCurveStyles('taxi')">
Elbow
</span>
</li>
<!-- 直向 TB | 橫向 LR -->
<li class="btn-toggle-content">
<span class="btn-toggle-item" :class="rank === 'LR'?'btn-toggle-show ':''" @click="switchRank('LR')">
Horizontal
</span>
<span class="btn-toggle-item" :class="rank === 'TB'?'btn-toggle-show':''" @click="switchRank('TB')">
Vertical
</span>
</li>
</ul>
</div>
<!-- Data Layer -->
<div>
<p class="h2">Data Layer</p>
<ul class="space-y-2">
<li class="flex justify-between mb-3" @change="switchDataLayerType($event, 'freq')">
<div class="flex items-center w-1/2">
<!-- <input type="radio" id="freq" value="freq" name="dataLayer" class="peer hidden" checked/>
<label for="freq" class="inline-block h-4 w-4 m-2 cursor-pointer rounded-full ring-2 ring-neutral-300 shadow-sm peer-checked:ring-2 peer-checked:ring-primary peer-checked:ring-offset-2 peer-checked:bg-primary">
</label>
<span class="inline-block ml-2">Frequency</span> -->
<RadioButton v-model="dataLayerType" inputId="freq" name="dataLayer" value="freq" class="mr-2"/>
<label>Frequency</label>
</div>
<div class="w-1/2">
<select class="border border-neutral-500 rounded p-1 w-full focus-visible:outline-primary" :disabled="dataLayerType === 'duration'">
<option v-for="(freq, index) in selectFrequency" :key="index" :value="freq.value" :disabled="freq.disabled" :selected="freq.value === selectedFreq">{{ freq.label }}</option>
</select>
</div>
</li>
<li class="flex justify-between mb-3" @change="switchDataLayerType($event, 'duration')">
<div class="flex items-center w-1/2">
<!-- <input type="radio" id="duration" value="duration" name="dataLayer" class="peer hidden" />
<label for="duration" class="inline-block h-4 w-4 m-2 cursor-pointer rounded-full ring-2 ring-neutral-300 shadow-sm peer-checked:ring-2 peer-checked:ring-primary peer-checked:ring-offset-2 peer-checked:bg-primary"></label>
<span class="inline-block ml-2">Duration</span> -->
<RadioButton v-model="dataLayerType" inputId="duration" name="dataLayer" value="duration" class="mr-2"/>
<label>Duration</label>
</div>
<div class="w-1/2">
<select class="border border-neutral-500 rounded p-1 w-full focus-visible:outline-primary" :disabled="dataLayerType === 'freq'">
<option v-for="(duration, index) in selectDuration" :key="index" :value="duration.value" :disabled="duration.disabled" :selected="duration.value === selectedDuration">{{ duration.label }}</option>
</select>
</div>
</li>
</ul>
</div>
</div>
</Sidebar>
</template>
<script>
export default {
props: {
sidebarView: {
type: Boolean,
require: true,
},
},
data() {
return {
selectFrequency: [
{ value:"total", label:"Total", disabled:false, },
{ value:"rel_freq", label:"Relative", disabled:false, },
{ value:"average", label:"Average", disabled:false, },
{ value:"median", label:"Median", disabled:false, },
{ value:"max", label:"Max", disabled:false, },
{ value:"min", label:"Min", disabled:false, },
{ value:"cases", label:"Number of cases", disabled:false, },
],
selectDuration:[
{ value:"total", label:"Total", disabled:false, },
{ value:"rel_duration", label:"Relative", disabled:false, },
{ value:"average", label:"Average", disabled:false, },
{ value:"median", label:"Median", disabled:false, },
{ value:"max", label:"Max", disabled:false, },
{ value:"min", label:"Min", disabled:false, },
],
curveStyle:'unbundled-bezier', // unbundled-bezier | taxi
mapType: 'processMap', // processMap | bpmn
dataLayerType: 'freq', // freq | duration
dataLayerOption: 'total',
selectedFreq: '',
selectedDuration: '',
rank: 'LR', // 直向 TB | 橫向 LR
}
},
methods: {
/**
* switch map type
* @param {string} type processMap | bpmn
*/
switchMapType(type) {
this.mapType = type;
this.$emit('switch-map-type', this.mapType);
},
/**
* switch curve style
* @param {string} style 直角 unbundled-bezier | taxi
*/
switchCurveStyles(style) {
this.curveStyle = style;
this.$emit('switch-curve-styles', this.curveStyle);
},
/**
* switch rank
* @param {string} rank 直向 TB | 橫向 LR
*/
switchRank(rank) {
this.rank = rank;
this.$emit('switch-rank', this.rank);
},
/**
* switch Data Layoer Type or Option.
* @param {string} e
* @param {string} type freq | duration
*/
switchDataLayerType(e, type){
let value = e.target.type === 'select-one'? e.target.value: 'total';
switch (type) {
case 'freq':
this.dataLayerType = type;
this.dataLayerOption = value;
this.selectedFreq = value;
break;
case 'duration':
this.dataLayerType = type;
this.dataLayerOption = value;
this.selectedDuration = value;
break;
};
this.$emit('switch-data-layer-type', this.dataLayerType, this.dataLayerOption);
},
}
}
</script>