Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into gt-sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
jfrer committed Jul 23, 2024
2 parents 32f0218 + 0fba3fc commit 34ec848
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 34 deletions.
2 changes: 1 addition & 1 deletion src/components/Workflows.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
</template>
<template v-else>
<div class="flex mb-6">
<p class="text-amber-600 flex-grow-0 px-4 py-2 bg-amber-100 rounded-md text-sm"><span class="font-semibold">Disclaimer:</span> This is an experimental view.</p>
<p class="text-amber-700 flex-grow-0 px-4 py-2 bg-amber-100 rounded-md text-sm"><span class="font-semibold">Disclaimer:</span> This is an experimental view.</p>
</div>
<WorkflowsIntroSection :page="<'timeline'|'table'>selectedOption.value" class="mb-6"></WorkflowsIntroSection>
<div class="flex mb-6">
Expand Down
88 changes: 60 additions & 28 deletions src/components/workflows/WorkflowsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,58 @@
import { watch, ref, onMounted, computed } from "vue"
import { useI18n } from "vue-i18n"
import { createReadableMetricValue, getEvalColor, mapGtId } from "@/helpers/utils"
import type { EvaluationRun } from "@/types"
import type { EvalDefinitions, EvaluationResultsDocumentWide, EvaluationRun, GroupedTableData } from "@/types"
import Dropdown from 'primevue/dropdown'
import Checkbox from 'primevue/checkbox'
import workflowsStore from "@/store/workflows-store"
import api from "@/helpers/api"
import filtersStore from "@/store/filters-store"
import TrendLegend from "@/components/workflows/TrendLegend.vue"
import WorkflowsTableSorter from "@/components/workflows/timeline/WorkflowTableSorter.vue"
const { t } = useI18n()
const groupedData = ref({})
const evals = ref([])
const groupedData = ref<GroupedTableData>({})
const sortedData = ref<GroupedTableData>({})
const evals = ref<string[]>([])
const sortBy = ref<keyof EvaluationResultsDocumentWide | null>(null)
const sortOptions = ref([{
const tableData = computed<GroupedTableData>(() => {
return (sortBy.value === null || Object.keys(sortedData.value).length === 0) ? groupedData.value : sortedData.value
})
const keepGroupsWhenSorting = ref(true)
const groupingOptions = ref([{
value: 'documents',
label: t('documents')
}, {
value: 'workflows',
label: t('workflows')
}])
const sortBy = ref(sortOptions.value[0])
const groupBy = ref(groupingOptions.value[0])
const latestRuns = ref<EvaluationRun[]>([])
const filteredRuns = ref<EvaluationRun[]>([])
const evalDefinitions = ref([])
const evalDefinitions = ref<EvalDefinitions>({})
const loading = ref(false)
onMounted(async () => {
loading.value = true
latestRuns.value = workflowsStore.getLatestRuns()
evalDefinitions.value = await api.getEvalDefinitions()
setFilteredRuns()
groupRuns(sortBy.value.value)
groupRuns(groupBy.value.value)
loading.value = false
})
watch(() => filtersStore.gt, () => {
setFilteredRuns()
groupRuns(sortBy.value.value)
groupRuns(groupBy.value.value)
})
watch(sortBy, () => {
groupRuns(sortBy.value.value)
watch(groupBy, () => {
groupRuns(groupBy.value.value)
})
function setFilteredRuns() {
Expand All @@ -53,6 +63,7 @@ function setFilteredRuns() {
function groupRuns(groupBy: string) {
if (groupBy === 'workflows') groupByWorkflows()
else if (groupBy === 'documents') groupByDocuments()
sortBy.value = null
}
const groupByWorkflows = () => {
Expand All @@ -66,7 +77,7 @@ const groupByWorkflows = () => {
label: workflowsStore.getGtById(mapGtId(cur.metadata.gt_workspace.id))?.label,
evaluations: Object.keys(cur.evaluation_results.document_wide).map(key => ({
name: key,
value: cur.evaluation_results.document_wide[key]
value: cur.evaluation_results.document_wide[key as keyof EvaluationResultsDocumentWide]
}))
}
if (!acc[ocrWorkflowId]) {
Expand All @@ -77,12 +88,12 @@ const groupByWorkflows = () => {
} else {
acc[ocrWorkflowId].subjects.push(subject)
acc[ocrWorkflowId].subjects.sort((a, b) => {
if (a.label > b.label) return 1
if ((a.label && b.label) && a.label > b.label) return 1
else return -1
})
}
return acc
}, {})
}, {} as GroupedTableData)
}
const groupByDocuments = () => {
Expand All @@ -94,7 +105,7 @@ const groupByDocuments = () => {
label: workflowsStore.getWorkflowById(mapGtId(cur.metadata.ocr_workflow['id']))?.label,
evaluations: Object.keys(cur.evaluation_results.document_wide).map(key => ({
name: key,
value: cur.evaluation_results.document_wide[key]
value: cur.evaluation_results.document_wide[key as keyof EvaluationResultsDocumentWide]
}))
}
if (!acc[gtWorkspaceId]) {
Expand All @@ -105,12 +116,21 @@ const groupByDocuments = () => {
} else {
acc[gtWorkspaceId].subjects.push(subject)
acc[gtWorkspaceId].subjects.sort((a, b) => {
if (a.label > b.label) return 1
if ((a.label && b.label) && a.label > b.label) return 1
else return -1
})
}
return acc
}, {})
}, {} as GroupedTableData)
}
const getSortValue = (key: keyof EvaluationResultsDocumentWide) => {
return sortBy.value === key
}
const setSorted = (event: GroupedTableData, key: keyof EvaluationResultsDocumentWide) => {
sortBy.value = key
sortedData.value = event
}
</script>

Expand All @@ -119,21 +139,33 @@ const groupByDocuments = () => {
Loading...
</template>
<template v-else>
<div class="flex flex-col" v-if="Object.keys(groupedData).length > 0">
<div class="flex flex-col" v-if="Object.keys(tableData).length > 0">
<div class="flex items-center mb-4 ml-auto">
<label for="keepGroupsCheckbox" class="mr-2">{{ $t('keep_grouping_when_sorting') }}</label>
<Checkbox v-model="keepGroupsWhenSorting" input-id="keepGroupsCheckbox" binary class="mr-8"></Checkbox>

<p class="mr-2">{{ $t('group_by') }}:</p>
<Dropdown v-model="sortBy" :options="sortOptions" optionLabel="label" placeholder="Choose something.." class="" />
<Dropdown v-model="groupBy" :options="groupingOptions" optionLabel="label" placeholder="Choose something.." class="" />
</div>
<TrendLegend :show-text-colors="false" class="ml-auto mb-4"/>
</div>
<table v-if="Object.keys(groupedData).length > 0" class="w-full border border-collapse rounded text-sm">
<table v-if="Object.keys(tableData).length > 0" class="w-full border border-collapse rounded text-sm">
<thead>
<tr>
<th class="p-2 border">{{ sortBy.value === 'documents' ? $t('documents') : $t('workflows') }}</th>
<th class="p-2 border">{{ sortBy.value === 'documents' ? $t('workflows') : $t('documents') }}</th>
<th class="p-2 border">{{ groupBy.value === 'documents' ? $t('documents') : $t('workflows') }}</th>
<th class="p-2 border">{{ groupBy.value === 'documents' ? $t('workflows') : $t('documents') }}</th>
<th v-for="(evalKey, i) in evals" :key="i" class="p-2 border">
<span class="def-label flex items-center justify-center cursor-pointer">
{{ evalDefinitions[evalKey] ? evalDefinitions[evalKey].label : evalKey }}
<WorkflowsTableSorter
:grouped-data="groupedData"
:metric="(evalKey as keyof EvaluationResultsDocumentWide)"
:sort="getSortValue(evalKey as keyof EvaluationResultsDocumentWide)"
:keep-grouping="keepGroupsWhenSorting"
@sorted-data="setSorted($event, evalKey as keyof EvaluationResultsDocumentWide)"
@unsorted-data="sortBy = null"
>
{{ evalDefinitions[evalKey] ? evalDefinitions[evalKey].label : evalKey }}
</WorkflowsTableSorter>
<i-icon name="ink-info"/>
<div class="def-tooltip">
<div class="flex p-2 bg-white border rounded">
Expand All @@ -146,22 +178,22 @@ const groupByDocuments = () => {
</tr>
</thead>
<tbody>
<template v-for="(key, i) in Object.keys(groupedData)" :key="i">
<tr v-for="(subject, j) in groupedData[key].subjects" :key="j">
<td v-if="j === 0" :rowspan="groupedData[key].subjects.length" class="align-top pl-2 border w-1/3">
<span class="font-bold">{{ groupedData[key].label }}</span>
<template v-for="(key, i) in Object.keys(tableData)" :key="i">
<tr v-for="(subject, j) in tableData[key].subjects" :key="j">
<td v-if="j === 0" :rowspan="tableData[key].subjects.length" class="align-top pl-2 border w-1/3">
<span class="font-bold">{{ tableData[key].label }}</span>
</td>
<td class="align-top pl-2 border">{{ subject.label }}</td>
<td
v-for="({ name, value }, k) in subject.evaluations"
:key="k"
class="text-center pt-1 border"
:class="(j === groupedData[key].subjects.length - 1) ? 'pb-5' : 'pb-1'"
:class="(j === tableData[key].subjects.length - 1) ? 'pb-5' : 'pb-1'"
>
<span
class="metric inline-block cursor-pointer text-sm leading-none p-1 rounded-lg min-w-[48px]"
:class="getEvalColor(name, value)">
{{ createReadableMetricValue(name, value) }}
{{ createReadableMetricValue(name as keyof EvaluationResultsDocumentWide, value) }}
</span>
</td>
</tr>
Expand Down
5 changes: 3 additions & 2 deletions src/components/workflows/timeline/TimelineItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ function toggleParameterOverlay(step: WorkflowStep, event: Event) {
<div class="flex flex-col px-4 pb-2">
<div class="flex items-center overflow-hidden">
<h2 class="text-xl font-bold truncate mr-8" :title="gt.label">{{ gt.label }}</h2>
<a :href="gt.metadata.url" class="text-gray-500 hover:text-gray-600 flex-shrink-0 ml-auto mr-2 flex items-center bg-gray-100 rounded-full py-1 px-2">
<a :href="gt.metadata.url" class="text-gray-600 hover:text-gray-700 flex-shrink-0 ml-auto mr-2 flex items-center bg-gray-100 rounded-full py-1 px-2">
<Icon icon="mdi:github" class="text-xl mr-1"/>
<span class="text-xs">{{ gt.metadata.title }}</span>
</a>
<a :href="gt.metadata.license[0].url" class="text-gray-500 hover:text-gray-600 flex-shrink-0 flex items-center bg-gray-100 rounded-full py-1 px-2">
<a :href="gt.metadata.license[0].url" class="text-gray-600 hover:text-gray-700 flex-shrink-0 flex items-center bg-gray-100 rounded-full py-1 px-2">
<Icon icon="octicon:law" class="text-xl mr-1"/>
<span class="text-xs">{{ gt.metadata.license[0].name }}</span>
</a>
Expand Down Expand Up @@ -174,5 +174,6 @@ function toggleParameterOverlay(step: WorkflowStep, event: Event) {
<style scoped lang="scss">
.text-highlight:hover {
color: var(--highlight-text-color);
background-color: var(--highlight-bg);
}
</style>
120 changes: 120 additions & 0 deletions src/components/workflows/timeline/WorkflowTableSorter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<script setup lang="ts">
import Button from "primevue/button"
import { Icon } from "@iconify/vue"
import { ref, watch } from "vue"
import type { GroupedTableData, EvaluationResultsDocumentWide, GroupedTableDataSubject } from "@/types"
const props = withDefaults(
defineProps<{
groupedData: GroupedTableData
metric: keyof EvaluationResultsDocumentWide
sort?: boolean
keepGrouping?: boolean
}>(),
{
sort: false,
keepGrouping: true
}
)
const emit = defineEmits<{
(event: 'sortedData', payload: GroupedTableData): void,
(event: 'unsortedData'): void
}>()
type GroupedEntry = [string, GroupedTableData[keyof GroupedTableData]]
enum sortStates {
none,
desc,
asc,
__LENGTH
}
const sortState = ref(sortStates.none)
//set sortState to none if sort changes to false to enable sorting by only a single column at a time
watch(() => props.sort, () => {
if(!props.sort) sortState.value = sortStates.none
})
watch(() => props.keepGrouping, () => {
if(sortState.value !== sortStates.none) sort()
})
const cycleAndSort = () => {
cycleSortState()
sort()
}
const cycleSortState = () => {
sortState.value = (sortState.value + 1) % sortStates.__LENGTH
}
const sort = () => {
if (sortState.value === sortStates.none) {
emit('unsortedData')
return
}
const groupedEntries = Object.entries(props.groupedData)
const entries = props.keepGrouping ? groupedEntries : transformToSingleSubjectGrouping(groupedEntries)
const entriesWithSortedSubjects = entries.map(elem => {
const sorted: typeof elem[1] = { label: elem[1].label, subjects: sortSubjects(elem[1].subjects, sortState.value === sortStates.desc) }
return [elem[0], sorted]
})
const sortedData: GroupedTableData = Object.fromEntries(
entriesWithSortedSubjects
.sort((left, right) => {
const leftData = left[1] as GroupedTableData[keyof GroupedTableData]
const rightData = right[1] as GroupedTableData[keyof GroupedTableData]
const compareByFirstSubject = (subjectsLeft: GroupedTableDataSubject[], subjectsRight: GroupedTableDataSubject[]) => {
return compareSubjects(subjectsLeft[0], subjectsRight[0])
}
return sortState.value === sortStates.desc ?
compareByFirstSubject(rightData.subjects, leftData.subjects)
: compareByFirstSubject(leftData.subjects, rightData.subjects)
})
)
emit('sortedData', sortedData)
}
const sortSubjects = (subjects: GroupedTableDataSubject[], desc: boolean) => {
return [...subjects] //copy to avoid sorting in place
.sort((left: GroupedTableDataSubject, right: GroupedTableDataSubject) => {
return desc ? compareSubjects(right, left) : compareSubjects(left, right)
})
}
const compareSubjects = (left: GroupedTableDataSubject, right: GroupedTableDataSubject) => {
const transformValue = (value: number | number [] | null | undefined): number => {
const definedValue = value ?? 0
return Array.isArray(definedValue) ? definedValue[0] : definedValue
}
const evaluationLeft = left.evaluations.find(elem => props.metric === elem.name)
const evaluationRight = right.evaluations.find(elem => props.metric === elem.name)
const valueLeft = transformValue(evaluationLeft?.value)
const valueRight = transformValue(evaluationRight?.value)
return valueLeft - valueRight
}
const transformToSingleSubjectGrouping = (groupedEntries: GroupedEntry[]): GroupedEntry[] => {
return groupedEntries.reduce((acc, curr) => {
curr[1].subjects.forEach((subject, index) => {
acc.push([`${curr[0]}_${index}`, { label: curr[1].label, subjects: [subject] }])
})
return acc
}, [] as GroupedEntry[])
}
</script>
<template>
<Button @click="cycleAndSort()" unstyled>
<div class="flex items-center space-x-2">
<slot/>
<span class="flex h-6 w-6 justify-center items-center">
<Icon v-if="sortState === sortStates.desc" icon="typcn:arrow-sorted-down"/>
<Icon v-else-if="sortState === sortStates.asc" icon="typcn:arrow-sorted-up"/>
<Icon v-else icon="typcn:arrow-unsorted"></Icon>
</span>
</div>
</Button>
</template>
4 changes: 2 additions & 2 deletions src/helpers/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EvaluationRun, GroundTruth, Workflow } from "@/types"
import type { EvalDefinitions, EvaluationRun, GroundTruth, Workflow } from "@/types"

const baseUrlOld = 'https://raw.githubusercontent.com/OCR-D/quiver-back-end/main/data'
const baseUrl = 'https://quiver-dev.sub.uni-goettingen.de/api'
Expand All @@ -10,7 +10,7 @@ async function getOcrdAllReleases() {
return await request(baseUrlOld + '/ocrd_all_releases.json')
}

async function getEvalDefinitions() {
async function getEvalDefinitions(): Promise<EvalDefinitions> {
return await request(baseUrlOld + '/metrics_definitions.json')
}

Expand Down
1 change: 1 addition & 0 deletions src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"select_a_date_range": "Zeitraum auswählen",
"select_a_workflow": "Workflow auswählen",
"select_a_processor": "Prozessor auswählen",
"keep_grouping_when_sorting": "Gruppierung beim Sortieren beibehalten",
"label_asc": "Titel (a - z)",
"label_desc": "Titel (z - a)",
"metric_desc": "Ausgewählte Metrik (absteigend)",
Expand Down
1 change: 1 addition & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"select_a_date_range": "Select a date range",
"select_a_workflow": "Select a workflow",
"select_a_processor": "Select a processor",
"keep_grouping_when_sorting": "Keep grouping when sorting",
"label_asc": "Label (a - z)",
"label_desc": "Label (z - a)",
"metric_desc": "Selected metric (descending)",
Expand Down
2 changes: 1 addition & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import App from './App.vue'
import router from './router'
import PrimeVue from 'primevue/config'

import 'primevue/resources/themes/lara-light-blue/theme.css'
import 'primevue/resources/themes/tailwind-light/theme.css'
import 'primeicons/primeicons.css'
import './assets/main.css'
import './assets/app.scss'
Expand Down
Loading

0 comments on commit 34ec848

Please sign in to comment.