Skip to content

Commit

Permalink
blast results : display table rows as feature triangles
Browse files Browse the repository at this point in the history
 ... works; MVP for discussing the features.
axis-ticks-selected.js : add featureIsTransient(f), and use in featuresOfBlockLookup() : show the features of un-viewed blocks, when block is reference.

blast-results.js :
change tableModal true -> false (i.e. initially table is in left panel, not in modal dialog).
add viewFeaturesFlag, dataFeatures() (factored from validateData()), blockNames(), viewFeaturesEffect().

paths-progressive.js : pushFeature() : default for param flowsService, return record found by peekRecord().

selected.js : toggle() : add param add : if true then add, if false then remove, if undefined then toggle (as before).

add transient.js : with pushFeature(), pushData() (based on pathsPro : pushFeature), datasetForSearch(), pushDatasetArgs(), pushBlockArgs(), blocksForSearch(), showFeatures().

blast-results.hbs : use viewFeaturesEffect, add checkbox viewFeaturesFlag.
  • Loading branch information
Don-Isdale committed Jun 2, 2021
1 parent cdcdcf4 commit 27e3a5d
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 26 deletions.
33 changes: 33 additions & 0 deletions frontend/app/components/draw/axis-ticks-selected.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@ const dLog = console.debug;

const CompName = 'components/axis-ticks-selected';

/*----------------------------------------------------------------------------*/
/** @return true if feature's block is not viewed and its dataset
* has tag transient.
*/
function featureIsTransient(f) {
let isTransient = ! f.get('blockId.isViewed');
if (isTransient) {
let d = f.get('blockId.datasetId');
d = d.get('content') || d;
isTransient = d.hasTag('transient');
}
return isTransient;
}

/*----------------------------------------------------------------------------*/



/** Display horizontal ticks on the axis to highlight the position of features
* found using Feature Search.
*
Expand Down Expand Up @@ -201,6 +219,21 @@ export default Component.extend(AxisEvents, {
if (clickedFeatures && clickedFeatures.length) {
features = features.concat(clickedFeatures);
}
/** not yet clear whether blast-results transient blocks should be
* viewed - that would introduce complications requiring API
* requests to be blocked when .datasetId.hasTag('transient')
* For the MVP, return the features of un-viewed blocks, when block is reference.
*/
if (! block.get('isData')) {
let clickedFeaturesByAxis = this.get('selected.clickedFeaturesByAxis'),
axisFeatures = clickedFeaturesByAxis && clickedFeaturesByAxis.get(block),
transientFeatures = axisFeatures && axisFeatures
.filter(featureIsTransient);
if (transientFeatures && transientFeatures.length) {
features = features.concat(transientFeatures);
}
}

if (trace)
dLog('featuresOfBlockLookup', featuresInBlocks, block, blockId, features);
return features;
Expand Down
94 changes: 75 additions & 19 deletions frontend/app/components/panel/upload/blast-results.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,25 @@ export default Component.extend({
*/
store : alias('apiServers.primaryServer.store'),
auth: service('auth'),
transient : service('data/transient'),


classNames: ['blast-results'],

/** true enables display of the table. */
tableVisible : true,
/** true enables display of the table in a modal dialog. */
tableModal : true,
tableModal : false,
/** true enables display of the search inputs. */
showSearch : false,

/** true means view the blocks of the dataset after it is added. */
/** true means view the blocks of the dataset after it is added.
* Used in upload-table.js : submitFile().
*/
viewDatasetFlag : false,
/** true means display the result rows as triangles - clickedFeatures. */
viewFeaturesFlag : true,


/*--------------------------------------------------------------------------*/

Expand Down Expand Up @@ -107,6 +113,38 @@ export default Component.extend({
cells = data ? data.map((r) => r.split('\t')) : [];
return cells;
}),
dataFeatures : computed('dataMatrix.[]', function () {
let data = this.get('dataMatrix');
/** the last row is empty, so it is filtered out. */
let features =
data
.filter((row) => (row[c_name] !== '') && (row[c_chr]))
.map((row) => {
let feature = {
name: row[c_name],
// blast output chromosome has prefix 'chr' e.g. 'chr2A'; Pretzel uses simply '2A'.
block: row[c_chr].replace(/^chr/, ''),
// Make sure val is a number, not a string.
val: Number(row[c_pos])
};
if (row[c_end] !== undefined) {
feature.end = Number(row[c_end]);
}
return feature;
});
dLog('dataFeatures', features.length, features[0]);
return features;
}),
blockNames : computed('dataMatrix.[]', function () {
let data = this.get('dataMatrix');
/** based on dataFeatures - see comments there. */
let names =
data
.filter((row) => (row[c_name] !== '') && (row[c_chr]))
.map((row) => row[c_chr].replace(/^chr/, ''));
dLog('blockNames', names.length, names[0]);
return names;
}),
dataMatrixEffect : computed('table', 'dataMatrix.[]', function () {
let table = this.get('table');
if (table) {
Expand Down Expand Up @@ -186,6 +224,40 @@ export default Component.extend({
}
},

/*--------------------------------------------------------------------------*/

viewFeaturesEffect : computed('dataFeatures.[]', 'viewFeaturesFlag', function () {
/** construct feature id from name + val (start position) because
* name is not unique, and in a blast search result, a feature
* name is associated with the sequence string to search for, and
* all features matching that sequence have the same name.
* We may use UUID (e.g. thaume/ember-cli-uuid or ivanvanderbyl/ember-uuid).
*/
let
features = this.get('dataFeatures')
.map((f) => ({
_id : f.name + '-' + f.val, name : f.name, blockId : f.block,
value : [f.val, f.end]}));
if (features && features.length) {
let viewFeaturesFlag = this.get('viewFeaturesFlag');
let
transient = this.get('transient'),
datasetName = this.get('newDatasetName') || 'blastResults',
namespace = this.get('namespace'),
dataset = transient.pushDatasetArgs(
datasetName,
this.get('search.parent'),
namespace
);
let blocks = transient.blocksForSearch(
datasetName,
this.get('blockNames'),
namespace
);
transient.showFeatures(dataset, blocks, features, viewFeaturesFlag);
}
}),

/*--------------------------------------------------------------------------*/
/** comments for activeEffect() and shownBsTab() in @see data-csv.js
* @desc
Expand Down Expand Up @@ -311,24 +383,8 @@ query ID, subject ID, % identity, length of HSP (hit), # mismatches, # gaps, que
if (table === null) {
resolve([]);
}
let sourceData = this.get('dataMatrix');
/** the last row is empty, so it is filtered out. */
let
validatedData = sourceData
.filter((row) => (row[c_name] !== '') && (row[c_chr]))
.map((row) => {
let feature = {
name: row[c_name],
// blast output chromosome has prefix 'chr' e.g. 'chr2A'; Pretzel uses simply '2A'.
block: row[c_chr].replace(/^chr/, ''),
// Make sure val is a number, not a string.
val: Number(row[c_pos])
};
if (row[c_end] !== undefined) {
feature.end = Number(row[c_end]);
}
return feature;
});
validatedData = this.get('dataFeatures');
resolve(validatedData);
});
}
Expand Down
6 changes: 6 additions & 0 deletions frontend/app/services/data/paths-progressive.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,19 @@ export default Service.extend({
return promise;
},

/** Push the feature into the store if it is not already there.
* @return the record handle of the existing or added feature record.
* @param flowsService optional, defaults to this.get('flowsService').
*/
pushFeature(store, f, flowsService) {
flowsService ||= this.get('flowsService');
let c;
let fr = store.peekRecord('feature', f._id);
if (fr) {
let verifyOK = verifyFeatureRecord(fr, f);
if (! verifyOK)
dLog('peekRecord feature', f._id, f, fr._internalModel.__data, fr);
c = fr;
}
else
{
Expand Down
29 changes: 22 additions & 7 deletions frontend/app/services/data/selected.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { contentOf } from '../../utils/common/promises';

const dLog = console.debug;

const trace = 1;

/**
* for #223 : Selections and defining intervals
*/
Expand Down Expand Up @@ -40,16 +42,26 @@ export default Service.extend(Evented, {
* This event is published here rather than in the component which receives
* the click (axis-tracks : clickTrack) because the feature 'clicked' status
* resides here.
*
* @param add undefined or boolean : if true then add, if false then remove
*/
toggle(listName, feature) {
toggle(listName, feature, add) {
let
features = this.get(listName),
i = features.indexOf(feature);
dLog('clickFeature', listName, i, feature, features);
let added = i === -1;
if (added) {
i = features && features.indexOf(feature);
if (! features || trace /*> 1*/) {
dLog('clickFeature', listName, i, feature, features);
}
/** indicates that the feature was initially not in the list. */
let absent = i === -1;
/** true / false indicates that the feature was added/removed to/from the list.
* undefined indicates no change.
*/
let added;
if (absent && (add !== false)) {
features.pushObject(feature);
} else {
added = true;
} else if (! absent && (add !== true)) {
features.removeAt(i, 1);
if (listName === 'features') {
/** clearing from .features is required to clear from the other 2 lists;
Expand All @@ -62,8 +74,11 @@ export default Service.extend(Evented, {
this.labelledFeatures.removeObject(feature);
this.shiftClickedFeatures.removeObject(feature);
}
added = false;
}
if (added !== undefined) {
this.trigger('toggleFeature', feature, added, listName);
}
this.trigger('toggleFeature', feature, added, listName);
},
clickFeature(feature) {
this.toggle('features', feature);
Expand Down
112 changes: 112 additions & 0 deletions frontend/app/services/data/transient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { computed } from '@ember/object';
import Service, { inject as service } from '@ember/service';

import { _internalModel_data } from '../../utils/ember-devel';


const dLog = console.debug;

const trace = 1;

/**
* Transient data objects (Features / Blocks / Datasets) which are
* created in frontend and not persisted to server & database. May be
* added to store.
*
* Purpose : provide display of Features in similar ways to database
* features, e.g. clickedFeatures as triangles.
*/

/**
* Related : services/data/selected.js
*
* for #239 : FASTA / DNA sequence search API, display results in frontend
* comment / section : multiple output tabs :
* - after viewing the added dataset : also put them into the feature search so they are highlighted
* - display table rows as Feature triangles
*/
export default Service.extend({
// push to local store for now; could use primaryServer.store.
store: service(),
pathsPro : service('data/paths-progressive'),
selected : service('data/selected'),

/*--------------------------------------------------------------------------*/

pushFeature(f) {
// pathsPro.pushFeature() will use default flowsService.
return this.get('pathsPro').pushFeature(this.get('store'), f, /*flowsService*/undefined);
},

/*--------------------------------------------------------------------------*/

pushData(store, modelName, d) {
let c;
let r = store.peekRecord(modelName, d._id);
if (r) {
// this can be a @param verifyFn : if (verifyFn) { verifyFn(d, r); }
if (modelName === 'dataset') {
// if ((r.parent !== d.parent) || (r.namespace !== d.namespace))
dLog('peekRecord', modelName, d._id, d, r.get(_internalModel_data), r);
dLog(r.parent, d.parent, r.namespace, d.namespace);
}
}
else
{
d.id = d._id;

// .name is primaryKey of dataset
let n = store.normalize(modelName, d);
c = store.push(n);

// if (trace > 2)
dLog(c.get('id'), c.get(_internalModel_data));
}
return c;
},

/**
* @param _id datasetName
*/
datasetForSearch(_id, parent, namespace) {
let
d =
{
name : _id,
_id, namespace, parent,
tags : [ 'transient' ],
meta : { paths : false }
};
return d;
},

pushDatasetArgs(_id, parent, namespace) {
let
data = this.datasetForSearch(_id, parent, namespace),
store = this.get('store'),
record = this.pushData(store, 'dataset', data);
return record;
},

pushBlockArgs(datasetId, name, namespace) {
let
/** prefix _id with datasetId to make it unique enough. May use UUID. */
data = {_id : /*datasetId + '-' +*/ name, scope : name, name, namespace, datasetId},
store = this.get('store'),
record = this.pushData(store, 'block', data);
return record;
},
blocksForSearch(datasetId, blockNames, namespace) {
let blocks = blockNames.map((name) => this.pushBlockArgs(datasetId, name, namespace));
return blocks;
},

showFeatures(dataset, blocks, features, viewFeaturesFlag) {
let
selected = this.get('selected'),
// may pass dataset, blocks to pushFeature()
stored = features.map((f) => this.pushFeature(f));
stored.forEach((feature) => selected.toggle('features', feature, viewFeaturesFlag));
}

});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{{dataMatrixEffect}}
{{resultEffect}}
{{activeEffect}}
{{viewFeaturesEffect}}

{{!-- --------------------------------------------------------------------- --}}

Expand Down Expand Up @@ -159,6 +160,10 @@
<label>View</label>
</span>

<span class="filter-group-col">
{{input type="checkbox" name="viewFeaturesFlag" checked=viewFeaturesFlag }}
<label>Show Features (triangles)</label>
</span>

<hr>

Expand Down

0 comments on commit 27e3a5d

Please sign in to comment.