Skip to content

Commit

Permalink
Merge pull request #138 from ExposuresProvider/improve-cam-frontend
Browse files Browse the repository at this point in the history
This PR implements a number of improvements to the CAM-KP Frontend:
- Reversed predicates should now be removed: any "reversed" predicate is reversed during loading, and duplicate predicates are removed.
- Added support for filtering search by model URL, allowing you to select a particular kind of model (e.g. CTD).
- We now AND subject-or-object searches instead of OR-ing them.
- Improved display of search results (which still don't make a heck of a lot of sense, but are better than they were previously).
- The "Edges" table can now be exported as a CSV file.
- The "Edges" table now defaults to a minimum display, but additional information (such as description, information content and equivalent identifiers) can be toggled on if needed.
- Added a "Relationships" table showing all edges from one CURIE to another.
- Improved displayed messages, such as when no model has been loaded and "Download in progress" messages.
- Updated query to Neo4J 5 (which requires `properties(edge)` to get the properties for an edge).
- Software code improvements, such as getting rid of the ListCAMs component and merging its contents into SearchCAMs and improved DisplayCAM organization.
- Added a shared method for mapping a URL to a CURIE.
  • Loading branch information
gaurav authored Aug 21, 2024
2 parents 96745d3 + 1f322d4 commit 2c2a37b
Show file tree
Hide file tree
Showing 8 changed files with 5,201 additions and 4,402 deletions.
9,057 changes: 4,830 additions & 4,227 deletions frontend/package-lock.json

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.5.5",
"@astrojs/vue": "^4.0.8",
"@astrojs/check": "^0.9.2",
"@astrojs/vue": "^4.5.0",
"@popperjs/core": "^2.11.8",
"@types/bootstrap": "^5.2.10",
"astro": "^4.4.4",
"astro": "^4.14.2",
"bootstrap": "^5.3.3",
"typescript": "^5.3.3",
"vue": "^3.4.19"
"export-to-csv": "^1.3.0",
"lodash": "^4.17.21",
"typescript": "^5.5.4",
"vue": "^3.4.38"
}
}
27 changes: 13 additions & 14 deletions frontend/src/components/CAMKPFrontend.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
<script setup lang="ts">
import ListCAMs from "./ListCAMs.vue";
import { ref } from 'vue'
import DisplayCAM from "./DisplayCAM.vue";
import SearchCAMs from "./SearchCAMs.vue";
// Some editable
const automatCAMKPEndpoint = ref("https://automat.renci.org/cam-kp")
const camList = ref<string[]>([]);
const selectedModel = ref("")
const selectedModelURL = ref('')
const searchIds = ref<Set<string>>(new Set<string>());
function changeSelectedModel(modelName: string) {
function changeSelectedModel(modelURL: string) {
// Allows other components to change the selected model.
selectedModel.value = modelName;
selectedModelURL.value = ''; // Reset the current view.
selectedModelURL.value = modelURL;
}
function changeCAMList(newCamList: string[]) {
// Allows other components to change the CAM list.
camList.value = newCamList;
function changeSearchIds(searchIdList: Set<string>) {
// Allows other components to change the list of IDs we're searching for.
searchIds.value = searchIdList;
}
</script>
Expand All @@ -40,11 +40,10 @@ function changeCAMList(newCamList: string[]) {
</div>
</div>

<SearchCAMs :automatCAMKPEndpoint="automatCAMKPEndpoint" :changeCAMList="changeCAMList" />

<ListCAMs :automatCAMKPEndpoint="automatCAMKPEndpoint" :camList="camList" :changeSelectedModel="changeSelectedModel" />

<DisplayCAM :selected-model="selectedModel"></DisplayCAM>
<div class="row">
<SearchCAMs :automatCAMKPEndpoint="automatCAMKPEndpoint" :changeSelectedModel="changeSelectedModel" :changeSearchIds="changeSearchIds" />
<DisplayCAM :selected-model-u-r-l="selectedModelURL" :search-ids="searchIds"></DisplayCAM>
</div>

<div class="accordion" id="advancedOptionsAccordion">
<div class="accordion-item">
Expand All @@ -70,4 +69,4 @@ function changeCAMList(newCamList: string[]) {

<style scoped>
</style>
</style>
243 changes: 179 additions & 64 deletions frontend/src/components/DisplayCAM.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
<script setup lang="ts">
import {computed, ref, watch, withDefaults} from "vue";
import {urlToID} from "./shared.ts";
import lodash from "lodash";
import { mkConfig, generateCsv, download } from "export-to-csv";
export interface Props {
automatCAMKPEndpoint?: string,
selectedModel: string,
selectedModelURL: string,
searchIds: Set<string>,
}
const props = withDefaults(defineProps<Props>(), {
automatCAMKPEndpoint: 'https://automat.renci.org/cam-kp',
searchIDs: new Set(),
});
const downloadInProgress = ref(false);
Expand All @@ -16,6 +21,13 @@ const spos = ref([]);
const labels = ref({});
const descriptions = ref({});
// Display flags
const display_descriptions = ref(false);
const display_information_content = ref(false);
const display_eq_identifiers = ref(false);
const display_xrefs = ref(false);
const display_primary_knowledge_source = ref(false);
const fromIds = computed(() => [...new Set(spos.value.map(spo => spo[0]).sort())]);
const toIds = computed(() => [...new Set(spos.value.map(spo => spo[1]).sort())]);
Expand All @@ -27,15 +39,17 @@ function getPredicates(fromId: string, toId: string) {
}).sort();
}
watch(() => props.selectedModel, (_, modelURL) => {
watch(() => props.selectedModelURL, (modelURL, _) => {
if (!modelURL) return;
modelRows.value = [];
spos.value = [];
labels.value = {};
descriptions.value = {};
getModelRows(modelURL).then(rows => {
modelRows.value = rows;
rows.map(row => {
rows.forEach(row => {
spos.value.push([row[0]['id'], row[2]['id'], row[3], row[4]]);
labels.value[row[0]['id']] = row[0]['name'];
Expand All @@ -48,6 +62,8 @@ watch(() => props.selectedModel, (_, modelURL) => {
});
async function getModelRows(modelURL: string) {
if (!modelURL) return [];
downloadInProgress.value = true;
const cypher_endpoint = props.automatCAMKPEndpoint + '/cypher';
Expand All @@ -58,7 +74,7 @@ async function getModelRows(modelURL: string) {
'Accept': 'application/json',
},
'body': JSON.stringify({
'query': `MATCH (s)-[p]-(o) WHERE '${modelURL}' IN p.xref RETURN DISTINCT s, p, o, TYPE(p) AS pred_type, CASE
'query': `MATCH (s)-[p]-(o) WHERE '${modelURL}' IN p.xref RETURN DISTINCT s, properties(p) AS p, o, TYPE(p) AS pred_type, CASE
WHEN startNode(p) = s THEN ''
WHEN endNode(p) = s THEN 'reverse'
ELSE ''
Expand All @@ -69,41 +85,151 @@ async function getModelRows(modelURL: string) {
downloadInProgress.value = false;
console.log("response = ", j);
const results = j['results'].flatMap(r => r['data']).map(r => r['row']);
console.log(results);
return results;
// Reverse any 'reverse' rows.
const reversedResults = lodash.uniqWith(results.map(row => {
if (row[4] === 'reverse') {
// If this row is "reversed", reverse the subject and the object.
const subj = {...row[0]};
row[0] = row[2];
row[2] = subj;
row[4] = '';
}
return row;
}), lodash.isEqual);
console.log("reversedResults = ", reversedResults);
return reversedResults;
}
// Download model rows as CSV.
function downloadModelRowsAsCSV() {
if (!modelRows.value) return;
const modelFilename = new URL(props.selectedModelURL).pathname.split('/').at(-1);
const csvConfig = mkConfig({
filename: modelFilename,
useKeysAsHeaders: true,
columnHeaders: [
'subject_id',
'subject_label',
'predicate',
'primary_knowledge_source',
'object_id',
'object_label'
]
});
console.log("modelRows.value = ", modelRows.value);
const csvData = modelRows.value.map(row => {
return {
'subject_id': row[0]['id'],
'subject_label': row[0]['name'],
'predicate': row[3],
'primary_knowledge_source': row[1]['primary_knowledge_source'],
'object_id': row[2]['id'],
'object_label': row[2]['name'],
};
});
const csv = generateCsv(csvConfig)(csvData);
download(csvConfig)(csv);
}
</script>

<template>

<div class="card my-2" v-if="downloadInProgress">
<div class="card-header">
Download in progress ...
<div class="col-8">
<div id="edges" class="card my-2">
<div class="card-header">Display</div>
<div class="card-body">
<div class="form-check">
<input class="form-check-input" type="checkbox" v-model="display_descriptions" id="displayDescription">
<label class="form-check-label" for="displayDescription">
Display node descriptions
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" v-model="display_eq_identifiers" id="displayEquivalentIdentifiers">
<label class="form-check-label" for="displayEquivalentIdentifiers">
Display equivalent identifiers
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" v-model="display_information_content" id="displayInformationContent">
<label class="form-check-label" for="displayInformationContent">
Display information content
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" v-model="display_primary_knowledge_source" id="displayPrimaryKnowledgeSource">
<label class="form-check-label" for="displayPrimaryKnowledgeSource">
Display primary knowledge source
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" v-model="display_xrefs" id="displayXrefs">
<label class="form-check-label" for="displayXrefs">
Display other models with this edge (Xrefs)
</label>
</div>
</div>
</div>
</div>

<div class="card my-2" v-if="!selectedModelURL">
<div class="card-header">
No model selected. Please search for one using the controls on the left.
</div>
</div>

<div class="card" v-if="!downloadInProgress">
<div class="card-header">
<strong>Relationships in selected CAM:</strong> <a target="cam" :href="selectedModel">{{selectedModel}}</a>
<div class="card my-2" v-if="downloadInProgress">
<div class="card-header">
Download of CAM <a target="cam" :href="selectedModelURL">{{ selectedModelURL }}</a> in progress ...
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover">

<div id="edges" class="card my-2">
<div class="card-header">
<button class="float-end" @click="downloadModelRowsAsCSV()">Download as CSV</button>
<strong>Edges in selected CAM:</strong> <a target="cam" :href="selectedModelURL">{{ selectedModelURL }}</a> (<a href="#relationships">Relationships</a>)
</div>
<div class="card-body" v-if="!downloadInProgress">
<table class="table table-bordered mb-2">
<thead>
<tr>
<th scope="col">From CURIE</th>
<th scope="col" v-for="toId in toIds">
<span :title="descriptions[toId]">{{toId}}</span><br />{{labels[toId]}}
</th>
<th>Subject</th>
<th>Edge</th>
<th>Object</th>
</tr>
</thead>
<tbody>
<tr v-for="fromId in fromIds">
<td><strong>{{fromId}}</strong> {{labels[fromId]}}<br/>{{descriptions[fromId]}}</td>
<td v-for="toId in toIds">
<span v-for="pred in getPredicates(fromId, toId)">{{pred}}<br /></span>
<tr v-for="row in modelRows">
<td :class="(row[0]['equivalent_identifiers'].some(v => searchIds.has(v))) ? 'bg-success-subtle' : ''">
<strong>{{row[0]['id']}}</strong> {{row[0]['name']}}
<p v-if="display_descriptions"><em>Description</em>: {{row[0]['description']}}</p>
<p v-if="display_information_content"><em>Information Content</em>: {{row[0]['information_content']}}</p>
<p v-if="display_eq_identifiers"><em>Equivalent identifiers</em>: {{row[0]['equivalent_identifiers']}}</p>
</td>
<td>
<div>{{row[3]}}<span v-if="row[4]"> [{{row[4]}}]</span></div>
<p v-if="display_primary_knowledge_source"><em>Primary knowledge source</em>: {{row[1]['primary_knowledge_source']}}</p>
<ul v-if="display_xrefs" class="overflow-auto" style="max-height: 20em">
<li v-for="xref in row[1]['xref']" :key="xref">
<a :href="xref" target="xref">{{urlToID(xref)}}</a>
</li>
</ul>
</td>
<td :class="(row[2]['equivalent_identifiers'].some(v => searchIds.has(v))) ? 'bg-success-subtle' : ''">
<strong>{{row[2]['id']}}</strong> {{row[2]['name']}}
<p v-if="display_descriptions"><em>Description</em>: {{row[2]['description']}}</p>
<p v-if="display_information_content"><em>Information Content</em>: {{row[2]['information_content']}}</p>
<p v-if="display_eq_identifiers"><em>Equivalent identifiers</em>: {{row[2]['equivalent_identifiers']}}</p>
</td>
</tr>
</tbody>
Expand All @@ -112,45 +238,34 @@ async function getModelRows(modelURL: string) {
</div>
</div>

<div class="card" v-if="!downloadInProgress">
<div class="card-header">
<strong>Edges in selected CAM:</strong> <a target="cam" :href="selectedModel">{{selectedModel}}</a>
</div>
<div class="card-body">
<table class="table table-bordered mb-2">
<thead>
<tr>
<th>Subject</th>
<th>Edge</th>
<th>Object</th>
</tr>
</thead>
<tbody>
<tr v-for="row in modelRows">
<td>
<strong>{{row[0]['id']}}</strong> {{row[0]['name']}}<br/><br/>
<em>Description</em>: {{row[0]['description']}}<br/>
<em>Information Content</em>: {{row[0]['information_content']}}<br/>
<em>Equivalent identifiers</em>: {{row[0]['equivalent_identifiers']}}
</td>
<td>
<strong>{{row[3]}}<span v-if="row[4]"> [{{row[4]}}]</span></strong><br/>
biolink:primary_knowledge_source: {{row[1]['biolink:primary_knowledge_source']}}
<ul>
<li v-for="xref in row[1]['xref']" :key="xref">
<a :href="xref" target="xref">{{xref}}</a>
</li>
</ul>
</td>
<td>
<strong>{{row[2]['id']}}</strong> {{row[2]['name']}}<br/><br/>
<em>Description</em>: {{row[2]['description']}}<br/>
<em>Information Content</em>: {{row[2]['information_content']}}<br/>
<em>Equivalent identifiers</em>: {{row[2]['equivalent_identifiers']}}
</td>
</tr>
</tbody>
</table>
<!-- This view is hard to compress, so let's give it the whole screen -->
<div class="col-12">
<div id="relationships" class="card my-2">
<div class="card-header">
<strong>Relationships in selected CAM:</strong> <a target="cam" :href="selectedModelURL">{{ selectedModelURL }}</a> (<a href="#edges">Edges</a>)
</div>
<div class="card-body" v-if="!downloadInProgress">
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th scope="col">From CURIE</th>
<th scope="col" v-for="toId in toIds" :class="(searchIds.has(toId)) ? 'bg-success-subtle' : ''">
<span :title="descriptions[toId]">{{toId}}</span><br />{{labels[toId]}}
</th>
</tr>
</thead>
<tbody>
<tr v-for="fromId in fromIds">
<td :class="(searchIds.has(fromId)) ? 'bg-success-subtle' : ''"><strong>{{fromId}}</strong> {{labels[fromId]}}<br/>{{descriptions[fromId]}}</td>
<td v-for="toId in toIds" :class="(searchIds.has(toId) || searchIds.has(fromId)) ? 'bg-success-subtle' : ''">
<span v-for="pred in getPredicates(fromId, toId)">{{pred}}<br /></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
Expand All @@ -168,4 +283,4 @@ async function getModelRows(modelURL: string) {
z-index: 2;
background-color: #fff; /* Match table background color */
}
</style>
</style>
Loading

0 comments on commit 2c2a37b

Please sign in to comment.