Skip to content

Commit

Permalink
add incremental counts for each facet value
Browse files Browse the repository at this point in the history
  • Loading branch information
rbuels committed May 16, 2012
1 parent 2a25702 commit fed2b24
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 36 deletions.
27 changes: 27 additions & 0 deletions faceted_track_selector.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,41 @@

/* Facet selection controls */

#faceted_tracksel .facetSelect {
width: 100%;
border-spacing: 0;
}
#faceted_tracksel .facetSelect .facetValue {
padding: 0.1em 0.4em;
cursor: pointer;
}
#faceted_tracksel .facetSelect .facetValue > * {
vertical-align: top;
}
#faceted_tracksel .facetSelect .disabled {
color: gray;
}

#faceted_tracksel .facetSelect .facetValue.disabled {
display: none;
}
#faceted_tracksel .facetSelect .facetValue.disabled.selected {
display: block;
}

.tundra #faceted_tracksel .facetSelect .facetValue:hover {
background: #D2E1F1;
}

#faceted_tracksel .facetSelect .facetValue .count {
padding: 0 0.7em 0 0.4em;
color: #333;
text-align: right;
}
#faceted_tracksel .facetSelect .facetValue .value {
width: 80%;
}

#faceted_tracksel .facetSelect .selected {
background: #b1d3f6;
}
Expand Down
128 changes: 98 additions & 30 deletions js/Model/TrackMetaData.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ dojo.declare( 'JBrowse.Model.TrackMetaData', null,
* @lends JBrowse.Model.TrackMetaData.prototype
*/
{

_noDataValue: '(no data)',

/**
* Data store for track metadata, supporting faceted
* (parameterized) searching. Keeps all of the track metadata,
Expand Down Expand Up @@ -83,11 +86,41 @@ dojo.declare( 'JBrowse.Model.TrackMetaData', null,
* @private
*/
_finishLoad: function() {
this.facets = this.facets.sort();

// sort the facet names
this.facets.sort();

// calculate the average bucket size for each facet index
dojo.forEach( dojof.values( this.facetIndexes.byName ), function(bucket) {
bucket.avgBucketSize = bucket.itemCount / bucket.bucketCount;
});
// calculate the rank of the facets: make an array of
// facet names sorted by bucket size, descending
this.facetIndexes.facetRank = dojo.clone(this.facets).sort(dojo.hitch(this,function(a,b){
return this.facetIndexes.byName[a].avgBucketSize - this.facetIndexes.byName[b].avgBucketSize;
}));

// sort the facet indexes by ident, so that we can do our
// kind-of-efficient N-way merging when querying
var itemSortFunction = dojo.hitch( this, '_itemSortFunc' );
dojo.forEach( dojof.values( this.facetIndexes.byName ), function( facetIndex ) {
dojo.forEach( dojof.keys( facetIndex.byValue ), function( value ) {
facetIndex.byValue[value].items = facetIndex.byValue[value].items.sort( itemSortFunction );
});
},this);

this.ready = true;
this.onReady();
},

_itemSortFunc: function(a,b) {
var ai = this.getIdentity(a),
bi = this.getIdentity(b);
return ai == bi ? 0 :
ai > bi ? 1 :
ai < bi ? -1 : 0;
},

_indexItems: function( args ) {
// get our (filtered) list of facets we will index for
var store = args.store,
Expand Down Expand Up @@ -171,25 +204,15 @@ dojo.declare( 'JBrowse.Model.TrackMetaData', null,
}, this);

// index the items that do not have data for this facet
var noDataValue = '(no data)';
dojo.forEach( new_facets, function(facet) {
var gotSomeWithNoData = false;
dojo.forEach( dojof.values( this.identIndex ), function(item) {
if( ! gotDataForItem[facet][this.getIdentity(item)] ) {
gotSomeWithNoData = true;
this._indexItem( facet, noDataValue, item );
this._indexItem( facet, this._noDataValue, item );
}
},this);
},this);

// calculate the rank of the facets: make an array of
// facet names sorted by smallest average bucket size,
// descending
this.facetIndexes.facetRank = this.facets.sort(dojo.hitch(this,function(a,b){
a = this.facetIndexes.byName[a];
b = this.facetIndexes.byName[b];
return b.itemCount/b.bucketCount - a.itemCount/a.bucketCount;
}));
}
},

Expand Down Expand Up @@ -239,6 +262,16 @@ dojo.declare( 'JBrowse.Model.TrackMetaData', null,
return this._fetchCount;
},


/**
* Get grouped counts for each facet of the distinct values
* possessed by tracks that matched the query that was last run.
* @returns {Object}
*/
getFacetCounts: function() {
return this._fetchFacetCounts;
},

/**
* Get an array of the text names of the facets that are defined
* in this track metadata.
Expand Down Expand Up @@ -270,8 +303,9 @@ dojo.declare( 'JBrowse.Model.TrackMetaData', null,
if( !index ) return {};

var stats = {};
dojo.forEach( ['itemCount','bucketCount'], function(attr) { stats[attr] = index[attr];});
stats.avgBucketSize = stats.itemCount / stats.bucketCount;
dojo.forEach( ['itemCount','bucketCount','avgBucketSize'],
function(attr) { stats[attr] = index[attr]; }
);
return stats;
},

Expand Down Expand Up @@ -324,10 +358,33 @@ dojo.declare( 'JBrowse.Model.TrackMetaData', null,
}
},this);

var results = this._doQuery( query );

// and finally, hand them to the finding callback
findCallback(results,keywordArgs);
this.onFetchSuccess();
},

/**
* @private
*/
_doQuery: function( /**Object*/ query ) {
var textFilter = query.text;
delete query.text;

var results;
var results = []; // array of items that completely match the query

// init counts
var facetMatchCounts = {};
var countItem = function( item ) {
dojo.forEach( this.facets, function( facetName ) {
var value = this.getValue( item, facetName, this._noDataValue );
var facetEntry = facetMatchCounts[ facetName ];
if( !facetEntry ) facetEntry = facetMatchCounts[ facetName ] = {};
if( !facetEntry[value] ) facetEntry[value] = 0;
facetEntry[value]++;
},this);
};

// if we don't actually have any facets specified in the
// query, the results are just all the items
Expand Down Expand Up @@ -368,17 +425,21 @@ dojo.declare( 'JBrowse.Model.TrackMetaData', null,
var desired_values = query[facetName] || [];
if( ! desired_values.length )
return;
results = dojo.filter(results, function(item) {
var value = this.getValue(item,facetName);
return dojo.some( desired_values, function(desired) {
return desired == value;
},this);
},this);
results = dojo.filter(
results,
function(item) {
var value = this.getValue(item,facetName);
return dojo.some(
desired_values,
function(desired) {
return desired == value;
},this);
},this);
},this);
}

// filter with the text filter, if we have it
if( typeof textFilter != 'undefined' ) {
if( typeof textFilter != 'undefined' ) {
var filter = this._compileTextFilter( textFilter );
results = dojo.filter( results, function(item) {
return dojo.some( this.facets, function(facetName) {
Expand All @@ -387,11 +448,13 @@ dojo.declare( 'JBrowse.Model.TrackMetaData', null,
},this);
}

dojo.forEach( results, function(item) {
countItem.call( this, item );
},this);
this._fetchFacetCounts = facetMatchCounts;
this._fetchCount = results.length;

// and finally, hand them to the finding callback
findCallback(results,keywordArgs);
this.onFetchSuccess();
return results;
},

/**
Expand All @@ -412,6 +475,8 @@ dojo.declare( 'JBrowse.Model.TrackMetaData', null,
* @private
*/
_compileTextFilter: function( textString ) {
if( textString === undefined )
return null;

// parse out words and quoted words, and convert each into a regexp
var rQuotedWord = /\s*["']([^"']+)["']\s*/g;
Expand All @@ -437,11 +502,14 @@ dojo.declare( 'JBrowse.Model.TrackMetaData', null,
wordREs.push( new RegExp(currentWord,'i') );
}

// return a function that returns true if all of the words
// match the string, but in any order
return function( text ) {
return dojof.every( wordREs, function(re) { return re.test(text); } );
};
// return a function that takes on item and returns true if it
// matches the text filter
return dojo.hitch(this, function(item) {
return dojo.some( this.facets, function(facetName) {
var text = this.getValue( item, facetName );
return dojof.every( wordREs, function(re) { return re.test(text); } );
},this);
});
},

getFeatures: function() {
Expand Down
53 changes: 47 additions & 6 deletions js/View/TrackList/Faceted.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ dojo.declare( 'JBrowse.View.TrackList.Faceted', null,
});
});
});
dojo.connect( this.trackDataStore, 'onReady', this, '_updateFacetCounts' ); // just once at start

dojo.connect( this.trackDataStore, 'onFetchSuccess', this, '_updateGridSelections' );
dojo.connect( this.trackDataStore, 'onFetchSuccess', this, '_updateMatchCount' );

},

/**
Expand Down Expand Up @@ -226,6 +226,7 @@ dojo.declare( 'JBrowse.View.TrackList.Faceted', null,
this._clearTextFilterControl();
this._clearAllFacetControls();
this.updateQuery();
this._updateFacetCounts();
})
}
),
Expand All @@ -238,7 +239,7 @@ dojo.declare( 'JBrowse.View.TrackList.Faceted', null,
this.mainContainer.addChild( centerPane );

this.mainContainer.startup();
this._updateMatchCount();
this.show();
},

renderGrid: function() {
Expand Down Expand Up @@ -349,6 +350,7 @@ dojo.declare( 'JBrowse.View.TrackList.Faceted', null,
dojo.hitch( this, function() {
this._updateTextFilterControl();
this.updateQuery();
this._updateFacetCounts();
this.textFilterInput.focus();
}),
500
Expand All @@ -367,6 +369,7 @@ dojo.declare( 'JBrowse.View.TrackList.Faceted', null,
onclick: dojo.hitch( this, function() {
this._clearTextFilterControl();
this.updateQuery();
this._updateFacetCounts();
}),
style: {
position: 'absolute',
Expand Down Expand Up @@ -454,20 +457,21 @@ dojo.declare( 'JBrowse.View.TrackList.Faceted', null,
});

// make a selection control for the values of this facet
var facetControl = dojo.create( 'div', {className: 'facetSelect'}, facetPane.containerNode );
var facetControl = dojo.create( 'table', {className: 'facetSelect'}, facetPane.containerNode );
// populate selector's options
this.facetSelectors[facetName] = dojo.map(
values,
function(val) {
var that = this;
var node = dojo.create(
'div',
'tr',
{ className: 'facetValue',
innerHTML: val,
innerHTML: '<td class="count"></td><td class="value">'+ val + '</td>',
onclick: function(evt) {
dojo.toggleClass(this, 'selected');
that._updateFacetControl( facetName );
that.updateQuery();
that._updateFacetCounts( facetName );
}
},
facetControl
Expand Down Expand Up @@ -502,20 +506,57 @@ dojo.declare( 'JBrowse.View.TrackList.Faceted', null,
this._updateFacetControl( facetName );
},

/**
* Incrementally update the facet counts as facet values are selected.
* @private
*/
_updateFacetCounts: function( /**String*/ skipFacetName ) {
var facetCounts = this.trackDataStore.getFacetCounts();
dojo.forEach( dojof.keys( this.facetSelectors ), function( facetName ) {
if( facetName == 'My Tracks' || facetName == skipFacetName )
return;
var thisFacetCounts = facetCounts[ facetName ];
dojo.forEach( this.facetSelectors[facetName] || [], function( selectorNode ) {
dojo.query('.count',selectorNode)
.forEach( function(countNode) {
var count = thisFacetCounts ? thisFacetCounts[ selectorNode.facetValue ] || 0 : 0;
countNode.innerHTML = Util.addCommas( count );
if( count )
dojo.removeClass( selectorNode, 'disabled');
else
dojo.addClass( selectorNode, 'disabled' );
},this);
//dojo.removeClass(selector,'selected');
},this);
this._updateFacetControl( facetName );
},this);
},

/**
* Update the title bar of the given facet control to reflect
* whether it has selected values in it.
*/
_updateFacetControl: function( facetName ) {
var titleContent = dojo.byId('facet_title_'+facetName);

// if we have some selected values
// if all our values are disabled, add 'disabled' to our
// title's CSS classes
if( dojo.every( this.facetSelectors[facetName] ||[], function(sel) {
return dojo.hasClass( sel, 'disabled' );
},this)
) {
dojo.addClass( titleContent, 'disabled' );
}

// if we have some selected values, make a "clear" button, and
// add 'selected' to our title's CSS classes
if( dojo.some( this.facetSelectors[facetName] || [], function(sel) {
return dojo.hasClass( sel, 'selected' );
}, this ) ) {
var clearFunc = dojo.hitch( this, function(evt) {
this._clearFacetControl( facetName );
this.updateQuery();
this._updateFacetCounts( facetName );
evt.stopPropagation();
});
dojo.addClass( titleContent, 'selected' );
Expand Down

0 comments on commit fed2b24

Please sign in to comment.