From a9d28f575dcf2ee3bab5cb2638fe71cd9980bd2d Mon Sep 17 00:00:00 2001 From: Remi PLANEL <rplanel@pasteur.fr> Date: Mon, 29 Apr 2024 16:04:03 +0200 Subject: [PATCH] use d3 link and transition --- components/OperonStructure.vue | 242 ++++++++++++++++++++++++--------- types/structure.ts | 1 + 2 files changed, 182 insertions(+), 61 deletions(-) diff --git a/components/OperonStructure.vue b/components/OperonStructure.vue index 7bf2dfb5..1cf816cf 100644 --- a/components/OperonStructure.vue +++ b/components/OperonStructure.vue @@ -14,6 +14,9 @@ const structureBasket = useStructuresBasket() const props = withDefaults(defineProps<Props>(), { genes: null }); + +const { genes: genesProps } = toRefs(props) +// const refGenes = ref() const height = ref<number>(200) const svgRef = ref<SVGElement | null>(null) const margin = ref<PlotMargin>({ @@ -23,6 +26,8 @@ const margin = ref<PlotMargin>({ marginLeft: 7, }) +const geneToHighlight = ref<string | null>(null) + const snackbar = ref(false) const color = d3.scaleOrdinal(d3.schemeCategory10); const plotHeight = computed(() => { @@ -63,6 +68,7 @@ const xScaleGenes = computed(() => { }) const yScale = ref(d3.scaleBand() + .paddingInner(0.5) .domain(['img', 'buff', 'buff2', 'gene']) .range([toValue(margin).marginTop, toValue(height)])); const gbContainer = ref(null) @@ -77,8 +83,8 @@ const computedPlotWidth = computed(() => { const computedGenes = computed<StructureOperonGene[]>(() => { - const genes = toValue(props.genes) - if (genes !== null) { + const genes = toValue(genesProps) + if (genes !== null && genes?.length > 0) { let currentSumSize = 0 return genes.map(d => { const size = d?.size ?? 10 @@ -87,14 +93,16 @@ const computedGenes = computed<StructureOperonGene[]>(() => { return { ...d, size, - position + position, + highlight: geneToHighlight.value === d.gene } }) } else { return [] } -}) + +}) const structureVersion = computed(() => { const genesVal = toValue(computedGenes) if (genesVal?.length > 0) { @@ -105,8 +113,33 @@ const structureVersion = computed(() => { } }) +const structureNodes = computed<StructureOperonGeneWithCoordinate[]>(() => { + const genes = toValue(computedGenes) + const xScaleVal = toValue(xScale) + const yScaleVal = toValue(yScale) + if (genes !== null) { + return genes.map(d => { + const r = d3.min([toValue(xScale).bandwidth(), toValue(yScale).step() * 3]) + // const geneWidth = xScaleVal(d.size) + // const position = xScaleVal(d.position) + // const geneCenter = xScaleVal(d.position) + geneWidth / 2 + // const xPostion = geneCenter - r / 2 + return { + ...d, + r: r, + // x: xPostion > 0 ? xPostion : 0, + x: xScaleVal(d.gene), + y: yScaleVal('img'), + width: toValue(xScale).bandwidth(), + // width: r, + height: yScaleVal.step() * 3 + } + }) + } + else { return [] } +}) -const genesWithCoord = computed<StructureOperonGeneWithCoordinate[]>(() => { +const geneNodes = computed<StructureOperonGeneWithCoordinate[]>(() => { const genes = toValue(computedGenes) const xScaleVal = toValue(xScaleGenes) const yScaleVal = toValue(yScale) @@ -123,11 +156,34 @@ const genesWithCoord = computed<StructureOperonGeneWithCoordinate[]>(() => { } else { return [] } }) + +const linksGenesStruct = computed(() => { + const geneNodesVal = toValue(geneNodes) + const structureNodesVal = toValue(structureNodes) + + return d3.zip(geneNodesVal, structureNodesVal).map(([source, target]) => { + return { + source: [source.x + source.width / 2, source.y], + target: [target.x + target.width / 2, target.y + target.height / 2 + 30], + highlight: source?.highlight || target?.highlight + } + }) + +}) + + onMounted(() => { + // const genePropsVal = toValue(genesProps) + // refGenes.value = genePropsVal?.map(d => d) draw() }) -watch(genesWithCoord, () => { +watch(structureNodes, () => { + draw() +}) + + +watch(geneNodes, () => { draw() }) @@ -156,49 +212,130 @@ function draw() { .attr("transform", 'rotate(-20)') .attr("text-anchor", "start") - let gGenes = createOrSelect(svg, "g", "genes") - gGenes + let gOperon = createOrSelect(svg, "g", "operon") + gOperon .attr("transform", `translate(${marginLeft},0)`) - .call(drawGenes, xScale, yScale) + .call(drawLinks) + .call(drawGenes) + .call(drawStructure) + } } -function drawGenes(genesGroup: d3.Selection<SVGElement, any, SVGElement, any>) { - const data = toValue(genesWithCoord) - const genesSelection = genesGroup - .selectAll("g.operon-item") // get all "existing" lines in svg - .data<StructureOperonGeneWithCoordinate>(data) // sync them with our data - .join( - enter => { - const gOperonItem = enter.append("g") - .classed("operon-item", true); +function adjacentlinks(nodes: Record<string, any>) { + const links = [] + for (let i = 0; i < nodes.length; i++) { + if (i < nodes.length - 1) { + links.push({ index: i, source: nodes[i], target: nodes[+1] }) + } + } + return links +} - // gene grp - const gGene = gOperonItem.append("g") - .classed("gene-grp", true) - gGene - .append("path") - .classed("gene", true) +function drawLinks(operonGroup: d3.Selection<SVGElement, any, SVGElement, any>) { + const stroke = "#555" // stroke for links + const strokeWidth = 1.5 // stroke width for links + const strokeOpacity = 0.4 // stroke opacity for links + operonGroup + .selectAll("path.link") + .data(linksGenesStruct.value) + .join("path") + .classed("link", true) + .attr("fill", "none") + .attr("stroke", "currentColor") + .attr("stroke-opacity", d => d.highlight ? 0.6 : strokeOpacity) + .attr("stroke-width", d => d.highlight ? strokeWidth + 2 : strokeWidth) + .attr("d", d3.link(d3.curveBumpY)) +} + +function drawStructure(operonGroup: d3.Selection<SVGElement, any, SVGElement, any>) { + const structureNodeVal = toValue(structureNodes) + + // const totalSize = domainGenes.value[1] + // const structureLinks = adjacentlinks(structures) + // const sim = d3.forceSimulation(structureNodeVal) + // .force("link", d3.forceLink(structureLinks)) + // .force("collide", d3.forceCollide((d) => d.r)) + // .stop() + // .tick(10); + // sim.tick() + + const structureSelection = operonGroup + .selectAll("g.structure") // get all "existing" lines in svg + .data<StructureOperonGeneWithCoordinate>(structureNodeVal) // sync them with our data + .join( + enter => { + const gStructure = enter.append("g") + .classed("structure", true); // img group - gOperonItem - .append("g").classed("img", true) + gStructure .append("image") .on("mouseover", function (event) { + const srcSelection = d3.select(event.srcElement) const target = d3.select(event.srcElement.parentElement) + const node = srcSelection.data() + geneToHighlight.value = node[0].gene + target - // .attr("stroke-width", 4) - // .attr("stroke", "darkred") .attr("cursor", "pointer") }) + .on("mouseout", function (event) { const target = d3.select(event.srcElement.parentElement) target - // .attr("stroke-width", 0) - // .attr("stroke", null) .attr("cursor", "unset") + geneToHighlight.value = null + }) + return gStructure + }, + update => update, + exit => exit.remove() + ) + const imageSelection = structureSelection + .attr("transform", d => `translate(0,${toValue(yScale)("img")})`) + .select("image") + imageSelection + .transition() + .attr("transform", d => `translate(${d.x},${d.highlight ? -10 : 0})`) + imageSelection + .attr("href", d => d?.structImg ?? null) + .attr("width", d => d.width) + .attr("height", d => d.height) + .attr("preserveAspectRatio", "xMidYMid meet") + .on("click", function (event) { + const data = d3.select<SVGElement, StructureOperonGeneWithCoordinate>(this).data() + structureBasket.set(data.map(s => s?.structPath ?? '')) + + }) + + +} + + +function drawGenes(operonGroup: d3.Selection<SVGElement, any, SVGElement, any>) { + const genesWithCoordVal = toValue(geneNodes) + const genes = genesWithCoordVal + + const links = adjacentlinks(genes) + + + const genesSelection = operonGroup + .selectAll("g.operon-item") // get all "existing" lines in svg + .data<StructureOperonGeneWithCoordinate>(genes) // sync them with our data + .join( + enter => { + const gOperonItem = enter.append("g") + .classed("operon-item", true); + + // gene grp + const gGene = gOperonItem.append("g") + .classed("gene-grp", true) + gGene + .append("path") + .classed("gene", true) gOperonItem.append("text") // .attr("fill", "white") @@ -215,45 +352,28 @@ function drawGenes(genesGroup: d3.Selection<SVGElement, any, SVGElement, any>) { exit => exit.remove() ) genesSelection.select("g.gene-grp").attr("transform", d => `translate(${d.x}, 0)`) - genesSelection.select("g.img") - .attr("transform", d => `translate(${xScale.value(d.gene)})`) - .select("image") - .attr("transform", d => `translate(0, ${toValue(yScale)("img")})`) - .attr("href", d => d?.structImg ?? null) - .attr("width", toValue(xScale).bandwidth()) - .attr("height", toValue(yScale).step() * 3) - .attr("preserveAspectRatio", "xMidYMid meet") - .on("click", function (event) { - const data = d3.select<SVGElement, StructureOperonGeneWithCoordinate>(this).data() - structureBasket.set(data.map(s => s?.structPath ?? '')) - }) - genesSelection.select("g.gene-grp").select("path.gene") + genesSelection.select("g.gene-grp") + .select("path.gene") + // .attr("stroke", d => d?.highlight ? d3.color(color(d.system))?.darker() : null) + .attr("stroke-width", d => d?.highlight ? 4 : 0) .attr("transform", d => `translate(0, ${d.y})`) - .attr("fill", d => color(d.system)) + .attr("fill", d => d?.highlight ? d3.color(color(d.system))?.brighter() : color(d.system)) .attr("d", d => drawGene(d).toString()) - genesSelection.select("line") - // x1="0" y1="80" x2="100" y2="20" stroke="black" - .attr("x1", d => xScale.value(d.gene) + xScale.value.bandwidth() / 2) - .attr("y1", d => yScale.value("buff2") + yScale.value.bandwidth() / 3) - .attr("x2", d => xScaleGenes.value(d.position) + xScaleGenes.value(d.size / 2)) - .attr("y2", d => yScale.value("gene") - 2) - .attr("stroke", "currentColor") -} + function drawGene({ width, height }) { + const context = d3.path() + context.moveTo(0, 0) + context.lineTo(width, 0) + context.lineTo(width, height) + context.lineTo(0, height) + context.closePath() -function drawGene({ width, height }) { - const context = d3.path() - context.moveTo(0, 0) - context.lineTo(width, 0) - context.lineTo(width, height) - context.lineTo(0, height) - context.closePath() - - return context + return context + } } </script> diff --git a/types/structure.ts b/types/structure.ts index e622d92c..2f5997a3 100644 --- a/types/structure.ts +++ b/types/structure.ts @@ -7,6 +7,7 @@ export interface StructureOperonGene { exchangeables: string[] size: number position: number + highlight?: boolean } -- GitLab