Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions js/components/charts/chartVenn.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<div class="venn-chart">
<div class="container">
<button title="PNG" class="export-button" data-bind="click: $component.export">PNG</button>
<button title="SVG" class="exportSvg-button" data-bind="click: $component.exportSvg">SVG</button>
<div id="venn"></div>
<div data-bind="attr: { id: diagramId }"></div>
<button class="btn btn-xs btn-primary export-button" data-bind="click: exportPng">
<i class="fa fa-save" aria-hidden="true"></i> PNG
</button>
<button class="btn btn-xs btn-primary exportSvg-button" data-bind="click: exportSvg">
<i class="fa fa-save" aria-hidden="true"></i> SVG
</button>
</div>
</div>
</div>
120 changes: 97 additions & 23 deletions js/components/charts/venn.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,58 @@ define([

class Venn extends Component {

constructor(params,container){
constructor(params, container){
super(params);
this.firstConceptSet = params.firstConceptSet();
this.secondConceptSet = params.secondConceptSet();
this.data = params.data();
this.container = container;

// Use provided diagramId or generate a unique one
this.diagramId = params.diagramId || `venn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

this.chartName = ko.computed(() => {

return `${this.firstConceptSet}_${this.secondConceptSet}_venn`.replaceAll(' ', '_')
});

this.conceptInBothConceptSets = [];
this.conceptInFirstConceptSetOnly = [];
this.conceptInSecondConceptSetOnly = [];
this.selectOutsideConceptSet = params.lastSelectedMatchFilter.extend({notify: 'always'});
this.updateOutsideFilters = params.updateOutsideFilters;

this.sets = ko.computed(() => {
// Clear arrays for recomputation
this.conceptInBothConceptSets = [];
this.conceptInFirstConceptSetOnly = [];
this.conceptInSecondConceptSetOnly = [];

// Helper function to get concept name, preferring vocab-specific names
const getConceptName = (concept) => {
// For cross-vocabulary comparisons, prefer vocab-specific names
if (concept.vocab1ConceptName && concept.vocab2ConceptName) {
// Both vocabularies have the concept, use vocab1 name (or could combine)
return concept.vocab1ConceptName;
} else if (concept.vocab1ConceptName) {
return concept.vocab1ConceptName;
} else if (concept.vocab2ConceptName) {
return concept.vocab2ConceptName;
}
// Fallback - should not happen in normal cases
return 'Unknown';
};

this.data.forEach(concept => {
if (concept.conceptIn1Only === 1) {
this.conceptInFirstConceptSetOnly.push(concept.conceptName);
const conceptName = getConceptName(concept);

if (concept.conceptInCS1Only === 1) {
this.conceptInFirstConceptSetOnly.push(conceptName);
}
if (concept.conceptIn2Only === 1) {
this.conceptInSecondConceptSetOnly.push(concept.conceptName);
if (concept.conceptInCS2Only === 1) {
this.conceptInSecondConceptSetOnly.push(conceptName);
}
if (concept.conceptIn1And2 === 1) {
this.conceptInBothConceptSets.push(concept.conceptName);
if (concept.conceptInCS1AndCS2 === 1) {
this.conceptInBothConceptSets.push(conceptName);
}
});

Expand All @@ -65,8 +93,8 @@ define([
}

const conceptSets = [
{sets: ['CS1'], size: lengthFirstConceptSets, tooltipText: this.conceptInFirstConceptSetOnly, amountOnly:this.conceptInFirstConceptSetOnly.length, count: this.conceptInFirstConceptSetOnly.length + this.conceptInBothConceptSets.length, name: this.firstConceptSet, key: '1 Only' },
{sets: ['CS2'], size: lengthSecondConceptSets, tooltipText: this.conceptInSecondConceptSetOnly,amountOnly:this.conceptInSecondConceptSetOnly.length,count: this.conceptInSecondConceptSetOnly.length + this.conceptInBothConceptSets.length, name: this.secondConceptSet, key: '2 Only'},
{sets: ['CS1'], size: lengthFirstConceptSets, tooltipText: this.conceptInFirstConceptSetOnly, amountOnly:this.conceptInFirstConceptSetOnly.length, count: this.conceptInFirstConceptSetOnly.length + this.conceptInBothConceptSets.length, name: this.firstConceptSet, key: 'CS1 Only' },
{sets: ['CS2'], size: lengthSecondConceptSets, tooltipText: this.conceptInSecondConceptSetOnly,amountOnly:this.conceptInSecondConceptSetOnly.length,count: this.conceptInSecondConceptSetOnly.length + this.conceptInBothConceptSets.length, name: this.secondConceptSet, key: 'CS2 Only'},
];
if (this.conceptInBothConceptSets.length > 0) {
conceptSets.push({
Expand All @@ -82,15 +110,41 @@ define([
return conceptSets.sort((a,b) => b.size - a.size);
});

// Defer rendering until after DOM is ready
this.subscriptions = [];

// Use setTimeout to ensure DOM element exists
setTimeout(() => {
this.renderDiagram();
}, 100);
}

renderDiagram() {
const diagramElement = document.getElementById(this.diagramId);

if (!diagramElement) {
console.error(`Venn diagram container #${this.diagramId} not found`);
return;
}

// Clear any existing content
d3.select(`#${this.diagramId}`).selectAll("*").remove();

let chart = venn.VennDiagram();
chart.wrap(false)
.height(450);

const textY = [0,0,30];
const colors = ['#1f77b4','#17becf', '#d62728'];
const defaultColors = ['#d9edf7','#bdf9ff','#f2dede'];
let div = d3.select("#venn").datum(this.sets()).call(chart);
div.selectAll("text").attr("y", function(d,i) { return textY[i] + (+d3.select(this).attr("y")); }).style("font-size", '12px').style("fill", 'black').style('visibility', function(d) { return d.amountOnly ? 'visible' : 'hidden'});

let div = d3.select(`#${this.diagramId}`).datum(this.sets()).call(chart);

div.selectAll("text")
.attr("y", function(d,i) { return textY[i] + (+d3.select(this).attr("y")); })
.style("font-size", '12px')
.style("fill", 'black')
.style('visibility', function(d) { return d.amountOnly ? 'visible' : 'hidden'});

div.selectAll("path")
.style("stroke", function(d,i) { return colors[i]; })
Expand All @@ -111,12 +165,15 @@ define([

// add a tooltip
let tooltip = d3.select("body").append("div")
.attr("class", "venntooltip");
.attr("class", "venntooltip")
.attr("data-diagram-id", this.diagramId); // Track which diagram this tooltip belongs to

const self = this;

// add listeners to all the groups to display tooltip on mouseover
div.selectAll("g")
.on("click", function(d) {
params.updateOutsideFilters(d.key);
self.updateOutsideFilters(d.key);
})

.on("mouseover", function(d) {
Expand Down Expand Up @@ -147,15 +204,13 @@ define([
.style("stroke-width", 2);
});

const subscriptions = [];
subscriptions.push(
this.selectOutsideConceptSet.subscribe(function (newValue) {
this.subscriptions.push(
this.selectOutsideConceptSet.subscribe((newValue) => {
if (this.selectOutsideConceptSet !== "") {
div.selectAll("path")
.filter(function(d) { return d.key === newValue;})
.classed("selected", function() { return !d3.select(this).classed("selected"); })
.style('fill', function() {

if (d3.select(this).classed("selected")) {
return d3.select(this).attr('color');
} else {
Expand Down Expand Up @@ -209,15 +264,34 @@ define([
return [csLeftCircle,csRightCircle,csCommonCircle];
}

export() {
const svg = this.container.element.querySelector('svg');
ChartUtils.downloadSvgAsPng(svg, this.chartName() || "untitled.png");
exportPng() {
const svg = document.querySelector(`#${this.diagramId} svg`);
if (svg) {
ChartUtils.downloadSvgAsPng(svg, this.chartName() || "untitled.png");
}
}

exportSvg() {
const svg = this.container.element.querySelector('svg');
ChartUtils.downloadSvg(svg, this.chartName() + ".svg" || "untitled.svg");
const svg = document.querySelector(`#${this.diagramId} svg`);
if (svg) {
ChartUtils.downloadSvg(svg, this.chartName() + ".svg" || "untitled.svg");
}
}

dispose() {
// Clean up subscriptions
if (this.subscriptions) {
this.subscriptions.forEach(sub => sub.dispose());
}

// Remove tooltip associated with this diagram
d3.selectAll(`.venntooltip[data-diagram-id="${this.diagramId}"]`).remove();

// Clear the diagram
d3.select(`#${this.diagramId}`).selectAll("*").remove();

super.dispose();
}
}

return commonUtils.build('venn-diagram', Venn, view);
Expand Down
1 change: 1 addition & 0 deletions js/const.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ define([
jobs: () => `${config.api.url}job/execution?comprehensivePage=true`,
job: (id) => `${config.api.url}job/${id}`,
jobByName: (name, type) => `${config.api.url}job/type/${type}/name/${name}`,
jobArtifact: (executionId) => `${config.api.url}job/${executionId}/artifact`,
};

const applicationStatuses = {
Expand Down
97 changes: 42 additions & 55 deletions js/pages/concept-sets/components/tabs/conceptset-compare.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
<th><i id="dtConeptManagerDPC" class="fa fa-database" aria-hidden="true"></i> DPC</th>
<th data-bind="text: ko.i18n('columns.domain', 'Domain')"></th>
<th data-bind="text: ko.i18n('columns.vocabulary', 'Vocabulary')"></th>
<th data-bind="text: ko.i18n('columns.standardConcept', 'Standard Concept')"></th>
<th data-bind="text: ko.i18n('columns.invalidReason', 'Invalid Reason')"></th>
</tr>
</script>

Expand All @@ -26,6 +28,17 @@
<div class="input-group">
<input class="form-control" type="text" disabled data-bind="value: $component.compareCS1Caption" />
<span class="input-group-btn">
<div class="btn-group" data-bind="enable: true">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<span data-bind="text: selectedVocabularyCS1Display"></span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" data-bind="foreach: vocabularySources">
<li><a href="#"
data-bind="text: '[' + sourceName + '] ' + version, click: function() { $parent.selectedVocabularyCS1($data); }"></a></li>
</ul>
</div>
<button class="btn btn-primary" data-bind="click: chooseCS1">
<i class="fa fa-folder-open"></i>
</button>
Expand All @@ -50,6 +63,17 @@
value: $component.compareCS2Caption
" />
<span class="input-group-btn">
<div class="btn-group" data-bind="enable: true">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<span data-bind="text: selectedVocabularyCS2Display"></span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" data-bind="foreach: vocabularySources">
<li><a href="#"
data-bind="text: '[' + sourceName + '] ' + version, click: function() { $parent.selectedVocabularyCS2($data); }"></a></li>
</ul>
</div>
<button class="btn btn-primary" data-bind="click:chooseCS2">
<i class="fa fa-folder-open"></i>
</button>
Expand Down Expand Up @@ -85,65 +109,28 @@
</div>
<div data-bind="if: $component.compareError()">
<div class="paddedWrapper">
<span class="compare-message" data-bind="text: ko.i18n('cs.browser.compare.sameWarning', 'You cannot compare the same concept sets.')"></span>
<span class="compare-message" data-bind="text: ko.i18n('cs.browser.compare.sameWarning', 'You cannot compare the same concept set over the same vocabulary. ')"></span>
</div>
</div>
<div data-bind="if: $component.compareReady() && $component.compareUnchanged() && $component.compareResults()">
<div class="heading compare-results">
<b data-bind="text: ko.i18n('cs.browser.compare.comparisonResults', 'Comparison Results')"></b>
</div>
<div class="container-fluid">
<div class="pull-left">
<button class="btn btn-sm btn-primary" data-bind="click: showSaveNewModal, text: ko.i18n('cs.browser.compare.saveMessage', 'Save New Concept Set From Selection Below')"></button>
</div>
<div class="pull-right compare-results">
<i data-bind="attr: { class: $component.recordCountClass }" aria-hidden="true">
</i>
<span data-bind="text: ko.i18n('cs.browser.compare.viewCountMessage', 'View database record counts (RC) and descendant record counts (DRC) for:')"></span>
<select data-bind="
options: $component.resultSources,
optionsText: 'sourceName',
optionsValue: 'sourceKey',
value: $component.currentResultSource().sourceKey,
event: { change: $component.refreshRecordCounts }
"></select>
</div>
</div>
<div class="panel panel-default" data-bind="collapsable: showDiagram, collapseTargetClass: 'panel-body', collapseOptions: { selectorClass: 'panel-heading', collapsabledClass: 'active' }">
<div class="panel-heading panel-subheading panel-heading-collapsible div-enabled" data-bind="click: toggleShowDiagram">
<span class="glyphicon-chevron-up"><i class="fas fa-chart-pie"></i> <span data-bind="text: ko.i18n('cs.browser.compare.vennDiagram', 'Venn Diagram')"></span></span>
</div>
<div class="panel-body collapse">
<!-- ko if: !compareResultsSame() -->
<div class="compare-results-same" data-bind="text: ko.i18n('cs.browser.compare.sameConcepts', 'Compared Concept Sets contain Concepts which are identical')"></div>
<!-- /ko -->
<!-- ko if: compareResultsSame() -->
<venn-diagram params="{
data: compareResults,
firstConceptSet: compareCS1Caption,
secondConceptSet:compareCS2Caption,
updateOutsideFilters: $component.updateOutsideFilters,
lastSelectedMatchFilter: $component.lastSelectedMatchFilter
}"></venn-diagram>
<!-- /ko -->
</div>
<div data-bind="if: $component.compareFailed()">
<div class="paddedWrapper">
<span class="compare-message" data-bind="text: $component.compareFailedMessage()"></span>
</div>
<div id="compareResults">
<faceted-datatable params="{
reference:$component.compareResults,
outsideFilters: $component.outsideFilters,
updateLastSelectedMatchFilter: $component.updateLastSelectedMatchFilter,
columns: compareResultsColumns,
options:compareResultsOptions,
order: $component.compareResultsOptions.order,
pageLength: $component.compareResultsOptions.pageLength,
lengthMenu: $component.compareResultsOptions.lengthMenu,
headersTemplateId: 'conceptsets-comparison-headers',
language: ko.i18n('datatable.language')
}">
</faceted-datatable>

</div>

</div>
<div data-bind="if: $component.compareReady() && $component.compareUnchanged() && $component.allCompareResults()">
<div class="heading compare-results">
<b data-bind="text: ko.i18n('cs.browser.compare.comparisonResults', 'Comparison Results')"></b>
</div>

<!-- NEW: Tab component to display results -->
<tabs params="
componentParams: $component.componentParams,
selectedTab: selectedResultsTab,
tabs: resultsTabs
"></tabs>

</div>
</div>

Expand Down
Loading
Loading