forked from craws/OpenAtlas-Discovery
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat/sigma network visualization (#13)
this adds a new network visualisation page, and also adds a network visualisation tab to entity details pages. the visualisation uses [sigma.js](https://www.sigmajs.org/) for rendering the graph on a webgl canvas, and [graphology](https://graphology.github.io/) for the graph layout.
- Loading branch information
1 parent
747e667
commit bb1ac5a
Showing
23 changed files
with
2,538 additions
and
1,711 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
<script setup lang="ts"> | ||
import Graph from "graphology"; | ||
import { DotIcon } from "lucide-vue-next"; | ||
import type { NetworkEntity } from "@/types/api"; | ||
import { colors } from "../project.config.json"; | ||
const props = defineProps<{ | ||
networkData: NetworkEntity; | ||
searchNode: string; | ||
}>(); | ||
const graph = new Graph(); | ||
const { entityColors } = colors; | ||
const defaultColor = colors.entityDefaultColor; | ||
watch( | ||
() => { | ||
return props.networkData; | ||
}, | ||
(networkData) => { | ||
/** Clear previous graph data. */ | ||
graph.clear(); | ||
if (networkData.length === 0) return; | ||
/** Add all nodes. */ | ||
networkData.forEach((entity) => { | ||
if (!graph.hasNode(entity.id) && entity.systemClass) { | ||
graph.addNode(entity.id, { | ||
label: entity.label, | ||
color: getNodeColor(entity.systemClass), | ||
size: networkConfig.sourceNodeSize, | ||
}); | ||
} | ||
}); | ||
//** Add edges. */ | ||
networkData.forEach((entity) => { | ||
entity.relations.forEach((element) => { | ||
if (!graph.hasEdge(entity.id, element)) { | ||
graph.addEdge(entity.id, element); | ||
} | ||
}); | ||
}); | ||
}, | ||
{ immediate: true }, | ||
); | ||
function getNodeColor(nodeClass: string) { | ||
//@ts-expect-error: no error occurs | ||
return entityColors[nodeClass] ?? defaultColor; | ||
} | ||
</script> | ||
|
||
<template> | ||
<div class="absolute z-10 m-3 flex w-full"></div> | ||
<Network v-if="graph.size > 0" :graph="graph" :search-node="props.searchNode" /> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
<script lang="ts" setup> | ||
import { z } from "zod"; | ||
import type { SearchFormData as CategoryFormData } from "@/components/network-legend-panel.vue"; | ||
import type { SearchFormData } from "@/components/network-search-form.vue"; | ||
const router = useRouter(); | ||
const route = useRoute(); | ||
const t = useTranslations(); | ||
const searchFiltersSchema = z.object({ | ||
search: z.string().catch(""), | ||
}); | ||
const searchFilters = computed(() => { | ||
return searchFiltersSchema.parse(route.query); | ||
}); | ||
type SearchFilters = z.infer<typeof searchFiltersSchema>; | ||
function onChangeSearchFilters(values: SearchFormData) { | ||
const query = { ...searchFilters.value, ...values }; | ||
if (values.search === "") { | ||
// @ts-expect-error Fix me later please | ||
delete query.search; | ||
} | ||
void router.push({ query }); | ||
} | ||
function onChangeCategory(values: CategoryFormData) { | ||
void router.push({ query: { ...searchFilters.value, ...values } }); | ||
} | ||
const { data, error, isPending, isPlaceholderData, suspense } = useGetNetworkData( | ||
computed(() => { | ||
return { | ||
exclude_system_classes: [ | ||
"type", | ||
"object_location", | ||
"reference_system", | ||
"file", | ||
"source_translation", | ||
"source", | ||
"bibliography", | ||
"external_reference", | ||
"administrative_unit", | ||
"edition", | ||
"type_tools", | ||
], | ||
}; | ||
}), | ||
); | ||
const isLoading = computed(() => { | ||
return isPending.value || isPlaceholderData.value; | ||
}); | ||
const entities = computed(() => { | ||
return ( | ||
data.value?.results.flatMap((result) => { | ||
return result; | ||
}) ?? [] | ||
); | ||
}); | ||
const systemClasses = computed(() => { | ||
const systemClasses: Array<string> = []; | ||
entities.value.forEach((entity) => { | ||
if (!systemClasses.includes(entity.systemClass)) { | ||
systemClasses.push(entity.systemClass); | ||
} | ||
}); | ||
return systemClasses; | ||
}); | ||
</script> | ||
|
||
<template> | ||
<div class="relative grid grid-rows-[auto_1fr] gap-4"> | ||
<NetworkSearchForm :search="searchFilters.search" @submit="onChangeSearchFilters" /> | ||
|
||
<VisualisationContainer | ||
v-slot="{ height, width }" | ||
class="border" | ||
:class="{ 'opacity-50 grayscale': isLoading }" | ||
> | ||
<NetworkLegendPanel | ||
v-if="height && width" | ||
class="absolute bottom-0 right-0 z-10 m-3" | ||
:system-classes="systemClasses" | ||
@submit="onChangeCategory" | ||
/> | ||
<DataGraph :network-data="entities" :search-node="searchFilters.search" /> | ||
<Centered v-if="isLoading" class="pointer-events-none"> | ||
<LoadingIndicator class="text-neutral-950" size="lg" /> | ||
</Centered> | ||
</VisualisationContainer> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
<script setup lang="ts"> | ||
import Graph from "graphology"; | ||
import circularpack from "graphology-layout/circlepack"; | ||
import { DotIcon } from "lucide-vue-next"; | ||
import type { EntityFeature } from "@/composables/use-create-entity"; | ||
import { networkConfig } from "@/config/network-visualisation.config"; | ||
import { colors } from "@/project.config.json"; | ||
const props = defineProps<{ | ||
networkData: EntityFeature; | ||
id: number; | ||
}>(); | ||
const { getUnprefixedId } = useIdPrefix(); | ||
const graph = new Graph(); | ||
const { entityColors } = colors; | ||
const defaultColor = project.colors.entityDefaultColor; | ||
const legendEntities: [string | undefined] = [""]; | ||
watch( | ||
() => { | ||
return props.networkData; | ||
}, | ||
(networkData) => { | ||
/** Clear previous graph data. */ | ||
graph.clear(); | ||
/** Add source node. */ | ||
graph.addNode(props.id, { | ||
label: networkData.properties.title, | ||
color: getNodeColor(networkData.systemClass), | ||
size: networkConfig.sourceNodeSize, | ||
}); | ||
/** Add source node to agenda of nodes */ | ||
legendEntities.push(networkData.systemClass); | ||
/** Add relations to target nodes. */ | ||
networkData.relations?.forEach((element) => { | ||
if (element.relationTo == null) return; | ||
const relationId = getUnprefixedId(element.relationTo); | ||
const nodeClass = element.relationSystemClass; | ||
if (nodeClass == null) return; | ||
if (!legendEntities.includes(nodeClass)) { | ||
legendEntities.push(nodeClass); | ||
} | ||
graph.addNode(relationId, { | ||
label: element.label, | ||
color: getNodeColor(nodeClass), | ||
size: networkConfig.relationNodeSize, | ||
url: element.relationTo, | ||
}); | ||
graph.addEdge(props.id, relationId); | ||
}); | ||
}, | ||
{ immediate: true }, | ||
); | ||
function getNodeColor(nodeClass: string) { | ||
//@ts-expect-error: no error occurs | ||
return entityColors[nodeClass] ?? defaultColor; | ||
} | ||
</script> | ||
|
||
<template> | ||
<div class="absolute z-10 m-3 flex w-full"> | ||
<Card class="w-max"> | ||
<span v-for="(color, entity) in entityColors" :key="entity"> | ||
<span v-if="legendEntities.includes(entity)" class="pr-4"> | ||
<DotIcon :size="50" :color="color" class="inline-block" /> | ||
<span>{{ entity }}</span> | ||
</span> | ||
</span> | ||
<span v-for="entry in legendEntities" :key="entry"> | ||
<span | ||
v-if="entry != null && entry !== '' && !Object.keys(entityColors).includes(entry)" | ||
class="pr-4" | ||
> | ||
<DotIcon :size="50" :color="defaultColor" class="inline-block" /> | ||
<span>{{ entry }}</span> | ||
</span> | ||
</span> | ||
</Card> | ||
</div> | ||
<Network v-if="graph.size > 0" :graph="graph" /> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<script setup lang="ts"> | ||
import EntityDataGraph from "@/components/entity-data-graph.vue"; | ||
const props = defineProps<{ | ||
networkData: EntityFeature | undefined; | ||
id: number; | ||
}>(); | ||
</script> | ||
|
||
<template> | ||
<Card class="h-96 overflow-hidden"> | ||
<VisualisationContainer> | ||
<EntityDataGraph | ||
v-if="props.networkData != null" | ||
:id="props.id" | ||
:network-data="props.networkData" | ||
/> | ||
</VisualisationContainer> | ||
</Card> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
<script setup lang="ts"> | ||
import { colors } from "@/project.config.json"; | ||
const t = useTranslations(); | ||
export interface SearchFormData { | ||
category: string; // TODO: stricter typings | ||
} | ||
const props = defineProps<{ | ||
systemClasses: Array<string>; | ||
}>(); | ||
const emit = defineEmits<{ | ||
(event: "submit", values: SearchFormData): void; | ||
}>(); | ||
// TODO: Fix me! Implement filtering by system classes | ||
function onSubmit(element: string) { | ||
emit("submit", { | ||
category: element, //Array, checkbox-group html + how do i get teh values in the submit event handler | ||
}); | ||
} | ||
const labels = { | ||
place: t("SystemClassNames.place"), | ||
source: t("SystemClassNames.source"), | ||
person: t("SystemClassNames.person"), | ||
group: t("SystemClassNames.group"), | ||
move: t("SystemClassNames.move"), | ||
event: t("SystemClassNames.event"), | ||
activity: t("SystemClassNames.activity"), | ||
acquisition: t("SystemClassNames.acquisition"), | ||
feature: t("SystemClassNames.feature"), | ||
human_remains: t("SystemClassNames.human_remains"), | ||
stratigraphic_unit: t("SystemClassNames.stratigraphic_unit"), | ||
artifact: t("SystemClassNames.artifact"), | ||
file: t("SystemClassNames.file"), | ||
type: t("SystemClassNames.type"), | ||
object_location: t("SystemClassNames.object_location"), | ||
bibliography: t("SystemClassNames.bibliography"), | ||
edition: t("SystemClassNames.edition"), | ||
administrative_unit: t("SystemClassNames.administrative_unit"), | ||
reference_system: t("SystemClassNames.reference_system"), | ||
source_translation: t("SystemClassNames.source_translation"), | ||
}; | ||
const systemClassColors = colors.entityColors; | ||
</script> | ||
|
||
<template> | ||
<aside | ||
class="flex max-h-72 gap-2 overflow-y-auto overflow-x-hidden rounded-md border-2 border-transparent bg-white px-4 py-2 text-sm shadow-md" | ||
> | ||
<div | ||
v-for="el in props.systemClasses" | ||
:key="el" | ||
class="grid grid-cols-[auto_1fr] gap-3" | ||
:style="`color: ${systemClassColors[el] ? systemClassColors[el] : '#666'}`" | ||
> | ||
<div class="grid grid-cols-[auto_1fr] gap-2"> | ||
<!-- <input | ||
:id="el" | ||
type="checkbox" | ||
name="systemClassCheckbox" | ||
:style="`accent-color: ${systemClassColors[el] ? systemClassColors[el] : '#666'}`" | ||
checked | ||
@change="onSubmit(el)" | ||
/> --> | ||
<span | ||
class="m-1.5 size-2 rounded-full" | ||
:style="`background-color: ${systemClassColors[el] ? systemClassColors[el] : '#666'}`" | ||
></span> | ||
<span v-if="labels[el]">{{ labels[el] }}</span> | ||
<span v-else> {{ el }}</span> | ||
</div> | ||
</div> | ||
</aside> | ||
</template> |
Oops, something went wrong.