From 850ea5ddbf8c3648f96c4256e0a09118aed4dab4 Mon Sep 17 00:00:00 2001 From: Ben Capodanno Date: Tue, 17 Dec 2024 12:16:19 -0800 Subject: [PATCH 001/136] Add Table Component for Contributor Supplied Calibrations --- src/components/CalibrationTable.vue | 77 +++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/components/CalibrationTable.vue diff --git a/src/components/CalibrationTable.vue b/src/components/CalibrationTable.vue new file mode 100644 index 00000000..f9bcdf2f --- /dev/null +++ b/src/components/CalibrationTable.vue @@ -0,0 +1,77 @@ + + + + + From 35e84ebd11029755fa2eaced7b7579c1599ac6f4 Mon Sep 17 00:00:00 2001 From: Ben Capodanno Date: Tue, 17 Dec 2024 12:17:43 -0800 Subject: [PATCH 002/136] Support Score Calibrations on the Histogram - Makes histogram shaded regions more generic - Adds first-class logic for calibration and range formatting - Adds CalibrationTable to Histogram display - Eliminates score range hardcoding --- src/components/ScoreSetHistogram.vue | 182 ++++++++++++++++-------- src/lib/calibrations.ts | 88 ++++++++++++ src/lib/histogram.ts | 205 ++++++++++++++++++--------- src/lib/ranges.ts | 39 +++++ 4 files changed, 389 insertions(+), 125 deletions(-) create mode 100644 src/lib/calibrations.ts create mode 100644 src/lib/ranges.ts diff --git a/src/components/ScoreSetHistogram.vue b/src/components/ScoreSetHistogram.vue index fe7063bc..e41b0f01 100644 --- a/src/components/ScoreSetHistogram.vue +++ b/src/components/ScoreSetHistogram.vue @@ -1,5 +1,22 @@ diff --git a/src/lib/calibrations.ts b/src/lib/calibrations.ts new file mode 100644 index 00000000..9bb77a35 --- /dev/null +++ b/src/lib/calibrations.ts @@ -0,0 +1,88 @@ +import { HistogramShader } from "@/lib/histogram" + +export const POSITIVE_EVIDENCE_STRENGTH_COLOR_SHADES = "#ff4444" +export const NEGATIVE_EVIDENCE_STRENGTH_COLOR_SHADES = "#4444ff" + +export interface Calibrations { + thresholds: number[] + positiveLikelihoodRatios: number[] + evidenceStrengths: number[] +} + +export function prepareThresholdsForHistogram(calibrations: Calibrations): HistogramShader[] { + const thresholdRanges: HistogramShader[] = [] + + const thresholdCardinality = inferCardinalityOfThresholds(calibrations) + + calibrations.thresholds.forEach((threshold, idx) => { + const evidenceStrengthAtIndex = calibrations.evidenceStrengths[idx] + const evidenceStrengthIsPositive = evidenceStrengthAtIndex > 0 + const thresholdColor = evidenceStrengthIsPositive ? POSITIVE_EVIDENCE_STRENGTH_COLOR_SHADES : NEGATIVE_EVIDENCE_STRENGTH_COLOR_SHADES + + const thresholdRange: HistogramShader = { + min: null, + max: null, + + color: thresholdColor, + thresholdColor: thresholdColor, + title: calibrations.evidenceStrengths[idx].toString(), + + startOpacity: 0.15, + stopOpacity: 0.05, + + gradientUUID: undefined, + } + + // The first and last threshold have no min or max. Depending on their cardinality, the threshold itself is the other + // value. + if (idx === 0 || idx === calibrations.thresholds.length - 1) { + thresholdCardinality[idx] > 0 ? thresholdRange.min = threshold : thresholdRange.max = threshold + } + // If the threshold cardinality is positive, the threshold maximum will be the next threshold. The opposite is true + // for thresholds with negative cardinality. + else { + if (thresholdCardinality[idx] > 0) { + thresholdRange.min = threshold + thresholdRange.max = calibrations.thresholds[idx+1] + } else { + thresholdRange.min = calibrations.thresholds[idx-1] + thresholdRange.max = threshold + } + } + + thresholdRanges.push(thresholdRange) + }) + + return thresholdRanges +} + + +// Infer the cardinality of a given threshold. +// Assumptions: +// - The thresholds are ordered in either ascending or descending fashion +// - The evidence strengths are ordered in either ascending or descending fashion +// - All positive evidence strengths have the same cardinality (the same applies for negative evidence strengths) +function inferCardinalityOfThresholds(calibrations: Calibrations): number[] { + + // If the first threshold is larger, that implies its initial cardinality is positive. + const initialCardinality = calibrations.thresholds[0] > calibrations.thresholds[-1] ? 1 : -1 + + const thresholdCardinality = [initialCardinality] + + let lastCardinality = initialCardinality + for (let i = 1; i < calibrations.thresholds.length; i++) { + const evidenceStrengthFlipped = calibrations.evidenceStrengths[i-1] * calibrations.evidenceStrengths[i] < 0 ? true : false + + // If the sign of the evidence strength flipped, flip the cardinality of the threhsold. Otherwise, the + // cardinality will be the same as the prior threshold. + if (evidenceStrengthFlipped) { + thresholdCardinality.push(lastCardinality * -1) + } else { + thresholdCardinality.push(lastCardinality) + } + + lastCardinality = thresholdCardinality[thresholdCardinality.length - 1] + } + + return thresholdCardinality +} diff --git a/src/lib/histogram.ts b/src/lib/histogram.ts index 8b0f6832..c573b851 100644 --- a/src/lib/histogram.ts +++ b/src/lib/histogram.ts @@ -1,12 +1,13 @@ import * as d3 from 'd3' import $ from 'jquery' import _ from 'lodash' +import { v4 as uuidv4 } from 'uuid'; type FieldGetter = ((d: HistogramDatum) => T) | string type Getter = () => T type Accessor = (value?: T) => T | Self -export const DEFAULT_RANGE_COLOR = '#333333' +export const DEFAULT_SHADER_COLOR = '#333333' export const DEFAULT_SERIES_COLOR = '#333333' const LABEL_SIZE = 10 @@ -61,11 +62,36 @@ export interface HistogramBin { seriesBins: d3.Bin[] } -export interface HistogramRange { +/** The definition for a shaded region */ +export interface HistogramShader { + /** The minimum and maximum x positions of this shaded region. */ min: number | null max: number | null + + /** The displayed title of this region. */ title: string | null + + /** The color of this shaded region. */ color: string | null + + /** The color of the lines demarcating the shaded region at positions min and max. Also used + * for the color of the title. + */ + thresholdColor: string | undefined + + /** How opaque the start and end of the gradient should be. Opacity will change linearly + * between these two values across the shaded region. + */ + startOpacity: number | undefined + stopOpacity: number | undefined + + /** Created dynamically by this histogram object. Identifies the linear gradient internally. */ + gradientUUID: string | undefined +} + +/** An object containing definitions of each of the possible shaded regions on this Histogram. */ +export interface HistogramShaderRegions { + [key: string]: HistogramShader[] } export interface Histogram { @@ -92,7 +118,6 @@ export interface Histogram { data: Accessor seriesOptions: Accessor seriesClassifier: Accessor<((d: HistogramDatum) => number[]) | null, Histogram> - ranges: Accessor numBins: Accessor // Data fields @@ -113,6 +138,10 @@ export interface Histogram { bottomAxisLabel: Accessor legendNote: Accessor + // Shaded regions + shaders: Accessor + renderShader: Accessor + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Getters ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -138,7 +167,6 @@ export default function makeHistogram(): Histogram { let data: HistogramDatum[] = [] let seriesOptions: HistogramSerieOptions[] | null = null let seriesClassifier: ((d: HistogramDatum) => number[]) | null = null - let ranges: HistogramRange[] = [] let numBins = 30 // Data fields @@ -159,6 +187,10 @@ export default function makeHistogram(): Histogram { let bottomAxisLabel: string | null = null let legendNote: string | null = null + // Shaded regions + let shaders: HistogramShaderRegions | null = null + let renderShader: string | null = null + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Read-only properties ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -259,7 +291,7 @@ export default function makeHistogram(): Histogram { bins = overallBins.map((bin, binIndex) => ({ x0: bin.x0 || 0, x1: bin.x1 || 0, - yMax: Math.max(...series.map((serie, i) => serie.bins[binIndex].length)), + yMax: Math.max(...series.map((serie) => serie.bins[binIndex].length)), seriesBins: series.map((serie) => serie.bins[binIndex]) })) } @@ -464,12 +496,12 @@ export default function makeHistogram(): Histogram { } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Ranges + // Shaders //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - const rangePolygon = (range: HistogramRange, yMax: number) => { + const shaderPolygon = (shaderRegion: HistogramShader, yMax: number) => { const points = [] - const {min: xMin, max: xMax} = visibleRange(range) + const {min: xMin, max: xMax} = visibleShaderRegion(shaderRegion) const yMin = yScale.domain()[0] // Start at the top left. @@ -512,10 +544,10 @@ export default function makeHistogram(): Histogram { return points } - const visibleRange = (range: HistogramRange) => { + const visibleShaderRegion = (region: HistogramShader) => { return { - min: range.min == null ? xScale.domain()[0] : Math.max(range.min, xScale.domain()[0]), - max: range.max == null ? xScale.domain()[1] : Math.min(range.max, xScale.domain()[1]) + min: region.min == null ? xScale.domain()[0] : Math.max(region.min, xScale.domain()[0]), + max: region.max == null ? xScale.domain()[1] : Math.min(region.max, xScale.domain()[1]) } } @@ -553,9 +585,9 @@ export default function makeHistogram(): Histogram { .attr('class', 'histogram-main') .attr('transform', `translate(${margins.left},${margins.top})`) mainGroup.append('g') - .attr('class', 'histogram-ranges') + .attr('class', 'histogram-shaders') mainGroup.append('g') - .attr('class', 'histogram-range-thresholds') + .attr('class', 'histogram-shader-thresholds') mainGroup.append('g') .attr('class', 'histogram-bars') mainGroup.append('g') @@ -776,7 +808,7 @@ export default function makeHistogram(): Histogram { // Hover target is the full height of the chart. hovers.append('rect') - .attr('class', (d) => `histogram-hover-target`) + .attr('class', () => `histogram-hover-target`) .attr('x', (d) => xScale(d.x0)) .attr('width', (d) => xScale(d.x1) - xScale(d.x0)) .attr('y', () => yScale(yMax)) @@ -785,7 +817,7 @@ export default function makeHistogram(): Histogram { // However, only the largest bin is highlighted on hover. hovers.append('rect') - .attr('class', (d) => `histogram-hover-highlight`) + .attr('class', () => `histogram-hover-highlight`) .attr('x', (d) => xScale(d.x0)) .attr('width', (d) => xScale(d.x1) - xScale(d.x0)) .attr('y', (d) => yScale(d.yMax)) @@ -795,64 +827,79 @@ export default function makeHistogram(): Histogram { .style('stroke-width', 1.5) .style('opacity', d => hoverOpacity(d)) - // Refresh the ranges. + // Refresh the shaded regions. + const activeShader = shaders && renderShader ? shaders[renderShader] : null + svg.select('defs') - .selectAll('linearGradient') - .data(ranges) - .join( - (enter) => { - const gradient = enter.append('linearGradient') - .attr('id', (d, i) => `histogram-range-gradient-${i}`) // TODO Include a UUID for this chart. - .attr('gradientTransform', 'rotate(45)') - gradient.append('stop') - .attr('offset', '0') - .attr('stop-color', (d) => d.color || DEFAULT_SERIES_COLOR) - .attr('stop-opacity', '0.15') - gradient.append('stop') - .attr('offset', '100%') - .attr('stop-color', (d) => d.color || DEFAULT_SERIES_COLOR) - .attr('stop-opacity', '0.05') - return gradient - } - ) - const rangeG = svg.select('g.histogram-ranges') - .selectAll('g.histogram-range') - .data(chartHasContent ? ranges : []) - .join( - (enter) => { - const g = enter.append('g') - g.append('polygon') - g.append('text') - return g - }, - (update) => update, - (exit) => exit.remove() - ) - rangeG.attr('class', 'histogram-range') - .style('fill', (d, i) => `url(#histogram-range-gradient-${i})`) - rangeG.select('polygon') - .attr('points', (d) => rangePolygon(d, yMax).map(([x, y]) => `${xScale(x)},${yScale(y)}`).join(' ')) - rangeG.select('text') - .attr('class', 'histogram-range-title') - .style('fill', (d) => d.color || '#000000') + .selectAll('linearGradient') + .remove() + + // Select the active shader elements. + const shaderG = svg.select('g.histogram-shaders') + .selectAll('g.histogram-shader') + .data(chartHasContent && activeShader ? activeShader : []) + .join( + (enter) => { + const g = enter.append('g') + g.append('polygon') + g.append('text') + return g + }, + (update) => update, + (exit) => exit.remove() + ) + + if (activeShader) { + // Add the gradients for the active shader. + svg.select('defs') + .selectAll('linearGradient') + .data(activeShader) + .join( + (enter) => { + const gradient = enter.append('linearGradient') + .attr('id', (d) => {d.gradientUUID = uuidv4(); return `histogram-gradient-${d.gradientUUID}`}) + .attr('gradientTransform', 'rotate(45)') + gradient.append('stop') + .attr('offset', '0') + .attr('stop-color', (d) => d.color || DEFAULT_SERIES_COLOR) + .attr('stop-opacity', (d) => d.startOpacity || '0.15') + gradient.append('stop') + .attr('offset', '100%') + .attr('stop-color', (d) => d.color || DEFAULT_SERIES_COLOR) + .attr('stop-opacity', (d) => d.stopOpacity || '0.05') + + return gradient + } + ) + + // Draw each shader region according to the shader definition. + shaderG.attr('class', 'histogram-shader') + .style('fill', (d) => `url(#histogram-gradient-${d.gradientUUID})`) + shaderG.select('polygon') + .attr('points', (d) => shaderPolygon(d, yMax).map(([x, y]) => `${xScale(x)},${yScale(y)}`).join(' ')) + shaderG.select('text') + .attr('class', 'histogram-shader-title') + .style('fill', (d) => d.thresholdColor || '#000000') .attr('x', (d) => { - const span = visibleRange(d) + const span = visibleShaderRegion(d) return xScale((span.min + span.max) / 2) }) .attr('y', 15) .style('text-anchor', 'middle') .style('visibility', (d) => d.title ? 'visible' : 'hidden') .text((d) => d.title) - const rangeThresholds = ranges.map((range) => [ - ...(range.min != null && range.min > xScale.domain()[0]) ? [{x: range.min, range}] : [], - ...(range.max != null && range.max < xScale.domain()[1]) ? [{x: range.max, range}] : [], - ]).flat() - svg.select('g.histogram-range-thresholds') - .selectAll('path.histogram-range-threshold') - .data(chartHasContent ? rangeThresholds : []) + + // Draw the shader thresholds. + const shaderThresholds = activeShader.map((region) => [ + ...(region.min != null && region.min > xScale.domain()[0]) ? [{x: region.min, region}] : [], + ...(region.max != null && region.max < xScale.domain()[1]) ? [{x: region.max, region}] : [], + ]).flat() + svg.select('g.histogram-shader-thresholds') + .selectAll('path.histogram-shader-threshold') + .data(chartHasContent ? shaderThresholds : []) .join('path') - .attr('class', 'histogram-range-threshold') - .attr('stroke', (d) => d.range.color || DEFAULT_RANGE_COLOR) + .attr('class', 'histogram-shader-threshold') + .attr('stroke', (d) => d.region.thresholdColor || DEFAULT_SHADER_COLOR) .attr('stroke-dasharray', '4 4') .attr('stroke-width', 1.5) .attr('d', (d) => { @@ -861,8 +908,14 @@ export default function makeHistogram(): Histogram { if (intersectedBinIndex != null && intersectedBinIndex > 0 && d.x == bins[intersectedBinIndex].x0) { yMin = Math.max(yMin, bins[intersectedBinIndex - 1].x1) } - return path([[d.x, yMin], [d.x, yMax]]) + return path([[d.x, 0], [d.x, yMax]]) }) + } else { + shaderG.remove() + svg.select('g.histogram-shader-thresholds') + .selectAll('path.histogram-shader-threshold') + .remove() + } } updateSelectionAfterRefresh() @@ -936,11 +989,27 @@ export default function makeHistogram(): Histogram { return chart }, - ranges: (value?: HistogramRange[]) => { + shaders: (value?: HistogramShaderRegions | null) => { if (value === undefined) { - return ranges + return shaders } - ranges = value + shaders = value + return chart + }, + + renderShader: (value?: string | null) => { + if (value === undefined) { + return renderShader + } + // Don't allow rendering of a shader which does not have a shader definition. + if (!shaders && value) { + return renderShader + } + else if (shaders && value && !(value in shaders)) { + return renderShader + } + + renderShader = value return chart }, diff --git a/src/lib/ranges.ts b/src/lib/ranges.ts new file mode 100644 index 00000000..0fa40f09 --- /dev/null +++ b/src/lib/ranges.ts @@ -0,0 +1,39 @@ +import { HistogramShader } from "@/lib/histogram" + +export const NORMAL_RANGE_DEFAULT_COLOR = "#4444ff" +export const ABNORMAL_RANGE_DEFAULT_COLOR = "#ff4444" + +export interface ScoreSetRanges { + wtScore: number + ranges: Array +} + +export interface ScoreSetRange { + label: string, + description: string | undefined, + classification: "normal" | "abnormal" + range: Array +} + +export function prepareRangesForHistogram(scoreRanges: ScoreSetRanges): HistogramShader[] { + const preparedRanges: HistogramShader[] = [] + + scoreRanges.ranges.forEach((range) => { + const rangeIsNormal = range.classification === "normal" ? true : false + + const scoreRange: HistogramShader = { + min: range.range[0], + max: range.range[1], + title: range.label, + color: rangeIsNormal ? NORMAL_RANGE_DEFAULT_COLOR : ABNORMAL_RANGE_DEFAULT_COLOR, + thresholdColor: rangeIsNormal ? NORMAL_RANGE_DEFAULT_COLOR : ABNORMAL_RANGE_DEFAULT_COLOR, + startOpacity: 0.15, + stopOpacity: 0.05, + gradientUUID: undefined + } + + preparedRanges.push(scoreRange) + }) + + return preparedRanges +} From dc2a4be36d0e582d23ada0b61e96ff974f7ca795 Mon Sep 17 00:00:00 2001 From: Ben Capodanno Date: Mon, 30 Dec 2024 23:17:07 -0800 Subject: [PATCH 003/136] Remove Calibrations from Controls; Show table based on active shader --- src/components/ScoreSetHistogram.vue | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/components/ScoreSetHistogram.vue b/src/components/ScoreSetHistogram.vue index e41b0f01..e1c275ec 100644 --- a/src/components/ScoreSetHistogram.vue +++ b/src/components/ScoreSetHistogram.vue @@ -32,11 +32,9 @@ -
- -
-
+
+
@@ -228,9 +226,6 @@ export default defineComponent({ if (someVariantsHaveClinicalSignificance) { options.push({label: 'Clinical View', view: 'clinical'}) } - if (this.scoreSet.scoreCalibrations) { - options.push({label: 'Calibrations', view: 'calibrations'}) - } // custom view should always come last if (someVariantsHaveClinicalSignificance) { @@ -261,10 +256,6 @@ export default defineComponent({ return this.activeViz == this.vizOptions.findIndex(item => item.view === 'custom') }, - showCalibrations: function() { - return this.activeViz == this.vizOptions.findIndex(item => item.view === 'calibrations') - }, - showShaders: function() { return this.shaderOptions.length > 1 }, From 0fff77fb5ecd546da73da05ab6447d708d7f056c Mon Sep 17 00:00:00 2001 From: Ben Capodanno Date: Tue, 31 Dec 2024 00:03:00 -0800 Subject: [PATCH 004/136] Add configurable text alignment to histogram shaders --- src/lib/calibrations.ts | 4 ++++ src/lib/histogram.ts | 40 +++++++++++++++++++++++++++++++++++++++- src/lib/ranges.ts | 1 + 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/lib/calibrations.ts b/src/lib/calibrations.ts index 9bb77a35..37708e50 100644 --- a/src/lib/calibrations.ts +++ b/src/lib/calibrations.ts @@ -26,6 +26,7 @@ export function prepareThresholdsForHistogram(calibrations: Calibrations): Histo color: thresholdColor, thresholdColor: thresholdColor, title: calibrations.evidenceStrengths[idx].toString(), + align: null, startOpacity: 0.15, stopOpacity: 0.05, @@ -37,6 +38,7 @@ export function prepareThresholdsForHistogram(calibrations: Calibrations): Histo // value. if (idx === 0 || idx === calibrations.thresholds.length - 1) { thresholdCardinality[idx] > 0 ? thresholdRange.min = threshold : thresholdRange.max = threshold + thresholdCardinality[idx] > 0 ? thresholdRange.align = "left" : thresholdRange.align = "right" } // If the threshold cardinality is positive, the threshold maximum will be the next threshold. The opposite is true // for thresholds with negative cardinality. @@ -44,9 +46,11 @@ export function prepareThresholdsForHistogram(calibrations: Calibrations): Histo if (thresholdCardinality[idx] > 0) { thresholdRange.min = threshold thresholdRange.max = calibrations.thresholds[idx+1] + thresholdRange.align = "left" } else { thresholdRange.min = calibrations.thresholds[idx-1] thresholdRange.max = threshold + thresholdRange.align = "right" } } diff --git a/src/lib/histogram.ts b/src/lib/histogram.ts index c573b851..f64af7ee 100644 --- a/src/lib/histogram.ts +++ b/src/lib/histogram.ts @@ -71,6 +71,9 @@ export interface HistogramShader { /** The displayed title of this region. */ title: string | null + /** The alignment of the title of this region. */ + align: "left" | "right" | "center" | null + /** The color of this shaded region. */ color: string | null @@ -306,6 +309,41 @@ export default function makeHistogram(): Histogram { return _.isString(field) ? (_.get(d, field) as T) : (field(d) as T) } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Canvas placement calculations + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + const alignTextInRegion = (min: number, max: number, align: string | null) => { + switch(align){ + case "left": + return min + case "right": + return max + default: + return (min + max) / 2 + } + } + + const padTextInElement = (elem: d3.Selection, align: string | null, text: string | null) => { + const tempText = elem.append('g') + .append('text') + .style('visibility', 'hidden') + .text(text) + const textWidth = (tempText.node()?.getBoundingClientRect()?.width || 0) + + console.log(textWidth) + + switch (align) { + case "left": + return 10 + case "right": + return -textWidth + default: + return 0 + } + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Hovering ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -882,7 +920,7 @@ export default function makeHistogram(): Histogram { .style('fill', (d) => d.thresholdColor || '#000000') .attr('x', (d) => { const span = visibleShaderRegion(d) - return xScale((span.min + span.max) / 2) + return xScale(alignTextInRegion(span.min, span.max, d.align)) + padTextInElement(shaderG, d.align, d.title) }) .attr('y', 15) .style('text-anchor', 'middle') diff --git a/src/lib/ranges.ts b/src/lib/ranges.ts index 0fa40f09..db0f1727 100644 --- a/src/lib/ranges.ts +++ b/src/lib/ranges.ts @@ -25,6 +25,7 @@ export function prepareRangesForHistogram(scoreRanges: ScoreSetRanges): Histogra min: range.range[0], max: range.range[1], title: range.label, + align: "center", color: rangeIsNormal ? NORMAL_RANGE_DEFAULT_COLOR : ABNORMAL_RANGE_DEFAULT_COLOR, thresholdColor: rangeIsNormal ? NORMAL_RANGE_DEFAULT_COLOR : ABNORMAL_RANGE_DEFAULT_COLOR, startOpacity: 0.15, From f4bcc66b49c50ff1af3c66962c9e775378019486 Mon Sep 17 00:00:00 2001 From: Ben Capodanno Date: Thu, 2 Jan 2025 11:19:57 -0800 Subject: [PATCH 005/136] Re-center Calibration Labels --- src/lib/calibrations.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lib/calibrations.ts b/src/lib/calibrations.ts index 37708e50..e6706805 100644 --- a/src/lib/calibrations.ts +++ b/src/lib/calibrations.ts @@ -26,7 +26,7 @@ export function prepareThresholdsForHistogram(calibrations: Calibrations): Histo color: thresholdColor, thresholdColor: thresholdColor, title: calibrations.evidenceStrengths[idx].toString(), - align: null, + align: "center", startOpacity: 0.15, stopOpacity: 0.05, @@ -38,7 +38,6 @@ export function prepareThresholdsForHistogram(calibrations: Calibrations): Histo // value. if (idx === 0 || idx === calibrations.thresholds.length - 1) { thresholdCardinality[idx] > 0 ? thresholdRange.min = threshold : thresholdRange.max = threshold - thresholdCardinality[idx] > 0 ? thresholdRange.align = "left" : thresholdRange.align = "right" } // If the threshold cardinality is positive, the threshold maximum will be the next threshold. The opposite is true // for thresholds with negative cardinality. @@ -46,11 +45,9 @@ export function prepareThresholdsForHistogram(calibrations: Calibrations): Histo if (thresholdCardinality[idx] > 0) { thresholdRange.min = threshold thresholdRange.max = calibrations.thresholds[idx+1] - thresholdRange.align = "left" } else { thresholdRange.min = calibrations.thresholds[idx-1] thresholdRange.max = threshold - thresholdRange.align = "right" } } From f92c28093ab6eaffcc84b6d101bce19c34505c08 Mon Sep 17 00:00:00 2001 From: Ben Capodanno Date: Thu, 2 Jan 2025 11:20:21 -0800 Subject: [PATCH 006/136] Remove Log Message in Histogram --- src/lib/histogram.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib/histogram.ts b/src/lib/histogram.ts index f64af7ee..aaf29918 100644 --- a/src/lib/histogram.ts +++ b/src/lib/histogram.ts @@ -332,8 +332,6 @@ export default function makeHistogram(): Histogram { .text(text) const textWidth = (tempText.node()?.getBoundingClientRect()?.width || 0) - console.log(textWidth) - switch (align) { case "left": return 10 From 6916f81505fbc0055e4b318cde8db2a3c5225880 Mon Sep 17 00:00:00 2001 From: Ben Capodanno Date: Thu, 2 Jan 2025 11:20:36 -0800 Subject: [PATCH 007/136] Refactor Odds Path Table into Child Component --- src/components/OddsPathTable.vue | 101 ++++++++++++++++++++++++ src/components/screens/ScoreSetView.vue | 77 ------------------ 2 files changed, 101 insertions(+), 77 deletions(-) create mode 100644 src/components/OddsPathTable.vue diff --git a/src/components/OddsPathTable.vue b/src/components/OddsPathTable.vue new file mode 100644 index 00000000..b3bbd2c4 --- /dev/null +++ b/src/components/OddsPathTable.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/components/screens/ScoreSetView.vue b/src/components/screens/ScoreSetView.vue index e5ea2063..ed83e3c6 100644 --- a/src/components/screens/ScoreSetView.vue +++ b/src/components/screens/ScoreSetView.vue @@ -111,24 +111,6 @@ @export-chart="setHeatmapExport" ref="heatmap" />
- - - - - - - - - - - - - -
Odds Path Abnormal*Odds Path Normal*
{{ evidenceStrengths.oddsOfPathogenicity.abnormal }}{{ evidenceStrengths.oddsOfPathogenicity.normal }}
{{ evidenceStrengths.evidenceCodes.abnormal }}{{ evidenceStrengths.evidenceCodes.normal }}
- -
- Click here for a preview of future clinical variant features. -
@@ -456,40 +438,6 @@ export default { ['familyName', 'givenName', 'orcidId'] ) }, - evidenceStrengths: function() { - if (this.config.CLINICAL_FEATURES_ENABLED) { - return { - 'urn:mavedb:00000050-a-1': { - oddsOfPathogenicity: { - abnormal: 24.9, - normal: 0.043 - }, - evidenceCodes: { - abnormal: 'PS3_Strong', - normal: 'BS3_Strong' - }, - source: 'https://pubmed.ncbi.nlm.nih.gov/36550560/' - }, - 'urn:mavedb:00000097-0-1': { - oddsOfPathogenicity: { - abnormal: 52.4, - normal: 0.02 - }, - evidenceCodes: { - abnormal: 'PS3_Strong', - normal: 'BS3_Strong' - }, - source: 'https://pubmed.ncbi.nlm.nih.gov/34793697/', - exampleVariant: { - urn: 'urn:mavedb:00000097-0-1#1697', - name: 'NM_007294.4(BRCA1):c.5237A>C (p.His1746Pro)' - } - } - }[this.item.urn] || null - } else { - return null - } - }, isMetaDataEmpty: function() { //If extraMetadata is empty, return value will be true. return Object.keys(this.item.extraMetadata).length === 0 @@ -991,31 +939,6 @@ export default { margin: 0 0.5em; } -/* Evidence strength */ - -table.mave-odds-path-table { - border-collapse: collapse; - margin: 1em auto 0.5em auto; -} - -table.mave-odds-path-table td, -table.mave-odds-path-table th { - border: 1px solid gray; - padding: 0.5em 1em; - text-align: center; -} - -table.mave-odds-path-table td.mave-evidence-code-PS3_Strong { - background-color: #b02418; - color: white; - font-weight: bold; -} -table.mave-odds-path-table td.mave-evidence-code-BS3_Strong { - background-color: #385492; - color: white; - font-weight: bold; -} - /* Formatting in Markdown blocks */ .mave-score-set-abstract { From ccb7ee1ab1d80240a07f78c85b196b9513b15afc Mon Sep 17 00:00:00 2001 From: Ben Capodanno Date: Thu, 2 Jan 2025 11:20:51 -0800 Subject: [PATCH 008/136] Emit Calibration Selection Changes --- src/components/CalibrationTable.vue | 40 ++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/components/CalibrationTable.vue b/src/components/CalibrationTable.vue index f9bcdf2f..f266a614 100644 --- a/src/components/CalibrationTable.vue +++ b/src/components/CalibrationTable.vue @@ -1,5 +1,10 @@ @@ -60,6 +62,7 @@ import makeHistogram, {DEFAULT_SERIES_COLOR, Histogram, HistogramSerieOptions, H import CalibrationTable from '@/components/CalibrationTable.vue' import { prepareThresholdsForHistogram } from '@/lib/calibrations' import { prepareRangesForHistogram } from '@/lib/ranges' +import OddsPathTable from './OddsPathTable.vue' const CLNSIG_FIELD = 'mavedb_clnsig' const CLNREVSTAT_FIELD = 'mavedb_clnrevstat' @@ -76,7 +79,7 @@ const DEFAULT_MIN_STAR_RATING = 1 export default defineComponent({ name: 'ScoreSetHistogram', - components: { Checkbox, Dropdown, RadioButton, Rating, TabMenu, CalibrationTable }, + components: { Checkbox, Dropdown, RadioButton, Rating, TabMenu, CalibrationTable, OddsPathTable }, emits: ['exportChart'], @@ -124,14 +127,13 @@ export default defineComponent({ data: function() { const scoreSetHasRanges = config.CLINICAL_FEATURES_ENABLED && this.scoreSet.scoreRanges != null - const selectedCalibrationKey = config.CLINICAL_FEATURES_ENABLED && this.scoreSet.scoreCalibrations ? Object.keys(this.scoreSet.scoreCalibrations)[0] : null return { config: config, activeViz: 0, activeShader: scoreSetHasRanges ? 'range' : 'null', - activeCalibrationKey: selectedCalibrationKey, + activeCalibrationKey: null, clinicalSignificanceClassificationOptions: CLINVAR_CLINICAL_SIGNIFICANCE_CLASSIFICATIONS, customMinStarRating: DEFAULT_MIN_STAR_RATING, @@ -463,6 +465,10 @@ export default defineComponent({ this.histogram.refresh() }, + childComponentSelectedCalibrations: function(calibrationKey: string) { + this.activeCalibrationKey = calibrationKey + }, + titleCase: (s) => s.replace (/^[-_]*(.)/, (_, c) => c.toUpperCase()) // Initial char (after -/_) .replace (/[-_]+(.)/g, (_, c) => ' ' + c.toUpperCase()) // First char after each -/_ From 55f4d50d0733c6983f2f5746990a822e3c08178d Mon Sep 17 00:00:00 2001 From: Ben Capodanno Date: Wed, 15 Jan 2025 10:01:42 -0800 Subject: [PATCH 010/136] Filter NaN Apply Fields from Heatmap During Data Preparation --- src/lib/histogram.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/histogram.ts b/src/lib/histogram.ts index aaf29918..c9cb5a86 100644 --- a/src/lib/histogram.ts +++ b/src/lib/histogram.ts @@ -239,8 +239,11 @@ export default function makeHistogram(): Histogram { ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const prepareData = () => { + // Filter NaN entries from the data property. We're unable to place such scores on the histogram. + const filteredData = data.filter((d) => !isNaN(applyField(d, valueField))) + // Bin all the data, regardless of what series each datum belongs to. - const overallBins = d3.bin().thresholds(numBins).value((d) => applyField(d, valueField))(data) + const overallBins = d3.bin().thresholds(numBins).value((d) => applyField(d, valueField))(filteredData) const thresholds = (overallBins.length > 0 ? [overallBins[0].x0, ...overallBins.map((bin) => bin.x1)] : []) .filter((t) => t != null) const domain: [number, number] = [thresholds[0] || 0, thresholds[thresholds.length - 1] || 0] @@ -252,7 +255,7 @@ export default function makeHistogram(): Histogram { .thresholds(thresholds) .value((d) => applyField(d, valueField)) series = seriesOptions.map((serieOptions, i) => ({ - bins: binClassifier(data.filter((datum) => classifier(datum).includes(i))), + bins: binClassifier(filteredData.filter((datum) => classifier(datum).includes(i))), x0: null, x1: null, maxBinSize: 0, From e9e0b59387bc9439dd7dde1b299a647977c9b40e Mon Sep 17 00:00:00 2001 From: Sally Grindstaff Date: Mon, 25 Nov 2024 16:23:40 -0800 Subject: [PATCH 011/136] Display badges for official collections on score set page --- src/components/CollectionBadge.vue | 69 +++++++++++++++++++++++++ src/components/screens/ScoreSetView.vue | 17 +++++- 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/components/CollectionBadge.vue diff --git a/src/components/CollectionBadge.vue b/src/components/CollectionBadge.vue new file mode 100644 index 00000000..e0d5e0d2 --- /dev/null +++ b/src/components/CollectionBadge.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/src/components/screens/ScoreSetView.vue b/src/components/screens/ScoreSetView.vue index ed83e3c6..cd1dee13 100644 --- a/src/components/screens/ScoreSetView.vue +++ b/src/components/screens/ScoreSetView.vue @@ -53,6 +53,13 @@ {{ item.title || 'Untitled score set' }} {{ item.urn }}
+
+ +
@@ -410,6 +417,7 @@ import Message from 'primevue/message' import ProgressSpinner from 'primevue/progressspinner' import ScrollPanel from 'primevue/scrollpanel'; +import CollectionBadge from '@/components/CollectionBadge' import ScoreSetHeatmap from '@/components/ScoreSetHeatmap' import ScoreSetHistogram from '@/components/ScoreSetHistogram' import EntityLink from '@/components/common/EntityLink' @@ -430,7 +438,7 @@ import { ref } from 'vue' export default { name: 'ScoreSetView', - components: { Accordion, AccordionTab, AutoComplete, Button, Chip, DefaultLayout, EntityLink, ScoreSetHeatmap, ScoreSetHistogram, TabView, TabPanel, Message, DataTable, Column, ProgressSpinner, ScrollPanel, PageLoading, ItemNotFound }, + components: { Accordion, AccordionTab, AutoComplete, Button, Chip, CollectionBadge, DefaultLayout, EntityLink, ScoreSetHeatmap, ScoreSetHistogram, TabView, TabPanel, Message, DataTable, Column, ProgressSpinner, ScrollPanel, PageLoading, ItemNotFound }, computed: { contributors: function() { return _.sortBy( @@ -914,6 +922,13 @@ export default { width: 100%; } +.mave-collection-badges { + flex: 1 1 auto; + padding: 0 0 0 7px; + font-size: 12px; + line-height: 29px; +} + /* Score set details */ .mave-score-set-section-title { From b5bdd02584f85f2e354092dddc7ac199f9baafe6 Mon Sep 17 00:00:00 2001 From: Sally Grindstaff Date: Mon, 25 Nov 2024 16:25:12 -0800 Subject: [PATCH 012/136] Add mave title styling for collection badge display --- src/assets/layout.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/assets/layout.css b/src/assets/layout.css index 75562cb3..f745f02e 100644 --- a/src/assets/layout.css +++ b/src/assets/layout.css @@ -15,6 +15,7 @@ } .mave-screen-title-bar { + align-items: center; display: flex; flex-direction: row; justify-content: space-between; @@ -24,6 +25,10 @@ margin: 10px 0; } +.mave-screen-title { + flex: 0 0 auto; +} + .mave-screen-title-controls { flex: 0 0 auto; } From aac0dfc092379145c5cddc12f8a092df99e25b27 Mon Sep 17 00:00:00 2001 From: Sally Grindstaff Date: Mon, 25 Nov 2024 16:25:57 -0800 Subject: [PATCH 013/136] Draft components to add and create collections --- src/components/CollectionAdder.vue | 186 +++++++++++++++++++++++++++ src/components/CollectionCreator.vue | 172 +++++++++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 src/components/CollectionAdder.vue create mode 100644 src/components/CollectionCreator.vue diff --git a/src/components/CollectionAdder.vue b/src/components/CollectionAdder.vue new file mode 100644 index 00000000..73bc163d --- /dev/null +++ b/src/components/CollectionAdder.vue @@ -0,0 +1,186 @@ + + + + + \ No newline at end of file diff --git a/src/components/CollectionCreator.vue b/src/components/CollectionCreator.vue new file mode 100644 index 00000000..794d3d0f --- /dev/null +++ b/src/components/CollectionCreator.vue @@ -0,0 +1,172 @@ + + + + + From ce2e7539d82d26238490850580c1a05721093a2d Mon Sep 17 00:00:00 2001 From: Sally Grindstaff Date: Wed, 4 Dec 2024 10:42:11 -0800 Subject: [PATCH 014/136] Create collection from collection adder dialog --- src/components/CollectionAdder.vue | 36 ++++- src/components/CollectionCreator.vue | 207 +++++++++++++++++++-------- 2 files changed, 177 insertions(+), 66 deletions(-) diff --git a/src/components/CollectionAdder.vue b/src/components/CollectionAdder.vue index 73bc163d..bb658a82 100644 --- a/src/components/CollectionAdder.vue +++ b/src/components/CollectionAdder.vue @@ -8,7 +8,13 @@ label="Add to collection" > - +
@@ -18,21 +24,30 @@ option-label="name" option-value="urn" placeholder="Select a collection" - lass="w-full md:w-14rem" + class="w-full md:w-14rem" />
- - +
- - + +
@@ -154,7 +169,14 @@ export default { } }, - // fetch collection + childComponentCreatedCollection: function(collection) { + // set creatorVisible to false + this.creatorVisible = false + // refresh the list of collections to display (or at least add the new collection) + this.collections.push(collection) + // select the new collection in the dropdown + this.selectedCollectionUrn = collection.urn + } } } diff --git a/src/components/CollectionCreator.vue b/src/components/CollectionCreator.vue index 794d3d0f..14abff07 100644 --- a/src/components/CollectionCreator.vue +++ b/src/components/CollectionCreator.vue @@ -21,12 +21,7 @@ + -->
Add contributors @@ -35,26 +30,64 @@ + + + + + + + + -
-
+
+
@@ -64,6 +97,7 @@ import axios from 'axios' import _ from 'lodash' +import Button from 'primevue/button' import Chips from 'primevue/chips' import InputSwitch from 'primevue/inputswitch' import InputText from 'primevue/inputtext' @@ -74,94 +108,149 @@ import {ORCID_ID_REGEX} from '@/lib/orcid' export default { name: 'CollectionCreator', - components: { Chips, InputSwitch, InputText }, + emits: ['createdCollection', 'canceled'], + + components: { Button, Chips, InputSwitch, InputText }, data: () => ({ collectionName: null, collectionDescription: null, collectionPrivate: true, - viewers: [], - editors: [], - admins: [] + contributors: { + viewers: [], + editors: [], + admins: [] + } }), methods: { - - clearViewerSearch: function() { + clearContributorSearch: function(contributorType) { // This could change with a new PrimeVue version. - const input = this.$refs.viewersInput + const input = this.$refs[`${contributorType}Input`] input.$refs.input.value = '' }, lookupUser: async function(orcidId) { // look up MaveDB user by Orcid ID - let allUsers = null let user = null try { - allUsers = (await axios.get(`${config.apiBaseUrl}/users`)).data - user = allUsers.filter((u) => u.username == orcidId) - // TODO is this too inefficient? - // should we load all users ahead of time so we don't have to do this call every time a new user is added? - + user = (await axios.get(`${config.apiBaseUrl}/users/${orcidId}`)).data } catch (err) { // Assume that the error was 404 Not Found. } - if (user) { - return orcidUser - } else { - // TODO throw error - } + return user }, - //TODO when validating orcid id, do not allow duplicates between viewers, editors, and admins + newContributorsAdded: async function(contributorType, event) { + // TODO enforce that contributorType can only be "viewers", "editors", or "admins" + const newContributors = event.value - newViewersAdded: async function(event) { - const newViewers = event.value - - // Convert any strings to ORCID users without names. - this.viewers = this.viewers.map((c) => _.isString(c) ? {orcidId: c} : c) + // Convert any strings to ORCID users without names, + // and create all contributors list, which will be checked against for duplicates + const contributorTypes = ["viewers", "editors", "admins"] + let allContributors = [] + for (const contributorTypeTemp of contributorTypes) { + this.contributors[contributorTypeTemp] = this.contributors[contributorTypeTemp].map((c) => _.isString(c) ? {orcidId: c} : c) + allContributors = allContributors.concat(this.contributors[contributorTypeTemp]) + } // Validate and look up each new contributor. - for (const newViewer of newViewers) { - if (_.isString(newViewer)) { - const orcidId = newViewer.trim() - if (orcidId && this.viewers.filter((v) => v.orcidId == orcidId).length > 1) { - const firstIndex = _.findIndex(this.viewers, (c) => c.orcidId == orcidId) - _.remove(this.viewers, (v, i) => i > firstIndex && v.orcidId == orcidId) + for (const newContributor of newContributors) { + if (_.isString(newContributor)) { + const orcidId = newContributor.trim() + // check for duplicates in contributorType, and then in entire list + // need to check in contributorType separately because if there is a duplicate within + // that list, then we need the index + // use else if, because there are already no duplicates between lists. + // so if there is a match in one list, there will be no matches in the other lists + if (orcidId && this.contributors[contributorType].filter((c) => c.orcidId == orcidId).length > 1) { + const firstIndex = _.findIndex(this.contributors[contributorType], (c) => c.orcidId == orcidId) + _.remove(this.contributors[contributorType], (c, i) => i > firstIndex && c.orcidId == orcidId) + } else if (orcidId && allContributors.filter((c) => c.orcidId == orcidId).length > 1) { + // if there is a match with another contributor type, remove the contributor + // from the list it was just added to (this.$contributorType) + _.remove(this.contributors[contributorType], (c) => c.orcidId == orcidId) + this.$toast.add({ + life: 3000, + severity: 'warn', + summary: "Each contributor can only be assigned to one role per collection." + }) } else if (orcidId && ORCID_ID_REGEX.test(orcidId)) { // Look up the ORCID ID in MaveDB (only allow for existing MaveDB users to be listed as contributors). - //const orcidUser = await this.lookupOrcidUser(orcidId) const user = await this.lookupUser(orcidId) if (user) { - // If found, update matching viewers. (There should only be one.) - for (const viewer of this.viewers) { - if (viewer.orcidId == user.orcidId) { - _.merge(viewer, user) + // If found, update matching contributors. (There should only be one.) + for (const contributor of this.contributors[contributorType]) { + if (contributor.orcidId == user.orcidId) { + _.merge(contributor, user) + } } - } } else { - // Otherwise remove the ciewer. - _.remove(this.viewers, (c) => c.orcidId == orcidId) - this.$toast.add({ - life: 3000, - severity: 'warn', - summary: `No MaveDB user was found with ORCID ID ${orcidId}.` - }) + // Otherwise remove the contributor. + _.remove(this.contributors[contributorType], (c) => c.orcidId == orcidId) + this.$toast.add({ + life: 3000, + severity: 'warn', + summary: `No MaveDB user was found with ORCID ID ${orcidId}.` + }) } } else { - _.remove(this.contributors, (c) => c.orcidId == orcidId) + _.remove(this.contributors[contributorType], (c) => c.orcidId == orcidId) this.$toast.add({ - life: 3000, - severity: 'warn', - summary: `${orcidId} is not a valid ORCID ID` + life: 3000, + severity: 'warn', + summary: `${orcidId} is not a valid ORCID ID` }) } } } + }, + saveCollection: async function() { + // check for required fields + if (this.collectionName != null) { + // create object that has field names assigned to this. + const newCollection = { + name: this.collectionName, + description: this.collectionDescription, + private: this.collectionPrivate, + viewers: this.contributors.viewers, + editors: this.contributors.editors, + admins: this.contributors.admins + } + // then, try to call the api with the object as body and catch errors - if error, send toast message with severity = error + let response = null + try { + response = await axios.post(`${config.apiBaseUrl}/collections`, newCollection) + } catch (e) { + response = e.response || { status: 500 } + this.$toast.add({ severity: 'error', summary: 'Error', life: 3000 }) + } + // if there is an error, probably keep the dialog open. log errors to console? + // then, if status is 200, throw toast success message and emit the saved collection + if (response.status == 200) { + const savedCollection = response.data + this.$toast.add({ severity: 'success', summary: 'Created new collection.', life: 3000 }) + this.$emit('createdCollection', savedCollection) + } else { + // TODO log errors to console here + } + } else { + this.$toast.add({ + life: 3000, + severity: 'warn', + summary: "Must provide collection name" + }) + } + }, + + cancel: function() { + this.$emit('canceled') + } + } } From 2635a2b123983d5e31cc331192006e7890847aab Mon Sep 17 00:00:00 2001 From: Sally Grindstaff Date: Wed, 4 Dec 2024 12:55:46 -0800 Subject: [PATCH 015/136] Add collection view --- src/components/screens/CollectionView.vue | 64 +++++++++++++++++++++++ src/lib/item-types.js | 5 ++ src/router/index.js | 6 +++ 3 files changed, 75 insertions(+) create mode 100644 src/components/screens/CollectionView.vue diff --git a/src/components/screens/CollectionView.vue b/src/components/screens/CollectionView.vue new file mode 100644 index 00000000..a59b9b6a --- /dev/null +++ b/src/components/screens/CollectionView.vue @@ -0,0 +1,64 @@ + + + + + \ No newline at end of file diff --git a/src/lib/item-types.js b/src/lib/item-types.js index c19d339c..15a1d930 100644 --- a/src/lib/item-types.js +++ b/src/lib/item-types.js @@ -241,6 +241,11 @@ const itemTypes = { restCollectionName: 'score-sets', primaryKey: 'urn' }, + 'collection': { + name: 'collection', + restCollectionName: 'collections', + primaryKey: 'urn' + }, 'target-gene-search': { name: 'target-gene', // TODO Redundant, change this structure restCollectionName: 'target-genes', diff --git a/src/router/index.js b/src/router/index.js index f833875b..9e249133 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,5 +1,6 @@ import {createRouter, createWebHistory} from 'vue-router' +import CollectionView from '@/components/screens/CollectionView' import DashboardView from '@/components/screens/DashboardView' import DocumentationView from '@/components/screens/DocumentationView' import ExperimentCreator from '@/components/screens/ExperimentCreator' @@ -102,6 +103,11 @@ const routes = [{ props: (route) => ({ itemId: route.params.urn, }) +}, { + path: '/collections/:urn', + name: 'collection', + component: CollectionView, + props: (route) => ({itemId: route.params.urn}) }, ...config.CLINICAL_FEATURES_ENABLED ? [{ path: '/variants/:urn', name: 'variant', From 351306a733391ba93847ec669f4091bf33dc978c Mon Sep 17 00:00:00 2001 From: Sally Grindstaff Date: Wed, 4 Dec 2024 15:41:16 -0800 Subject: [PATCH 016/136] Do not display owned collections Collection owners are automatically assigned as collection admins (see VariantEffect/mavedb-api@0ce14cf). --- src/components/CollectionAdder.vue | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/CollectionAdder.vue b/src/components/CollectionAdder.vue index bb658a82..fe077115 100644 --- a/src/components/CollectionAdder.vue +++ b/src/components/CollectionAdder.vue @@ -97,10 +97,9 @@ export default { if (response.status == 200) { const allCollections = response.data - const ownedCollections = allCollections["owner"] const adminCollections = allCollections["admin"] const editorCollections = allCollections["editor"] - this.collections = ownedCollections.concat(adminCollections, editorCollections) + this.collections = adminCollections.concat(editorCollections) } else if (response.data && response.data.detail) { // TODO what to do in event of error? } From 1c91b53a9dd7c37d0854a7f7c92b1239ef3c9c08 Mon Sep 17 00:00:00 2001 From: Sally Grindstaff Date: Mon, 9 Dec 2024 14:04:31 -0800 Subject: [PATCH 017/136] Add content to collection view --- src/components/screens/CollectionView.vue | 101 +++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/src/components/screens/CollectionView.vue b/src/components/screens/CollectionView.vue index a59b9b6a..5fc7c2da 100644 --- a/src/components/screens/CollectionView.vue +++ b/src/components/screens/CollectionView.vue @@ -1,7 +1,79 @@