Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance comparison table #1331

Merged
merged 8 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 20 additions & 46 deletions report-viewer/src/components/ComparisonsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@
<!-- Body -->
<div class="flex flex-grow flex-col overflow-hidden">
<DynamicScroller v-if="topComparisons.length > 0" :items="comparisonList" :min-item-size="48">
<template v-slot="{ item, index, active }">
<template #default="{ item, index, active }">
<DynamicScrollerItem
:item="item"
:active="active"
:size-dependencies="[
item.firstSubmissionId,
item.secondSubmissionId,
isAnonymous(item.firstSubmissionId),
isAnonymous(item.secondSubmissionId)
store().isAnonymous(item.firstSubmissionId),
store().isAnonymous(item.secondSubmissionId)
]"
:data-index="index"
>
Expand All @@ -60,12 +60,14 @@
'bg-container-secondary-light dark:bg-container-secondary-dark': item.id % 2 == 1
}"
>
<RouterLink
:to="{
name: 'ComparisonView',
params: { firstId: item.firstSubmissionId, secondId: item.secondSubmissionId }
}"
class="flex flex-grow flex-row"
<div
@click="
router.push({
name: 'ComparisonView',
params: { firstId: item.firstSubmissionId, secondId: item.secondSubmissionId }
})
"
class="flex flex-grow cursor-pointer flex-row"
>
<!-- Index in sorted list -->
<div class="tableCellNumber">
Expand All @@ -74,26 +76,8 @@

<!-- Names -->
<div class="tableCellName">
<div
class="break-anywhere w-1/2 px-2"
:class="{ 'blur-[1px]': isAnonymous(item.firstSubmissionId) }"
>
{{
isAnonymous(item.firstSubmissionId)
? 'Hidden'
: displayName(item.firstSubmissionId)
}}
</div>
<div
class="break-anywhere w-1/2 px-2"
:class="{ 'blur-[1px]': isAnonymous(item.secondSubmissionId) }"
>
{{
isAnonymous(item.secondSubmissionId)
? 'Hidden'
: displayName(item.secondSubmissionId)
}}
</div>
<NameElement :id="item.firstSubmissionId" class="h-full w-1/2 px-2" />
<NameElement :id="item.secondSubmissionId" class="h-full w-1/2 px-2" />
</div>

<!-- Similarities -->
Expand All @@ -105,7 +89,7 @@
{{ (item.similarities[MetricType.MAXIMUM] * 100).toFixed(2) }}%
</div>
</div>
</RouterLink>
</div>

<!-- Clusters -->
<div class="tableCellCluster flex !flex-col items-center" v-if="displayClusters">
Expand Down Expand Up @@ -143,6 +127,10 @@
</div>
</DynamicScrollerItem>
</template>

<template #after>
<slot name="footer"></slot>
</template>
</DynamicScroller>
</div>
</div>
Expand All @@ -160,6 +148,8 @@ import { faUserGroup } from '@fortawesome/free-solid-svg-icons'
import { generateColors } from '@/utils/ColorUtils'
import ToolTipComponent from './ToolTipComponent.vue'
import { MetricType, metricToolTips } from '@/model/MetricType'
import NameElement from './NameElement.vue'
import { router } from '@/router'

library.add(faUserGroup)

Expand All @@ -178,22 +168,6 @@ const comparisonList = toRef(props, 'topComparisons')

const displayClusters = props.clusters != undefined

/**
* @param submissionId Id to get name for
* @returns The display name of the submission with the given id.
*/
function displayName(submissionId: string) {
return store().submissionDisplayName(submissionId)
}

/**
* @param id SubmissionId to check
* @returns Whether the name should be hidden.
*/
function isAnonymous(id: string) {
return store().state.anonymous.has(id)
}

let clusterIconColors = [] as Array<string>
if (props.clusters != undefined) {
clusterIconColors = generateColors(props.clusters.length, 0.8, 0.5, 1)
Expand Down
55 changes: 55 additions & 0 deletions report-viewer/src/components/NameElement.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<template>
<div class="group relative flex items-center justify-center">
<div
class="break-anywhere"
:class="{ 'blur-[1px]': store().isAnonymous(props.id) && anonymousBlur }"
>
{{ store().isAnonymous(props.id) ? anonymousName : store().submissionDisplayName(id) }}
</div>
<div
class="invisible absolute right-0 top-0 z-10 flex h-full cursor-pointer items-center p-2 delay-0 group-hover:visible group-hover:delay-100"
@click="(event) => changeAnonymous(event)"
>
<FontAwesomeIcon
class="text-gray-500"
:icon="store().isAnonymous(props.id) ? ['fas', 'eye'] : ['fas', 'eye-slash']"
/>
</div>
</div>
</template>

<script setup lang="ts">
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'
import { store } from '@/stores/store'

library.add(faEye)
library.add(faEyeSlash)

const props = defineProps({
id: {
type: String,
required: true
},
anonymousBlur: {
type: Boolean,
required: false,
default: true
},
anonymousName: {
type: String,
required: false,
default: 'Hidden'
}
})

function changeAnonymous(event: Event) {
event.stopPropagation()
if (store().isAnonymous(props.id)) {
store().removeAnonymous([props.id])
} else {
store().addAnonymous([props.id])
}
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@
import { ref } from 'vue'
import ToolTipComponent from '../ToolTipComponent.vue'
import OptionComponent from './OptionComponent.vue'

type ToolTipLabel = {
displayValue: string
tooltip: string
}
import { type ToolTipLabel } from '@/model/ui/ToolTip'

const props = defineProps({
title: {
Expand Down
2 changes: 1 addition & 1 deletion report-viewer/src/model/MetricType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export enum MetricType {
MAXIMUM = 'MAX'
}

type MetricToolTipData = {
export type MetricToolTipData = {
longName: string
shortName: string
tooltip: string
Expand Down
4 changes: 4 additions & 0 deletions report-viewer/src/model/ui/ToolTip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type ToolTipLabel = {
displayValue: string
tooltip: string
}
1 change: 1 addition & 0 deletions report-viewer/src/stores/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export interface LoadConfiguration {
export interface UIState {
useDarkMode: boolean
comparisonTableSortingMetric: MetricType
comparisonTableClusterSorting: boolean
distributionChartConfig: DistributionChartConfig
}

Expand Down
10 changes: 9 additions & 1 deletion report-viewer/src/stores/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const store = defineStore('store', {
uiState: {
useDarkMode: false,
comparisonTableSortingMetric: MetricType.AVERAGE,
comparisonTableClusterSorting: false,
distributionChartConfig: {
metric: MetricType.AVERAGE,
xScale: 'linear'
Expand All @@ -48,7 +49,7 @@ const store = defineStore('store', {
* @returns the display name of the submission
*/
submissionDisplayName: (state) => (id: string) => {
return state.state.fileIdToDisplayName.get(id)
return state.state.fileIdToDisplayName.get(id) ?? id
},
/**
* @returns the Ids of all submissions
Expand Down Expand Up @@ -77,6 +78,13 @@ const store = defineStore('store', {
: undefined
return index != undefined ? this.state.files[index] : undefined
}
},
/**
* @param id the id to check for
* @returns whether this submission should be anonymised
*/
isAnonymous: (state) => (submissionId: string) => {
return state.state.anonymous.has(submissionId)
}
},
actions: {
Expand Down
14 changes: 13 additions & 1 deletion report-viewer/src/views/ClusterView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@
</Container>
<Container class="flex max-h-0 min-h-full w-1/3 flex-col space-y-2">
<h2>Comparisons of Cluster Members:</h2>
<ComparisonsTable :topComparisons="comparisons" class="min-h-0 flex-1" />
<ComparisonsTable :topComparisons="comparisons" class="min-h-0 flex-1">
<template #footer v-if="comparisons.length < maxAmountOfComparisonsInCluster">
<p class="w-full pt-1 text-center font-bold">
Not all comparisons inside the cluster are shown. To see more, re-run JPlag with a
higher maximum number argument.
</p>
</template>
</ComparisonsTable>
</Container>
</div>
</div>
Expand Down Expand Up @@ -96,6 +103,11 @@ const clusterListElement: Ref<ClusterListElement> = computed(() => {
}
})

/** The amount of comparisons if every single one was included */
const maxAmountOfComparisonsInCluster = computed(() => {
return props.cluster.members.length ** 2 / 2 - props.cluster.members.length
})

onErrorCaptured((error) => {
redirectOnError(error, 'Error displaying cluster:\n', 'OverviewView', 'Back to overview')
return false
Expand Down
81 changes: 73 additions & 8 deletions report-viewer/src/views/OverviewView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -125,18 +125,24 @@
}}
</Button>
</div>
<MetricSelector
<OptionsSelector
title="Sort By:"
:defaultSelected="store().uiState.comparisonTableSortingMetric"
@selection-changed="
(metric: MetricType) => (store().uiState.comparisonTableSortingMetric = metric)
"
:defaultSelected="getSortingMetric()"
:labels="tableSortingOptions"
@selection-changed="(index: number) => changeSortingMetric(index)"
/>
<ComparisonsTable
:clusters="overview.clusters"
:top-comparisons="displayedComparisons"
class="min-h-0 flex-1"
/>
>
<template #footer v-if="overview.topComparisons.length < overview.totalComparisons">
<p class="w-full pt-1 text-center font-bold">
Not all comparisons are shown. To see more, re-run JPlag with a higher maximum number
argument.
</p>
</template>
</ComparisonsTable>
</Container>
</div>
</div>
Expand All @@ -151,14 +157,16 @@ import { store } from '@/stores/store'
import Container from '@/components/ContainerComponent.vue'
import Button from '@/components/ButtonComponent.vue'
import ScrollableComponent from '@/components/ScrollableComponent.vue'
import { MetricType } from '@/model/MetricType'
import { MetricType, metricToolTips } from '@/model/MetricType'
import SearchBarComponent from '@/components/SearchBarComponent.vue'
import TextInformation from '@/components/TextInformation.vue'
import type { ComparisonListElement } from '@/model/ComparisonListElement'
import MetricSelector from '@/components/optionsSelectors/MetricSelector.vue'
import ToolTipComponent from '@/components/ToolTipComponent.vue'
import OptionsSelector from '@/components/optionsSelectors/OptionsSelectorComponent.vue'
import type { Overview } from '@/model/Overview'
import { Overview } from '@/model/Overview'
import type { Cluster } from '@/model/Cluster'
import type { ToolTipLabel } from '@/model/ui/ToolTip'

const props = defineProps({
overview: {
Expand All @@ -168,6 +176,30 @@ const props = defineProps({
})

const searchString = ref('')
const tableSortingMetricOptions = [MetricType.AVERAGE, MetricType.MAXIMUM]
const tableSortingOptions = computed(() => {
const options: (ToolTipLabel | string)[] = tableSortingMetricOptions.map((metric) => {
return {
displayValue: metricToolTips[metric].longName,
tooltip: metricToolTips[metric].tooltip
}
})
options.push('Cluster')
return options
})

function changeSortingMetric(index: number) {
store().uiState.comparisonTableSortingMetric =
index < tableSortingMetricOptions.length ? tableSortingMetricOptions[index] : MetricType.AVERAGE
store().uiState.comparisonTableClusterSorting = tableSortingOptions.value[index] == 'Cluster'
}

function getSortingMetric() {
if (store().uiState.comparisonTableClusterSorting) {
return tableSortingOptions.value.indexOf('Cluster')
}
return tableSortingMetricOptions.indexOf(store().uiState.comparisonTableSortingMetric)
}

/**
* This funtion gets called when the search bar for the compariosn table has been updated.
Expand All @@ -193,12 +225,45 @@ function getFilteredComparisons(comparisons: ComparisonListElement[]) {
})
}

function getClusterIndexFor(id1: string, id2: string) {
let clusterIndex = -1
props.overview.clusters.forEach((c: Cluster, index: number) => {
if (c.members.includes(id1) && c.members.includes(id2) && c.members.length > 2) {
clusterIndex = index
}
})
return clusterIndex
}

function getClusterFor(id1: string, id2: string) {
const index = getClusterIndexFor(id1, id2)
if (index < 0) {
return { averageSimilarity: 0 }
}
return props.overview.clusters[index]
}

function getSortedComparisons(comparisons: ComparisonListElement[]) {
comparisons.sort(
(a, b) =>
b.similarities[store().uiState.comparisonTableSortingMetric] -
a.similarities[store().uiState.comparisonTableSortingMetric]
)

if (store().uiState.comparisonTableClusterSorting) {
comparisons.sort(
(a, b) =>
getClusterIndexFor(b.firstSubmissionId, b.secondSubmissionId) -
getClusterIndexFor(a.firstSubmissionId, a.secondSubmissionId)
)

comparisons.sort(
(a, b) =>
getClusterFor(b.firstSubmissionId, b.secondSubmissionId).averageSimilarity -
getClusterFor(a.firstSubmissionId, a.secondSubmissionId).averageSimilarity
)
}

let index = 0
comparisons.forEach((c) => {
c.sortingPlace = index++
Expand Down
Loading