From 94f859444681d3d77f3c753314e9dcc03de0c7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Wed, 21 Feb 2024 10:08:33 +0100 Subject: [PATCH 01/39] Start Developing Editing Mode Distinguish 'viewing' and 'editing' mode + Add Context Menus for Editing Mode + Implement Logic for Adding a new Node to the Diagram using a dialog --- demo/index.html | 1 + src/BiowcPathwaygraph.ts | 396 ++++++++++++++++++++++++++++++---- src/biowc-pathwaygraph.css.ts | 36 ++++ stories/index.stories.ts | 41 ++++ 4 files changed, 429 insertions(+), 45 deletions(-) diff --git a/demo/index.html b/demo/index.html index 6a07908..bc396c7 100644 --- a/demo/index.html +++ b/demo/index.html @@ -25,6 +25,7 @@ .ptmInputList = ${StoryFixtures.ptmGraphWithDetailsFixture.ptmInputList} .fullProteomeInputList = ${StoryFixtures.proteinExpressionFixture.fullProteomeInputList} .hue= ${'foldchange'} + .applicationMode = ${'editing'} > `, diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 5c7388f..daf8b18 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -14,13 +14,15 @@ import { ContextMenu, ExecuteOptions, } from '@api-client/context-menu'; -import { ContextMenuCommand } from '@api-client/context-menu/src/types'; +import { ContextMenuCommand, Point } from '@api-client/context-menu/src/types'; import styles from './biowc-pathwaygraph.css'; type PossibleRegulationCategoriesType = 'up' | 'down' | 'not'; type PossibleHueType = 'direction' | 'foldchange' | 'potency'; +type PossibleApplicationMode = 'viewing' | 'editing'; + const NODE_HEIGHT = 10; const PTM_NODE_WIDTH = 15; const PTM_NODE_HEIGHT = 10; @@ -233,6 +235,9 @@ export class BiowcPathwaygraph extends LitElement { @property({ attribute: false }) hue!: PossibleHueType; + @property({ attribute: false }) + applicationMode!: PossibleApplicationMode; + graphdataPTM?: { nodes: (PTMNode | PTMSummaryNode)[]; links: PathwayGraphLinkInput[]; @@ -283,6 +288,31 @@ export class BiowcPathwaygraph extends LitElement { contextMenuStore?: Map; + // TODO: Is there no way I can generalize this to rect.node-rect? + static geneProteinPathwayCompoundsNodes: string[] = [ + 'rect.node-rect.gene_protein', + 'rect.node-rect.gene_protein.down', + 'rect.node-rect.gene_protein.up', + 'rect.node-rect.gene_protein.both', + 'rect.node-rect.gene_protein.not', + 'rect.node-rect.pathway', + 'rect.node-rect.compound', + ]; + + static nodeTypes: { id: string; label: string }[] = [ + { id: 'gene_protein', label: 'Gene/Protein' }, + { id: 'compound', label: 'Metabolite/Compound' }, + { id: 'pathway', label: 'Pathway' }, + ]; + + static edgeTypes: { id: string; label: string }[] = [ + { id: 'activation', label: 'Activation' }, + { id: 'inhibition', label: 'Inhibition' }, + { id: 'binding/association', label: 'Binding/Association' }, + { id: 'indirect effect', label: 'Indirect Effect' }, + { id: 'other', label: 'Other' }, + ]; + render() { return html`
@@ -400,6 +430,43 @@ export class BiowcPathwaygraph extends LitElement {
+ +
+
+ + +
+
+ + +
+ + +
+
+ + +
+ +
`; @@ -414,6 +481,12 @@ export class BiowcPathwaygraph extends LitElement { } protected firstUpdated(_changedProperties: PropertyValues) { + // Initially the mode is always viewing unless explicitly asked for + if (this.applicationMode !== 'editing') { + this.applicationMode = 'viewing'; + } + this.switchApplicationMode(this.applicationMode); + this.d3Nodes = []; this.d3Links = []; this._getMainDiv().append('g').attr('id', 'linkG'); @@ -428,6 +501,103 @@ export class BiowcPathwaygraph extends LitElement { ['show-not', true], ]); + // Initialize the "Add Node" and "Add Edge" Forms + // TODO: Make this its own method, it deserves it. + const addNodeTypeSelect: HTMLSelectElement = this.shadowRoot?.querySelector( + '#add-node-type-select' + )!; + BiowcPathwaygraph.nodeTypes.forEach(nodeType => { + const option = document.createElement('option'); + option.value = nodeType.id; + option.label = nodeType.label; + addNodeTypeSelect.options.add(option); + }); + + addNodeTypeSelect.onchange = () => { + const addNodePrimaryNameLabel: HTMLLabelElement = + this.shadowRoot?.querySelector('#add-node-primary-name-label')!; + const addNodeAlternativeGeneNames: HTMLDivElement = + this.shadowRoot?.querySelector('#add-node-alternative-gene-names')!; + const addNodeUniprots: HTMLDivElement = this.shadowRoot?.querySelector( + '#add-node-uniprot-accession' + )!; + if (addNodeTypeSelect.value === 'gene_protein') { + addNodePrimaryNameLabel.textContent = 'Primary Gene Name:'; + addNodeAlternativeGeneNames.style.display = 'block'; + addNodeUniprots.style.display = 'block'; + } else { + addNodePrimaryNameLabel.textContent = 'Name:'; + addNodeAlternativeGeneNames.style.display = 'none'; + addNodeUniprots.style.display = 'none'; + } + }; + + const addNodeForm: HTMLFormElement = + this.shadowRoot?.querySelector('#add-node-form')!; + const confirmButton: HTMLButtonElement = this.shadowRoot?.querySelector( + '#add-node-confirm-button' + )!; + confirmButton.onclick = e => { + const formData: FormData = new FormData(addNodeForm); + + // nodeType and nodePrimaryName are required + if (formData.get('nodePrimaryName') === '' || !formData.get('nodeType')) { + e.preventDefault(); + return; + } + + // Some regex like s.match(/\(\d+,\d+\)/) + const nodeType = String(formData.get('nodeType')!); + const nodeAddPoint: Point = this.contextMenuStore?.get('clickPoint')!; + const transformString = this._getMainDiv() + .select('#nodeG') + .attr('transform'); + const [, translateX, translateY, scale] = transformString.match( + /translate\((-?[\d|.]+),(-?[\d|.]+)\) scale\((-?[\d|.]+)\)/ + )!; + + // Create a node + // @ts-ignore + const newNode: GeneProteinNodeD3 = { + nodeId: `customNode-${crypto.getRandomValues(new Uint32Array(1))[0]}`, + type: nodeType, + x: (nodeAddPoint.x - Number(translateX)) / Number(scale), + y: (nodeAddPoint.y - Number(translateY)) / Number(scale), + }; + + if (nodeType === 'gene_protein') { + newNode.geneNames = String( + formData.get('nodeAlternativeGeneNames')! + ).split(/[;,\n]/); + newNode.uniprotAccs = String(formData.get('nodeUniprotAccs')!).split( + /[;,\n]/ + ); + newNode.geneNames.unshift(String(formData.get('nodePrimaryName')!)); + newNode.defaultName = String(formData.get('nodePrimaryName')!); + } else { + newNode.label = String(formData.get('nodePrimaryName')!); + } + + // Add it to the graphdataSkeleton + this.graphdataSkeleton.nodes.push(newNode); + this.graphdataSkeleton.nodes = [...this.graphdataSkeleton.nodes]; + + // Refresh + this.updated(new Map()); // TODO: Forcing 'updated' with an empty map feels hacky... + }; + const cancelButton: HTMLButtonElement = this.shadowRoot?.querySelector( + '#add-node-cancel-button' + )!; + cancelButton.onclick = () => { + (( + this.shadowRoot?.querySelector('#add-node-dialog')! + )).close(); + // Reset the state of the form + addNodeForm.reset(); + // Simulate a change on the select so it snaps back into default state + addNodeTypeSelect.dispatchEvent(new Event('change')); + }; + super.firstUpdated(_changedProperties); } @@ -3283,15 +3453,7 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'>container); + private _setUpViewingModeContextMenu() { this.contextMenuCommands = [ // Context Menu for Canvas { @@ -3342,7 +3504,6 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { - // For hue, initialize to initial hue. Nothing is working so far... const storeId = `show-${ctx.item.id}`; ctx.store.set(storeId, !ctx.store.get(storeId)); this._refreshGraph(true); @@ -3415,20 +3576,13 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> {}, children: [ @@ -3457,16 +3611,13 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { - // Hide the tooltip when the context menu is shown - this._getMainDiv().select('#nodetooltip').style('opacity', '0'); - const nameAlternatives = BiowcPathwaygraph._calcPossibleLabels( - node - ); - const alternativeNamesCommand = - this.contextMenuCommands!.pop() as ContextMenuCommand; - alternativeNamesCommand!.children = nameAlternatives.map( - alternative => ({ - label: alternative, - execute: () => { - // eslint-disable-next-line no-param-reassign - node.currentDisplayedLabel = alternative; - this._refreshGraph(true); - }, - }) - ); - this.contextMenuCommands!.push(alternativeNamesCommand); - this.contextMenu?.registerCommands(this.contextMenuCommands!); + // Only do this when in viewing mode + if (this.applicationMode === 'viewing') { + // Hide the tooltip when the context menu is shown + this._getMainDiv().select('#nodetooltip').style('opacity', '0'); + const nameAlternatives = BiowcPathwaygraph._calcPossibleLabels( + node + ); + const alternativeNamesCommand = + this.contextMenuCommands!.pop() as ContextMenuCommand; + alternativeNamesCommand!.children = nameAlternatives.map( + alternative => ({ + label: alternative, + execute: () => { + // eslint-disable-next-line no-param-reassign + node.currentDisplayedLabel = alternative; + this._refreshGraph(true); + }, + }) + ); + this.contextMenuCommands!.push(alternativeNamesCommand); + this.contextMenu?.registerCommands(this.contextMenuCommands!); + } }); + } - this.contextMenu.registerCommands(this.contextMenuCommands); + private _setUpEditingModeContextMenu() { + this.contextMenuCommands = [ + // Context Menu for Canvas + { + target: 'svg', + label: 'Add Node', + execute: ctx => { + this.contextMenuStore?.set('clickPoint', ctx.clickPoint); + const addNodeDialog: HTMLDialogElement = + this.shadowRoot?.querySelector('#add-node-dialog')!; + addNodeDialog.showModal(); + }, + }, + { + target: 'svg', + label: 'Add Edge', + execute: () => { + console.log('TODO: Implement'); + }, + }, + { + target: 'svg', + label: 'Create Node Group', + execute: () => { + console.log('TODO: Implement'); + }, + }, + { + target: BiowcPathwaygraph.geneProteinPathwayCompoundsNodes.concat([ + 'path.group-path', + ]), + label: 'Add Edge from this Node', + execute: () => { + console.log('TODO: Implement'); + }, + }, + { + target: BiowcPathwaygraph.geneProteinPathwayCompoundsNodes.concat([ + 'path.group-path', + ]), + label: 'Add Edge to this Node', + execute: () => { + console.log('TODO: Implement'); + }, + }, + { + target: BiowcPathwaygraph.geneProteinPathwayCompoundsNodes, + label: 'Change Node Type', + execute: () => { + console.log('TODO: Implement'); + }, + children: BiowcPathwaygraph.nodeTypes.map(nodeType => ({ + type: 'radio', + id: nodeType.id, + label: nodeType.label, + checked: ctx => { + console.log(ctx.target); + return false; + }, + })), + }, + { + target: BiowcPathwaygraph.geneProteinPathwayCompoundsNodes, + label: 'Edit Node Identifiers', + execute: () => { + console.log('TODO: Implement'); + }, + }, + { + target: BiowcPathwaygraph.geneProteinPathwayCompoundsNodes, + label: 'Remove Node', + execute: () => { + console.log('TODO: Implement'); + }, + }, + { + target: 'path.group-path', + label: 'Remove Group', + execute: () => { + console.log('TODO: Implement'); + }, + }, + { + target: 'line.link', + label: 'Change Edge Type', + execute: () => { + console.log('TODO: Implement'); + }, + children: BiowcPathwaygraph.edgeTypes.map(edgeType => ({ + type: 'radio', + id: edgeType.id, + label: edgeType.label, + checked: ctx => { + console.log(ctx.target); + return false; + }, + })), + }, + { + target: 'line.link', + label: 'Change Edge Label', + execute: () => { + console.log('TODO: Implement'); + }, + }, + { + target: 'line.link', + label: 'Remove Edge', + execute: () => { + console.log('TODO: Implement'); + }, + }, + ]; + } + + private _initContextMenu() { + if (this.contextMenu) this.contextMenu.disconnect(); + + const container = d3v6 + // @ts-ignore + .select(this.shadowRoot) + .select('#pathwayContainer') + .node(); + this.contextMenu = new ContextMenu(container); + + // TODO: I probably need to do most of this only once, not every time the mode is switched. + if (this.applicationMode === 'viewing') { + this._setUpViewingModeContextMenu(); + } else { + this._setUpEditingModeContextMenu(); + } + + // Add the store - it is saved as a separate variable, so it is persistent across incarnations of the contextmenu + this.contextMenu.store = this.contextMenuStore!; + this.contextMenu.registerCommands(this.contextMenuCommands!); this.contextMenu.connect(); } @@ -3654,4 +3943,21 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> = (args: ArgTypes) => html` @@ -302,4 +304,43 @@ DownloadGraphAsSVG.args = { 'by the user.', }; +const EditingModeTemplate: Story = (args: ArgTypes) => html` +
${args.storyTitle}
+
${args.storyDescription}
+ + + + + + +`; + +export const EditingMode = EditingModeTemplate.bind({}); +EditingMode.args = { + ...ColoringNodesByPotency.args, + hue: 'potency', + storyTitle: 'Editing Mode', + storyDescription: 'TODO', + applicationMode: 'viewing', +}; + // TODO: Events: selectedNodeTooltip and selectionDetails are dispatched when? From 305c89ad75c436c37b17ed66ddc44212f24946e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Wed, 21 Feb 2024 14:11:35 +0100 Subject: [PATCH 02/39] Refactoring & Reset Form after adding of node --- src/BiowcPathwaygraph.ts | 60 +++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index daf8b18..a62ee6e 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -501,8 +501,32 @@ export class BiowcPathwaygraph extends LitElement { ['show-not', true], ]); - // Initialize the "Add Node" and "Add Edge" Forms - // TODO: Make this its own method, it deserves it. + this._initEditModeForms(); + super.firstUpdated(_changedProperties); + } + + protected updated(_changedProperties: PropertyValues) { + this.graphdataSkeleton.geneToNodeMap = this._createPathwayGeneToNodeMap(); + + // Map PTM Input to Skeleton Nodes + this.graphdataPTM = this._addPTMInformationToPathway(); + // Add Full Proteome Input to Skeleton Nodes + if (this.fullProteomeInputList) { + this.graphdataSkeleton.nodes = + this._addFullProteomeInformationToPathway(); + } + + this._createD3GraphObject(); + this._calculateHueRange(); + this._renderLegend(); + this._renderGraph(); + this._initContextMenu(); + this._updateRangeSliderVisibility(); + + super.updated(_changedProperties); + } + + private _initEditModeForms() { const addNodeTypeSelect: HTMLSelectElement = this.shadowRoot?.querySelector( '#add-node-type-select' )!; @@ -584,6 +608,15 @@ export class BiowcPathwaygraph extends LitElement { // Refresh this.updated(new Map()); // TODO: Forcing 'updated' with an empty map feels hacky... + + // Reset the state of the form + addNodeForm.reset(); + // Simulate a change on the select so it snaps back into default state + addNodeTypeSelect.dispatchEvent(new Event('change')); + // Close the dialog + (( + this.shadowRoot?.querySelector('#add-node-dialog')! + )).close(); }; const cancelButton: HTMLButtonElement = this.shadowRoot?.querySelector( '#add-node-cancel-button' @@ -597,29 +630,6 @@ export class BiowcPathwaygraph extends LitElement { // Simulate a change on the select so it snaps back into default state addNodeTypeSelect.dispatchEvent(new Event('change')); }; - - super.firstUpdated(_changedProperties); - } - - protected updated(_changedProperties: PropertyValues) { - this.graphdataSkeleton.geneToNodeMap = this._createPathwayGeneToNodeMap(); - - // Map PTM Input to Skeleton Nodes - this.graphdataPTM = this._addPTMInformationToPathway(); - // Add Full Proteome Input to Skeleton Nodes - if (this.fullProteomeInputList) { - this.graphdataSkeleton.nodes = - this._addFullProteomeInformationToPathway(); - } - - this._createD3GraphObject(); - this._calculateHueRange(); - this._renderLegend(); - this._renderGraph(); - this._initContextMenu(); - this._updateRangeSliderVisibility(); - - super.updated(_changedProperties); } private _createPathwayGeneToNodeMap(): { [key: string]: GeneProteinNode[] } { From 813af3cc1230d94d844ae91cb52932cd7d7afadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Wed, 21 Feb 2024 18:04:11 +0100 Subject: [PATCH 03/39] Implement edge adding --- demo/index.html | 4 +- src/BiowcPathwaygraph.ts | 365 +++++++++++++++++++++++----------- src/biowc-pathwaygraph.css.ts | 4 + 3 files changed, 253 insertions(+), 120 deletions(-) diff --git a/demo/index.html b/demo/index.html index bc396c7..93697dc 100644 --- a/demo/index.html +++ b/demo/index.html @@ -20,8 +20,8 @@ render( html` ; + isAddingEdge: Boolean = false; + // TODO: Is there no way I can generalize this to rect.node-rect? static geneProteinPathwayCompoundsNodes: string[] = [ 'rect.node-rect.gene_protein', @@ -431,7 +434,7 @@ export class BiowcPathwaygraph extends LitElement { -
+
+ + +
+ + +
+
+
+

+ After clicking "Confirm", please click on the Source Node, then on the Target Node. +

+
+
+ + +
+
+
`; @@ -558,10 +586,9 @@ export class BiowcPathwaygraph extends LitElement { const addNodeForm: HTMLFormElement = this.shadowRoot?.querySelector('#add-node-form')!; - const confirmButton: HTMLButtonElement = this.shadowRoot?.querySelector( - '#add-node-confirm-button' - )!; - confirmButton.onclick = e => { + const addNodeConfirmButton: HTMLButtonElement = + this.shadowRoot?.querySelector('#add-node-confirm-button')!; + addNodeConfirmButton.onclick = e => { const formData: FormData = new FormData(addNodeForm); // nodeType and nodePrimaryName are required @@ -573,6 +600,9 @@ export class BiowcPathwaygraph extends LitElement { // Some regex like s.match(/\(\d+,\d+\)/) const nodeType = String(formData.get('nodeType')!); const nodeAddPoint: Point = this.contextMenuStore?.get('clickPoint')!; + // Clear the clickPoint, it has served its purpose + this.contextMenuStore?.delete('clickPoint'); + const transformString = this._getMainDiv() .select('#nodeG') .attr('transform'); @@ -604,24 +634,22 @@ export class BiowcPathwaygraph extends LitElement { // Add it to the graphdataSkeleton this.graphdataSkeleton.nodes.push(newNode); - this.graphdataSkeleton.nodes = [...this.graphdataSkeleton.nodes]; // Refresh this.updated(new Map()); // TODO: Forcing 'updated' with an empty map feels hacky... - // Reset the state of the form - addNodeForm.reset(); // Simulate a change on the select so it snaps back into default state addNodeTypeSelect.dispatchEvent(new Event('change')); // Close the dialog (( this.shadowRoot?.querySelector('#add-node-dialog')! )).close(); + // Reset the state of the form + addNodeForm.reset(); }; - const cancelButton: HTMLButtonElement = this.shadowRoot?.querySelector( - '#add-node-cancel-button' - )!; - cancelButton.onclick = () => { + const addNodeCancelButton: HTMLButtonElement = + this.shadowRoot?.querySelector('#add-node-cancel-button')!; + addNodeCancelButton.onclick = () => { (( this.shadowRoot?.querySelector('#add-node-dialog')! )).close(); @@ -630,6 +658,63 @@ export class BiowcPathwaygraph extends LitElement { // Simulate a change on the select so it snaps back into default state addNodeTypeSelect.dispatchEvent(new Event('change')); }; + + const addEdgeTypeSelect: HTMLSelectElement = this.shadowRoot?.querySelector( + '#add-edge-type-select' + )!; + BiowcPathwaygraph.edgeTypes.forEach(edgeType => { + const option = document.createElement('option'); + option.value = edgeType.id; + option.label = edgeType.label; + addEdgeTypeSelect.options.add(option); + }); + + const addEdgeForm: HTMLFormElement = + this.shadowRoot?.querySelector('#add-edge-form')!; + const addEdgeConfirmButton: HTMLButtonElement = + this.shadowRoot?.querySelector('#add-edge-confirm-button')!; + addEdgeConfirmButton.onclick = e => { + const formData: FormData = new FormData(addEdgeForm); + + // edgeType is required + if (!formData.get('edgeType')) { + e.preventDefault(); + return; + } + + this.isAddingEdge = true; + + // Save the type and label in the store, so they can be retrieved when the edge is ready + this.contextMenuStore!.set( + 'newEdgeType', + String(formData.get('edgeType')) + ); + this.contextMenuStore!.set( + 'newEdgeLabel', + String(formData.get('edgeLabel')) + ); + + // Reset the state of the form + addEdgeForm.reset(); + // Simulate a change on the select so it snaps back into default state + addEdgeTypeSelect.dispatchEvent(new Event('change')); + // Close the dialog + (( + this.shadowRoot?.querySelector('#add-edge-dialog')! + )).close(); + }; + + const addEdgeCancelButton: HTMLButtonElement = + this.shadowRoot?.querySelector('#add-edge-cancel-button')!; + addEdgeCancelButton.onclick = () => { + (( + this.shadowRoot?.querySelector('#add-edge-dialog')! + )).close(); + // Reset the state of the form + addEdgeForm.reset(); + // Simulate a change on the select so it snaps back into default state + addEdgeTypeSelect.dispatchEvent(new Event('change')); + }; } private _createPathwayGeneToNodeMap(): { [key: string]: GeneProteinNode[] } { @@ -1157,7 +1242,10 @@ export class BiowcPathwaygraph extends LitElement { .join('g') .attr( 'class', - d => `node ${d.type} ${BiowcPathwaygraph._computeRegulationClass(d)} ` + d => + `node ${d.type} ${BiowcPathwaygraph._computeRegulationClass(d)} ${ + (d).isHighlighted ? 'highlight' : '' + } ` ) .attr('id', d => `node-${d.nodeId}`); @@ -1169,7 +1257,9 @@ export class BiowcPathwaygraph extends LitElement { .attr( 'class', d => - `node-rect ${d.type} ${BiowcPathwaygraph._computeRegulationClass(d)}` + `node-rect ${d.type} ${BiowcPathwaygraph._computeRegulationClass( + d + )} ${(d).isHighlighted ? 'highlight' : ''}` ) .attr('rx', NODE_HEIGHT) .attr('ry', NODE_HEIGHT) @@ -2906,110 +2996,148 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'>('g') .on('click', (e, node) => { - // Do not propagate event to canvas, because that would remove the highlighting - e.stopPropagation(); - // Check if it is a double click - this.recentClicks += 1; - if (this.recentClicks === 1) { - // Wait for a possible doubleclick using a timeout - // If a double click happens within DBL_CLICK_TIMEOUT milliseconds, - // the event is canceled using clearTimeout below - dblClickTimer = setTimeout(() => { - this.recentClicks = 0; - // Unless the CTRL key is pressed, unselect everything first - if (!e.ctrlKey) { + // Selection is only active in viewing mode, not in editing mode + if (this.applicationMode === 'viewing') { + // Do not propagate event to canvas, because that would remove the highlighting + e.stopPropagation(); + // Check if it is a double click + this.recentClicks += 1; + if (this.recentClicks === 1) { + // Wait for a possible doubleclick using a timeout + // If a double click happens within DBL_CLICK_TIMEOUT milliseconds, + // the event is canceled using clearTimeout below + dblClickTimer = setTimeout(() => { + this.recentClicks = 0; + // Unless the CTRL key is pressed, unselect everything first + if (!e.ctrlKey) { + this._getMainDiv() + .select('#nodeG') + .selectAll('g') + .each(d => { + /* eslint-disable no-param-reassign */ + d.selected = false; + /* eslint-enable no-param-reassign */ + }); + } + // CTRL + Click on a selected node deselects the node, otherwise the node becomes selected + const isSelected = !(e.ctrlKey && node.selected); + // Apply this new value to the node itself and all attached ptm nodes + /* eslint-disable no-param-reassign */ + node.selected = isSelected; + /* eslint-enable no-param-reassign */ this._getMainDiv() - .select('#nodeG') - .selectAll('g') - .each(d => { + .selectAll( + '.ptmlink:not(.legend)' + ) + .each(l => { /* eslint-disable no-param-reassign */ - d.selected = false; + // If clicked node is a protein, select all its PTM nodes + if (l.target === node) + (l.source).selected = isSelected; + // If clicked node is a PTM and it was a selection (not a deselection), we also want to select the protein + // We don't want the opposite, so if it is a deselection, don't deselect the protein as well + if (l.source === node && isSelected) { + (l.target).selected = true; + } /* eslint-enable no-param-reassign */ }); - } - // CTRL + Click on a selected node deselects the node, otherwise the node becomes selected - const isSelected = !(e.ctrlKey && node.selected); - // Apply this new value to the node itself and all attached ptm nodes - /* eslint-disable no-param-reassign */ - node.selected = isSelected; - /* eslint-enable no-param-reassign */ - this._getMainDiv() - .selectAll( - '.ptmlink:not(.legend)' - ) - .each(l => { - /* eslint-disable no-param-reassign */ - // If clicked node is a protein, select all its PTM nodes - if (l.target === node) - (l.source).selected = isSelected; - // If clicked node is a PTM and it was a selection (not a deselection), we also want to select the protein - // We don't want the opposite, so if it is a deselection, don't deselect the protein as well - if (l.source === node && isSelected) { - (l.target).selected = true; - } - /* eslint-enable no-param-reassign */ - }); - // If the node is a PTM summary node, apply its selection status to its individual PTM nodes - if (node.type.includes('summary')) { - (node).ptmNodes!.forEach(d => { - /* eslint-disable no-param-reassign */ - d.selected = isSelected; - /* eslint-enable no-param-reassign */ - }); - } + // If the node is a PTM summary node, apply its selection status to its individual PTM nodes + if (node.type.includes('summary')) { + (node).ptmNodes!.forEach(d => { + /* eslint-disable no-param-reassign */ + d.selected = isSelected; + /* eslint-enable no-param-reassign */ + }); + } - // If the node is either a Gene/Protein node or a (non-summary) PTM node - // and it's a non-CTRL selection event, - // throw an event to display the tooltip information permanently in the parent - if ( - node.type.includes('ptm') && - !node.type.includes('summary') && - !e.ctrlKey - ) { - this.dispatchEvent( - new CustomEvent('selectedNodeTooltip', { - bubbles: true, - cancelable: true, - detail: BiowcPathwaygraph._getPTMTooltipText( - node as PTMNodeD3 - ), - }) - ); - } else if (node.type.includes('gene_protein') && !e.ctrlKey) { - this.dispatchEvent( - new CustomEvent('selectedNodeTooltip', { - bubbles: true, - cancelable: true, - detail: BiowcPathwaygraph._getGeneProteinTooltipText( - node as GeneProteinNodeD3 - ), - }) - ); - } else { - this.dispatchEvent( - new CustomEvent('selectedNodeTooltip', { - bubbles: true, - cancelable: true, - detail: undefined, - }) - ); - } + // If the node is either a Gene/Protein node or a (non-summary) PTM node + // and it's a non-CTRL selection event, + // throw an event to display the tooltip information permanently in the parent + if ( + node.type.includes('ptm') && + !node.type.includes('summary') && + !e.ctrlKey + ) { + this.dispatchEvent( + new CustomEvent('selectedNodeTooltip', { + bubbles: true, + cancelable: true, + detail: BiowcPathwaygraph._getPTMTooltipText( + node as PTMNodeD3 + ), + }) + ); + } else if (node.type.includes('gene_protein') && !e.ctrlKey) { + this.dispatchEvent( + new CustomEvent('selectedNodeTooltip', { + bubbles: true, + cancelable: true, + detail: BiowcPathwaygraph._getGeneProteinTooltipText( + node as GeneProteinNodeD3 + ), + }) + ); + } else { + this.dispatchEvent( + new CustomEvent('selectedNodeTooltip', { + bubbles: true, + cancelable: true, + detail: undefined, + }) + ); + } - // If the node is a group, select all its members - if (node.type === 'group') { - (node).componentNodes.forEach(comp => { - /* eslint-disable no-param-reassign */ - comp.selected = isSelected; - /* eslint-enable no-param-reassign */ - }); - } + // If the node is a group, select all its members + if (node.type === 'group') { + (node).componentNodes.forEach(comp => { + /* eslint-disable no-param-reassign */ + comp.selected = isSelected; + /* eslint-enable no-param-reassign */ + }); + } - this._onSelectedNodesChanged(); - }, DBL_CLICK_TIMEOUT); - } else { - // If it is a doubleclick, the above code wrapped in the timeout should not be executed - clearTimeout(dblClickTimer); - this.recentClicks = 0; + this._onSelectedNodesChanged(); + }, DBL_CLICK_TIMEOUT); + } else { + // If it is a doubleclick, the above code wrapped in the timeout should not be executed + clearTimeout(dblClickTimer); + this.recentClicks = 0; + } + } + // Logic for adding an edge + if (this.isAddingEdge) { + /* eslint-disable no-param-reassign */ + if (!this.contextMenuStore!.has('newEdgeSource')) { + this.contextMenuStore!.set('newEdgeSource', node.nodeId); + + (node).isHighlighted = true; + this._refreshGraph(true); + } else { + const sourceId = this.contextMenuStore!.get('newEdgeSource'); + + const newEdge = { + linkId: `customRelation-${ + crypto.getRandomValues(new Uint32Array(1))[0] + }`, + sourceId, + targetId: node.nodeId, + types: [this.contextMenuStore!.get('newEdgeType')], // TODO: Get from context menu - save in store and delete afterwards + label: this.contextMenuStore!.get('newEdgeLabel'), + }; + this.graphdataSkeleton.links.push(newEdge); + + const sourceNode: PathwayGraphNodeD3 = this.d3Nodes!.filter( + nd => nd.nodeId === sourceId + )[0]; + (sourceNode).isHighlighted = false; + this.contextMenuStore!.delete('newEdgeSource'); + this.contextMenuStore!.delete('newEdgeType'); + this.contextMenuStore!.delete('newEdgeLabel'); + this.isAddingEdge = false; + // Refresh + this.updated(new Map()); // TODO: Forcing 'updated' with an empty map feels hacky... + } + /* eslint-enable no-param-reassign */ } }); } @@ -3677,7 +3805,9 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { - console.log('TODO: Implement'); + const addEdgeDialog: HTMLDialogElement = + this.shadowRoot?.querySelector('#add-edge-dialog')!; + addEdgeDialog.showModal(); }, }, { @@ -3708,17 +3838,16 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { - console.log('TODO: Implement'); + execute: ctx => { + // @ts-ignore + ctx.target.__data__.type = ctx.item.id; + this._refreshGraph(true); }, children: BiowcPathwaygraph.nodeTypes.map(nodeType => ({ type: 'radio', id: nodeType.id, label: nodeType.label, - checked: ctx => { - console.log(ctx.target); - return false; - }, + checked: ctx => ctx.target.classList.contains(nodeType.id), })), }, { diff --git a/src/biowc-pathwaygraph.css.ts b/src/biowc-pathwaygraph.css.ts index f379d3c..cda3d3d 100644 --- a/src/biowc-pathwaygraph.css.ts +++ b/src/biowc-pathwaygraph.css.ts @@ -101,6 +101,10 @@ export default css` fill: var(--compound-color); } + .node-rect.highlight { + stroke-width: 3; + } + strong { display: inline-block; text-align: left; From eff59c57ecd5ca2630c55f69e417ccab38340286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Wed, 21 Feb 2024 18:42:30 +0100 Subject: [PATCH 04/39] Implement Add Edge TO/FROM --- src/BiowcPathwaygraph.ts | 79 +++++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 53cbd1a..94e0c86 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -485,8 +485,7 @@ export class BiowcPathwaygraph extends LitElement {
-

- After clicking "Confirm", please click on the Source Node, then on the Target Node. +

@@ -714,6 +713,13 @@ export class BiowcPathwaygraph extends LitElement { addEdgeForm.reset(); // Simulate a change on the select so it snaps back into default state addEdgeTypeSelect.dispatchEvent(new Event('change')); + // Remove highlighting of node, if present + this.d3Nodes?.forEach(d => { + /* eslint-disable-next-line no-param-reassign */ + (d).isHighlighted = false; + }); + this._refreshGraph(true); + this.isAddingEdge = false; }; } @@ -3113,24 +3119,37 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'>node).isHighlighted = true; this._refreshGraph(true); } else { + this.contextMenuStore!.set('newEdgeTarget', node.nodeId); + + (node).isHighlighted = true; + this._refreshGraph(true); + } + + if ( + this.contextMenuStore!.has('newEdgeSource') && + this.contextMenuStore!.has('newEdgeTarget') + ) { const sourceId = this.contextMenuStore!.get('newEdgeSource'); + const targetId = this.contextMenuStore!.get('newEdgeTarget'); const newEdge = { linkId: `customRelation-${ crypto.getRandomValues(new Uint32Array(1))[0] }`, sourceId, - targetId: node.nodeId, - types: [this.contextMenuStore!.get('newEdgeType')], // TODO: Get from context menu - save in store and delete afterwards + targetId, + types: [this.contextMenuStore!.get('newEdgeType')], label: this.contextMenuStore!.get('newEdgeLabel'), }; this.graphdataSkeleton.links.push(newEdge); - const sourceNode: PathwayGraphNodeD3 = this.d3Nodes!.filter( - nd => nd.nodeId === sourceId - )[0]; - (sourceNode).isHighlighted = false; + this.d3Nodes!.filter(nd => + [sourceId, targetId].includes(nd.nodeId) + ).forEach(nd => { + (nd).isHighlighted = false; + }); this.contextMenuStore!.delete('newEdgeSource'); + this.contextMenuStore!.delete('newEdgeTarget'); this.contextMenuStore!.delete('newEdgeType'); this.contextMenuStore!.delete('newEdgeLabel'); this.isAddingEdge = false; @@ -3805,6 +3824,10 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { + const addEdgeInfoText: HTMLParagraphElement = + this.shadowRoot?.querySelector('#add-edge-info-text')!; + addEdgeInfoText.textContent = + 'After clicking "Confirm", please click on the Source Node, then on the Target Node.'; const addEdgeDialog: HTMLDialogElement = this.shadowRoot?.querySelector('#add-edge-dialog')!; addEdgeDialog.showModal(); @@ -3821,18 +3844,46 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { - console.log('TODO: Implement'); + label: 'Add Edge FROM this Node', + execute: ctx => { + const addEdgeInfoText: HTMLParagraphElement = + this.shadowRoot?.querySelector('#add-edge-info-text')!; + addEdgeInfoText.textContent = + 'After clicking "Confirm", please click on the Target Node.'; + const addEdgeDialog: HTMLDialogElement = + this.shadowRoot?.querySelector('#add-edge-dialog')!; + addEdgeDialog.showModal(); + // @ts-ignore + const { nodeId } = ctx.target.__data__; + this.contextMenuStore!.set('newEdgeSource', nodeId); + + // @ts-ignore + ctx.target.__data__.isHighlighted = true; + this._refreshGraph(true); + this.isAddingEdge = true; }, }, { target: BiowcPathwaygraph.geneProteinPathwayCompoundsNodes.concat([ 'path.group-path', ]), - label: 'Add Edge to this Node', - execute: () => { - console.log('TODO: Implement'); + label: 'Add Edge TO this Node', + execute: ctx => { + const addEdgeInfoText: HTMLParagraphElement = + this.shadowRoot?.querySelector('#add-edge-info-text')!; + addEdgeInfoText.textContent = + 'After clicking "Confirm", please click on the Source Node.'; + const addEdgeDialog: HTMLDialogElement = + this.shadowRoot?.querySelector('#add-edge-dialog')!; + addEdgeDialog.showModal(); + // @ts-ignore + const { nodeId } = ctx.target.__data__; + this.contextMenuStore!.set('newEdgeTarget', nodeId); + + // @ts-ignore + ctx.target.__data__.isHighlighted = true; + this._refreshGraph(true); + this.isAddingEdge = true; }, }, { From 02cb07d36faa98588c44073bf379ffa39b089a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Wed, 21 Feb 2024 18:52:34 +0100 Subject: [PATCH 05/39] Implement change edge type --- demo/index.html | 4 ++-- src/BiowcPathwaygraph.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/demo/index.html b/demo/index.html index 93697dc..7432040 100644 --- a/demo/index.html +++ b/demo/index.html @@ -20,8 +20,8 @@ render( html` { - console.log('TODO: Implement'); + execute: ctx => { + // @ts-ignore + ctx.target.__data__.types = [ctx.item.id]; + this._refreshGraph(true); }, children: BiowcPathwaygraph.edgeTypes.map(edgeType => ({ type: 'radio', id: edgeType.id, label: edgeType.label, - checked: ctx => { - console.log(ctx.target); - return false; - }, + // @ts-ignore + checked: ctx => ctx.target.__data__.types.includes(edgeType.id), })), }, { From 1a033ea8667c517ec2c8f5fb7281568967939498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Fri, 23 Feb 2024 09:12:58 +0100 Subject: [PATCH 06/39] Implement delete edge and node (but there is a mistake...) --- src/BiowcPathwaygraph.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 232323d..b3c601a 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -3911,8 +3911,19 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { - console.log('TODO: Implement'); + execute: ctx => { + // @ts-ignore + const nodeIdToRemove = ctx.target.__data__.nodeId; + this.graphdataSkeleton.nodes = this.graphdataSkeleton.nodes.filter( + node => node.nodeId !== nodeIdToRemove + ); + this.graphdataSkeleton.links = this.graphdataSkeleton.links.filter( + link => + link.sourceId !== nodeIdToRemove && + link.targetId !== nodeIdToRemove + ); + // Refresh + this.updated(new Map()); // TODO: Forcing 'updated' with an empty map feels hacky... }, }, { @@ -3948,8 +3959,15 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { - console.log('TODO: Implement'); + execute: ctx => { + // @ts-ignore + const linkIdToRemove = ctx.target.__data__.linkId; + // TODO: Higher order links, here and in the node removal + this.graphdataSkeleton.links = this.graphdataSkeleton.links.filter( + link => link.linkId !== linkIdToRemove + ); + // Refresh + this.updated(new Map()); // TODO: Forcing 'updated' with an empty map feels hacky... }, }, ]; From d92df179e1a078e0c8a99d75083a2fd6c865ef2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Sat, 24 Feb 2024 12:23:40 +0100 Subject: [PATCH 07/39] Implement deletion of nodes and edges --- package-lock.json | 4 ++-- src/BiowcPathwaygraph.ts | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ac62c3..d1eeeb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "biowc-pathwaygraph", - "version": "0.0.19", + "version": "0.0.20", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "biowc-pathwaygraph", - "version": "0.0.19", + "version": "0.0.20", "license": "Apache-2.0", "dependencies": { "@api-client/context-menu": "^0.4.1", diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index b3c601a..5fd6ddd 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -3922,6 +3922,15 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> link.linkId) + .concat(this.graphdataSkeleton.nodes.map(node => node.nodeId)); + this.graphdataSkeleton.links = this.graphdataSkeleton.links.filter( + link => + nodeAndLinkIds.includes(link.sourceId) && + nodeAndLinkIds.includes(link.targetId) + ); // Refresh this.updated(new Map()); // TODO: Forcing 'updated' with an empty map feels hacky... }, @@ -3962,9 +3971,13 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { // @ts-ignore const linkIdToRemove = ctx.target.__data__.linkId; - // TODO: Higher order links, here and in the node removal this.graphdataSkeleton.links = this.graphdataSkeleton.links.filter( - link => link.linkId !== linkIdToRemove + // Remove the link if it is either the very link to remove, or if it is + // an anchor that has the link to remove as source or target + link => + ![link.linkId, link.sourceId, link.targetId].includes( + linkIdToRemove + ) ); // Refresh this.updated(new Map()); // TODO: Forcing 'updated' with an empty map feels hacky... From 1ff4b8bfe3993a63d1c4befbe6be8246754a3e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Sat, 24 Feb 2024 14:08:02 +0100 Subject: [PATCH 08/39] Try to implement edge label changing (but did not succeed...) --- src/BiowcPathwaygraph.ts | 64 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 5fd6ddd..e71c26b 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -494,6 +494,20 @@ export class BiowcPathwaygraph extends LitElement {
+ +
+
+ + +
+
+ + +
+
+
`; @@ -721,6 +735,40 @@ export class BiowcPathwaygraph extends LitElement { this._refreshGraph(true); this.isAddingEdge = false; }; + + const editEdgeLabelForm: HTMLFormElement = this.shadowRoot?.querySelector( + '#edit-edge-label-form' + )!; + const editEdgeLabelConfirmButton: HTMLButtonElement = + this.shadowRoot?.querySelector('#edit-edge-label-confirm-button')!; + editEdgeLabelConfirmButton.onclick = () => { + const formData: FormData = new FormData(editEdgeLabelForm); + // Get the edge + const edgeToUpdate = this.contextMenuStore?.get('edgeToUpdate'); + // @ts-ignore + edgeToUpdate.__data__.label = String(formData.get('edgeLabel')); + this.contextMenuStore?.delete('edgeToUpdate'); + // Reset the state of the form + editEdgeLabelForm.reset(); + + // Close the dialog + (( + this.shadowRoot?.querySelector('#edit-edge-label-dialog')! + )).close(); + + // Reload graph + this._refreshGraph(true); + }; + + const editEdgeLabelCancelButton: HTMLButtonElement = + this.shadowRoot?.querySelector('#edit-edge-label-cancel-button')!; + editEdgeLabelCancelButton.onclick = () => { + (( + this.shadowRoot?.querySelector('#edit-edge-label-dialog')! + )).close(); + // Reset the state of the form + editEdgeLabelForm.reset(); + }; } private _createPathwayGeneToNodeMap(): { [key: string]: GeneProteinNode[] } { @@ -1014,6 +1062,7 @@ export class BiowcPathwaygraph extends LitElement { } private _createD3GraphObject() { + console.log('Fn doing it.'); // Essentially, the d3Nodes and d3Links objects consist of all nodes and links // from the skeleton and the ptm objects. So in principle we could recreate them // from the concatenation of skeleton and ptm every time. @@ -3961,8 +4010,19 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { - console.log('TODO: Implement'); + execute: ctx => { + console.log('There is a bug in here...'); + this.contextMenuStore?.set('edgeToUpdate', ctx.target); + const editEdgeLabelInput: HTMLInputElement = + this.shadowRoot?.querySelector('#edit-edge-label-input')!; + editEdgeLabelInput.setRangeText(''); + // @ts-ignore + const currentLabel = ctx.target.__data__.label; + if (currentLabel) editEdgeLabelInput.setRangeText(currentLabel); + + const editEdgeLabelDialog: HTMLDialogElement = + this.shadowRoot?.querySelector('#edit-edge-label-dialog')!; + editEdgeLabelDialog.showModal(); }, }, { From 80b025674cd598142ee4a12520b4c8b611282a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Sun, 25 Feb 2024 17:54:26 +0100 Subject: [PATCH 09/39] Implement group adding --- demo/index.html | 4 +-- src/BiowcPathwaygraph.ts | 65 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/demo/index.html b/demo/index.html index 7432040..93697dc 100644 --- a/demo/index.html +++ b/demo/index.html @@ -20,8 +20,8 @@ render( html` node).nUp; existingNode.nDown = (node).nDown; existingNode.nNot = (node).nNot; + // Also update label, groupId, ... - it might have changed when in editing mode + existingNode.groupId = (node).groupId; } }); @@ -1823,6 +1826,7 @@ export class BiowcPathwaygraph extends LitElement { /* eslint-disable no-param-reassign */ .each(group => { group.polygon = <[number, number][]>polygonGenerator(group.nodeId); + if (!group.polygon) return; group.centroid = d3v6.polygonCentroid(group.polygon); group.minX = Math.min(...group.polygon.map(point => point[0])); group.maxX = Math.max(...group.polygon.map(point => point[0])); @@ -1852,7 +1856,7 @@ export class BiowcPathwaygraph extends LitElement { .select('#nodeG') .selectAll('.group-path') .attr('d', group => - group.centroid + group.centroid && group.polygon ? valueline( group.polygon!.map(point => [ point[0] - group.centroid![0], @@ -3159,6 +3163,18 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'>node).isHighlighted = true; + this._refreshGraph(true); + /* eslint-enable no-param-reassign */ + } + // Logic for adding an edge if (this.isAddingEdge) { /* eslint-disable no-param-reassign */ @@ -3886,7 +3902,12 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { - console.log('TODO: Implement'); + // eslint-disable-next-line no-alert + alert( + 'Click on all nodes that should be part of the group.\nThen, right-click to finish the group.' + ); + this.isCreatingGroup = true; + this.contextMenuStore!.set('groupMemberIds', []); }, }, { @@ -4012,6 +4033,7 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { console.log('There is a bug in here...'); + // TODO: Maybe try d3Links instead of skeleton, update label there. this.contextMenuStore?.set('edgeToUpdate', ctx.target); const editEdgeLabelInput: HTMLInputElement = this.shadowRoot?.querySelector('#edit-edge-label-input')!; @@ -4057,7 +4079,42 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'>container); // TODO: I probably need to do most of this only once, not every time the mode is switched. - if (this.applicationMode === 'viewing') { + if (this.isCreatingGroup) { + this.contextMenuCommands = [ + { + target: 'svg', + label: 'Finish Group', + execute: () => { + const groupId = `customGroup-${ + crypto.getRandomValues(new Uint32Array(1))[0] + }`; + this.graphdataSkeleton.nodes + .filter(node => + this.contextMenuStore!.get('groupMemberIds').includes( + node.nodeId + ) + ) + .forEach(node => { + // eslint-disable-next-line no-param-reassign + (node).groupId = groupId; + }); + // @ts-ignore + this.graphdataSkeleton.nodes.push({ + nodeId: groupId, + type: 'group', + }); + this.contextMenuStore!.delete('groupMemberIds'); + this.d3Nodes?.forEach(d => { + /* eslint-disable-next-line no-param-reassign */ + (d).isHighlighted = false; + }); + this.isCreatingGroup = false; + this.updated(new Map()); + this._refreshGraph(true); + }, + }, + ]; + } else if (this.applicationMode === 'viewing') { this._setUpViewingModeContextMenu(); } else { this._setUpEditingModeContextMenu(); From e76148408a29f79a660b13ee0a12e2be971967aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Sun, 25 Feb 2024 18:44:14 +0100 Subject: [PATCH 10/39] Fix changing of edge labels --- src/BiowcPathwaygraph.ts | 60 +++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index bbe812c..bb972c1 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -746,10 +746,17 @@ export class BiowcPathwaygraph extends LitElement { editEdgeLabelConfirmButton.onclick = () => { const formData: FormData = new FormData(editEdgeLabelForm); // Get the edge - const edgeToUpdate = this.contextMenuStore?.get('edgeToUpdate'); - // @ts-ignore - edgeToUpdate.__data__.label = String(formData.get('edgeLabel')); - this.contextMenuStore?.delete('edgeToUpdate'); + const edgeIdToUpdate = this.contextMenuStore?.get('edgeIdToUpdate'); + const edgeToUpdate = this.graphdataSkeleton.links.filter( + l => l.linkId === edgeIdToUpdate + )[0]; + edgeToUpdate.label = String(formData.get('edgeLabel')); + // Update visually. + this._getMainDiv() + .select(`#edgelabel-${edgeToUpdate.linkId}`) + .select('textPath') + .text(edgeToUpdate.label); + this.contextMenuStore?.delete('edgeIdToUpdate'); // Reset the state of the form editEdgeLabelForm.reset(); @@ -759,6 +766,7 @@ export class BiowcPathwaygraph extends LitElement { )).close(); // Reload graph + this.updated(new Map()); this._refreshGraph(true); }; @@ -1082,6 +1090,11 @@ export class BiowcPathwaygraph extends LitElement { d3NodesDict[node.nodeId] = node; } + const d3LinksDict: { [key: string]: PathwayGraphLinkD3 } = {}; + for (const link of this.d3Links) { + d3LinksDict[link.linkId] = link; + } + const d3LinkIds = new Set(this.d3Links!.map(link => link.linkId)); const graphdataNodeIds = new Set( this.graphdataSkeleton.nodes @@ -1119,6 +1132,10 @@ export class BiowcPathwaygraph extends LitElement { existingNode.nNot = (node).nNot; // Also update label, groupId, ... - it might have changed when in editing mode existingNode.groupId = (node).groupId; + existingNode.defaultName = (node).defaultName; + existingNode.label = (node).label; + existingNode.geneNames = (node).geneNames; + existingNode.uniprotAccs = (node).uniprotAccs; } }); @@ -1131,6 +1148,11 @@ export class BiowcPathwaygraph extends LitElement { .forEach(link => { if (!d3LinkIds.has(link.linkId!)) { this.d3Links!.push({ ...link } as PathwayGraphLinkD3); + } else { + // Update label and type, they might have changed in editing mode + const existingLink: PathwayGraphLinkD3 = d3LinksDict[link.linkId]; + existingLink.label = link.label; + existingLink.types = link.types; } }); @@ -1453,7 +1475,7 @@ export class BiowcPathwaygraph extends LitElement { .attr('class', 'edgepath') .attr('fill-opacity', 0) .attr('stroke-opacity', 0) - .attr('id', (d, i) => `edgepath-${i}`); + .attr('id', d => `edgepath-${d.linkId}`); // Add the actual edgelabels const edgelabels = linkG @@ -1463,29 +1485,26 @@ export class BiowcPathwaygraph extends LitElement { link => (link.sourceIsAnchor || (link.source)?.visible) && - (link.targetIsAnchor || - (link.target)?.visible) && - link.label && - link.label !== '' + (link.targetIsAnchor || (link.target)?.visible) ) ) .join('text') .attr('class', 'edgelabel') .attr('fill', 'var(--edge-label-color)') - .attr('id', (d, i) => `edgelabel-${i}`); + .attr('id', d => `edgelabel-${d.linkId}`); // Put the edgelabels onto the paths edgelabels // Filter for paths that do not have a label yet - .filter((edgepath, i) => + .filter(edgepath => this._getMainDiv() .select('#linkG') - .select(`#edgelabel-${i}`) + .select(`#edgelabel-${edgepath.linkId}`) .select('textPath') .empty() ) .append('textPath') - .attr('xlink:href', (d, i) => `#edgepath-${i}`) + .attr('xlink:href', d => `#edgepath-${d.linkId}`) .attr('startOffset', '50%') .text(link => link.label || ''); } @@ -3960,6 +3979,13 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { + // Get the node + const nodeToUpdate = this.graphdataSkeleton.nodes + // @ts-ignore + .filter(n => n.nodeId === ctx.target.__data__.nodeId)[0]; + nodeToUpdate.type = ctx.item.id; + // For now, we also directly update it here, since it doesn't change visually otherwise + // So the change in graphdataSkeleton is just for when we export it // @ts-ignore ctx.target.__data__.type = ctx.item.id; this._refreshGraph(true); @@ -4032,9 +4058,11 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { - console.log('There is a bug in here...'); - // TODO: Maybe try d3Links instead of skeleton, update label there. - this.contextMenuStore?.set('edgeToUpdate', ctx.target); + this.contextMenuStore?.set( + 'edgeIdToUpdate', + // @ts-ignore + ctx.target.__data__.linkId + ); const editEdgeLabelInput: HTMLInputElement = this.shadowRoot?.querySelector('#edit-edge-label-input')!; editEdgeLabelInput.setRangeText(''); From c0ebd78857760f6a76ac0969b6d6b48f8af177d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Sun, 25 Feb 2024 19:15:46 +0100 Subject: [PATCH 11/39] Fix change node type --- src/BiowcPathwaygraph.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index bb972c1..15345c5 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -1130,7 +1130,8 @@ export class BiowcPathwaygraph extends LitElement { existingNode.nUp = (node).nUp; existingNode.nDown = (node).nDown; existingNode.nNot = (node).nNot; - // Also update label, groupId, ... - it might have changed when in editing mode + // Also update everything else except for the coordinates - it might have changed when in editing mode + existingNode.type = (node).type; existingNode.groupId = (node).groupId; existingNode.defaultName = (node).defaultName; existingNode.label = (node).label; @@ -3986,8 +3987,7 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> ({ From 2fa1527593cd75527ffb16440f3b59e04f2a1051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Sun, 25 Feb 2024 20:34:11 +0100 Subject: [PATCH 12/39] Implement edit node names --- demo/index.html | 4 +- src/BiowcPathwaygraph.ts | 160 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 157 insertions(+), 7 deletions(-) diff --git a/demo/index.html b/demo/index.html index 93697dc..bc396c7 100644 --- a/demo/index.html +++ b/demo/index.html @@ -20,8 +20,8 @@ render( html` + + +
+
+ + +
+ + +
+
+ + +
+ +
+
@@ -738,6 +773,56 @@ export class BiowcPathwaygraph extends LitElement { this.isAddingEdge = false; }; + const editNodeForm: HTMLFormElement = + this.shadowRoot?.querySelector('#edit-node-form')!; + + const editNodeConfirmButton: HTMLButtonElement = + this.shadowRoot?.querySelector('#edit-node-confirm-button')!; + editNodeConfirmButton.onclick = () => { + const formData: FormData = new FormData(editNodeForm); + // Get the node + const nodeIdToUpdate = this.contextMenuStore?.get('nodeIdToUpdate'); + const nodeToUpdate = this.graphdataSkeleton.nodes.filter( + node => node.nodeId === nodeIdToUpdate + )[0] as GeneProteinNode; + if (nodeToUpdate.type === 'gene_protein') { + nodeToUpdate.geneNames = String( + formData.get('nodeAlternativeGeneNames')! + ).split(/[;,\n]/); + nodeToUpdate.uniprotAccs = String( + formData.get('nodeUniprotAccs')! + ).split(/[;,\n]/); + nodeToUpdate.geneNames.unshift( + String(formData.get('nodePrimaryName')!) + ); + nodeToUpdate.defaultName = String(formData.get('nodePrimaryName')!); + } else { + nodeToUpdate.label = String(formData.get('nodePrimaryName')!); + } + this.contextMenuStore?.delete('nodeIdToUpdate'); + editNodeForm.reset(); + // Close the dialog + (( + this.shadowRoot?.querySelector('#edit-node-dialog')! + )).close(); + + // Reload graph + this.updated(new Map()); + this._refreshGraph(true); + }; + + // TODO: Escape should do the same, also for all other cancel buttons + // Right now escape does not clear the forms + const editNodeCancelButton: HTMLButtonElement = + this.shadowRoot?.querySelector('#edit-node-cancel-button')!; + editNodeCancelButton.onclick = () => { + (( + this.shadowRoot?.querySelector('#edit-node-dialog')! + )).close(); + // Reset the state of the form + editNodeForm.reset(); + }; + const editEdgeLabelForm: HTMLFormElement = this.shadowRoot?.querySelector( '#edit-edge-label-form' )!; @@ -1137,6 +1222,8 @@ export class BiowcPathwaygraph extends LitElement { existingNode.label = (node).label; existingNode.geneNames = (node).geneNames; existingNode.uniprotAccs = (node).uniprotAccs; + [existingNode.currentDisplayedLabel] = + BiowcPathwaygraph._calcPossibleLabels(node); } }); @@ -4000,8 +4087,73 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { - console.log('TODO: Implement'); + execute: ctx => { + const editNodePrimaryNameLabel: HTMLLabelElement = + this.shadowRoot?.querySelector('#edit-node-primary-name-label')!; + const editNodeAlternativeGeneNames: HTMLDivElement = + this.shadowRoot?.querySelector( + '#edit-node-alternative-gene-names' + )!; + const editNodeAlternativeGeneNamesTextArea: HTMLTextAreaElement = + this.shadowRoot?.querySelector( + '#edit-node-alternative-gene-names-textarea' + )!; + const editNodeUniprots: HTMLDivElement = + this.shadowRoot?.querySelector('#edit-node-uniprot-accession')!; + const editNodeUniprotsTextArea: HTMLTextAreaElement = + this.shadowRoot?.querySelector( + '#edit-node-uniprot-accession-textarea' + )!; + // @ts-ignore + if (ctx.target.__data__.type === 'gene_protein') { + editNodePrimaryNameLabel.textContent = 'Primary Gene Name:'; + editNodeAlternativeGeneNames.style.display = 'block'; + editNodeUniprots.style.display = 'block'; + // @ts-ignore + editNodeAlternativeGeneNamesTextArea.setRangeText( + // @ts-ignore + ctx.target.__data__.geneNames + ?.filter( + (name: String) => + ![ + // @ts-ignore + ctx.target.__data__.label, + // @ts-ignore + ctx.target.__data__.currentDisplayedLabel, + ].includes(name) + ) + .join('\n') || '' + ); + // @ts-ignore + editNodeUniprotsTextArea.setRangeText( + // @ts-ignore + ctx.target.__data__.uniprotAccs?.join('\n') || '' + ); + } else { + editNodePrimaryNameLabel.textContent = 'Name:'; + editNodeAlternativeGeneNames.style.display = 'none'; + editNodeUniprots.style.display = 'none'; + } + + this.contextMenuStore?.set( + 'nodeIdToUpdate', + // @ts-ignore + ctx.target.__data__.nodeId + ); + + const editNodePrimaryNameInput: HTMLInputElement = + this.shadowRoot?.querySelector('#edit-node-primary-name-input')!; + editNodePrimaryNameInput.setRangeText( + // @ts-ignore + ctx.target.__data__.label || + // @ts-ignore + ctx.target.__data__.currentDisplayedLabel || + '' + ); + + const editNodeDialog: HTMLDialogElement = + this.shadowRoot?.querySelector('#edit-node-dialog')!; + editNodeDialog.showModal(); }, }, { @@ -4065,10 +4217,8 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> Date: Fri, 1 Mar 2024 17:17:06 +0100 Subject: [PATCH 13/39] Add export to json --- src/BiowcPathwaygraph.ts | 32 ++++++++++++++++++++++++++++++++ stories/index.stories.ts | 10 +++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 5834846..3e5de3d 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -3717,6 +3717,38 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'>this.d3Nodes) + ?.filter(d3node => !d3node.nodeId.includes('ptm')) + .map(d3node => ({ + nodeId: d3node.nodeId, + geneNames: d3node.geneNames, + type: d3node.type, + x: Number(d3node.x.toFixed(1)), + y: Number(d3node.y.toFixed(1)), + uniprotAccs: d3node.uniprotAccs, + })), + links: this.d3Links + ?.filter(d3link => !d3link.linkId.includes('ptm')) + .map(d3link => { + const { linkId, sourceId, targetId, types } = d3link; + return { linkId, sourceId, targetId, types }; + }), + }, + null, + 2 + ); + const blob = new Blob([currentSkeletonJSON], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.download = 'pathway_diagram.json'; + a.href = url; + a.click(); + } + public selectNodesDownstreamOfSelection() { this.d3Nodes!.filter(node => node.selected).forEach(node => this._selectDownstreamNodesWorker(node) diff --git a/stories/index.stories.ts b/stories/index.stories.ts index 588a319..9a26467 100644 --- a/stories/index.stories.ts +++ b/stories/index.stories.ts @@ -312,15 +312,19 @@ const EditingModeTemplate: Story = (args: ArgTypes) => html` name="modeswitch" id="viewing" onclick="document.getElementById('pathwaygraph').switchApplicationMode(this.id);" - checked /> - + Date: Sun, 3 Mar 2024 00:26:12 +0100 Subject: [PATCH 14/39] Implement remove group and refactor remove node so code is reused --- demo/index.html | 4 +- src/BiowcPathwaygraph.ts | 80 +++++++++++++++++++++++++++++----------- 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/demo/index.html b/demo/index.html index bc396c7..93697dc 100644 --- a/demo/index.html +++ b/demo/index.html @@ -20,8 +20,8 @@ render( html` { // @ts-ignore const nodeIdToRemove = ctx.target.__data__.nodeId; - this.graphdataSkeleton.nodes = this.graphdataSkeleton.nodes.filter( - node => node.nodeId !== nodeIdToRemove - ); - this.graphdataSkeleton.links = this.graphdataSkeleton.links.filter( - link => - link.sourceId !== nodeIdToRemove && - link.targetId !== nodeIdToRemove - ); - // Remove all higher-order links that just lost their endpoint - const nodeAndLinkIds = this.graphdataSkeleton.links - .map(link => link.linkId) - .concat(this.graphdataSkeleton.nodes.map(node => node.nodeId)); - this.graphdataSkeleton.links = this.graphdataSkeleton.links.filter( - link => - nodeAndLinkIds.includes(link.sourceId) && - nodeAndLinkIds.includes(link.targetId) - ); - // Refresh - this.updated(new Map()); // TODO: Forcing 'updated' with an empty map feels hacky... + this._removeNode(nodeIdToRemove); }, }, { target: 'path.group-path', label: 'Remove Group', - execute: () => { - console.log('TODO: Implement'); + execute: ctx => { + // @ts-ignore + const groupIdToRemove = ctx.target.__data__.nodeId; + this._removeGroup(groupIdToRemove); }, }, { @@ -4278,6 +4262,39 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> node.nodeId !== nodeIdToRemove + ); + this.graphdataSkeleton.links = this.graphdataSkeleton.links.filter( + link => + link.sourceId !== nodeIdToRemove && link.targetId !== nodeIdToRemove + ); + // Remove all higher-order links that just lost their endpoint + const nodeAndLinkIds = this.graphdataSkeleton.links + .map(link => link.linkId) + .concat(this.graphdataSkeleton.nodes.map(node => node.nodeId)); + this.graphdataSkeleton.links = this.graphdataSkeleton.links.filter( + link => + nodeAndLinkIds.includes(link.sourceId) && + nodeAndLinkIds.includes(link.targetId) + ); + // Refresh + this.updated(new Map()); + } + + private _removeGroup(groupIdToRemove: string) { + // Unlink every member of the group + this.graphdataSkeleton.nodes.forEach(node => { + if ((node).groupId === groupIdToRemove) { + // eslint-disable-next-line no-param-reassign + (node).groupId = undefined; + } + }); + + this._removeNode(groupIdToRemove); + } + private _initContextMenu() { if (this.contextMenu) this.contextMenu.disconnect(); @@ -4314,10 +4331,31 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { /* eslint-disable-next-line no-param-reassign */ (d).isHighlighted = false; }); + // Check if any groups have become empty by the creation of the new group + // It can happen if the new group "steals" all remaining members + // In that case remove the group, there might be dangling edges otherwise + const allGroupIDs = this.graphdataSkeleton.nodes + .filter(node => node.type === 'group') + .map(node => node.nodeId); + + allGroupIDs + .filter( + currentGroupId => + // If no node has the group as an id... + !this.graphdataSkeleton.nodes.some( + node => (node).groupId === currentGroupId + ) + ) + .forEach(currentGroupId => { + // ...get rid of the group + this._removeGroup(currentGroupId); + }); + this.isCreatingGroup = false; this.updated(new Map()); this._refreshGraph(true); From a0e0631aa194d06971d788e8785fd7d25c21af87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Sun, 3 Mar 2024 00:39:11 +0100 Subject: [PATCH 15/39] Allow addition of anchor links --- src/BiowcPathwaygraph.ts | 99 +++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 43 deletions(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 2604625..28b89e5 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -3284,53 +3284,65 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'>node).isHighlighted = true; + this._addEdgeFromOrTo(node.nodeId); + } + }); + } - (node).isHighlighted = true; - this._refreshGraph(true); - } else { - this.contextMenuStore!.set('newEdgeTarget', node.nodeId); + private _enableLinkSelection() { + this._getMainDiv() + .select('#linkG') + .selectAll('g') + .on('click', (e, link) => { + // Links can - for now - only be selected when in edge-adding mode + if (this.isAddingEdge) { + this._addEdgeFromOrTo(link.linkId); + } + }); + } - (node).isHighlighted = true; - this._refreshGraph(true); - } + private _addEdgeFromOrTo(sourceOrTargetId: string) { + if (!this.contextMenuStore!.has('newEdgeSource')) { + this.contextMenuStore!.set('newEdgeSource', sourceOrTargetId); + this._refreshGraph(true); + } else { + this.contextMenuStore!.set('newEdgeTarget', sourceOrTargetId); + this._refreshGraph(true); + } + if ( + this.contextMenuStore!.has('newEdgeSource') && + this.contextMenuStore!.has('newEdgeTarget') + ) { + const sourceId = this.contextMenuStore!.get('newEdgeSource'); + const targetId = this.contextMenuStore!.get('newEdgeTarget'); + + const newEdge = { + linkId: `customRelation-${ + crypto.getRandomValues(new Uint32Array(1))[0] + }`, + sourceId, + targetId, + types: [this.contextMenuStore!.get('newEdgeType')], + label: this.contextMenuStore!.get('newEdgeLabel'), + }; + this.graphdataSkeleton.links.push(newEdge); - if ( - this.contextMenuStore!.has('newEdgeSource') && - this.contextMenuStore!.has('newEdgeTarget') - ) { - const sourceId = this.contextMenuStore!.get('newEdgeSource'); - const targetId = this.contextMenuStore!.get('newEdgeTarget'); - - const newEdge = { - linkId: `customRelation-${ - crypto.getRandomValues(new Uint32Array(1))[0] - }`, - sourceId, - targetId, - types: [this.contextMenuStore!.get('newEdgeType')], - label: this.contextMenuStore!.get('newEdgeLabel'), - }; - this.graphdataSkeleton.links.push(newEdge); - - this.d3Nodes!.filter(nd => - [sourceId, targetId].includes(nd.nodeId) - ).forEach(nd => { - (nd).isHighlighted = false; - }); - this.contextMenuStore!.delete('newEdgeSource'); - this.contextMenuStore!.delete('newEdgeTarget'); - this.contextMenuStore!.delete('newEdgeType'); - this.contextMenuStore!.delete('newEdgeLabel'); - this.isAddingEdge = false; - // Refresh - this.updated(new Map()); // TODO: Forcing 'updated' with an empty map feels hacky... - } - /* eslint-enable no-param-reassign */ - } + this.d3Nodes!.filter(nd => + [sourceId, targetId].includes(nd.nodeId) + ).forEach(nd => { + // eslint-disable-next-line no-param-reassign + (nd).isHighlighted = false; }); + this.contextMenuStore!.delete('newEdgeSource'); + this.contextMenuStore!.delete('newEdgeTarget'); + this.contextMenuStore!.delete('newEdgeType'); + this.contextMenuStore!.delete('newEdgeLabel'); + this.isAddingEdge = false; + // Refresh + this.updated(new Map()); // TODO: Forcing 'updated' with an empty map feels hacky... + } } private _onSelectedNodesChanged() { @@ -3384,6 +3396,7 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> Date: Mon, 4 Mar 2024 08:30:47 +0100 Subject: [PATCH 16/39] Add notes document to git --- custom_pathway_notes.txt | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 custom_pathway_notes.txt diff --git a/custom_pathway_notes.txt b/custom_pathway_notes.txt new file mode 100644 index 0000000..46f9492 --- /dev/null +++ b/custom_pathway_notes.txt @@ -0,0 +1,55 @@ +Done: +- mode: viewing/editing + - Create storybook with switch, same as for the hue + upload and download button + - Clear PTM & FP Inputlist when switching to edit mode +- New Context Menu: + - Add Node + - Popup Form where you can enter: + - Type of Node (options: Gene/Protein, Metabolite/Compound, Pathway) + - For Gene/Protein: + - List of Names (first one will implicitly become default one) + - List of Uniprot Accession Numbers + -> Create node at position of right-click + - Just add to what is parsed from the JSON + - Add Edge + - Await two clicks 'After clicking confirm, please click on the source, then the target node.' + - Increase Border of node after click to feedback selection + - Popup Form where you can choose edge type and optionally set a label (e.g. '+p') +- Export: Create JSON using current positions + - Node: Add Edge to... + - Node: Add Edge from... + - Edge: Change Edge Type + - Edge: Delete Edge + - Node: Delete Node -> Need to delete all adjacent edges too -> AND HIGHER ORDER LINKS! + - Create Group + - Edge: Change Label + - Node: Change Node Type + - Node: Change Name(s) + - Group: Delete Group -> Merge code with Delete Node, it is similar + - Group: When addition of group clears another group, delete orphaned edges (you can probably reuse code from delete group, since you're implicitly deleting a group here) + - Edge: Add Anchor Edge + + +Todo: + + - Edge: Change Type: Snaps back when something else changes! + - When adding edge, highlight selected groups and edges similarly to how nodes are highlighted + +- In PTMNavigator: + - User should be able to enter editing mode when no pathway is loaded (initialize with empty skeleton) + - User should be able to give pathway a name + - "Save" button, when leaving edit mode ask to save/export +TODO: Add Enrichment Result Types to some table (CV maybe) +TODO: Then, implement function in lib that converts enrichment type into enrichment type id + +Update or overwrite a Pathway in the DB (Upsert?) Must work by ID somehow... user needs to be able to edit their Previously desogned pws + + + +Check if works: +ci/sql-hooks/post_import.sql +ci/sql-hooks/grant_privileges.sql +hana-packages/proteomicsdb/logic/secure/lib/libStoreCustomPathway.xsjslib +hana-packages/proteomicsdb/logic/secure/storeCustomPathway.xsjs + + From ed3551353bbc14cc80a684d49121c6d722f7cca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Tue, 5 Mar 2024 08:46:30 +0100 Subject: [PATCH 17/39] Implement highlight of edges and groups during edge adding --- custom_pathway_notes.txt | 5 +++-- src/BiowcPathwaygraph.ts | 41 ++++++++++++++++++++++++----------- src/biowc-pathwaygraph.css.ts | 9 ++++++++ 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/custom_pathway_notes.txt b/custom_pathway_notes.txt index 46f9492..597975a 100644 --- a/custom_pathway_notes.txt +++ b/custom_pathway_notes.txt @@ -28,12 +28,13 @@ Done: - Group: Delete Group -> Merge code with Delete Node, it is similar - Group: When addition of group clears another group, delete orphaned edges (you can probably reuse code from delete group, since you're implicitly deleting a group here) - Edge: Add Anchor Edge + - When adding edge, highlight selected groups and edges similarly to how nodes are highlighted Todo: - Edge: Change Type: Snaps back when something else changes! - - When adding edge, highlight selected groups and edges similarly to how nodes are highlighted + - Indirect effect is not styled anymore - In PTMNavigator: - User should be able to enter editing mode when no pathway is loaded (initialize with empty skeleton) @@ -43,7 +44,7 @@ TODO: Add Enrichment Result Types to some table (CV maybe) TODO: Then, implement function in lib that converts enrichment type into enrichment type id Update or overwrite a Pathway in the DB (Upsert?) Must work by ID somehow... user needs to be able to edit their Previously desogned pws - +Inconsistent use of Link and Edge throughout the code Check if works: diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 28b89e5..52fabca 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -165,13 +165,13 @@ interface PathwayGraphNodeD3 extends PathwayGraphNode { leftX?: number; rightX?: number; currentDisplayedLabel?: string; + isHighlighted?: boolean; } interface GeneProteinNodeD3 extends GeneProteinNode, PathwayGraphNodeD3 { // Interfaces are hoisted, so we can reference GroupNodeD3 before defining it // eslint-disable-next-line no-use-before-define groupNode?: GroupNodeD3; - isHighlighted: boolean; } interface GroupNodeD3 extends PathwayGraphNodeD3 { @@ -205,6 +205,7 @@ interface PathwayGraphLinkD3 extends PathwayGraphLink { targetX?: number; sourceY?: number; targetY?: number; + isHighlighted?: boolean; } export class BiowcPathwaygraph extends LitElement { @@ -767,7 +768,7 @@ export class BiowcPathwaygraph extends LitElement { // Remove highlighting of node, if present this.d3Nodes?.forEach(d => { /* eslint-disable-next-line no-param-reassign */ - (d).isHighlighted = false; + d.isHighlighted = false; }); this._refreshGraph(true); this.isAddingEdge = false; @@ -1412,13 +1413,13 @@ export class BiowcPathwaygraph extends LitElement { 'class', d => `node ${d.type} ${BiowcPathwaygraph._computeRegulationClass(d)} ${ - (d).isHighlighted ? 'highlight' : '' + d.isHighlighted ? 'highlight' : '' } ` ) .attr('id', d => `node-${d.nodeId}`); // Draw each node as a rectangle - except for groups - nodesSvg.selectAll('.node-rect').remove(); // TODO: Check if we actually need to do this + nodesSvg.selectAll('.node-rect').remove(); nodesSvg .filter(d => d.type !== 'group') .append('rect') @@ -1427,7 +1428,7 @@ export class BiowcPathwaygraph extends LitElement { d => `node-rect ${d.type} ${BiowcPathwaygraph._computeRegulationClass( d - )} ${(d).isHighlighted ? 'highlight' : ''}` + )} ${d.isHighlighted ? 'highlight' : ''}` ) .attr('rx', NODE_HEIGHT) .attr('ry', NODE_HEIGHT) @@ -1493,6 +1494,7 @@ export class BiowcPathwaygraph extends LitElement { // Initialize paths for the group nodes // The actual polygons are drawn in the 'tick' callback of addAnimation + nodesSvg.selectAll('.group-path').remove(); nodesSvg .filter( d => @@ -1504,7 +1506,7 @@ export class BiowcPathwaygraph extends LitElement { .empty() ) .append('path') - .attr('class', 'group-path'); + .attr('class', d => `group-path ${d.isHighlighted ? 'highlight' : ''}`); // Draw links as lines with appropriate arrowheads const linksSvg = linkG @@ -1520,10 +1522,13 @@ export class BiowcPathwaygraph extends LitElement { .join('g') .attr('class', d => `linkgroup ${d.types.join(' ')}`); - linksSvg.selectAll('.link').remove(); // TODO: Check if we actually need to do this + linksSvg.selectAll('.link').remove(); linksSvg .append('line') - .attr('class', d => `link ${d.types.join(' ')}`) + .attr( + 'class', + d => `link ${d.types.join(' ')} ${d.isHighlighted ? 'highlight' : ''}` + ) .attr('marker-end', d => { if (d.types.includes('inhibition')) { return 'url(#inhibitionMarker)'; @@ -1557,7 +1562,7 @@ export class BiowcPathwaygraph extends LitElement { .join('g') .attr('class', 'linklabelpathgroup'); - linkLabelPaths.selectAll('.edgepath').remove(); // TODO: Check if we actually need to do this + linkLabelPaths.selectAll('.edgepath').remove(); linkLabelPaths .append('path') .attr('class', 'edgepath') @@ -3277,7 +3282,7 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'>node).isHighlighted = true; + node.isHighlighted = true; this._refreshGraph(true); /* eslint-enable no-param-reassign */ } @@ -3285,7 +3290,7 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'>node).isHighlighted = true; + node.isHighlighted = true; this._addEdgeFromOrTo(node.nodeId); } }); @@ -3298,6 +3303,8 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { // Links can - for now - only be selected when in edge-adding mode if (this.isAddingEdge) { + // eslint-disable-next-line no-param-reassign + link.isHighlighted = true; this._addEdgeFromOrTo(link.linkId); } }); @@ -3333,8 +3340,16 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { // eslint-disable-next-line no-param-reassign - (nd).isHighlighted = false; + nd.isHighlighted = false; }); + + this.d3Links!.filter(lk => + [sourceId, targetId].includes(lk.linkId) + ).forEach(lk => { + // eslint-disable-next-line no-param-reassign + lk.isHighlighted = false; + }); + this.contextMenuStore!.delete('newEdgeSource'); this.contextMenuStore!.delete('newEdgeTarget'); this.contextMenuStore!.delete('newEdgeType'); @@ -4347,7 +4362,7 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { /* eslint-disable-next-line no-param-reassign */ - (d).isHighlighted = false; + d.isHighlighted = false; }); // Check if any groups have become empty by the creation of the new group // It can happen if the new group "steals" all remaining members diff --git a/src/biowc-pathwaygraph.css.ts b/src/biowc-pathwaygraph.css.ts index cda3d3d..c384b4e 100644 --- a/src/biowc-pathwaygraph.css.ts +++ b/src/biowc-pathwaygraph.css.ts @@ -11,6 +11,7 @@ export default css` --group-fill-color: #6c7a74; --group-stroke-color: #3b423f; --link-color: #999999; + --link-color-highlight: #3d3d3d; --edge-label-color: #4e4e4e; --legend-frame-color: #a9a9a9; --font-stack: 'Roboto Light', 'Helvetica Neue', 'Verdana', sans-serif; @@ -31,6 +32,10 @@ export default css` stroke-width: 3; } + .link.highlight { + stroke: var(--link-color-highlight); + } + .link.ptmlink { visibility: hidden; } @@ -190,6 +195,10 @@ export default css` stroke: var(--group-stroke-color); } + .group-path.highlight { + stroke-width: 3px; + } + #pathwayContainer { position: relative; } From fac26f4e042392c14943b284e1b508d535ec7abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Tue, 5 Mar 2024 08:52:31 +0100 Subject: [PATCH 18/39] Fix bug: Edge types snap back after change when smth else changes --- custom_pathway_notes.txt | 2 +- src/BiowcPathwaygraph.ts | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/custom_pathway_notes.txt b/custom_pathway_notes.txt index 597975a..65e348a 100644 --- a/custom_pathway_notes.txt +++ b/custom_pathway_notes.txt @@ -33,7 +33,7 @@ Done: Todo: - - Edge: Change Type: Snaps back when something else changes! + - Cancel Button does not reset edit node type (and maybe other menues?) - Indirect effect is not styled anymore - In PTMNavigator: diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 52fabca..0e3f48a 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -4132,10 +4132,7 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> n.nodeId === ctx.target.__data__.nodeId)[0]; nodeToUpdate.type = ctx.item.id; - // For now, we also directly update it here, since it doesn't change visually otherwise - // So the change in graphdataSkeleton is just for when we export it this.updated(new Map()); - this._refreshGraph(true); }, children: BiowcPathwaygraph.nodeTypes.map(nodeType => ({ type: 'radio', @@ -4238,9 +4235,11 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { - // @ts-ignore - ctx.target.__data__.types = [ctx.item.id]; - this._refreshGraph(true); + const edgeToUpdate = this.graphdataSkeleton.links + // @ts-ignore + .filter(e => e.linkId === ctx.target.__data__.linkId)[0]; + edgeToUpdate.types = [ctx.item.id]; + this.updated(new Map()); }, children: BiowcPathwaygraph.edgeTypes.map(edgeType => ({ type: 'radio', From da46685a905b067998068251d4cf62e1dcbf04ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Wed, 6 Mar 2024 10:31:43 +0100 Subject: [PATCH 19/39] nodeId -> id --- src/BiowcPathwaygraph.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 0e3f48a..efc2e28 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -3752,7 +3752,7 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'>this.d3Nodes) ?.filter(d3node => !d3node.nodeId.includes('ptm')) .map(d3node => ({ - nodeId: d3node.nodeId, + id: d3node.nodeId, geneNames: d3node.geneNames, type: d3node.type, x: Number(d3node.x.toFixed(1)), From 051a956477686e4efc2989fa585e47369f45ac82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Wed, 6 Mar 2024 15:31:35 +0100 Subject: [PATCH 20/39] Figure out positioning of tooltip and new nodes --- custom_pathway_notes.txt | 15 ++------------- src/BiowcPathwaygraph.ts | 24 +++++++++++++----------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/custom_pathway_notes.txt b/custom_pathway_notes.txt index 65e348a..eb54f24 100644 --- a/custom_pathway_notes.txt +++ b/custom_pathway_notes.txt @@ -34,23 +34,12 @@ Done: Todo: - Cancel Button does not reset edit node type (and maybe other menues?) - - Indirect effect is not styled anymore - In PTMNavigator: - User should be able to enter editing mode when no pathway is loaded (initialize with empty skeleton) - User should be able to give pathway a name - "Save" button, when leaving edit mode ask to save/export -TODO: Add Enrichment Result Types to some table (CV maybe) -TODO: Then, implement function in lib that converts enrichment type into enrichment type id -Update or overwrite a Pathway in the DB (Upsert?) Must work by ID somehow... user needs to be able to edit their Previously desogned pws +Update or overwrite a Pathway in the DB (Upsert?) Must work by ID somehow... user needs to be able to edit their Previously designed pws + -> If pathway already exists, it has an id. So send this alongside the JSON. If it sent, do not query for new ID, and update instead of inserting. Inconsistent use of Link and Edge throughout the code - - -Check if works: -ci/sql-hooks/post_import.sql -ci/sql-hooks/grant_privileges.sql -hana-packages/proteomicsdb/logic/secure/lib/libStoreCustomPathway.xsjslib -hana-packages/proteomicsdb/logic/secure/storeCustomPathway.xsjs - - diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index efc2e28..710ec86 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -214,12 +214,6 @@ export class BiowcPathwaygraph extends LitElement { @property({ attribute: false }) graphWidth: number = document.body.clientWidth; - @property({ attribute: false }) - tooltipVerticalOffset: number = 0; - - @property({ attribute: false }) - tooltipHorizontalOffset: number = 0; - @property({ attribute: false }) graphdataSkeleton!: { nodes: PathwayGraphNode[]; @@ -597,7 +591,7 @@ export class BiowcPathwaygraph extends LitElement { this._createD3GraphObject(); this._calculateHueRange(); - this._renderLegend(); + if (this.applicationMode === 'viewing') this._renderLegend(); this._renderGraph(); this._initContextMenu(); this._updateRangeSliderVisibility(); @@ -661,13 +655,21 @@ export class BiowcPathwaygraph extends LitElement { /translate\((-?[\d|.]+),(-?[\d|.]+)\) scale\((-?[\d|.]+)\)/ )!; + const mainDivBoundingClientRect = this.shadowRoot + ?.querySelector('#pathwayContainer')! + .getBoundingClientRect()!; + // Create a node // @ts-ignore const newNode: GeneProteinNodeD3 = { nodeId: `customNode-${crypto.getRandomValues(new Uint32Array(1))[0]}`, type: nodeType, - x: (nodeAddPoint.x - Number(translateX)) / Number(scale), - y: (nodeAddPoint.y - Number(translateY)) / Number(scale), + x: + (nodeAddPoint.x - Number(translateX) - mainDivBoundingClientRect.x) / + Number(scale), + y: + (nodeAddPoint.y - Number(translateY) - mainDivBoundingClientRect.y) / + Number(scale), }; if (nodeType === 'gene_protein') { @@ -2090,8 +2092,8 @@ export class BiowcPathwaygraph extends LitElement { const mousemove = (e: MouseEvent) => { tooltip // The offset is trial and error, I could not figure this out programmatically - .style('top', `${e.pageY + this.tooltipVerticalOffset}px`) - .style('left', `${e.pageX + this.tooltipHorizontalOffset + 15}px`); + .style('top', `${e.offsetY}px`) + .style('left', `${e.offsetX + 15}px`); }; const mouseleave = () => { From a6e2fdcbefbc5d1049301c03171e24b0e83cb48c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Sun, 10 Mar 2024 18:55:02 +0100 Subject: [PATCH 21/39] Changes after adding 'save' button to PTMNavigator --- custom_pathway_notes.txt | 21 +++++++++++++-------- src/BiowcPathwaygraph.ts | 22 ++++++++++++---------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/custom_pathway_notes.txt b/custom_pathway_notes.txt index eb54f24..4ce5de3 100644 --- a/custom_pathway_notes.txt +++ b/custom_pathway_notes.txt @@ -32,14 +32,19 @@ Done: Todo: + BiowcPathwaygraph: + - Cancel Button does not reset edit node type (and maybe other menues?) - - Cancel Button does not reset edit node type (and maybe other menues?) -- In PTMNavigator: - - User should be able to enter editing mode when no pathway is loaded (initialize with empty skeleton) - - User should be able to give pathway a name - - "Save" button, when leaving edit mode ask to save/export -Update or overwrite a Pathway in the DB (Upsert?) Must work by ID somehow... user needs to be able to edit their Previously designed pws - -> If pathway already exists, it has an id. So send this alongside the JSON. If it sent, do not query for new ID, and update instead of inserting. -Inconsistent use of Link and Edge throughout the code + In PTMNavigator: + - Canvas could look different when in edit mode, e.g. light grey + - User should be able to enter editing mode when no pathway is loaded (initialize with empty skeleton) + - User should be able to give pathway a name + - Whenever an action leads to leaving edit mode ask to save + - Clear Canvas should set skeleton to empty, not undefined + - Also switching to edit while not having loaded a pathway should set skeleton to empty + - When selecting existing pathway for edit (not for view!!), ask if user wants to work from this or from copy + - append "(COPY)" to name + + diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 710ec86..3e2f081 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -3748,7 +3748,7 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'>this.d3Nodes) @@ -3760,23 +3760,25 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> !d3link.linkId.includes('ptm')) .map(d3link => { - const { linkId, sourceId, targetId, types } = d3link; - return { linkId, sourceId, targetId, types }; + const { linkId, sourceId, targetId, types, label } = d3link; + return { id: linkId, sourceId, targetId, types, label }; }), }, null, - 2 + 0 ); - const blob = new Blob([currentSkeletonJSON], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.download = 'pathway_diagram.json'; - a.href = url; - a.click(); + + // const blob = new Blob([currentSkeletonJSON], { type: 'text/plain' }); + // const url = URL.createObjectURL(blob); + // const a = document.createElement('a'); + // a.download = 'pathway_diagram.json'; + // a.href = url; + // a.click(); } public selectNodesDownstreamOfSelection() { From a2ef9e7f0bca0e0db5580122b68ad3d9466223ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Wed, 13 Mar 2024 17:52:11 +0100 Subject: [PATCH 22/39] Edit notes --- custom_pathway_notes.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_pathway_notes.txt b/custom_pathway_notes.txt index 4ce5de3..ce2bcbd 100644 --- a/custom_pathway_notes.txt +++ b/custom_pathway_notes.txt @@ -46,5 +46,9 @@ Todo: - Also switching to edit while not having loaded a pathway should set skeleton to empty - When selecting existing pathway for edit (not for view!!), ask if user wants to work from this or from copy - append "(COPY)" to name - + - Switch back to Viewing mode should clear the skeleton + - Switch to Edit while canonical is selected should start editing this canonical pathway. + - Switch to Edit while custom selected should ask if this or copy + - In Edit Mode, download button should also allow download JSON of skeleton + - In Edit Mode, add upload button next to download button From 9226583bbcf971176c377729c8db36ff92bc3d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Mon, 18 Mar 2024 14:21:13 +0100 Subject: [PATCH 23/39] Remove commented code (was migrated to PTMNavigator now) --- src/BiowcPathwaygraph.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 3e2f081..4ec1909 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -3772,13 +3772,6 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> Date: Mon, 18 Mar 2024 14:41:29 +0100 Subject: [PATCH 24/39] Remove legend when changing to edit mode --- src/BiowcPathwaygraph.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 4ec1909..6c7b929 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -4568,6 +4568,11 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'>('#pathwayLegend') + .selectAll('*') + .remove(); } } } From 1bead9cab0de78528388e1a6f14b470c7035523d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Mon, 18 Mar 2024 15:11:02 +0100 Subject: [PATCH 25/39] Esc keys get same behaviour as 'cancel' button by implementing a 'cancel' event handler --- src/BiowcPathwaygraph.ts | 80 ++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 6c7b929..2a7824e 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -633,6 +633,10 @@ export class BiowcPathwaygraph extends LitElement { this.shadowRoot?.querySelector('#add-node-form')!; const addNodeConfirmButton: HTMLButtonElement = this.shadowRoot?.querySelector('#add-node-confirm-button')!; + + const addNodeDialog: HTMLDialogElement = + this.shadowRoot?.querySelector('#add-node-dialog')!; + addNodeConfirmButton.onclick = e => { const formData: FormData = new FormData(addNodeForm); @@ -694,22 +698,23 @@ export class BiowcPathwaygraph extends LitElement { // Simulate a change on the select so it snaps back into default state addNodeTypeSelect.dispatchEvent(new Event('change')); // Close the dialog - (( - this.shadowRoot?.querySelector('#add-node-dialog')! - )).close(); + addNodeDialog.close(); // Reset the state of the form addNodeForm.reset(); }; - const addNodeCancelButton: HTMLButtonElement = - this.shadowRoot?.querySelector('#add-node-cancel-button')!; - addNodeCancelButton.onclick = () => { - (( - this.shadowRoot?.querySelector('#add-node-dialog')! - )).close(); + + addNodeDialog.addEventListener('cancel', () => { + addNodeDialog.close(); // Reset the state of the form addNodeForm.reset(); // Simulate a change on the select so it snaps back into default state addNodeTypeSelect.dispatchEvent(new Event('change')); + }); + + const addNodeCancelButton: HTMLButtonElement = + this.shadowRoot?.querySelector('#add-node-cancel-button')!; + addNodeCancelButton.onclick = () => { + addNodeDialog.dispatchEvent(new Event('cancel')); }; const addEdgeTypeSelect: HTMLSelectElement = this.shadowRoot?.querySelector( @@ -726,6 +731,10 @@ export class BiowcPathwaygraph extends LitElement { this.shadowRoot?.querySelector('#add-edge-form')!; const addEdgeConfirmButton: HTMLButtonElement = this.shadowRoot?.querySelector('#add-edge-confirm-button')!; + + const addEdgeDialog: HTMLDialogElement = + this.shadowRoot?.querySelector('#add-edge-dialog')!; + addEdgeConfirmButton.onclick = e => { const formData: FormData = new FormData(addEdgeForm); @@ -752,17 +761,11 @@ export class BiowcPathwaygraph extends LitElement { // Simulate a change on the select so it snaps back into default state addEdgeTypeSelect.dispatchEvent(new Event('change')); // Close the dialog - (( - this.shadowRoot?.querySelector('#add-edge-dialog')! - )).close(); + addEdgeDialog.close(); }; - const addEdgeCancelButton: HTMLButtonElement = - this.shadowRoot?.querySelector('#add-edge-cancel-button')!; - addEdgeCancelButton.onclick = () => { - (( - this.shadowRoot?.querySelector('#add-edge-dialog')! - )).close(); + addEdgeDialog.addEventListener('cancel', () => { + addEdgeDialog.close(); // Reset the state of the form addEdgeForm.reset(); // Simulate a change on the select so it snaps back into default state @@ -774,6 +777,12 @@ export class BiowcPathwaygraph extends LitElement { }); this._refreshGraph(true); this.isAddingEdge = false; + }); + + const addEdgeCancelButton: HTMLButtonElement = + this.shadowRoot?.querySelector('#add-edge-cancel-button')!; + addEdgeCancelButton.onclick = () => { + addEdgeDialog.dispatchEvent(new Event('cancel')); }; const editNodeForm: HTMLFormElement = @@ -814,18 +823,25 @@ export class BiowcPathwaygraph extends LitElement { this._refreshGraph(true); }; - // TODO: Escape should do the same, also for all other cancel buttons + const editNodeDialog: HTMLDialogElement = + this.shadowRoot?.querySelector('#edit-node-dialog')!; + + editNodeDialog.addEventListener('cancel', () => { + editNodeDialog.close(); + // Reset the state of the form + editNodeForm.reset(); + }); + // Right now escape does not clear the forms const editNodeCancelButton: HTMLButtonElement = this.shadowRoot?.querySelector('#edit-node-cancel-button')!; editNodeCancelButton.onclick = () => { - (( - this.shadowRoot?.querySelector('#edit-node-dialog')! - )).close(); - // Reset the state of the form - editNodeForm.reset(); + editNodeDialog.dispatchEvent(new Event('cancel')); }; + const editEdgeLabelDialog: HTMLDialogElement = + this.shadowRoot?.querySelector('#edit-edge-label-dialog')!; + const editEdgeLabelForm: HTMLFormElement = this.shadowRoot?.querySelector( '#edit-edge-label-form' )!; @@ -849,23 +865,23 @@ export class BiowcPathwaygraph extends LitElement { editEdgeLabelForm.reset(); // Close the dialog - (( - this.shadowRoot?.querySelector('#edit-edge-label-dialog')! - )).close(); + editEdgeLabelDialog.close(); // Reload graph this.updated(new Map()); this._refreshGraph(true); }; + editEdgeLabelDialog.addEventListener('cancel', () => { + editEdgeLabelDialog.close(); + // Reset the state of the form + editEdgeLabelForm.reset(); + }); + const editEdgeLabelCancelButton: HTMLButtonElement = this.shadowRoot?.querySelector('#edit-edge-label-cancel-button')!; editEdgeLabelCancelButton.onclick = () => { - (( - this.shadowRoot?.querySelector('#edit-edge-label-dialog')! - )).close(); - // Reset the state of the form - editEdgeLabelForm.reset(); + editEdgeLabelDialog.dispatchEvent(new Event('cancel')); }; } From 41f24088ab4e5e4822119e76ecfe42295f3a2202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Tue, 19 Mar 2024 11:39:12 +0100 Subject: [PATCH 26/39] Change notes doc --- custom_pathway_notes.txt | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/custom_pathway_notes.txt b/custom_pathway_notes.txt index ce2bcbd..468365c 100644 --- a/custom_pathway_notes.txt +++ b/custom_pathway_notes.txt @@ -33,22 +33,15 @@ Done: Todo: BiowcPathwaygraph: - - Cancel Button does not reset edit node type (and maybe other menues?) - - In PTMNavigator: + - In Edit Mode, add upload button next to download button - Canvas could look different when in edit mode, e.g. light grey - - User should be able to enter editing mode when no pathway is loaded (initialize with empty skeleton) - - User should be able to give pathway a name - - Whenever an action leads to leaving edit mode ask to save - - Clear Canvas should set skeleton to empty, not undefined - - Also switching to edit while not having loaded a pathway should set skeleton to empty - - When selecting existing pathway for edit (not for view!!), ask if user wants to work from this or from copy - - append "(COPY)" to name - - Switch back to Viewing mode should clear the skeleton - Switch to Edit while canonical is selected should start editing this canonical pathway. - - Switch to Edit while custom selected should ask if this or copy - - In Edit Mode, download button should also allow download JSON of skeleton - - In Edit Mode, add upload button next to download button + - Switching to view mode should unselect both custom and canonical, switching to edit mode should unselect the one not in use + - In Backend, sanitize json before inserting it into SQL query! + + - Line 1195 (and once more) + // TODO: Deduplicate, code is repeated in editing part ++ // ...and showEdit... should only be toggled by editSaveOrDiscard \ No newline at end of file From 404eee370e436bf59eb257a557a7b25f6bbf45d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Tue, 19 Mar 2024 18:20:22 +0100 Subject: [PATCH 27/39] Update todo --- custom_pathway_notes.txt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/custom_pathway_notes.txt b/custom_pathway_notes.txt index 468365c..29c3ff6 100644 --- a/custom_pathway_notes.txt +++ b/custom_pathway_notes.txt @@ -35,13 +35,11 @@ Todo: BiowcPathwaygraph: In PTMNavigator: - - In Edit Mode, add upload button next to download button - Canvas could look different when in edit mode, e.g. light grey - Switch to Edit while canonical is selected should start editing this canonical pathway. - Switching to view mode should unselect both custom and canonical, switching to edit mode should unselect the one not in use - - In Backend, sanitize json before inserting it into SQL query! - - Line 1195 (and once more) - // TODO: Deduplicate, code is repeated in editing part -+ // ...and showEdit... should only be toggled by editSaveOrDiscard \ No newline at end of file +- perform enrichment during upload, store in prdb (also sanitize the json, someone could call the endpoint directly!) + +HANA: Which files to push? Check commit from yesterday \ No newline at end of file From 0313d70f004e0311d0e949dd98ebe4fd10296f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Wed, 20 Mar 2024 15:24:48 +0100 Subject: [PATCH 28/39] Use different style for editing mode --- src/BiowcPathwaygraph.ts | 11 +++++------ src/biowc-pathwaygraph.css.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 2a7824e..1550edd 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -322,7 +322,6 @@ export class BiowcPathwaygraph extends LitElement { min-height: 1500px; display: block; margin: auto; - background-color: white; border-radius: 5px" > @@ -4571,16 +4570,16 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> Date: Thu, 28 Mar 2024 16:58:27 +0100 Subject: [PATCH 29/39] Implement CSV Download of mapped peptides --- custom_pathway_notes.txt | 10 +-------- src/BiowcPathwaygraph.ts | 45 ++++++++++++++++++++++++++++++++++++++++ stories/index.stories.ts | 26 +++++++++++++++++++++++ 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/custom_pathway_notes.txt b/custom_pathway_notes.txt index 29c3ff6..d8d1407 100644 --- a/custom_pathway_notes.txt +++ b/custom_pathway_notes.txt @@ -34,12 +34,4 @@ Done: Todo: BiowcPathwaygraph: - In PTMNavigator: - - Canvas could look different when in edit mode, e.g. light grey - - Switch to Edit while canonical is selected should start editing this canonical pathway. - - Switching to view mode should unselect both custom and canonical, switching to edit mode should unselect the one not in use - - -- perform enrichment during upload, store in prdb (also sanitize the json, someone could call the endpoint directly!) - -HANA: Which files to push? Check commit from yesterday \ No newline at end of file + In PTMNavigator: \ No newline at end of file diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 1550edd..8dcb240 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -3762,6 +3762,51 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> node.type === 'ptm') + .map(node => { + + //Filter details for those that are plain strings, the others we don't want in the csv + let detailsFiltered = {} + if(!!(node).details) { + detailsFiltered = Object.keys((node).details!) + .filter(key => typeof (node).details![key] !== 'object') + .reduce((obj, key) => { + // @ts-ignore + obj[key] = (node).details![key]; + return obj; + }, {}); + } + + return { + 'Genes':(node).geneNames?.join(','), + 'Uniprot':(node).uniprotAccs?.join(','), + 'Regulation':(node).regulation, + ...detailsFiltered + }}) + + const replacer = (key:string, value:string|null) => value === null ? '' : value // specify how you want to handle null values here + const header = Object.keys(peptidesJSON[0]) + const peptidesCSV = [ + header.join('\t'), // header row first + // @ts-ignore + ...peptidesJSON.map(row => header.map(fieldName => JSON.stringify(row[fieldName], replacer)).join('\t')) + ].join('\r\n') + + const blob = new Blob([peptidesCSV], {type: 'text/plain'}); + const url = URL.createObjectURL(blob) + + const a = document.createElement('a'); + a.download = 'mappedPeptides.csv'; + a.href = url; + a.click(); + + + + } + public exportSkeleton(name: string, title: string) { return JSON.stringify( { diff --git a/stories/index.stories.ts b/stories/index.stories.ts index 9a26467..1a8d587 100644 --- a/stories/index.stories.ts +++ b/stories/index.stories.ts @@ -304,6 +304,32 @@ DownloadGraphAsSVG.args = { 'by the user.', }; +const DownloadCSVTemplate: Story = (args: ArgTypes) => html` +
${args.storyTitle}
+
${args.storyDescription}
+ + + +`; + +export const DownloadMappedPeptidesCSV = DownloadCSVTemplate.bind({}); +DownloadMappedPeptidesCSV.args = { + ...ColoringNodesByPotency.args, + hue: 'direction', + storyTitle: 'Download Mapped Peptides as CSV', + storyDescription: + 'TODO: Change the finally in the previous story ;-)', +}; + const EditingModeTemplate: Story = (args: ArgTypes) => html`
${args.storyTitle}
${args.storyDescription}
From b2d28e72726ad14f0aaa4b8d17fb2b1512e1d67a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Thu, 28 Mar 2024 19:22:59 +0100 Subject: [PATCH 30/39] Optionally display ptm node labels --- demo/index.html | 6 +-- src/BiowcPathwaygraph.ts | 89 ++++++++++++++++++++++++++-------------- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/demo/index.html b/demo/index.html index 93697dc..649b90d 100644 --- a/demo/index.html +++ b/demo/index.html @@ -20,12 +20,12 @@ render( html` `, diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 8dcb240..eaa0a31 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -288,6 +288,8 @@ export class BiowcPathwaygraph extends LitElement { isCreatingGroup: Boolean = false; + ptmNodeLabelsVisible: Boolean = false; + // TODO: Is there no way I can generalize this to rect.node-rect? static geneProteinPathwayCompoundsNodes: string[] = [ 'rect.node-rect.gene_protein', @@ -3763,48 +3765,66 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> node.type === 'ptm') - .map(node => { - - //Filter details for those that are plain strings, the others we don't want in the csv - let detailsFiltered = {} - if(!!(node).details) { - detailsFiltered = Object.keys((node).details!) - .filter(key => typeof (node).details![key] !== 'object') - .reduce((obj, key) => { - // @ts-ignore - obj[key] = (node).details![key]; - return obj; - }, {}); - } + // Provides a CSV file that contains all peptides that are mapped in the currently displayed diagram + const peptidesJSON = this.graphdataPTM!.nodes.filter( + node => node.type === 'ptm' + ).map(node => { + // Filter details for those that are plain strings, the others we don't want in the csv + let detailsFiltered = {}; + if ((node).details) { + detailsFiltered = Object.keys((node).details!) + .filter(key => typeof (node).details![key] !== 'object') + .reduce((obj, key) => { + // @ts-ignore + // eslint-disable-next-line no-param-reassign + obj[key] = (node).details![key]; + return obj; + }, {}); + } - return { - 'Genes':(node).geneNames?.join(','), - 'Uniprot':(node).uniprotAccs?.join(','), - 'Regulation':(node).regulation, - ...detailsFiltered - }}) + return { + Genes: (node).geneNames?.join(','), + Uniprot: (node).uniprotAccs?.join(','), + Regulation: (node).regulation, + ...detailsFiltered, + }; + }); - const replacer = (key:string, value:string|null) => value === null ? '' : value // specify how you want to handle null values here - const header = Object.keys(peptidesJSON[0]) + const replacer = (key: string, value: string | null) => + value === null ? '' : value; // specify how you want to handle null values here + const header = Object.keys(peptidesJSON[0]); const peptidesCSV = [ header.join('\t'), // header row first - // @ts-ignore - ...peptidesJSON.map(row => header.map(fieldName => JSON.stringify(row[fieldName], replacer)).join('\t')) - ].join('\r\n') + ...peptidesJSON.map(row => + header + // @ts-ignore + .map(fieldName => JSON.stringify(row[fieldName], replacer)) + .join('\t') + ), + ].join('\r\n'); - const blob = new Blob([peptidesCSV], {type: 'text/plain'}); - const url = URL.createObjectURL(blob) + const blob = new Blob([peptidesCSV], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.download = 'mappedPeptides.csv'; a.href = url; a.click(); + } - - + public toggleLabelPeptideNodes() { + this.ptmNodeLabelsVisible = !this.ptmNodeLabelsVisible; + this.d3Nodes!.forEach(node => { + if (node.type === 'ptm') { + // TODO: If available, use Site identifier before sequence + const ptmNodeLabel = (node).details!.Sequence || 'test'; + // eslint-disable-next-line no-param-reassign + node.currentDisplayedLabel = this.ptmNodeLabelsVisible + ? String(ptmNodeLabel) + : ''; + } + }); + this._refreshGraph(true); } public exportSkeleton(name: string, title: string) { @@ -3946,6 +3966,13 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> this.toggleLabelPeptideNodes(), + type: 'radio', + checked: () => this.ptmNodeLabelsVisible, + }, { target: 'svg', label: 'Show...', From 99dfdd8adb786b1a74183f759cb9bb1ee38e8583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Thu, 28 Mar 2024 19:29:21 +0100 Subject: [PATCH 31/39] Fix display of indirect effect links --- demo/index.html | 4 ++-- src/BiowcPathwaygraph.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/index.html b/demo/index.html index 649b90d..f9e3437 100644 --- a/demo/index.html +++ b/demo/index.html @@ -20,8 +20,8 @@ render( html` { if (d.types.includes('binding/association')) return '3 3'; - if (d.types.includes('indirect')) return '7 2'; + if (d.types.includes('indirect effect')) return '7 2'; return null; }); From a5a05d71534966013f66ead25ac21cb996cc08d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Tue, 2 Apr 2024 16:57:38 +0200 Subject: [PATCH 32/39] Add optional PTM Node Labels --- src/BiowcPathwaygraph.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 48b4b45..178e821 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -1467,6 +1467,9 @@ export class BiowcPathwaygraph extends LitElement { nodesSvg .selectAll('.node-label') .text(d => d.currentDisplayedLabel || '') + // Position the PTM node labels above the nodes and make them italic + .attr('y', d => (d.type === 'ptm' ? -10 : 0)) + .style('font-style', d => (d.type === 'ptm' ? 'italic' : '')) .each((d, i, nodes) => { // Adjust width of the node based on the length of the text const circleWidth = NODE_HEIGHT * 2; @@ -3817,7 +3820,12 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> { if (node.type === 'ptm') { // TODO: If available, use Site identifier before sequence - const ptmNodeLabel = (node).details!.Sequence || 'test'; + const ptmNodeLabel = + // @ts-ignore + (node).details?.Site?.text || + (node).details!['Modified Sequence'] || + (node).details!.Sequence || + 'test'; // eslint-disable-next-line no-param-reassign node.currentDisplayedLabel = this.ptmNodeLabelsVisible ? String(ptmNodeLabel) From a4150d8c2a0ba92c67299be9b7906dd332aa3978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Tue, 2 Apr 2024 17:53:03 +0200 Subject: [PATCH 33/39] Clean up CSV Download --- src/BiowcPathwaygraph.ts | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 178e821..ec6386d 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -3771,27 +3771,14 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> node.type === 'ptm' - ).map(node => { - // Filter details for those that are plain strings, the others we don't want in the csv - let detailsFiltered = {}; - if ((node).details) { - detailsFiltered = Object.keys((node).details!) - .filter(key => typeof (node).details![key] !== 'object') - .reduce((obj, key) => { - // @ts-ignore - // eslint-disable-next-line no-param-reassign - obj[key] = (node).details![key]; - return obj; - }, {}); - } - - return { - Genes: (node).geneNames?.join(','), - Uniprot: (node).uniprotAccs?.join(','), - Regulation: (node).regulation, - ...detailsFiltered, - }; - }); + ).map(node => ({ + 'Modified Sequence': (node).details?.['Modified Sequence'], + 'Gene Name(s)': (node).details?.['Gene Name(s)'], + // @ts-ignore + Uniprot: (node).details?.Uniprot_Accession_Number?.text, + Experiment: (node).details?.['Experiment Name'], + Regulation: (node).regulation, + })); const replacer = (key: string, value: string | null) => value === null ? '' : value; // specify how you want to handle null values here From f8b63e2b8f5ef63492911443a6b3d8ef458a0751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Fri, 5 Apr 2024 17:08:46 +0200 Subject: [PATCH 34/39] Implement optional links between annotated upstream kinases and their substrates --- src/BiowcPathwaygraph.ts | 79 ++++++++++++++++++++++++++++++++-- src/biowc-pathwaygraph.css.ts | 6 +++ test/fixtures/StoryFixtures.ts | 10 +++++ 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index ec6386d..ea0061d 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -290,6 +290,8 @@ export class BiowcPathwaygraph extends LitElement { ptmNodeLabelsVisible: Boolean = false; + kinaseSubstrateLinksVisible: Boolean = false; + // TODO: Is there no way I can generalize this to rect.node-rect? static geneProteinPathwayCompoundsNodes: string[] = [ 'rect.node-rect.gene_protein', @@ -382,6 +384,24 @@ export class BiowcPathwaygraph extends LitElement { stroke="none" /> + + + ('.linkgroup.ptmlink') - .attr('display', 'none'); + // TODO: It looks like this never did anything, commenting out to see if that is true + // this._getMainDiv() + // .selectAll('.linkgroup.ptmlink') + // .attr('display', 'none'); // If a timeout is already running, cancel it (can happen if user clicks too fast) if (this.currentTimeoutId) clearTimeout(this.currentTimeoutId); @@ -1559,12 +1608,24 @@ export class BiowcPathwaygraph extends LitElement { if (d.types.includes('activation')) { return 'url(#activationMarker)'; } + if (d.types.includes('kinaseSubstrateLink')) { + return 'url(#kinaseSubstrateLinkMarker)'; + } return 'url(#otherInteractionMarker)'; }) .attr('stroke-dasharray', d => { if (d.types.includes('binding/association')) return '3 3'; if (d.types.includes('indirect effect')) return '7 2'; return null; + }) + .style('visibility', d => { + if (d.types.includes('kinaseSubstrateLink')) { + return this.kinaseSubstrateLinksVisible ? 'visible' : 'hidden'; + } + if (d.types.includes('ptmlink')) { + return 'hidden'; + } + return 'visible'; }); // Add paths for the edgelabels @@ -3822,6 +3883,11 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> this.ptmNodeLabelsVisible, }, + { + target: 'svg', + label: 'Show Kinase-Substrate Relationships', + execute: () => this.toggleKinaseSubstrateLinks(), + type: 'radio', + checked: () => this.kinaseSubstrateLinksVisible, + }, { target: 'svg', label: 'Show...', diff --git a/src/biowc-pathwaygraph.css.ts b/src/biowc-pathwaygraph.css.ts index 7b30268..50bd215 100644 --- a/src/biowc-pathwaygraph.css.ts +++ b/src/biowc-pathwaygraph.css.ts @@ -12,6 +12,7 @@ export default css` --group-stroke-color: #3b423f; --link-color: #999999; --link-color-highlight: #3d3d3d; + --kinase-substrate-link-color: #00a61a; --edge-label-color: #4e4e4e; --legend-frame-color: #a9a9a9; --font-stack: 'Roboto Light', 'Helvetica Neue', 'Verdana', sans-serif; @@ -40,6 +41,11 @@ export default css` visibility: hidden; } + .link.kinaseSubstrateLink { + stroke: var(--kinase-substrate-link-color); + stroke-width: 2; + } + .link.maplink { stroke-dasharray: '5 2'; } diff --git a/test/fixtures/StoryFixtures.ts b/test/fixtures/StoryFixtures.ts index 91f4049..b03bdf5 100644 --- a/test/fixtures/StoryFixtures.ts +++ b/test/fixtures/StoryFixtures.ts @@ -266,6 +266,11 @@ export default { 'Fold Change': 0.1, 'p Value': 0.0002, '-log(EC50)': 9, + 'Upstream Kinase(s)': { + display: true, + indentKey: true, + text: 'GeneA, Protein D', + }, }, }, { @@ -285,6 +290,11 @@ export default { indentKey: true, }, 'Something else': 'Hello!', + 'Upstream Kinase(s)': { + display: true, + indentKey: true, + text: 'Protein B, Protein C', + }, }, }, { From 694a0a76ab3ae2a7eccf09c767fb1340a92b8436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Mon, 8 Apr 2024 12:38:23 +0200 Subject: [PATCH 35/39] Fix a bug in the display of upstream kinases --- src/BiowcPathwaygraph.ts | 4 +++- test/fixtures/StoryFixtures.ts | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index ea0061d..7069b19 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -1069,7 +1069,9 @@ export class BiowcPathwaygraph extends LitElement { // Check if the PTM node has Upstream kinase annotations if ( ptmPeptide.details && - ptmPeptide.details!['Upstream Kinase(s)'] + ptmPeptide.details!['Upstream Kinase(s)'] && + // @ts-ignore + !!ptmPeptide.details!['Upstream Kinase(s)'].text ) { const currentUpstreamKinases = // @ts-ignore diff --git a/test/fixtures/StoryFixtures.ts b/test/fixtures/StoryFixtures.ts index b03bdf5..ddddee6 100644 --- a/test/fixtures/StoryFixtures.ts +++ b/test/fixtures/StoryFixtures.ts @@ -263,6 +263,7 @@ export default { regulation: 'down', details: { Sequence: 'TRY(ph)AGAINK', + Site: { text: 'S42' }, 'Fold Change': 0.1, 'p Value': 0.0002, '-log(EC50)': 9, @@ -278,6 +279,7 @@ export default { regulation: 'not', details: { Sequence: 'GILGVIVT(ph)LK', + Site: { text: 'Y512' }, 'Fold Change': 0.9, 'p Value': 0.0003, '-log(EC50)': 6, @@ -302,6 +304,7 @@ export default { regulation: 'up', details: { Sequence: 'VNIPRVT(ph)K', + 'Fold Change': 4, 'p Value': 0.0004, '-log(EC50)': 10, @@ -312,6 +315,7 @@ export default { regulation: 'up', details: { Sequence: 'GENENAMES(ph)K', + Site: { text: 'T90' }, 'Fold Change': 2, 'p Value': 0.001, '-log(EC50)': 8, From 922863d209334ab20a1853738881bd93ebab771f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Tue, 9 Apr 2024 14:25:18 +0200 Subject: [PATCH 36/39] Add property to highlight perturbed nodes --- demo/index.html | 1 + src/BiowcPathwaygraph.ts | 29 ++++++++++++++++++++++++++++- src/biowc-pathwaygraph.css.ts | 12 ++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/demo/index.html b/demo/index.html index f9e3437..035417c 100644 --- a/demo/index.html +++ b/demo/index.html @@ -26,6 +26,7 @@ .fullProteomeInputList = ${StoryFixtures.proteinExpressionFixture.fullProteomeInputList} .hue= ${'foldchange'} .applicationMode = ${'viewing'} + .perturbedNodes = ${{up: ['Protein B', 'Protein D'], down:['Protein A', 'Protein C']}} > `, diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index 7069b19..b048519 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -234,6 +234,9 @@ export class BiowcPathwaygraph extends LitElement { @property({ attribute: false }) applicationMode!: PossibleApplicationMode; + @property({ attribute: false }) + perturbedNodes?: { up: String[]; down: String[] }; + graphdataPTM?: { nodes: (PTMNode | PTMSummaryNode)[]; links: PathwayGraphLinkInput[]; @@ -1498,7 +1501,9 @@ export class BiowcPathwaygraph extends LitElement { d => `node-rect ${d.type} ${BiowcPathwaygraph._computeRegulationClass( d - )} ${d.isHighlighted ? 'highlight' : ''}` + )} ${d.isHighlighted ? 'highlight' : ''} + ${this._computeIsPerturbed(d)} + ` ) .attr('rx', NODE_HEIGHT) .attr('ry', NODE_HEIGHT) @@ -2252,6 +2257,28 @@ export class BiowcPathwaygraph extends LitElement { ); } + private _computeIsPerturbed(node: PathwayGraphNodeD3) { + // @ts-ignore + if (node.geneNames) { + const geneProteinNode = node as GeneProteinNodeD3; + if ( + geneProteinNode.geneNames.filter(geneName => + this.perturbedNodes?.down.includes(geneName) + ).length > 0 + ) { + return 'highlight-down'; + } + if ( + geneProteinNode.geneNames.filter(geneName => + this.perturbedNodes?.up.includes(geneName) + ).length > 0 + ) { + return 'highlight-up'; + } + } + return ''; + } + private _computeNodeColor(node: PathwayGraphNodeD3) { if (node.type !== 'ptm') { // Default to whatever is in the css diff --git a/src/biowc-pathwaygraph.css.ts b/src/biowc-pathwaygraph.css.ts index 50bd215..585aabe 100644 --- a/src/biowc-pathwaygraph.css.ts +++ b/src/biowc-pathwaygraph.css.ts @@ -4,6 +4,8 @@ export default css` :host { --upregulated-color: #ea0000; --downregulated-color: #2571ff; + --upregulated-perturbed-color: #c20000; + --downregulated-perturbed-color: #0043c2; --unregulated-color: #a4a4a4; --pathway-color: #89b9ce; --gene-protein-color: #efefef; @@ -72,6 +74,16 @@ export default css` fill: var(--gene-protein-color); } + .node-rect.gene_protein.highlight-up { + stroke: var(--upregulated-perturbed-color); + stroke-width: 4; + } + + .node-rect.gene_protein.highlight-down { + stroke: var(--downregulated-perturbed-color); + stroke-width: 4; + } + .node-rect.group { opacity: 0.25; } From f6343c2ade81d3e75f097e153c4be0b2fe24c721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Tue, 9 Apr 2024 15:02:32 +0200 Subject: [PATCH 37/39] Change color for kinase substrate links --- src/biowc-pathwaygraph.css.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/biowc-pathwaygraph.css.ts b/src/biowc-pathwaygraph.css.ts index 585aabe..1963f1e 100644 --- a/src/biowc-pathwaygraph.css.ts +++ b/src/biowc-pathwaygraph.css.ts @@ -14,7 +14,7 @@ export default css` --group-stroke-color: #3b423f; --link-color: #999999; --link-color-highlight: #3d3d3d; - --kinase-substrate-link-color: #00a61a; + --kinase-substrate-link-color: #7e1d17; --edge-label-color: #4e4e4e; --legend-frame-color: #a9a9a9; --font-stack: 'Roboto Light', 'Helvetica Neue', 'Verdana', sans-serif; From 8c230325ef044bc0c535c32da8093e6c5e946aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Tue, 9 Apr 2024 16:29:44 +0200 Subject: [PATCH 38/39] Fix potency color scale initially to 5-9 --- demo/index.html | 2 +- src/BiowcPathwaygraph.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/demo/index.html b/demo/index.html index 035417c..90a5582 100644 --- a/demo/index.html +++ b/demo/index.html @@ -24,7 +24,7 @@ links: StoryFixtures.linkTypesFixture.links} } .ptmInputList = ${StoryFixtures.ptmGraphWithDetailsFixture.ptmInputList} .fullProteomeInputList = ${StoryFixtures.proteinExpressionFixture.fullProteomeInputList} - .hue= ${'foldchange'} + .hue= ${'potency'} .applicationMode = ${'viewing'} .perturbedNodes = ${{up: ['Protein B', 'Protein D'], down:['Protein A', 'Protein C']}} > diff --git a/src/BiowcPathwaygraph.ts b/src/BiowcPathwaygraph.ts index b048519..20d2150 100644 --- a/src/BiowcPathwaygraph.ts +++ b/src/BiowcPathwaygraph.ts @@ -2477,8 +2477,8 @@ export class BiowcPathwaygraph extends LitElement { }, this.maxPotency!); // Set min and max values to min and max potency (the user may change this later): - this.colorRangeMin = this.minPotency; - this.colorRangeMax = this.maxPotency; + this.colorRangeMin = 5; + this.colorRangeMax = 9; break; default: break; @@ -4608,8 +4608,8 @@ font-family: "Roboto Light", "Helvetica Neue", "Verdana", sans-serif'> Date: Tue, 9 Apr 2024 16:32:58 +0200 Subject: [PATCH 39/39] Bump version to 0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1aea212..4c398c0 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "Webcomponent biowc-pathwaygraph following open-wc recommendations", "license": "Apache-2.0", "author": "biowc-pathwaygraph", - "version": "0.0.20", + "version": "0.1.0", "main": "dist/src/index.js", "module": "dist/src/index.js", "exports": {