Router: change /Discover to /Discover/map/type/filterId
This commit is contained in:
169
src/components/Discover/Map/Filter/ActAndSeq.vue
Normal file
169
src/components/Discover/Map/Filter/ActAndSeq.vue
Normal 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 ({{ 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 ({{ 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>
|
||||
76
src/components/Discover/Map/Filter/ActOcc.vue
Normal file
76
src/components/Discover/Map/Filter/ActOcc.vue
Normal 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 }} ({{ 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>
|
||||
80
src/components/Discover/Map/Filter/ActOccCase.vue
Normal file
80
src/components/Discover/Map/Filter/ActOccCase.vue
Normal 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 }} ({{ 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>
|
||||
130
src/components/Discover/Map/Filter/Funnel.vue
Normal file
130
src/components/Discover/Map/Filter/Funnel.vue
Normal 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 }}: <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 順序不亂掉,將值指向 0,submitAll 時再刪掉
|
||||
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>
|
||||
316
src/components/Discover/Map/Filter/Timeframes.vue
Normal file
316
src/components/Discover/Map/Filter/Timeframes.vue
Normal 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>
|
||||
271
src/components/Discover/Map/Filter/Trace.vue
Normal file
271
src/components/Discover/Map/Filter/Trace.vue
Normal 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>
|
||||
532
src/components/Discover/Map/SidebarFilter.vue
Normal file
532
src/components/Discover/Map/SidebarFilter.vue
Normal 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>
|
||||
266
src/components/Discover/Map/SidebarState.vue
Normal file
266
src/components/Discover/Map/SidebarState.vue
Normal 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:
|
||||
<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">, </span>
|
||||
</span>
|
||||
</li>
|
||||
<li>Inbound connections:
|
||||
<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">, </span>
|
||||
</span>
|
||||
</li>
|
||||
<li>Outbound connections:
|
||||
<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">, </span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="h2">Most time-consuming</p>
|
||||
<ul class="list-disc ml-6 mb-4">
|
||||
<li class="w-full">Activity:
|
||||
<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">, </span>
|
||||
</span>
|
||||
</li>
|
||||
<li>Connection:
|
||||
<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"> <span class="material-symbols-outlined text-lg align-sub ">arrow_forward</span> </span>
|
||||
</span><span v-if="key !== insights.most_time_edges.length - 1" class="text-neutral-900">, </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"> <span class="material-symbols-outlined text-lg align-sub">sync_alt</span> </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"> <span class="material-symbols-outlined text-lg align-sub">arrow_forward</span> </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"> <span class="material-symbols-outlined text-lg align-sub">arrow_forward</span> </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"> <span class="material-symbols-outlined text-lg align-sub">arrow_forward</span> </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>
|
||||
207
src/components/Discover/Map/SidebarTraces.vue
Normal file
207
src/components/Discover/Map/SidebarTraces.vue
Normal 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>
|
||||
167
src/components/Discover/Map/SidebarView.vue
Normal file
167
src/components/Discover/Map/SidebarView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user