From 52ff438e5cca90650f4fbf60dc4b9ef13708c02b Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 10 Sep 2019 21:17:06 +1000 Subject: [PATCH] Update axis ticks and scale when paths are loaded. This addresses an issue of paths loading and one or more of the axes showing only tick 0, with all the paths connecting to it. This was working but a recent commit disconnected the update, as has happened several times before. Use CP dependencies to update the axis ticks and scale. draw-map.js : add to axisApi : cmNameAdd, makeMapChrName, axisIDAdd. axis-1d.js : added CPs : dataBlocksDomains, referenceBlock, blocksDomains, blocksDomain, blocksDomainEffect, domainEffect. models/block.js : add CP : featuresDomain. feature.js : add CP : valueOrdered. data/block.js : use cmNameAdd(). (viewed() may not be updating - trialling an added dependency and changed use of blockValues()). draw/axes-1d.hbs and axis-1d.hbs : commented-out display of values which may be changed during the render (refn https://github.com/emberjs/ember.js/issues/13948) : axis.view, currentPosition.yDomain. Use blocksDomainEffect. utility-chromosome.js : factor copyChrData() out of chrData(); add cmNameAdd() based on draw-map.js:receiveChr(). frontend/app/ : 6e37a2f 223853 Sep 10 20:25 components/draw-map.js 62cdfc0 22369 Sep 10 21:12 components/draw/axis-1d.js 2b94ee2 24542 Sep 10 20:25 components/draw/block-adj.js 563096e 2943 Sep 10 16:51 mixins/axis-position.js 06c02ae 6638 Sep 10 18:30 models/block.js 930fc67 1021 Sep 9 16:59 models/feature.js 4e7d202 14286 Sep 10 15:14 services/data/block.js cf7716e 356 Sep 10 17:24 templates/components/draw/axes-1d.hbs ed752af 578 Sep 10 18:42 templates/components/draw/axis-1d.hbs 8c18360 3946 Sep 10 15:36 utils/utility-chromosome.js added : 998f0c9 1623 Sep 10 18:30 utils/interval-calcs.js --- frontend/app/components/draw-map.js | 24 ++++- frontend/app/components/draw/axis-1d.js | 97 +++++++++++++++++-- frontend/app/components/draw/block-adj.js | 22 +++++ frontend/app/mixins/axis-position.js | 1 + frontend/app/models/block.js | 18 ++++ frontend/app/models/feature.js | 19 +++- frontend/app/services/data/block.js | 7 +- .../app/templates/components/draw/axes-1d.hbs | 2 +- .../app/templates/components/draw/axis-1d.hbs | 5 +- frontend/app/utils/interval-calcs.js | 52 ++++++++++ frontend/app/utils/utility-chromosome.js | 67 +++++++++++-- 11 files changed, 288 insertions(+), 26 deletions(-) create mode 100644 frontend/app/utils/interval-calcs.js diff --git a/frontend/app/components/draw-map.js b/frontend/app/components/draw-map.js index 4164f4e12..53dd2450f 100644 --- a/frontend/app/components/draw-map.js +++ b/frontend/app/components/draw-map.js @@ -18,7 +18,7 @@ scheduleIntoAnimationFrame = scheduleFrame.default; import config from '../config/environment'; import { EventedListener } from '../utils/eventedListener'; -import { chrData } from '../utils/utility-chromosome'; +import { chrData, cmNameAdd } from '../utils/utility-chromosome'; import { eltWidthResizable, eltResizeToAvailableWidth, noShiftKeyfilter, eltClassName, tabActive, inputRangeValue, expRange } from '../utils/domElements'; import { /*fromSelectionArray,*/ logSelectionLevel, logSelection, logSelectionNodes, selectImmediateChildNodes } from '../utils/log-selection'; import { parseOptions } from '../utils/common/strings'; @@ -484,7 +484,10 @@ export default Ember.Component.extend(Ember.Evented, { axisName2MapChr, axisStackChanged, axisScaleChanged, - axisRange2Domain + axisRange2Domain, + cmNameAdd, + makeMapChrName, + axisIDAdd }; console.log('draw-map stacks', stacks); this.set('stacks', stacks); @@ -1176,8 +1179,21 @@ export default Ember.Component.extend(Ember.Evented, { sBlock = oa.stacks.blocks[d], addedBlock = ! sBlock; if (! sBlock) { - oa.stacks.blocks[d] = sBlock = new Block(dBlock); - dBlock.set('view', sBlock); + /** sBlock may already be associated with dBlock */ + let view = dBlock.get('view'); + sBlock = view || new Block(dBlock); + oa.stacks.blocks[d] = sBlock; + if (! view) { + /* this .set() was getting assertion fail (https://github.com/emberjs/ember.js/issues/13948), + * hence the catch and trace; this has been resolved by not displaying .view in .hbs + */ + try { + dBlock.set('view', sBlock); + } + catch (exc) { + console.log('ensureAxis', d, dBlock, sBlock, addedBlock, view, oa.stacks.blocks, exc.stack || exc); + } + } } let s = Stacked.getStack(d); if (trace_stack > 1) diff --git a/frontend/app/components/draw/axis-1d.js b/frontend/app/components/draw/axis-1d.js index 35212f050..257b2466a 100644 --- a/frontend/app/components/draw/axis-1d.js +++ b/frontend/app/components/draw/axis-1d.js @@ -15,6 +15,9 @@ import { selectAxis } from '../../utils/draw/stacksAxes'; import { breakPoint } from '../../utils/breakPoint'; import { configureHorizTickHover } from '../../utils/hover'; import { getAttrOrCP } from '../../utils/ember-devel'; +import { intervalExtent } from '../../utils/interval-calcs'; +import { updateDomain } from '../../utils/stacksLayout'; + /* global d3 */ /* global require */ @@ -310,6 +313,75 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, AxisPosition, { } return dataBlocks; }), + /** @return the domains of the data blocks of this axis. + * The result does not contain a domain for data blocks with no features loaded. + */ + dataBlocksDomains : Ember.computed('dataBlocks.@each.featuresDomain', function () { + let dataBlocks = this.get('dataBlocks'), + dataBlockDomains = dataBlocks.map(function (b) { return b.get('featuresDomain'); } ) + /* featuresDomain() will return undefined when block has no features loaded. */ + .filter(d => d !== undefined); + return dataBlockDomains; + }), + referenceBlock : Ember.computed.alias('axisS.referenceBlock'), + /** @return the domains of all the blocks of this axis, including the reference block if any. + * @description related @see axesDomains() (draw/block-adj) + */ + blocksDomains : Ember.computed('dataBlocksDomains.[]', 'referenceBlock.range', function () { + let + /* alternative : + * dataBlocksMap = this.get('blockService.dataBlocks'), + * axisId = this.get('axis.id'), + * datablocks = dataBlocksMap.get(axisId), + */ + /** see also domainCalc(), blocksUpdateDomain() */ + blocksDomains = this.get('dataBlocksDomains'), + /** equivalent : Stacked:referenceDomain() */ + referenceRange = this.get('referenceBlock.range'); + if (referenceRange) { + console.log('referenceRange', referenceRange, blocksDomains); + blocksDomains.push(referenceRange); + } + return blocksDomains; + }), + /** @return the union of blocksDomains[], i.e. the interval which contains all + * the blocksDomains intervals. + */ + blocksDomain : Ember.computed('blocksDomains.[]', function () { + let + blocksDomains = this.get('blocksDomains'), + domain = intervalExtent(blocksDomains); + console.log('blocksDomain', blocksDomains, domain); + return domain; + }), + blocksDomainEffect : Ember.computed('blocksDomain', function () { + let domain = this.get('blocksDomain'), + /** if domain is [0,0] or [false, false] then consider that undefined. */ + domainDefined = domain && domain.length && (domain[0] || domain[1]); + if (domainDefined && ! this.get('zoomed')) + /* defer setting yDomain to the end of this render, to avoid assert fail + * re. change of domainChanged, refn issues/13948; + * that also breaks progressive loading and axis & path updates from zoom. + */ + Ember.run.later(() => { + this.setDomain(domain); + }); + }), + /** same as domainChanged, not used. */ + domainEffect : Ember.computed('domain', function () { + let domain = this.get('domain'); + if (domain) { + /* Similar to this.updateDomain(), defined in axis-position.js, */ + let axisS = this.get('axisS'); + console.log('domainEffect', domain, axisS); + if (axisS) { + let y = axisS.getY(), ys = axisS.ys; + updateDomain(axisS.y, axisS.ys, axisS); + } + } + return domain; + }), + /** count of features of .dataBlocks */ featureLength : Ember.computed('dataBlocks.@each.featuresLength', function () { let dataBlocks = this.get('dataBlocks'), @@ -324,6 +396,7 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, AxisPosition, { */ featureLengthEffect : Ember.computed('featureLength', 'axisS', function () { let featureLength = this.get('featureLength'); + this.renderTicksDebounce(); let axisApi = stacks.oa.axisApi, /** defined after first brushHelper() call. */ @@ -394,20 +467,24 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, AxisPosition, { /** position as of the last zoom. */ domain : Ember.computed.alias('currentPosition.yDomain'), - /** this is an alias of .domain, but it updates when the array elements update. */ + /** Updates when the array elements of .domain[] update. + * @return undefined; value is unused. + */ domainChanged : Ember.computed( 'domain.0', 'domain.1', function () { let domain = this.get('domain'); - // use the VLinePosition:toString() for the position-s - console.log('domainChanged', domain, this.get('axisS'), ''+this.get('currentPosition'), ''+this.get('lastDrawn')); - // this.notifyChanges(); - if (! this.get('axisS')) - console.log('domainChanged() no axisS yet', domain, this.get('axis.id')); - else - this.updateAxis(); - - return domain; + // domain is initially undefined + if (domain) { + // use the VLinePosition:toString() for the position-s + console.log('domainChanged', domain, this.get('axisS'), ''+this.get('currentPosition'), ''+this.get('lastDrawn')); + // this.notifyChanges(); + if (! this.get('axisS')) + console.log('domainChanged() no axisS yet', domain, this.get('axis.id')); + else + this.updateAxis(); + } + return undefined; }), notifyChanges() { let axisID = this.get('axis.id'); diff --git a/frontend/app/components/draw/block-adj.js b/frontend/app/components/draw/block-adj.js index edf042416..c2e6c8f52 100644 --- a/frontend/app/components/draw/block-adj.js +++ b/frontend/app/components/draw/block-adj.js @@ -401,6 +401,20 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, { .attr("d", function(d) { return d.pathU() /*get('pathU')*/; }); }, + /** Call updateAxis() for the axes which bound this block-adj. + * See comment in updatePathsPositionDebounce(). + */ + updateAxesScale() { + let + axes = this.get('axes'), + /** reference blocks */ + axesBlocks = axes.mapBy('blocks'); + console.log('updateAxesScale', axesBlocks.map((blocks) => blocks.mapBy('axisName'))); + axesBlocks.forEach(function (blocks) { + blocks[0].axis.axis1d.updateAxis(); + }); + }, + /*--------------------------------------------------------------------------*/ axesDomains : Ember.computed.alias('blockAdj.axesDomains'), @@ -419,6 +433,14 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, { domainsChanged = this.get('axesDomains'); console.log('updatePathsPositionDebounce', this.get('blockAdjId'), heightChanged, count, domainsChanged); this.updatePathsPosition(); + + /* this update is an alternative trigger for updating the axes ticks and + * scale when their domains change, e.g. when loaded features extend a + * block's domain. The solution used instead is the ComputedProperty + * side-effect axis-1d : domainChanged(), which is a similar approach, but + * it localises the dependencies to a single axis whereas this would + * duplicate updates. */ + // this.updateAxesScale(); return count; }), diff --git a/frontend/app/mixins/axis-position.js b/frontend/app/mixins/axis-position.js index 4856facf3..a285cb99f 100644 --- a/frontend/app/mixins/axis-position.js +++ b/frontend/app/mixins/axis-position.js @@ -49,6 +49,7 @@ export default Mixin.create({ if (! axisS) { /** This replicates the role of axis-1d.js:axisS(); this will be solved * when Stacked is created and owned by axis-1d. + * (also : now using ensureAxis() in data/block.js : axesBlocks()) */ let axisName = this.get('axis.id'); axisS = Stacked.getAxis(axisName); diff --git a/frontend/app/models/block.js b/frontend/app/models/block.js index ca0493e04..2cebf3663 100644 --- a/frontend/app/models/block.js +++ b/frontend/app/models/block.js @@ -3,6 +3,9 @@ import DS from 'ember-data'; import attr from 'ember-data/attr'; // import { PartialModel, partial } from 'ember-data-partial-model/utils/model'; +import { intervalMerge } from '../utils/interval-calcs'; + + export default DS.Model.extend({ datasetId: DS.belongsTo('dataset'), annotations: DS.hasMany('annotation', { async: false }), @@ -71,6 +74,21 @@ export default DS.Model.extend({ console.log('featuresLength', featuresLength, this.get('id')); return featuresLength; }), + /** @return undefined if ! features.length, + * otherwise [min, max] of block's feature.value + */ + featuresDomain : Ember.computed('features.[]', function () { + let featuresDomain, features = this.get('features'); + if (features.length) { + featuresDomain = features + .mapBy('value') + .reduce(intervalMerge, []); + + console.log('featuresDomain', featuresDomain, this.get('id')); + } + return featuresDomain; + }), + isChartable : Ember.computed('datasetId.tags', function () { let tags = this.get('datasetId.tags'), diff --git a/frontend/app/models/feature.js b/frontend/app/models/feature.js index 543d75cce..8d7ffabd2 100644 --- a/frontend/app/models/feature.js +++ b/frontend/app/models/feature.js @@ -1,3 +1,4 @@ +import { computed } from '@ember/object'; import DS from 'ember-data'; import attr from 'ember-data/attr'; //import Fragment from 'model-fragments/fragment'; @@ -10,5 +11,21 @@ export default DS.Model.extend({ value: attr(), range: attr(), parentId: DS.belongsTo('feature', {inverse: 'features'}), - features: DS.hasMany('feature', {inverse: 'parentId'}) + features: DS.hasMany('feature', {inverse: 'parentId'}), + + /*--------------------------------------------------------------------------*/ + + /** feature can have a direction, i.e. (value[0] > value[1]) + * For domain calculation, the ordered value is required. + */ + valueOrdered : computed('value', function () { + let value = this.get('value'); + if (value[0] > value[1]) { + let value = [value[1], value[0]]; + } + return value; + }) + + /*--------------------------------------------------------------------------*/ + }); diff --git a/frontend/app/services/data/block.js b/frontend/app/services/data/block.js index e0ef28030..1e2a73068 100644 --- a/frontend/app/services/data/block.js +++ b/frontend/app/services/data/block.js @@ -266,9 +266,10 @@ export default Service.extend(Ember.Evented, { return records; // .toArray() }), viewed: Ember.computed( + 'blockValues.[]', 'blockValues.@each.isViewed', function() { - let records = this.get('blockValues') + let records = this.get('store').peekAll('block') // this.get('blockValues') .filterBy('isViewed', true); if (trace_block) console.log('viewed', records.toArray()); @@ -374,8 +375,10 @@ export default Service.extend(Ember.Evented, { function (map, block) { let axis = block.get('axis'); if (! axis) { + let oa = stacks.oa, axisApi = oa.axisApi; + axisApi.cmNameAdd(oa, block); console.log('axesBlocks ensureAxis', block.get('id')); - stacks.oa.axisApi.ensureAxis(block.get('id')); + axisApi.ensureAxis(block.get('id')); stacks.forEach(function(s){s.log();}); axis = block.get('axis'); console.log('axesBlocks', axis); diff --git a/frontend/app/templates/components/draw/axes-1d.hbs b/frontend/app/templates/components/draw/axes-1d.hbs index 257a29832..338f33f03 100644 --- a/frontend/app/templates/components/draw/axes-1d.hbs +++ b/frontend/app/templates/components/draw/axes-1d.hbs @@ -1,6 +1,6 @@
axesP : {{axesP.length}}
{{#each axesP as |axis axisIndex|}} -
axis : {{axisIndex}}, {{axis.view}} {{axis.view.axisName}}, {{axis.extended}}
+
axis : {{axisIndex}}, {{axis.extended}}
{{#draw/axis-1d drawMap=drawMap axis=axis axes2d=axes2d as |axis1d|}} {{draw/axis-ticks-selected axis1d=axis1d axisId=axis.id drawMap=drawMap diff --git a/frontend/app/templates/components/draw/axis-1d.hbs b/frontend/app/templates/components/draw/axis-1d.hbs index 53a2c4565..4b776b5e4 100644 --- a/frontend/app/templates/components/draw/axis-1d.hbs +++ b/frontend/app/templates/components/draw/axis-1d.hbs @@ -1,3 +1,6 @@ -axis-1d: {{ extended }} {{ axis.id }} {{ domainChanged }} , {{ position }}, {{ currentPosition }}, {{ lastDrawn }}, {{ currentPosition.yDomain }} {{ zoomed }}, {{ dataBlocks.length }} {{ dataBlocks.0.features.length }} {{ featureLengthEffect }} +{{!-- commented-out values may be changed during the render which + will cause displaying their value to produce an exception, + refn https://github.com/emberjs/ember.js/issues/13948 --}} +axis-1d: {{ extended }} {{ axis.id }}, {{!-- axis.view}} {{axis.view.axisName , --}} {{ domainChanged }} , {{ position }}, {{ currentPosition }}, {{ lastDrawn }}, {{!-- currentPosition.yDomain --}} {{ zoomed }}, {{ dataBlocks.length }} {{ dataBlocks.0.features.length }} {{ featureLengthEffect }} {{ blocksDomainEffect }} {{ log 'axis-1d rendered domainContents' position }} {{yield this }} diff --git a/frontend/app/utils/interval-calcs.js b/frontend/app/utils/interval-calcs.js new file mode 100644 index 000000000..b8f9d4f5e --- /dev/null +++ b/frontend/app/utils/interval-calcs.js @@ -0,0 +1,52 @@ + +/*----------------------------------------------------------------------------*/ + +/* global d3 */ + +/*----------------------------------------------------------------------------*/ + +const +intervalLimit = [d3.min, d3.max], +/** Choose the outside values, as with d3.extent() + * true if value a is outside the domain limit b. + */ +intervalOutside = [(a, b) => (a < b), + (a, b) => (a > b), + ]; + +/** Merge the given interval v into the domain, so that the result domain + * contains the interval. + * + * Used within .reduce(), e.g. : + * intervals.reduce(intervalMerge, []); + * @param domain result of merging the intervals. + * form is [min, max]. + * @param v a single interval (feature value). can be either direction, i.e. doesn't assume f[0] < f[1] + * @see intervalExtent() + */ +function intervalMerge(domain, v) { + // let v = f.get('valueOrdered'); + + [0, 1].forEach(function (i) { + /** the limit value of the interval v, in the direction i. + * The result domain is ordered [min, max] whereas the input values v are + * not; this translates the unordered value to the ordered result. + */ + let limit = intervalLimit[i](v); + if ((domain[i] === undefined) || intervalOutside[i](limit, domain[i])) + domain[i] = limit; + }); + + return domain; +} + +/** Calculate the union of the given intervals. + */ +function intervalExtent(intervals) { + let extent = intervals.reduce(intervalMerge, []); + return extent; +} + +/*----------------------------------------------------------------------------*/ + +export { intervalLimit, intervalOutside, intervalMerge, intervalExtent }; diff --git a/frontend/app/utils/utility-chromosome.js b/frontend/app/utils/utility-chromosome.js index 03fc53000..e93af3fb0 100644 --- a/frontend/app/utils/utility-chromosome.js +++ b/frontend/app/utils/utility-chromosome.js @@ -2,13 +2,10 @@ import { breakPoint } from '../utils/breakPoint'; /*----------------------------------------------------------------------------*/ -/** bundle chr data (incl features) for draw-map:draw(). - * copy of Ember.RSVP.hash(promises).then(); factor these together. - * @param c aka chrs[chr] +/** Copy fields from a chromosome (block) Ember DS object. + * @param c chromosome (block) object in the Ember data store */ -function chrData(c) { - /* factored from controllers/mapview.js, where it was originally developed. */ - +function copyChrData(c) { let map = c.get('datasetId'), // replaces c.get('map'), /* rc aka retHash[chr] */ @@ -21,6 +18,19 @@ function chrData(c) { rc[fieldName] = c.get(fieldName); }); + return rc; +} + +/** bundle chr data (incl features) for draw-map:draw(). + * copy of Ember.RSVP.hash(promises).then(); factor these together. + * @param c aka chrs[chr] + */ +function chrData(c) { + /* factored from controllers/mapview.js, where it was originally developed. */ + + let rc = copyChrData(c), + map = rc.map; + let f = c.get('features'); f.forEach(function(feature) { let featureName = feature.get('name'); @@ -50,4 +60,47 @@ function chrData(c) { /*----------------------------------------------------------------------------*/ -export { chrData }; +/** Support some data structures which are wrappers around Ember store data : + * cmName and mapChr2Axis. + * + * There is some value in being framework-independent, which these data + * structures (and z etc) offer, but the advantages of integrating more fully with + * Ember, e.g. ComputedProperty-s, make it worthwhile. + * So these data structures can be progressively replaced via the strangler fig + * model (as described by Martin Fowler, https://www.martinfowler.com/bliki/StranglerApplication.html). + */ +function cmNameAdd(oa, block) { + let + axis = block.get('id'), + cmName = oa.cmName, + mapChr2Axis = oa.mapChr2Axis, + c = copyChrData(block); + /* Based on draw-map.js:receiveChr() */ + + let dataset = c.dataset, + datasetName = dataset && dataset.get('name'), + parent = dataset && dataset.get('parent'), + parentName = parent && parent.get('name') + ; + // oa.datasets[] seems unused. + if (oa.datasets[datasetName] === undefined) + { + oa.datasets[datasetName] = dataset; + console.log(datasetName, dataset.get('meta.shortName')); + } + + cmName[axis] = {mapName : c.mapName, chrName : c.chrName + , parent: parentName + , name : c.name, range : c.range + , scope: c.scope, featureType: c.featureType + , dataset : dataset + }; + + let mapChrName = oa.axisApi.makeMapChrName(c.mapName, c.chrName); + mapChr2Axis[mapChrName] = axis; + + oa.axisApi.axisIDAdd(axis); +} +/*----------------------------------------------------------------------------*/ + +export { chrData, cmNameAdd };