diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 380d1e126b399cb3ff3dea1b94161993d6acdba9..a6a5becf63af2205e37415f043b228774979b443 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -204,7 +204,7 @@ lint: - > df-wiki-cli meilisearch - delete-all-documents system-operon-structure + delete-all-documents systemoperonstruct - > df-wiki-cli content diff --git a/components/Nav/Drawer.vue b/components/Nav/Drawer.vue index 645e512f65f55c2c9248aa653baf8edd2d1353fb..6cdff1f0e09d9075e4e095b3c5c4e768a36c43ac 100644 --- a/components/Nav/Drawer.vue +++ b/components/Nav/Drawer.vue @@ -27,7 +27,7 @@ const { navigation, page } = useContent(); // }); </script> <template> - <v-navigation-drawer :model-value="drawer" :border="1" color="background"> + <v-navigation-drawer name="drawer" :model-value="drawer" :border="1" color="background"> <v-list nav density="compact" :lines="false"> <NavNavigation :navigation="navigation" /> </v-list> diff --git a/components/OperonStructure.vue b/components/OperonStructure.vue index 4b5365d635de29e42f0dd98e0c80337f0e1e3dc1..f7ad97e2766e9939f2e178f12dada8000218681a 100644 --- a/components/OperonStructure.vue +++ b/components/OperonStructure.vue @@ -2,12 +2,12 @@ import { useElementSize } from '@vueuse/core' import * as d3 from "d3"; import type { PlotMargin } from "../types/plot"; -import type { StructureOperonGene } from "../types/structure" +import type { StructureOperonGene, StructureOperonGeneWithCoordinate, StructureOperonGeneWithImg } from "../types/structure" import { useStructuresBasket } from '~/stores/structuresBasket'; interface Props { - genes: StructureOperonGene[] | null + genes: StructureOperonGeneWithImg[] | null } const structureBasket = useStructuresBasket() @@ -15,10 +15,7 @@ const props = withDefaults(defineProps<Props>(), { genes: null }); const height = ref<number>(150) -const svgRef = ref(null) - - - +const svgRef = ref<SVGElement | null>(null) const margin = ref<PlotMargin>({ marginTop: 10, marginRight: 7, @@ -26,34 +23,41 @@ const margin = ref<PlotMargin>({ marginLeft: 7, }) + + const plotHeight = computed(() => { const { marginTop, marginBottom } = toValue(margin) return toValue(height) + marginTop + marginBottom }) -const xScale = ref( - d3.scaleBand().paddingInner(0.1) -); +const domain = computed(() => { + const genes = toValue(computedGenes) + return genes?.map(d => { return d.gene }) +}) + +const xScale = computed(() => { + return d3.scaleBand() + .paddingInner(0.1) + .domain(toValue(domain)) + .range([0, computedPlotWidth.value]) +}) + + const yScale = ref(d3.scaleBand() .domain(['1']) .range([toValue(margin).marginTop, toValue(height)])); const gbContainer = ref(null) -const gbContainerWidth = ref(useElementSize(gbContainer, { width: 500, height: 0 }, { box: 'border-box' }).width) - const computedContainerWidth = computed(() => { - return gbContainerWidth.value + return useElementSize(gbContainer, { width: 500, height: 0 }, { box: 'border-box' }).width.value }) - const computedPlotWidth = computed(() => { const { marginLeft, marginRight } = toValue(margin) return computedContainerWidth.value - marginLeft - marginRight }) -onMounted(() => { - draw() -}) -const computedGenes = computed(() => { + +const computedGenes = computed<StructureOperonGene[]>(() => { const genes = toValue(props.genes) if (genes !== null) { return genes.map(d => { @@ -67,7 +71,6 @@ const computedGenes = computed(() => { const structureVersion = computed(() => { - const genesVal = toValue(computedGenes) if (genesVal?.length > 0) { return genesVal[0].version @@ -77,16 +80,8 @@ const structureVersion = computed(() => { } }) -watch(computedGenes, () => { - const genes = toValue(computedGenes) - const domain = genes?.map(d => { return d.gene }) - xScale.value.domain(domain) - .range([0, computedPlotWidth.value]) -}) - - -const genesWithCoord = computed(() => { +const genesWithCoord = computed<StructureOperonGeneWithCoordinate[]>(() => { const genes = toValue(computedGenes) const xScaleVal = toValue(xScale) const yScaleVal = toValue(yScale) @@ -103,12 +98,15 @@ const genesWithCoord = computed(() => { } else { return [] } }) +onMounted(() => { + draw() +}) watch(genesWithCoord, () => { draw() }) -function createOrSelect(container, tag, selectionClass) { +function createOrSelect(container: d3.Selection<SVGElement, any, HTMLElement | null, any>, tag: string, selectionClass: string) { let selection = container.select(`${tag}.${selectionClass}`) if (selection.empty()) { selection = container.append(tag).classed(selectionClass, true) @@ -117,39 +115,41 @@ function createOrSelect(container, tag, selectionClass) { } function draw() { - const svg = d3.select(svgRef.value); - const { marginLeft } = toValue(margin) - const xAxis = d3.axisBottom(xScale.value) - const gx = createOrSelect(svg, 'g', 'x-axis') - - gx - .attr("transform", `translate(${marginLeft},${toValue(height)})`) - .call(xAxis) - .selectAll("text") - .attr("transform", 'rotate(45)') - .attr("text-anchor", "start") - - const gxTitle = createOrSelect(gx, "text", "x-axis-title") - gxTitle - .attr("text-anchor", "end") - .attr("fill", "currentColor") - - gxTitle - .attr("x", toValue(computedPlotWidth)) - .attr("y", - 10) - - - let gGenes = createOrSelect(svg, "g", "genes") - gGenes - .attr("transform", `translate(${marginLeft},0)`) - .call(drawGenes, xScale, yScale) + + if (svgRef.value !== null) { + const svg = d3.select<SVGElement, undefined>(svgRef.value); + const { marginLeft } = toValue(margin) + const xAxis = d3.axisBottom(xScale.value) + const gx = createOrSelect(svg, 'g', 'x-axis') + gx + .attr("transform", `translate(${marginLeft},${toValue(height)})`) + .call(xAxis) + .selectAll("text") + .attr("transform", 'rotate(45)') + .attr("text-anchor", "start") + + const gxTitle = createOrSelect(gx, "text", "x-axis-title") + gxTitle + .attr("text-anchor", "end") + .attr("fill", "currentColor") + + gxTitle + .attr("x", toValue(computedPlotWidth)) + .attr("y", - 10) + + + let gGenes = createOrSelect(svg, "g", "genes") + gGenes + .attr("transform", `translate(${marginLeft},0)`) + .call(drawGenes, xScale, yScale) + } } -function drawGenes(genesGroup) { - console.log(toValue(genesWithCoord)) +function drawGenes(genesGroup: d3.Selection<SVGElement, any, SVGElement, any>) { + const data = toValue(genesWithCoord) const genesSelection = genesGroup .selectAll("g.gene") // get all "existing" lines in svg - .data(toValue(genesWithCoord)) // sync them with our data + .data<StructureOperonGeneWithCoordinate>(data) // sync them with our data .join( enter => { const g = enter.append("g") @@ -159,7 +159,6 @@ function drawGenes(genesGroup) { g.append("image") .on("mouseover", function (event) { - console.log(event.srcElement) d3.select(event.srcElement.previousSibling) .attr("stroke-width", 4) .attr("stroke", "darkred") @@ -184,12 +183,12 @@ function drawGenes(genesGroup) { ) genesSelection.attr("transform", d => `translate(${d.x},${d.y})`) genesSelection.select("image") - .attr("href", d => d?.structImg) + .attr("href", d => d?.structImg ?? null) .attr("width", d => d.width) .attr("height", d => d.height) .on("click", function (event) { - const data = d3.select(this).data() - structureBasket.set(data.map(s => s.structPath)) + const data = d3.select<SVGElement, StructureOperonGeneWithCoordinate>(this).data() + structureBasket.set(data.map(s => s?.structPath ?? '')) }) genesSelection.select("path.gene") @@ -212,7 +211,6 @@ function drawGene({ width, height }) { <div ref="gbContainer"> <v-card flat color="transparent"> <v-card-subtitle>model version : {{ structureVersion }}</v-card-subtitle> - <!-- <v-img :href=""></v-img> --> <svg ref="svgRef" :width="computedContainerWidth" :height="plotHeight"> <g class="x-axis" /> </svg> diff --git a/components/content/ArticleStructure.vue b/components/content/ArticleStructure.vue index 29b36c314e3d3f2c41a9986e09eb96df74263cd1..8333dc672f2bcb521a4b1f699e761d375b2d2495 100644 --- a/components/content/ArticleStructure.vue +++ b/components/content/ArticleStructure.vue @@ -11,7 +11,6 @@ import { useStructuresBasket } from '~/stores/structuresBasket'; const { page } = useContent(); const client = useMeiliSearchRef() const structureBasket = useStructuresBasket() - // get the structures const structures = ref<SearchResponse<StructureItem, SearchParams> | undefined>() const structureTitle = ref("Structure") @@ -67,9 +66,7 @@ function displayStructure(item) { const sanitizedStructures = computed(() => { const toValStructures = toValue(structures) if (toValStructures?.hits && toValStructures?.hits?.length > 0) { - return toValStructures.hits.map(item => { - console.log(item) return { ...item, structuresUrls: [`/${item.system.toLowerCase()}/${pdbNameToCif(item.pdb)}`, `/${item.system.toLowerCase()}/${item.pdb}`] .map(url => { @@ -111,16 +108,8 @@ async function fetchStructures() { <v-card flat> <template v-for="[subsystem, structures] in perSubsystemStructures" :key="subsystem[0]"> <ProseH3>{{ subsystem }}</ProseH3> - <v-row align="end"> - <v-col cols="6" class="ml-0 "> - <SystemOperonStructure :system="computedSystem" :subsystem="subsystem" :structures="structures" /> - </v-col> - <v-col cols="6"> - <PdockqMatrix :subsystem="subsystem" /> - </v-col> - </v-row> + <SystemOperonStructure :system="computedSystem" :subsystem="subsystem" :structures="structures" /> </template> - <ProseH3>Summary</ProseH3> <v-data-table :headers="headers" :items="sanitizedStructures" :group-by="groupBy"> <template #[`item.proteins_in_the_prediction`]="{ item }"> diff --git a/components/content/PdockqMatrix.vue b/components/content/PdockqMatrix.vue index 289b7739fb5e5bf0ad0834cfe772daaf8e1aba4a..cd5735135e4e7afc19572c4465b323382113b139 100644 --- a/components/content/PdockqMatrix.vue +++ b/components/content/PdockqMatrix.vue @@ -152,8 +152,6 @@ function on(mark) { // but in simple cases it works const data = this.data; - console.log(data) - const key = data[0].key // 🌶 since a point or band scale doesn't have an inverse, create one from its domain and range const g = render.apply(this, arguments); const r = d3.select(g).selectChildren(); diff --git a/components/content/SystemOperonStructure.vue b/components/content/SystemOperonStructure.vue index c1a69d2fb02a414cf2c14d4c2e79218cfd34ec2e..6809308484eb8ecec2b74d34f1b3d303a41623b1 100644 --- a/components/content/SystemOperonStructure.vue +++ b/components/content/SystemOperonStructure.vue @@ -1,7 +1,8 @@ <script setup lang="ts"> -import type { StructureItem, StructureOperonGene } from "../../types/structure"; +import type { StructureItem, StructureOperonGene, StructureOperonGeneWithImg } from "../../types/structure"; import type { SearchResponse } from "meilisearch"; -import { withTrailingSlash, withLeadingSlash, joinURL } from 'ufo' +import { joinURL } from 'ufo' +import { useColumnLayout } from '~/composables/useColumnLayout'; interface Props { @@ -17,6 +18,8 @@ const { system, subsystem, structures } = withDefaults(defineProps<Props>(), { const client = useMeiliSearchRef() const pending = ref<boolean>(false) +const sizeGene = ref<number>(100) + const msIndexName = ref<'systemoperonstruct'>("systemoperonstruct") const msResponse = ref<SearchResponse<StructureOperonGene> | undefined>(undefined) @@ -38,7 +41,7 @@ const monomerStructures = computed(() => { } }) -const sanitizedHits = computed(() => { +const sanitizedHits = computed<StructureOperonGeneWithImg[]>(() => { const toValMsResponse = toValue(msResponse) if (toValMsResponse && toValMsResponse?.hits?.length > 0) { return toValMsResponse.hits.map(hit => { @@ -52,7 +55,7 @@ const sanitizedHits = computed(() => { const rawImgUrl = joinURL(`/${system.toLowerCase()}`, struct?.structImg ?? '') const rawStructUrl = joinURL(`/${system.toLowerCase()}`, struct?.structPath ?? '') const { refinedUrl: structImgHref } = useRefinedUrl(rawImgUrl) - return { ...hit, gene: sanitizedGene, structImg: toValue(structImgHref), structPath: rawStructUrl} + return { ...hit, gene: sanitizedGene, structImg: toValue(structImgHref), structPath: rawStructUrl } } else { return { ...hit, gene: sanitizedGene } @@ -80,6 +83,13 @@ const sanitizedSubsystem = computed(() => { } }) +const contentWidth = computed(() => { + return sanitizedHits.value.length * sizeGene.value +}) + +const { wholeRow } = useColumnLayout({ contentWidth, widthThreshold: 600 }) + + onMounted(() => { fetchOperonStructure() }) @@ -105,9 +115,12 @@ async function fetchOperonStructure() { </script> <template> - - <v-card variant="flat"> - <OperonStructure :genes="sanitizedHits" :system :subsystem /> - </v-card> - + <v-row align="center"> + <v-col :cols="wholeRow ? 12 : 6" class="pa-0"> + <OperonStructure :genes="sanitizedHits" :system :subsystem /> + </v-col> + <v-col :cols="wholeRow ? 12 : 6" class="pa-0"> + <PdockqMatrix :subsystem="subsystem" /> + </v-col> + </v-row> </template> \ No newline at end of file diff --git a/composables/useColumnLayout.ts b/composables/useColumnLayout.ts new file mode 100644 index 0000000000000000000000000000000000000000..dca16406be60da91b9c12b77c04e259360599dd1 --- /dev/null +++ b/composables/useColumnLayout.ts @@ -0,0 +1,33 @@ + +import { useDisplay, useLayout } from "vuetify"; + +interface ParametersColumnLayout { + contentWidth: MaybeRef<number>, + widthThreshold: MaybeRef<number> | ((width: Ref<number>) => number) +} + +export function useColumnLayout({ contentWidth, widthThreshold }: ParametersColumnLayout) { + const { getLayoutItem } = useLayout() + const { width } = useDisplay() + const widthTresholdRef = computed(() => { + if (typeof widthThreshold === "function") { + return widthThreshold(width) + } + else { + return toValue(widthThreshold) + } + }) + + const computedWidth = computed(() => { + const drawerLayout = getLayoutItem("drawer") + if (drawerLayout && drawerLayout.size > 0) { + return toValue(width) - drawerLayout.size + } + return toValue(width) + }) + + const wholeRow = computed(() => { + return toValue(contentWidth) > toValue(computedWidth) / 2 + }) + return { widthTresholdRef, wholeRow } +} \ No newline at end of file diff --git a/packages/df-wiki-cli/df_wiki_cli/content/main.py b/packages/df-wiki-cli/df_wiki_cli/content/main.py index 48a89a26ed9f91b7fb893e758d6c924eb3691821..a71043813f3fd9b097cfa10ae42c9210eb70a4d3 100644 --- a/packages/df-wiki-cli/df_wiki_cli/content/main.py +++ b/packages/df-wiki-cli/df_wiki_cli/content/main.py @@ -8,7 +8,7 @@ import matplotlib.pyplot as plt from operator import itemgetter from pandas.errors import ParserError from typing_extensions import Annotated -from typing import Optional, List, Tuple +from typing import Optional, List from pathlib import Path from pydantic import BaseModel, ValidationError import frontmatter @@ -286,10 +286,17 @@ def system_operon_structure( for row in tsvreader: systems.append({"system": row["system"], "subsystem": row["subsystem"]}) system_genes = [] + system_genes_got = set() for system_def in systems: system, subsystem = itemgetter("system", "subsystem")(system_def) + system_id = f"{system}-{subsystem}" list_paths = list(gen_model_path(model_dirs)) - if system != "#N/A" and subsystem != "#N/A": + if ( + system != "#N/A" + and subsystem != "#N/A" + and system_id not in system_genes_got + ): + system_genes_got.add(system_id) def_path = find_model_definition(system, subsystem, list_paths) in_exchangeables = False current_gene = {} diff --git a/types/structure.ts b/types/structure.ts index ef0f6badfcad03fc85798db79e427ac5169e857c..7e90fde5ee523e1295ddb46499e325fefdb3471a 100644 --- a/types/structure.ts +++ b/types/structure.ts @@ -6,6 +6,17 @@ export interface StructureOperonGene { system: string } +export interface StructureOperonGeneWithImg extends StructureOperonGene { + structImg?: string + structPath?: string +} + +export interface StructureOperonGeneWithCoordinate extends StructureOperonGeneWithImg { + width: number + height: number + x: number | undefined + y: number | undefined +} export type StructurePredictionType = "monomer" | "multimer" | "multimer(dimer)" | "multimer(homodimer)" export type StructureType = "Validated" | "DF" | "na"