From 3885584cdf884427b005215479a3260a9a81427c Mon Sep 17 00:00:00 2001 From: Don Date: Sun, 5 Jan 2020 14:34:50 +1100 Subject: [PATCH 01/57] rename components/axis-chart.js as utils/draw/chart1.js After the split, most of the axis-chart.js will be in chart1.js, so rename before edit maintains more of the code history frontend/app/ : 8b91567 components/axis-chart.js aka utils/draw/chart1.js --- frontend/app/{components/axis-chart.js => utils/draw/chart1.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/app/{components/axis-chart.js => utils/draw/chart1.js} (100%) diff --git a/frontend/app/components/axis-chart.js b/frontend/app/utils/draw/chart1.js similarity index 100% rename from frontend/app/components/axis-chart.js rename to frontend/app/utils/draw/chart1.js From 5a3b303b7d60323564c4e56a475b0c2a76379fee Mon Sep 17 00:00:00 2001 From: Don Date: Mon, 6 Jan 2020 10:07:48 +1100 Subject: [PATCH 02/57] Split Chart1 out of axis-chart Most of axis-chart.js is moved to chart1.js, so I did a rename to chart1.js in the previous commit and this commit moves the remainder back to axis-chart.js, i.e. axis-chart component and its attributes : blockService, className, didRender, blockFeatures(), featuresCounts(), drawBlockFeatures0(), drawBlockFeaturesCounts(), drawBlockFeatures(), redraw(), parseTextData(), pasteProcess(). layoutAndDrawChart() (containing Chart1) remains; next step will move layoutAndDrawChart into class Chart1. layoutAndDrawChart() : change some this to axisChart. chart1.js: import path ../utils/ is now ../. Handle extra data in Feature.value, after the range .value[0..1], e.g. .value[2] : block-features.js blockFeatureLimits() : group : slice value to get [0..1] and filter out null values. paths-aggr.js : valueBound() : address value[1] instead of value[length-1] - same effect, ignores extra data. backend/common/utilities/ : 30787d0 4839 Jan 5 21:19 block-features.js 74c8a7c 28697 Jan 5 21:53 paths-aggr.js frontend/app/ : 06dd3ad 13950 Jan 5 15:55 utils/draw/chart1.js ea5ac9f 7888 Jan 5 22:41 components/axis-chart.js --- backend/common/utilities/block-features.js | 10 +- backend/common/utilities/paths-aggr.js | 2 +- frontend/app/components/axis-chart.js | 225 +++++++++++++++++++++ frontend/app/utils/draw/chart1.js | 212 ++----------------- 4 files changed, 253 insertions(+), 196 deletions(-) create mode 100644 frontend/app/components/axis-chart.js diff --git a/backend/common/utilities/block-features.js b/backend/common/utilities/block-features.js index 865781b22..a2597951d 100644 --- a/backend/common/utilities/block-features.js +++ b/backend/common/utilities/block-features.js @@ -114,10 +114,18 @@ exports.blockFeatureLimits = function(db, blockId) { console.log('blockFeatureLimits', blockId); let ObjectId = ObjectID; + /** unwind the values array of Features, and group by blockId, with the min & + * max values within the Block. + * value may be [from, to, anyValue], so use slice to extract just [from, to], + * and $match $ne null to ignore to === undefined. + * The version prior to adding this comment assumed value was just [from, to] (optional to); + * so we can revert to that code if we separate additional values from the [from,to] location range. + */ let group = [ - {$project : {_id : 1, name: 1, blockId : 1, value : 1 }}, + {$project : {_id : 1, name: 1, blockId : 1, value : {$slice : ['$value', 2]} }}, {$unwind : '$value'}, + {$match: { $or: [ { value: { $ne: null } } ] } }, {$group : { _id : '$blockId' , featureCount : { $sum: 1 }, diff --git a/backend/common/utilities/paths-aggr.js b/backend/common/utilities/paths-aggr.js index c9b7e8431..68c051b87 100644 --- a/backend/common/utilities/paths-aggr.js +++ b/backend/common/utilities/paths-aggr.js @@ -311,7 +311,7 @@ function keyValue (k,v) { let r = {}; r[k] = v; return r; }; function valueBound(intervals, b, l) { let r = keyValue( l ? '$lte' : '$gte', - [ keyValue('$arrayElemAt', ['$value', l ? -1 : 0]), + [ keyValue('$arrayElemAt', ['$value', l ? 1 : 0]), +intervals.axes[b].domain[l]] ); return r; diff --git a/frontend/app/components/axis-chart.js b/frontend/app/components/axis-chart.js new file mode 100644 index 000000000..9376413c8 --- /dev/null +++ b/frontend/app/components/axis-chart.js @@ -0,0 +1,225 @@ +import Ember from 'ember'; +const { inject: { service } } = Ember; + +import { className, layoutAndDrawChart /*Chart1*/ } from '../utils/draw/chart1'; +import InAxis from './in-axis'; + +/*----------------------------------------------------------------------------*/ + +const dLog = console.debug; + +/*----------------------------------------------------------------------------*/ + + +/* global d3 */ + +/** Display data which has a numeric value for each y axis position (feature). + * Shown as a line curve or bar chart, with the y axis of the graph as the baseline. + * + * @param block a block returned by viewedChartable() + * @param chart data (field name is className); may be either : + * result of parseTextData() : array of {name : , value : , description : } + * or chartBlock passed in : .features + * @param axis axisComponent; parent axis-2d component + * @param axisID axisID + * @param data oa + * @param width resizedWidth + *---------------- + * data attributes created locally, not passed in : + * @param chart1 + */ +export default InAxis.extend({ + blockService: service('data/block'), + + className : className, + + didRender() { + console.log("components/axis-chart didRender()"); + }, + + blockFeatures : Ember.computed('block', 'block.features.[]', 'axis.axis1d.domainChanged', function () { + if (this.get('block.isChartable')) + this.drawBlockFeatures0(); + }), + featuresCounts : Ember.computed('block', 'block.featuresCounts.[]', 'axis.axis1d.domainChanged', function () { + /* perhaps later draw both, for the moment just draw 1, and since all data + * blocks have featuresCounts, plot the features of those blocks which are + * chartable, so that we can see both capabilities are working. + */ + if (! this.get('block.isChartable')) + this.drawBlockFeaturesCounts(); + return this.get('block.featuresCounts'); + }), + + drawBlockFeatures0 : function() { + let features = this.get('block.features'); + let domain = this.get('axis.axis1d.domainChanged'); + console.log('blockFeatures', features.length, domain); + if (features.length) // - should also handle drawing when .length changes to 0 + { + if (features.hasOwnProperty('promise')) + features = features.toArray(); + if (features[0] === undefined) + dLog('drawBlockFeatures0', features.length, domain); + else + this.drawBlockFeatures(features); + } + }, + drawBlockFeaturesCounts : function() { + let featuresCounts = this.get('block.featuresCounts'); + let domain = this.get('axis.axis1d.domainChanged'); + if (featuresCounts) { + console.log('drawBlockFeaturesCounts', featuresCounts.length, domain, this.get('block.id')); + + /** example element of array f : */ + const dataExample = + { + "_id": { + "min": 100, + "max": 160 + }, + "count": 109 + }; + let f = featuresCounts.toArray(), + /** the min, max will be passed also - first need to factor out part of axis-chart for featuresCounts. */ + fa = f; // .map(function (f0) { return f0.count;}); + console.log('drawBlockFeaturesCounts', f); + let + featureCountData = { + dataTypeName : 'featureCountData', + datum2Location : function datum2Location(d) { return d._id.min; }, // todo : use .max + datum2Value : function(d) { return d.count; }, + datum2Description : function(d) { return JSON.stringify(d._id); } + }; + // pass alternate dataConfig to layoutAndDrawChart(), defining alternate functions for {datum2Value, datum2Location } + this.layoutAndDrawChart(fa, featureCountData); + } + }, + drawBlockFeatures : function(features) { + let f = features.toArray(), + fa = f.map(function (f0) { return f0._internalModel.__data;}); + + let axisID = this.get("axis.axisID"), + oa = this.get('data'), + za = oa.z[axisID]; + /* if za has not been populated with features, it will have just .dataset + * and .scope, i.e. .length === 2 */ + if (Object.keys(za).length == 2) { + dLog('drawBlockFeatures()', axisID, za, fa); + // add features to za. + fa.forEach((f) => za[f.name] = f.value); + } + + this.layoutAndDrawChart(fa); + }, + + redraw : function(axisID, t) { + let data = this.get(className), + layoutAndDrawChart = this.get('layoutAndDrawChart'); + if (data) { + console.log("redraw", this, (data === undefined) || data.length, axisID, t); + if (data) + layoutAndDrawChart.apply(this, [data]); + } + else { // use block.features when not using data parsed from table. + this.drawBlockFeatures0(); + } + }, + + /*--------------------------------------------------------------------------*/ + /* These 2 functions provide a way to paste an array of data to be plotted in the chart. + * This was used in early prototype; an alternative is to use upload tab to + * add a data block - maybe this more direct and lightweight approach could + * have some use. + */ + + /** Convert input text to an array. + * Used to process text from clip-board, by @see pasteProcess(). + * @param tableText text string, TSV, rows separated by \n and/or \r. + * First row may contain a header with column names, indicated by leading #. + * Column names "name", "value" and "description" indicate the columns containing those values, + * otherwise the default columns are 0, 1, 2 respectively. + * Other columns are appended to the description value of the row. + */ + parseTextData(tableText) + { + /* can replace most of this function with d3.tsv; + * It currently implements the feature that header is optional; to replicate that can use + * dsv.parse() when header is input and dsv.parseRows() otherwise. + */ + let values = []; + let rows = tableText.split(/[\n\r]+/); + let colIdx = {name : 0, value : 1, description : 2}; + for (let i=0; i za[f.name] = f.value); - } - - this.layoutAndDrawChart(fa); - }, - - redraw : function(axisID, t) { - let data = this.get(className), - layoutAndDrawChart = this.get('layoutAndDrawChart'); - if (data) { - console.log("redraw", this, (data === undefined) || data.length, axisID, t); - if (data) - layoutAndDrawChart.apply(this, [data]); - } - else { // use block.features when not using data parsed from table. - this.drawBlockFeatures0(); - } - }, - - /** Convert input text to an array. - * @param tableText text string, TSV, rows separated by \n and/or \r. - * First row may contain a header with column names, indicated by leading #. - * Column names "name", "value" and "description" indicate the columns containing those values, - * otherwise the default columns are 0, 1, 2 respectively. - * Other columns are appended to the description value of the row. - */ - parseTextData(tableText) - { - /* can replace most of this function with d3.tsv; - * It currently implements the feature that header is optional; to replicate that can use - * dsv.parse() when header is input and dsv.parseRows() otherwise. - */ - let values = []; - let rows = tableText.split(/[\n\r]+/); - let colIdx = {name : 0, value : 1, description : 2}; - for (let i=0; i // g.axis-outer#id let - axisComponent = this.get("axis"), + axisComponent = axisChart.get("axis"), axisID = axisComponent.axisID, gAxis = d3.select("g.axis-outer#id" + axisID + "> g.axis-use"), /** relative to the transform of parent g.axis-outer */ @@ -300,7 +138,7 @@ export default InAxis.extend({ /** isBlockData is not used if dataConfig is defined. this can be moved out to the caller. */ isBlockData = chart.length && (chart[0].description === undefined), valueName = chart.valueName || "Values", - oa = this.get('data'), + oa = axisChart.get('data'), // axisID = gAxis.node().parentElement.__data__, yAxis = oa.y[axisID], // this.get('y') yAxisDomain = yAxis.domain(), yDomain; @@ -321,7 +159,7 @@ export default InAxis.extend({ return inRange(dataConfig.datum2Location(d), yDomain); }, data = chart.filter(withinZoomRegion); - let resizedWidth = this.get('width'); + let resizedWidth = axisChart.get('width'); console.log(resizedWidth, bbox, yDomain, pxSize, data.length, (data.length == 0) || dataConfig.datum2Location(data[0])); if (resizedWidth) bbox.width = resizedWidth; @@ -533,7 +371,7 @@ export default InAxis.extend({ * complexity in ensuring that the instance rendered by toggleBarsLineClosure() is * the same one whose options.bbox.width is updated from axis-chart.width. */ - let chart1 = this.get("chart1"); + let chart1 = axisChart.get("chart1"); if (chart1) { chart1.options.bbox.width = bbox.width; @@ -549,7 +387,7 @@ export default InAxis.extend({ datum2Value : dataConfig.datum2Value, datum2Description : dataConfig.datum2Description }); - this.set("chart1", chart1); + axisChart.set("chart1", chart1); addParentClass(g); } let b = chart1; // b for barChart @@ -572,24 +410,10 @@ export default InAxis.extend({ b.chartTypeToggle = chartTypeToggle; b.draw(data); - }, - - pasteProcess: function(textPlain) { - console.log("components/axis-chart pasteProcess", textPlain.length); - - let - parseTextData = this.get('parseTextData'), - layoutAndDrawChart = this.get('layoutAndDrawChart'); - - let chart = parseTextData(textPlain); - this.set(className, chart); // used by axisStackChanged() : redraw() : layoutAndDrawChart() - let forTable = chart; - chart.valueName = "values"; // add user config - // ; draw chart. - layoutAndDrawChart.apply(this, [chart]); - - this.set('data.chart', forTable); - }, + } +; +/*----------------------------------------------------------------------------*/ -}); +/* subsequent step will move layoutAndDrawChart into class Chart1, and export that instead. */ +export { className, layoutAndDrawChart /*Chart1*/ }; From ee7d30abe0a99c849288c344f571106c98228e67 Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 8 Jan 2020 11:26:14 +1100 Subject: [PATCH 03/57] featuresCounts: use .max for rect height axis-2d : add a local filter (by axisName) of viewedChartable(). axis-tracks.js : tracksTree() : intervals : handle null in interval[1]. axis-chart.js : use .max for rect height, and to support this : chart1.js : add middle(), scaleMaybeInterval(), inRangeEither() (in zoomPanCalcs.js), rectHeight(); y scale : use scaleLinear instead of scaleBand, rectHeight() in place of bandwidth(); factor scale setup and domain extent to form scaleLinear(); the data locations may now by intervals so flatten them for extent. Add .block to Chart1; un-factor configureChartHover(), because using block from closure. Add a hoverTextFn() for featureCountData, and add it to dataConfig. frontend/app/ : a1d75d9 10984 Jan 7 13:35 components/axis-2d.js aec4639 8402 Jan 7 19:36 components/axis-chart.js 055f827 25150 Jan 6 16:13 components/axis-tracks.js b2abf50 1834 Jan 7 13:35 templates/components/axis-2d.hbs b33a174 17787 Jan 7 22:59 utils/draw/chart1.js 0a19b1b 10642 Jan 7 16:12 utils/draw/zoomPanCalcs.js 67eef09 1062 Jan 7 18:41 utils/hover.js --- frontend/app/components/axis-2d.js | 9 + frontend/app/components/axis-chart.js | 18 +- frontend/app/components/axis-tracks.js | 4 + frontend/app/templates/components/axis-2d.hbs | 2 +- frontend/app/utils/draw/chart1.js | 161 +++++++++++++----- frontend/app/utils/draw/zoomPanCalcs.js | 11 +- frontend/app/utils/hover.js | 3 + 7 files changed, 164 insertions(+), 44 deletions(-) diff --git a/frontend/app/components/axis-2d.js b/frontend/app/components/axis-2d.js index 43acb14c0..93938194e 100644 --- a/frontend/app/components/axis-2d.js +++ b/frontend/app/components/axis-2d.js @@ -69,6 +69,15 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, { return dataBlocks; }), + viewedChartable : Ember.computed('blockService.viewedChartable.[]', 'axisID', + function () { + let + id = this.get('axisID'), + viewedChartable = this.get('blockService.viewedChartable') + .filter((b) => b.axis.axisName === id); + console.log('viewedChartable', id, viewedChartable); + return viewedChartable; + }), /*--------------------------------------------------------------------------*/ diff --git a/frontend/app/components/axis-chart.js b/frontend/app/components/axis-chart.js index 9376413c8..4b18ff4ee 100644 --- a/frontend/app/components/axis-chart.js +++ b/frontend/app/components/axis-chart.js @@ -87,9 +87,16 @@ export default InAxis.extend({ let featureCountData = { dataTypeName : 'featureCountData', - datum2Location : function datum2Location(d) { return d._id.min; }, // todo : use .max + datum2Location : function datum2Location(d) { return [d._id.min, d._id.max]; }, datum2Value : function(d) { return d.count; }, - datum2Description : function(d) { return JSON.stringify(d._id); } + /** datum2Description() is not used; possibly intended for the same + * purpose as hoverTextFn(), so they could be assimilated. */ + datum2Description : function(d) { return JSON.stringify(d._id); }, + hoverTextFn : function (d, block) { + let valueText = '[' + d._id.min + ',' + d._id.max + '] : ' + d.count, + blockName = block.view && block.view.longName(); + return valueText + '\n' + blockName; + } }; // pass alternate dataConfig to layoutAndDrawChart(), defining alternate functions for {datum2Value, datum2Location } this.layoutAndDrawChart(fa, featureCountData); @@ -121,8 +128,11 @@ export default InAxis.extend({ if (data) layoutAndDrawChart.apply(this, [data]); } - else { // use block.features when not using data parsed from table. - this.drawBlockFeatures0(); + else { // use block.features or block.featuresCounts when not using data parsed from table. + if (this.get('block.isChartable')) + this.drawBlockFeatures0(); + else + this.drawBlockFeaturesCounts(); } }, diff --git a/frontend/app/components/axis-tracks.js b/frontend/app/components/axis-tracks.js index 7dceb967b..8a04eaa66 100644 --- a/frontend/app/components/axis-tracks.js +++ b/frontend/app/components/axis-tracks.js @@ -587,6 +587,10 @@ export default InAxis.extend({ if (! interval.length || (interval.length == 1)) interval = [interval, interval]; /* interval-tree:createIntervalTree() assumes the intervals are positive, and gets stack overflow if not. */ + else if (interval[1] === null) { + /* undefined / null value[1] indicates 0-length interval. */ + interval[1] = interval[0]; + } else if (interval[0] > interval[1]) { let swap = interval[0]; interval[0] = interval[1]; diff --git a/frontend/app/templates/components/axis-2d.hbs b/frontend/app/templates/components/axis-2d.hbs index 76ed85951..976e4ec28 100644 --- a/frontend/app/templates/components/axis-2d.hbs +++ b/frontend/app/templates/components/axis-2d.hbs @@ -35,7 +35,7 @@
axis-2d :{{this}}, {{axisID}}, {{targetEltId}}, {{subComponents.length}} :
subComponents : - {{#each blockService.viewedChartable as |chartBlock|}} + {{#each viewedChartable as |chartBlock|}} {{axis-chart data=data axis=this axisID=axisID block=chartBlock}} {{/each}} {{log 'blockService' blockService 'viewedChartable' blockService.viewedChartable}} diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index 8fe4a9223..5fe927e3d 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -4,6 +4,7 @@ import { configureHorizTickHover } from '../hover'; import { eltWidthResizable, noShiftKeyfilter } from '../domElements'; import { noDomain } from '../draw/axis'; import { stacks } from '../stacks'; // just for oa.z and .y, don't commit this. +import { inRangeEither } from './zoomPanCalcs'; const className = "chart", classNameSub = "chartRow"; @@ -16,15 +17,7 @@ const dLog = console.debug; /*----------------------------------------------------------------------------*/ /* Copied from draw-map.js */ - /** - * @return true if a is in the closed interval range[] - * @param a value - * @param range array of 2 values - limits of range. - */ - function inRange(a, range) - { - return range[0] <= a && a <= range[1]; - } + function featureLocation(oa, axisID, d) { @@ -37,7 +30,33 @@ function featureLocation(oa, axisID, d) else return feature.location; } + +/** If the given value is an interval, convert it to a single value by calculating the middle of the interval. + * @param location is a single value or an array [from, to] + * @return the middle of an interval + */ +function middle(location) { + let result = location.length ? + location.reduce((sum, val) => sum + val, 0) / location.length + : location; + return result; +} +/** @return a function to map a chart datum to a y value or interval. + */ +function scaleMaybeInterval(datum2Location, yScale) { + /* In both uses in this file, the result is passed to middle(), so an argument + * could be added to scaleMaybeInterval() to indicate the result should be a + * single value (using mid-point if datum location is an interval). + */ + + function datum2LocationScaled(d) { + /** location may be an interval [from, to] or a single value. */ + let l = datum2Location(d); + return l.length ? l.map((li) => yScale(li)) : yScale(l); }; + return datum2LocationScaled; +} + /*----------------------------------------------------------------------------*/ /* based on axis-1d.js: hoverTextFn() and setupHover() */ @@ -45,25 +64,21 @@ function featureLocation(oa, axisID, d) function hoverTextFn (feature, block) { let value = getAttrOrCP(feature, 'value'), + /** undefined values are filtered out below. */ valueText = value && (value.length ? ('' + value[0] + ' - ' + value[1]) : value), - blockR = block.block, + /** block.view is the Stacks Block. */ + blockName = block.view && block.view.longName(), featureName = getAttrOrCP(feature, 'name'), /** common with dataConfig.datum2Description */ description = value && JSON.stringify(value), - text = [featureName, valueText, description] + text = [featureName, valueText, description, blockName] .filter(function (x) { return x; }) .join(" : "); return text; }; -function configureChartHover(feature) -{ - let block = this.parentElement.__data__; - return configureHorizTickHover.apply(this, [feature, block, hoverTextFn]); -}; - /** Add a .hasChart class to the which contains this chart. * Currently this is used to hide the so that hover events are @@ -102,12 +117,14 @@ let oa = stacks.oa, datum2Location, datum2Value : datum2Value, datum2Description : function(d) { return d.description; } - }, blockData = { dataTypeName : 'blockData', datum2Location, - datum2Value : function(d) { return d.value[0]; }, + /** The effects data is placed in .value[2] (the interval is in value[0..1]). + * Use the first effects value by default, but later will combine other values. + */ + datum2Value : function(d) { let v = d.value[2]; if (v.length) v = v[0]; return v; }, datum2Description : function(d) { return JSON.stringify(d.value); } }; @@ -150,13 +167,16 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) else yDomain = [yAxis.invert(yrange[0]), yAxis.invert(yrange[1])]; - if (! dataConfig) + if (! dataConfig) { dataConfig = isBlockData ? blockData : parsedData; + if (! dataConfig.hoverTextFn) + dataConfig.hoverTextFn = hoverTextFn; + } let pxSize = (yDomain[1] - yDomain[0]) / bbox.height, withinZoomRegion = function(d) { - return inRange(dataConfig.datum2Location(d), yDomain); + return inRangeEither(dataConfig.datum2Location(d), yDomain); }, data = chart.filter(withinZoomRegion); let resizedWidth = axisChart.get('width'); @@ -187,23 +207,30 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) parentH = options.bbox.height, // +pp.attr("height") width = parentW - margin.left - margin.right, height = parentH - margin.top - margin.bottom; + /** The chart is perpendicular to the usual presentation. + * The names x & y (in {x,y}Range and yLine) match the orientation on the + * screen, rather than the role of abscissa / ordinate; the data maps + * .name (location) -> .value, which is named y -> x. + */ let xRange = [0, width], - yRange = [height, 0], - y = d3.scaleBand().rangeRound(yRange).padding(0.1), + yRange = [0, height], + // yRange is used as range of yLine by this.scaleLinear(). + /* scaleBand would suit a data set with evenly spaced or ordinal / nominal y values. + * yBand = d3.scaleBand().rangeRound(yRange).padding(0.1), + */ + y = this.scaleLinear(yRange, data), x = d3.scaleLinear().rangeRound(xRange); // datum2LocationScaled() uses me.x rather than the value in the closure in which it was created. this.x = x; // Used by bars() - could be moved there, along with datum2LocationScaled(). this.y = y; - // line() does not use y; it creates yLine and uses yRange, to set its range. - this.yRange = yRange; console.log("Chart1", parentW, parentH, xRange, yRange, options.dataTypeName); let me = this; /* these can be renamed datum2{abscissa,ordinate}{,Scaled}() */ /* apply y after scale applied by datum2Location */ - function datum2LocationScaled(d) { return me.y(options.datum2Location(d)); } + let datum2LocationScaled = scaleMaybeInterval(options.datum2Location, me.y); function datum2ValueScaled(d) { return me.x(options.datum2Value(d)); } this.datum2LocationScaled = datum2LocationScaled; this.datum2ValueScaled = datum2ValueScaled; @@ -224,7 +251,8 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) * + alternate view : line * + transition between views, zoom, stack */ - y.domain(data.map(options.datum2Location)); + // scaleBand() domain is a list of all y values. + // yBand.domain(data.map(options.datum2Location)); x.domain([0, d3.max(data, options.datum2Value)]); let axisXa = @@ -247,6 +275,7 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) .attr("text-anchor", "end") .text(valueName); + data = data.sort((a,b) => middle(this.options.datum2Location(a)) - middle(this.options.datum2Location(b))); this.drawContent(data); this.currentData = data; }; @@ -254,6 +283,7 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) { let options = this.options, + block = this.block, g = this.g; let rs = g @@ -265,32 +295,85 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) .append("rect"); ra .attr("class", options.barClassName) - .each(configureChartHover); + /** parent datum is currently 1, but could be this.block; + * this.parentElement.parentElement.__data__ has the axis id (not the blockId), + */ + .each(function (d) { configureHorizTickHover.apply(this, [d, block, options.hoverTextFn]); }); ra .merge(rs) .transition().duration(1500) .attr("x", 0) - .attr("y", this.datum2LocationScaled) - .attr("height", this.y.bandwidth()) + .attr("y", (d) => { let li = this.datum2LocationScaled(d); return li.length ? li[0] : li; }) + // yBand.bandwidth() + .attr("height", this.rectHeight.bind(this)) .attr("width", this.datum2ValueScaled); rx.remove(); console.log(gAxis.node(), rs.nodes(), re.nodes()); }; - Chart1.prototype.line = function (data) + /** Calculate the height of rectangle to be used for this data point + * @param this is Chart1, not DOM element. + */ + Chart1.prototype.rectHeight = function (d, i, g) + { + let height, locationScaled; + /* if locationScaled is an interval, calculate height from it. + * Otherwise, use adjacent points to indicate height. + */ + if ((locationScaled = this.datum2LocationScaled(d)).length) { + height = Math.abs(locationScaled[1] - locationScaled[0]); + } + else { + /* the boundary between 2 adjacent points is midway between them. + * So sum that half-distance for the previous and next points. + * the y range distance from the previous point to the next point. + * If this point is either end, simply double the other half-distance. + */ + if (! g.length) + height = this.yLine.range() / 10; + else { + let r = []; + if (i > 0) + r.push(g[i-1].__data__); + r.push(d); + if (i < g.length-1) + r.push(g[i+1].__data__); + let y = + r.map(this.datum2ValueScaled); + height = Math.abs(y[y.length-1] - y[0]) * 2 / (y.length-1); + dLog('rectHeight', d, i, /*g,*/ r, y, height); + if (! height) + height = 1; + } + } + return height; + }; + Chart1.prototype.scaleLinear = function (yRange, data) { // based on https://bl.ocks.org/mbostock/3883245 if (! this.yLine) - this.yLine = d3.scaleLinear() - .rangeRound(this.yRange); + this.yLine = d3.scaleLinear(); + let y = this.yLine; + y.rangeRound(yRange); + let + /** location may be an interval, so flatten the result. + * Later Array.flat() can be used. + */ + yFlat = data + .map(this.options.datum2Location) + .reduce((acc, val) => acc.concat(val), []); + y.domain(d3.extent(yFlat)); + console.log('scaleLinear domain', y.domain(), yFlat); + return y; + }; + Chart1.prototype.line = function (data) + { let y = this.yLine, options = this.options; - function datum2LocationScaled(d) { return y(options.datum2Location(d)); } - + let datum2LocationScaled = scaleMaybeInterval(options.datum2Location, y); let line = d3.line() .x(this.datum2ValueScaled) - .y(datum2LocationScaled); + .y((d) => middle(datum2LocationScaled(d))); - y.domain(d3.extent(data, this.options.datum2Location)); console.log("line x domain", this.x.domain(), this.x.range()); let @@ -385,8 +468,10 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) dataTypeName : dataConfig.dataTypeName, datum2Location : dataConfig.datum2Location, datum2Value : dataConfig.datum2Value, - datum2Description : dataConfig.datum2Description + datum2Description : dataConfig.datum2Description, + hoverTextFn : dataConfig.hoverTextFn }); + chart1.block = axisChart.get('block'); axisChart.set("chart1", chart1); addParentClass(g); } diff --git a/frontend/app/utils/draw/zoomPanCalcs.js b/frontend/app/utils/draw/zoomPanCalcs.js index d43abf67a..92afe159b 100644 --- a/frontend/app/utils/draw/zoomPanCalcs.js +++ b/frontend/app/utils/draw/zoomPanCalcs.js @@ -30,6 +30,15 @@ function inRange(a, range) */ return (range[0] <= a) == (a <= range[1]); } +/** Same as inRange / subInterval, using the appropriate function for the type of value a. + * @param a may be a single value or an interval [from, to] + * @param range an interval [from, to] + */ +function inRangeEither(a, range) +{ + return a.length ? subInterval(a, range) : inRange(a, range); +} + /** Test if an array of values, which can be a pair defining an interval, is * contained within another interval. @@ -271,4 +280,4 @@ function wheelNewDomain(axis, axisApi, inFilter) { /*----------------------------------------------------------------------------*/ -export { wheelNewDomain }; +export { inRangeEither, wheelNewDomain }; diff --git a/frontend/app/utils/hover.js b/frontend/app/utils/hover.js index 6afc2567f..129253ed8 100644 --- a/frontend/app/utils/hover.js +++ b/frontend/app/utils/hover.js @@ -1,3 +1,6 @@ + +/* global Ember */ + /*------------------------------------------------------------------------*/ /* copied from draw-map.js - will import when that is split */ /* also @see configurejQueryTooltip() */ From 33f22ec7b00a2bc291cebc8807f224e225016938 Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 16 Jan 2020 11:10:49 +1100 Subject: [PATCH 04/57] axis-chart : add valueIsArea : divide value by bar height Add rectWidth(). Pass gIsData to rect{Height,Width}() to indicate when param g is an array of data values, not DOM elements. frontend/app/ : a781e9c 8432 Jan 8 11:28 components/axis-chart.js 087f7ae 6066 Jan 15 18:29 components/in-axis.js d227fe8 19856 Jan 16 10:55 utils/draw/chart1.js --- frontend/app/components/axis-chart.js | 3 +- frontend/app/components/in-axis.js | 12 +++--- frontend/app/utils/draw/chart1.js | 57 +++++++++++++++++++++------ 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/frontend/app/components/axis-chart.js b/frontend/app/components/axis-chart.js index 4b18ff4ee..8a8d6ef62 100644 --- a/frontend/app/components/axis-chart.js +++ b/frontend/app/components/axis-chart.js @@ -96,7 +96,8 @@ export default InAxis.extend({ let valueText = '[' + d._id.min + ',' + d._id.max + '] : ' + d.count, blockName = block.view && block.view.longName(); return valueText + '\n' + blockName; - } + }, + valueIsArea : true }; // pass alternate dataConfig to layoutAndDrawChart(), defining alternate functions for {datum2Value, datum2Location } this.layoutAndDrawChart(fa, featureCountData); diff --git a/frontend/app/components/in-axis.js b/frontend/app/components/in-axis.js index d5628f5ca..834d3330f 100644 --- a/frontend/app/components/in-axis.js +++ b/frontend/app/components/in-axis.js @@ -36,11 +36,13 @@ export default Ember.Component.extend({ /** @param [axisID, t] */ redrawOnce(axisID_t) { console.log("redrawOnce", axisID_t); - // - redraw if axisID matches this axis - // possibly use transition t for redraw - let redraw = this.get('redraw'); - if (redraw) - redraw.apply(this, axisID_t); + if (! this.isDestroying) { + // - redraw if axisID matches this axis + // possibly use transition t for redraw + let redraw = this.get('redraw'); + if (redraw) + redraw.apply(this, axisID_t); + } }, redrawDebounced(axisID_t) { Ember.run.debounce(this, this.redrawOnce, axisID_t, 1000); diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index 5fe927e3d..d01b5996c 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -1,3 +1,4 @@ +import Ember from 'ember'; import { getAttrOrCP } from '../ember-devel'; import { configureHorizTickHover } from '../hover'; @@ -140,7 +141,12 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) let axisComponent = axisChart.get("axis"), axisID = axisComponent.axisID, - gAxis = d3.select("g.axis-outer#id" + axisID + "> g.axis-use"), + gAxis = d3.select("g.axis-outer#id" + axisID + "> g.axis-use"); + if (! gAxis.node()) { /* probably because axisShowExtend() has not added the g.axis-use yet - will sort out the dependencies ... . */ + dLog('layoutAndDrawChart', d3.select("g.axis-outer#id" + axisID).node(), 'no g.axis-use', this, axisComponent, axisID); + return; + } + let /** relative to the transform of parent g.axis-outer */ bbox = gAxis.node().getBBox(), yrange = [bbox.y, bbox.height]; @@ -171,6 +177,8 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) dataConfig = isBlockData ? blockData : parsedData; if (! dataConfig.hoverTextFn) dataConfig.hoverTextFn = hoverTextFn; + if (dataConfig.valueIsArea === undefined) + dataConfig.valueIsArea = false; } let @@ -231,6 +239,7 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) /* these can be renamed datum2{abscissa,ordinate}{,Scaled}() */ /* apply y after scale applied by datum2Location */ let datum2LocationScaled = scaleMaybeInterval(options.datum2Location, me.y); + /** related @see rectWidth(). */ function datum2ValueScaled(d) { return me.x(options.datum2Value(d)); } this.datum2LocationScaled = datum2LocationScaled; this.datum2ValueScaled = datum2ValueScaled; @@ -253,7 +262,7 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) */ // scaleBand() domain is a list of all y values. // yBand.domain(data.map(options.datum2Location)); - x.domain([0, d3.max(data, options.datum2Value)]); + x.domain([0, d3.max(data, this.rectWidth.bind(this, /*scaled*/false, /*gIsData*/false))]); let axisXa = gsa.append("g") @@ -305,16 +314,40 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) .attr("x", 0) .attr("y", (d) => { let li = this.datum2LocationScaled(d); return li.length ? li[0] : li; }) // yBand.bandwidth() - .attr("height", this.rectHeight.bind(this)) - .attr("width", this.datum2ValueScaled); + .attr("height", this.rectHeight.bind(this, /*gIsData*/false)) // equiv : (d, i, g) => this.rectHeight(false, d, i, g) + .attr("width", this.rectWidth.bind(this, /*scaled*/true, /*gIsData*/false)); rx.remove(); console.log(gAxis.node(), rs.nodes(), re.nodes()); }; /** Calculate the height of rectangle to be used for this data point * @param this is Chart1, not DOM element. + * @param scaled true means apply scale (x) to the result + * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. + * gIsData will be true when called from d3.max(), and false for d3 attr functions. + */ + Chart1.prototype.rectWidth = function (scaled, gIsData, d, i, g) + { + Ember.assert('rectWidth arguments.length === 5', arguments.length === 5); + /** The scale is linear, so it is OK to scale before dividing by rectHeight. + * Otherwise could use datum2Value() here and apply this.x() before the return. + */ + let d2v = (scaled ? this.datum2ValueScaled : this.options.datum2Value), + width = d2v(d); + if (this.options.valueIsArea) { + let h; + width /= (h = this.rectHeight(gIsData, d, i, g)); + // dLog('rectWidth', h, width, gIsData); + } + return width; + }; + /** Calculate the height of rectangle to be used for this data point + * @param this is Chart1, not DOM element. + * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. + * gIsData will be true when called from d3.max(), and false for d3 attr functions. */ - Chart1.prototype.rectHeight = function (d, i, g) + Chart1.prototype.rectHeight = function (gIsData, d, i, g) { + Ember.assert('rectHeight arguments.length === 4', arguments.length === 4); let height, locationScaled; /* if locationScaled is an interval, calculate height from it. * Otherwise, use adjacent points to indicate height. @@ -332,15 +365,16 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) height = this.yLine.range() / 10; else { let r = []; + function gData(i) { let gi = g[i]; return gIsData ? gi : gi.__data__; }; if (i > 0) - r.push(g[i-1].__data__); + r.push(gData(i)); r.push(d); if (i < g.length-1) - r.push(g[i+1].__data__); + r.push(gData(i+1)); let y = - r.map(this.datum2ValueScaled); + r.map(this.datum2LocationScaled); height = Math.abs(y[y.length-1] - y[0]) * 2 / (y.length-1); - dLog('rectHeight', d, i, /*g,*/ r, y, height); + dLog('rectHeight', gIsData, d, i, /*g,*/ r, y, height); if (! height) height = 1; } @@ -371,7 +405,7 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) let datum2LocationScaled = scaleMaybeInterval(options.datum2Location, y); let line = d3.line() - .x(this.datum2ValueScaled) + .x(this.rectWidth.bind(this, /*scaled*/true, /*gIsData*/false)) .y((d) => middle(datum2LocationScaled(d))); console.log("line x domain", this.x.domain(), this.x.range()); @@ -469,7 +503,8 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) datum2Location : dataConfig.datum2Location, datum2Value : dataConfig.datum2Value, datum2Description : dataConfig.datum2Description, - hoverTextFn : dataConfig.hoverTextFn + hoverTextFn : dataConfig.hoverTextFn, + valueIsArea : dataConfig.valueIsArea }); chart1.block = axisChart.get('block'); axisChart.set("chart1", chart1); From b9dfd78b2e05a09558933fbe5b76a6c3f03a4b74 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 21 Jan 2020 20:01:08 +1100 Subject: [PATCH 05/57] various cleanups when removing axes. draw-map.js : deleteButtonS.click : call removeBrushExtent(axisName). axis-1d.js : cleanup axis-1d.js when deleting / un-viewing an axis. stacks.js : unviewBlocks() : clear .isViewed in later(), also clear .view. f042eed 239707 Jan 20 12:59 components/draw-map.js c1dde26 29096 Jan 16 13:04 components/draw/axis-1d.js 5e12df5 72589 Jan 16 15:24 utils/stacks.js --- frontend/app/components/draw-map.js | 28 ++++++++++++++++++++----- frontend/app/components/draw/axis-1d.js | 16 +++++++++++--- frontend/app/utils/stacks.js | 14 ++++++++++--- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/frontend/app/components/draw-map.js b/frontend/app/components/draw-map.js index cba1e7343..3b1829235 100644 --- a/frontend/app/components/draw-map.js +++ b/frontend/app/components/draw-map.js @@ -341,7 +341,8 @@ export default Ember.Component.extend(Ember.Evented, { console.log("create", axisID, axis, "in", axes2d); } else - axis.set('extended', enabled); // was axis2DEnabled + Ember.run.later( + () => axis.set('extended', enabled)); // was axis2DEnabled console.log("enableAxis2D in components/draw-map", axisID, enabled, axis); console.log("splitAxes", this.get('splitAxes')); console.log("axes2d", this.get('axes2d')); @@ -1210,11 +1211,13 @@ export default Ember.Component.extend(Ember.Evented, { receivedBlock2.apply(me, [dBlock]); - if (! sBlock) { + if (! sBlock || ! dBlock.get('view')) { /** sBlock may already be associated with dBlock */ let view = dBlock.get('view'); sBlock = view || new Block(dBlock); - oa.stacks.blocks[d] = sBlock; + // if view, then this is already set. + if (oa.stacks.blocks[d] !== sBlock) + 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 @@ -5433,6 +5436,7 @@ export default Ember.Component.extend(Ember.Evented, { { // removeBlockByName() is already done above + // this can be factored with : deleteButtonS.on('click', ... ) let stack = axis && axis.stack; // axes[axisName] is deleted by removeStacked1() let stackID = Stack.removeStacked(axisName); @@ -5527,6 +5531,7 @@ export default Ember.Component.extend(Ember.Evented, { .on('click', function (buttonElt /*, i, g*/) { console.log("delete", axisName, this); // this overlaps with the latter part of blockIsUnviewed() + // and can be factored with that. let axis = oa.axes[axisName], stack = axis && axis.stack; // axes[axisName] is deleted by removeStacked1() let stackID = Stack.removeStacked(axisName); @@ -5534,6 +5539,7 @@ export default Ember.Component.extend(Ember.Evented, { let sBlock = oa.stacks.blocks[axisName]; console.log('sBlock.axis', sBlock.axis); sBlock.setAxis(undefined); + removeBrushExtent(axisName); removeAxisMaybeStack(axisName, stackID, stack); me.send('mapsToViewDelete', axisName); // filter axisName out of selectedFeatures and selectedAxes @@ -5759,9 +5765,13 @@ export default Ember.Component.extend(Ember.Evented, { DropTarget.prototype.showResize(); } Ember.run.later( function () { - /* probably better to do .trigger() within .later(); it works either way. */ + /* This does .trigger() within .later(), which seems marginally better than vice versa; it works either way. (Planning to replace event:resize soon). */ if (widthChanged || heightChanged) - me.trigger('resized', widthChanged, heightChanged, useTransition); + try { + me.trigger('resized', widthChanged, heightChanged, useTransition); + } catch (exc) { + console.log('showResize', 'resized', me, me.resized, widthChanged, heightChanged, useTransition, graphDim, brushedDomains, exc.stack || exc); + } showSynteny(oa.syntenyBlocks, undefined); }); }; @@ -6067,6 +6077,14 @@ export default Ember.Component.extend(Ember.Evented, { return changed; }, + /** draw-map sends resized event to listening sub-components using trigger(). + * It does not listen to this event, or need to, but defining resized() may fix : + * 't[m] is not a function, at Object.applyStr (ember-utils.js:524)' + */ + resized : function(prevSize, currentSize) { + console.log("resized in components/draw-map", this, prevSize, currentSize); + }, + resize : function() { console.log("resize", this, arguments); /** when called via .observes(), 'this' is draw-map object. When called diff --git a/frontend/app/components/draw/axis-1d.js b/frontend/app/components/draw/axis-1d.js index 0eaf7ac42..02a6b8c60 100644 --- a/frontend/app/components/draw/axis-1d.js +++ b/frontend/app/components/draw/axis-1d.js @@ -84,6 +84,7 @@ function FeatureTicks(axis, axisApi, axis1d) /** Draw horizontal ticks on the axes, at feature locations. * * @param axis block (Ember object), result of stacks-view:axesP + * If the axis has multiple (data) blocks, this is the reference block. */ FeatureTicks.prototype.showTickLocations = function (featuresOfBlockLookup, setupHover, groupName, blockFilter) { @@ -304,8 +305,12 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, AxisPosition, { let axisName = this.get('axis.id'), axisS = Stacked.getAxis(axisName); - if (axisS && ! axisS.axis1d) { - axisS.axis1d = this; + if (axisS) { + if (axisS.axis1d === this && this.isDestroying) + axisS.axis1d = undefined; + else if (! axisS.axis1d && ! this.isDestroying) { + axisS.axis1d = this; + } } return axisS; }), @@ -725,8 +730,13 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, AxisPosition, { dLog('axis-1d didInsertElement', this, this.get('listen') !== undefined); }, willDestroyElement() { - dLog('willDestroyElement'); + dLog('willDestroyElement', this.get('axis.id')); this.removeTicks(); + let axisS = this.get('axisS'); + if (axisS) { + if (axisS.axis1d === this) + delete axisS.axis1d; + } this._super(...arguments); }, removeTicks() { diff --git a/frontend/app/utils/stacks.js b/frontend/app/utils/stacks.js index 4edf8016e..6e0f520f1 100644 --- a/frontend/app/utils/stacks.js +++ b/frontend/app/utils/stacks.js @@ -2135,9 +2135,17 @@ Stacked.prototype.setZoomed = function (zoomed) Stacked.prototype.unviewBlocks = function () { - this.blocks.forEach((sBlock) => { - if (sBlock.block) - sBlock.block.set('isViewed', false); + /** Ember data objects. */ + let blocks = this.blocks.mapBy('block') + .filter((b) => b); + Ember.run.later(() => { + blocks.forEach((block) => { + // undefined .block-s are filtered out above + block.setProperties({ + 'view': undefined, + 'isViewed': false + }); + }); }); }; From 8081cbeb2384e1f3d2bdabf87a2a7c0ffa9e9da2 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 21 Jan 2020 20:02:43 +1100 Subject: [PATCH 06/57] Split functions in chart1 to enable composing them into the planned components. This is working as per the previous commit, i.e. bar & line charts of featuresCounts and blockData are OK; toggle works; resizing seems OK; the testing goal was to check that features were not dropped in the restructuring, exhaustive testing is not needed because there is further re-organisation coming (partition the remaining overlap between AxisCharts and Chart1, organise the stream : block features -> chart line data -> axis-charts). axis-chart.js : add axisID, gAxis, yAxesScales, yAxisScale, axisCharts, factor countsChart() out of drawBlockFeaturesCounts(). chart1.js : Add AxisCharts; formalise options / dataConfig to form DataConfig. layoutAndDrawChart() is split into : selectParentContainer(), getBBox(), getRanges(), getRanges3(), (and after the Chart1 functions) commonFrame(), configure(), controls(), and then replaced with a proxy which calls those functions. Chart1.draw() is split into : getRanges2(), prepareScales(), group(), drawAxes(), data(). axis-2d.js : viewedChartable() : use .get for axis. frontend/app/ : facffb4 11258 Jan 17 08:46 components/axis-2d.js aa0e9c5 9595 Jan 21 19:17 components/axis-chart.js 0500efb 24448 Jan 21 19:17 utils/draw/chart1.js --- frontend/app/components/axis-2d.js | 8 +- frontend/app/components/axis-chart.js | 87 ++++-- frontend/app/utils/draw/chart1.js | 400 ++++++++++++++++++-------- 3 files changed, 351 insertions(+), 144 deletions(-) diff --git a/frontend/app/components/axis-2d.js b/frontend/app/components/axis-2d.js index 93938194e..8fb5ca4de 100644 --- a/frontend/app/components/axis-2d.js +++ b/frontend/app/components/axis-2d.js @@ -35,6 +35,10 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, { console.log('axis1d', axis1d, axesPCount); return axis1d; }), + /** @return the blocks of this axis. + * Result type is [] of (stack.js) Block. + * For the Ember data Object blocks, @see dataBlocks(). + */ blocks : Ember.computed('axis', function() { let axis = this.get('axis'); return axis && axis.blocks; @@ -69,12 +73,14 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, { return dataBlocks; }), + /** @return blocks which are viewedChartable, and whose axis is this axis. + */ viewedChartable : Ember.computed('blockService.viewedChartable.[]', 'axisID', function () { let id = this.get('axisID'), viewedChartable = this.get('blockService.viewedChartable') - .filter((b) => b.axis.axisName === id); + .filter((b) => { let axis = b.get('axis'); return axis && axis.axisName === id; }); console.log('viewedChartable', id, viewedChartable); return viewedChartable; }), diff --git a/frontend/app/components/axis-chart.js b/frontend/app/components/axis-chart.js index 8a8d6ef62..73a26500d 100644 --- a/frontend/app/components/axis-chart.js +++ b/frontend/app/components/axis-chart.js @@ -1,8 +1,7 @@ import Ember from 'ember'; const { inject: { service } } = Ember; -import { className, layoutAndDrawChart /*Chart1*/ } from '../utils/draw/chart1'; -import InAxis from './in-axis'; +import { className, AxisCharts, layoutAndDrawChart, Chart1, DataConfig } from '../utils/draw/chart1'; /*----------------------------------------------------------------------------*/ @@ -20,15 +19,16 @@ const dLog = console.debug; * @param chart data (field name is className); may be either : * result of parseTextData() : array of {name : , value : , description : } * or chartBlock passed in : .features - * @param axis axisComponent; parent axis-2d component - * @param axisID axisID - * @param data oa + * @param axis axisComponent; parent axis-2d component - + * @param gContainer - + * @param axisID axisID - local value ? + * @param data oa - * @param width resizedWidth *---------------- * data attributes created locally, not passed in : * @param chart1 */ -export default InAxis.extend({ +export default Ember.Component.extend({ blockService: service('data/block'), className : className, @@ -37,18 +37,48 @@ export default InAxis.extend({ console.log("components/axis-chart didRender()"); }, + axisID : Ember.computed.alias('axis.axisID'), + + gAxis : Ember.computed('axisID', function () { + let axisID = this.get('axisID'); + let axisCharts = this.get('axisCharts'); + let gAxis = axisCharts.selectParentContainer(axisID); + return gAxis; + }), + + yAxesScales : Ember.computed('data', function () { + let oa = this.get('data'); + return oa.y; + }), + yAxisScale : Ember.computed('axisID', 'yAxesScales', function () { + let yAxesScales = this.get('yAxesScales'), + axisID = this.get('axisID'), + yAxis = yAxesScales[axisID]; + dLog('yAxisScale', axisID, yAxis && yAxis.domain()); + return yAxis; + }), + + axisCharts : Ember.computed(function () { + return new AxisCharts(); + }), + + + + + blockFeatures : Ember.computed('block', 'block.features.[]', 'axis.axis1d.domainChanged', function () { if (this.get('block.isChartable')) this.drawBlockFeatures0(); }), featuresCounts : Ember.computed('block', 'block.featuresCounts.[]', 'axis.axis1d.domainChanged', function () { + let featuresCounts = this.get('block.featuresCounts'); /* perhaps later draw both, for the moment just draw 1, and since all data * blocks have featuresCounts, plot the features of those blocks which are * chartable, so that we can see both capabilities are working. */ if (! this.get('block.isChartable')) - this.drawBlockFeaturesCounts(); - return this.get('block.featuresCounts'); + this.drawBlockFeaturesCounts(featuresCounts); + return featuresCounts; }), drawBlockFeatures0 : function() { @@ -65,12 +95,19 @@ export default InAxis.extend({ this.drawBlockFeatures(features); } }, - drawBlockFeaturesCounts : function() { - let featuresCounts = this.get('block.featuresCounts'); + drawBlockFeaturesCounts : function(featuresCounts) { + if (! featuresCounts) + featuresCounts = this.get('featuresCounts'); let domain = this.get('axis.axis1d.domainChanged'); if (featuresCounts) { console.log('drawBlockFeaturesCounts', featuresCounts.length, domain, this.get('block.id')); + let countsChart = this.get('countsChart'); + // pass alternate dataConfig to layoutAndDrawChart(), defining alternate functions for {datum2Value, datum2Location } + this.layoutAndDrawChart(featuresCounts, countsChart); + } + }, + countsChart : Ember.computed(function() { /** example element of array f : */ const dataExample = { @@ -80,12 +117,8 @@ export default InAxis.extend({ }, "count": 109 }; - let f = featuresCounts.toArray(), - /** the min, max will be passed also - first need to factor out part of axis-chart for featuresCounts. */ - fa = f; // .map(function (f0) { return f0.count;}); - console.log('drawBlockFeaturesCounts', f); let - featureCountData = { + featureCountDataProperties = { dataTypeName : 'featureCountData', datum2Location : function datum2Location(d) { return [d._id.min, d._id.max]; }, datum2Value : function(d) { return d.count; }, @@ -98,16 +131,17 @@ export default InAxis.extend({ return valueText + '\n' + blockName; }, valueIsArea : true - }; - // pass alternate dataConfig to layoutAndDrawChart(), defining alternate functions for {datum2Value, datum2Location } - this.layoutAndDrawChart(fa, featureCountData); - } - }, + }, + featureCountData = new DataConfig(featureCountDataProperties); + let countsChart = new Chart1(this.get('axisCharts.dom.gAxis'), featureCountData); + return countsChart; + }), + drawBlockFeatures : function(features) { let f = features.toArray(), fa = f.map(function (f0) { return f0._internalModel.__data;}); - let axisID = this.get("axis.axisID"), + let axisID = this.get("axisID"), oa = this.get('data'), za = oa.z[axisID]; /* if za has not been populated with features, it will have just .dataset @@ -201,8 +235,15 @@ export default InAxis.extend({ return result; }, - layoutAndDrawChart(chart, dataConfig) { - layoutAndDrawChart(this, chart, dataConfig); + layoutAndDrawChart(chartData, chart1) { + let + axisID = this.get("axisID"), + axisCharts = this.get('axisCharts'), + block = this.get('block'), + yAxisScale = this.get('yAxisScale'), + resizedWidth = this.get('width'); + chart1 = layoutAndDrawChart( + axisID, axisCharts, chart1, chartData, block, /*dataConfig*/undefined, yAxisScale, resizedWidth); }, diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index d01b5996c..5308a6b65 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -9,6 +9,9 @@ import { inRangeEither } from './zoomPanCalcs'; const className = "chart", classNameSub = "chartRow"; +/** Enables per-chart axes; X axes will be often useful; Y axis might be used if + * zooming into different regions of an axis. */ +const showChartAxes = true; /* global d3 */ @@ -19,17 +22,17 @@ const dLog = console.debug; /*----------------------------------------------------------------------------*/ /* Copied from draw-map.js */ +let blockFeatures = stacks.oa.z; -function featureLocation(oa, axisID, d) +function featureLocation(blockId, d) { - let feature = oa.z[axisID][d]; + let feature = blockFeatures[blockId][d]; if (feature === undefined) { - console.log("axis-chart featureY_", axisID, oa.z[axisID], "does not contain feature", d); - return undefined; + console.log("axis-chart featureY_", blockId, blockFeatures[blockId], "does not contain feature", d); } - else - return feature.location; + let location = feature && feature.location; + return location; } /** If the given value is an interval, convert it to a single value by calculating the middle of the interval. @@ -97,31 +100,41 @@ function addParentClass(g) { }; /*----------------------------------------------------------------------------*/ -let oa = stacks.oa, - axisID0; +class DataConfig { + /* + dataTypeName; + datum2Location; + datum2Value; + datum2Description; + */ + constructor (properties) { + if (properties) + Object.assign(this, properties); + } +}; + /** @param name is a feature or gene name */ - function name2Location(name) + function name2Location(name, blockId) { /** @param ak1 axis name, (exists in axisIDs[]) * @param d1 feature name, i.e. ak1:d1 */ - let ak1 = axisID0, d1 = name; - return featureLocation(oa, ak1, d1); + return featureLocation(blockId, name); } /** Used for both blockData and parsedData. */ - function datum2Location(d) { return name2Location(d.name); } + function datum2LocationWithBlock(d, blockId) { return name2Location(d.name, blockId); } function datum2Value(d) { return d.value; } let parsedData = { dataTypeName : 'parsedData', - datum2Location, + // datum2LocationWithBlock assigned later, datum2Value : datum2Value, datum2Description : function(d) { return d.description; } }, blockData = { dataTypeName : 'blockData', - datum2Location, + // datum2LocationWithBlock assigned later, /** The effects data is placed in .value[2] (the interval is in value[0..1]). * Use the first effects value by default, but later will combine other values. */ @@ -132,38 +145,118 @@ let oa = stacks.oa, /*----------------------------------------------------------------------------*/ -function layoutAndDrawChart(axisChart, chart, dataConfig) +class AxisCharts { + /* + dataConfig; // DataConfig (options) + ranges; // .bbox, .height + dom; // .gs, .gsa, + */ + + constructor(axisID, dataConfig) { + this.axisID = axisID; + this.dataConfig = dataConfig; + this.ranges = { }; + this.dom = { }; + // this will move to Chart1. + this.scales = { /* x, y, yLine */ }; + } + }; + +/** + * @param yAxisScale + */ +function layoutAndDrawChart(axisID, axisCharts, chart1, chartData, block, dataConfig, yAxisScale, resizedWidth) +{ + + axisCharts.selectParentContainer(axisID); + axisCharts.getBBox(); + axisCharts.scales.yAxis = yAxisScale; + + if (! chart1) + chart1 = new Chart1(axisCharts.dom.g, dataConfig); + + // Plan is for Axischarts to own .ranges and Chart1 to own .scales, but for now there is some overlap. + if (chart1 && ! chart1.ranges) { + chart1.ranges = axisCharts.ranges; + chart1.scales = axisCharts.scales; + } + + /* dataConfig may be set up (for featuresCounts) by countsChart(), + * or (for blockData and parsedData) by .getRanges(). + * In coming commits the latter will be factored out so that DataConfig it is + * uniformly passed in to Chart1(). This copying of .dataConfig is just provisional. + */ + if (! dataConfig && ! axisCharts.dataConfig && chart1 && chart1.dataConfig) + axisCharts.dataConfig = chart1.dataConfig; + axisCharts.getRanges(chartData, block.get('id'), resizedWidth); + if (! dataConfig && chart1 && ! chart1.dataConfig && axisCharts.dataConfig) + chart1.dataConfig = axisCharts.dataConfig; + axisCharts.getRanges3(chartData, resizedWidth); + + // axisCharts.size(this.get('yAxisScale'), /*axisID, gAxis,*/ chartData, resizedWidth); // - split size/width and data/draw + + axisCharts.commonFrame(/*axisID, gAxis*/); + + addParentClass(axisCharts.dom.gc); + + if (dataConfig) + axisCharts.configure(dataConfig); + axisCharts.controls(chart1); + + // following are from b.draw(data) + axisCharts.getRanges2(); + + chart1.prepareScales(chartData, axisCharts.ranges.drawSize); + + chart1.g = + axisCharts.group(axisCharts.dom.gc, 'axis-chart'); + if (showChartAxes) + axisCharts.drawAxes(chartData); + chart1.block = block; // used in Chart1:bars() for hover text. + chart1.data(chartData); + + + + return chart1; +}; + +AxisCharts.prototype.selectParentContainer = function (axisID) { - console.log("layoutAndDrawChart", chart, dataConfig && dataConfig.dataTypeName); - // initial version supports only 1 split axis; next identify axis by axisID (and possibly stack id) + this.axisID = axisID; + // to support multiple split axes, identify axis by axisID (and possibly stack id) // // g.axis-outer#id - let - axisComponent = axisChart.get("axis"), - axisID = axisComponent.axisID, - gAxis = d3.select("g.axis-outer#id" + axisID + "> g.axis-use"); + let gAxis = d3.select("g.axis-outer#id" + axisID + "> g.axis-use"); if (! gAxis.node()) { /* probably because axisShowExtend() has not added the g.axis-use yet - will sort out the dependencies ... . */ - dLog('layoutAndDrawChart', d3.select("g.axis-outer#id" + axisID).node(), 'no g.axis-use', this, axisComponent, axisID); - return; + dLog('layoutAndDrawChart', d3.select("g.axis-outer#id" + axisID).node(), 'no g.axis-use', this, axisID); } + this.dom.gAxis = gAxis; + return gAxis; +}; +AxisCharts.prototype.getBBox = function () + { + let axisID = this.axisID, + gAxis = this.dom.gAxis; let /** relative to the transform of parent g.axis-outer */ bbox = gAxis.node().getBBox(), yrange = [bbox.y, bbox.height]; - axisID0 = axisID; if (bbox.x < 0) { console.log("x < 0", bbox); bbox.x = 0; } - let - barWidth = 10, + this.ranges.bbox = bbox; + this.ranges.yrange = yrange; +}; +AxisCharts.prototype.getRanges = function (chart, blockId, resizedWidth) { + let + {yrange } = this.ranges, + {yAxis } = this.scales, /** isBlockData is not used if dataConfig is defined. this can be moved out to the caller. */ isBlockData = chart.length && (chart[0].description === undefined), - valueName = chart.valueName || "Values", - oa = axisChart.get('data'), // axisID = gAxis.node().parentElement.__data__, - yAxis = oa.y[axisID], // this.get('y') + yAxisDomain = yAxis.domain(), yDomain; if (noDomain(yAxisDomain) && chart.length) { yAxisDomain = [chart[0]._id.min, chart[chart.length-1]._id.max]; @@ -173,55 +266,84 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) else yDomain = [yAxis.invert(yrange[0]), yAxis.invert(yrange[1])]; + let dataConfig = this.dataConfig; if (! dataConfig) { - dataConfig = isBlockData ? blockData : parsedData; + let dataConfigProperties = isBlockData ? blockData : parsedData; + dataConfigProperties.datum2Location = + (d) => datum2LocationWithBlock(d, blockId); + this.dataConfig = dataConfig = + new DataConfig(dataConfigProperties); if (! dataConfig.hoverTextFn) dataConfig.hoverTextFn = hoverTextFn; if (dataConfig.valueIsArea === undefined) dataConfig.valueIsArea = false; } + if (dataConfig) { + if (! dataConfig.barClassName) + dataConfig.barClassName = classNameSub; + if (! dataConfig.valueName) + dataConfig.valueName = chart.valueName || "Values"; + } + + this.ranges.pxSize = (yDomain[1] - yDomain[0]) / this.ranges.bbox.height; +}; +AxisCharts.prototype.getRanges3 = function (chart, resizedWidth) { let - pxSize = (yDomain[1] - yDomain[0]) / bbox.height, - withinZoomRegion = function(d) { - return inRangeEither(dataConfig.datum2Location(d), yDomain); + {bbox} = this.ranges, + {yAxis} = this.scales, + yDomain = yAxis.domain(), + withinZoomRegion = (d) => { + return inRangeEither(this.dataConfig.datum2Location(d), yDomain); }, data = chart.filter(withinZoomRegion); - let resizedWidth = axisChart.get('width'); - console.log(resizedWidth, bbox, yDomain, pxSize, data.length, (data.length == 0) || dataConfig.datum2Location(data[0])); + + console.log(resizedWidth, bbox, yDomain, data.length, (data.length == 0) || this.dataConfig.datum2Location(data[0])); if (resizedWidth) bbox.width = resizedWidth; +}; + /* axis * x .value * y .name Location */ /** 1-dimensional chart, within an axis. */ - function Chart1(parentG, options) + function Chart1(parentG, dataConfig) { this.parentG = parentG; - this.options = options; + this.dataConfig = dataConfig; } Chart1.prototype.barsLine = true; - Chart1.prototype.draw = function (data) + AxisCharts.prototype.getRanges2 = function () { // based on https://bl.ocks.org/mbostock/3885304, axes x & y swapped. let - options = this.options, - parentG = this.parentG, - margin = {top: 10, right: 20, bottom: 40, left: 20}, + // parentG = this.parentG, + bbox = this.ranges.bbox, + margin = showChartAxes ? + {top: 10, right: 20, bottom: 40, left: 20} : + {top: 0, right: 0, bottom: 0, left: 0}, // pp=parentG.node().parentElement, - parentW = options.bbox.width, // +pp.attr("width") - parentH = options.bbox.height, // +pp.attr("height") + parentW = bbox.width, // +pp.attr("width") + parentH = bbox.height, // +pp.attr("height") width = parentW - margin.left - margin.right, height = parentH - margin.top - margin.bottom; + this.ranges.drawSize = {width, height}; + dLog('getRanges2', parentW, parentH, this.ranges.drawSize); + }; + Chart1.prototype.prepareScales = function (data, drawSize) + { /** The chart is perpendicular to the usual presentation. * The names x & y (in {x,y}Range and yLine) match the orientation on the * screen, rather than the role of abscissa / ordinate; the data maps * .name (location) -> .value, which is named y -> x. */ let - xRange = [0, width], + dataConfig = this.dataConfig, + width = drawSize.width, + height = drawSize.height, + xRange = [0, width], yRange = [0, height], // yRange is used as range of yLine by this.scaleLinear(). /* scaleBand would suit a data set with evenly spaced or ordinal / nominal y values. @@ -229,30 +351,44 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) */ y = this.scaleLinear(yRange, data), x = d3.scaleLinear().rangeRound(xRange); - // datum2LocationScaled() uses me.x rather than the value in the closure in which it was created. - this.x = x; + // datum2LocationScaled() uses me.scales.x rather than the value in the closure in which it was created. + this.scales.x = x; // Used by bars() - could be moved there, along with datum2LocationScaled(). - this.y = y; - console.log("Chart1", parentW, parentH, xRange, yRange, options.dataTypeName); + this.scales.y = y; + console.log("Chart1", xRange, yRange, dataConfig.dataTypeName); let me = this; /* these can be renamed datum2{abscissa,ordinate}{,Scaled}() */ /* apply y after scale applied by datum2Location */ - let datum2LocationScaled = scaleMaybeInterval(options.datum2Location, me.y); + let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, me.scales.y); /** related @see rectWidth(). */ - function datum2ValueScaled(d) { return me.x(options.datum2Value(d)); } - this.datum2LocationScaled = datum2LocationScaled; - this.datum2ValueScaled = datum2ValueScaled; + function datum2ValueScaled(d) { return me.scales.x(dataConfig.datum2Value(d)); } + dataConfig.datum2LocationScaled = datum2LocationScaled; + dataConfig.datum2ValueScaled = datum2ValueScaled; + }; - let gs = parentG - .selectAll("g > g") - .data([1]), +AxisCharts.prototype.group = function (parentG, groupClassName) { + /** parentG is g.axis-use. add g.(groupClassName); + * within parentG there is also a sibling g.axis-html. */ + let data = parentG.data(), + gs = parentG + .selectAll("g > g." + groupClassName) + .data(data), // inherit g.datum(), or perhaps [groupClassName] gsa = gs .enter() .append("g") // maybe drop this g, move margin calc to gp - .attr("transform", "translate(" + margin.left + "," + margin.top + ")"), + // if drawing internal chart axes then move them inside the clip rect + // .attr("transform", "translate(" + margin.left + "," + margin.top + ")"), + .attr("class", groupClassName), g = gsa.merge(gs); - this.g = g; + dLog('group', this, parentG, g.node()); + this.dom.g = g; + this.dom.gs = gs; + this.dom.gsa = gsa; + return g; +}; + +AxisCharts.prototype.drawAxes = function (data) { /** first draft showed all data; subsequently adding : * + select region from y domain @@ -261,8 +397,15 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) * + transition between views, zoom, stack */ // scaleBand() domain is a list of all y values. - // yBand.domain(data.map(options.datum2Location)); - x.domain([0, d3.max(data, this.rectWidth.bind(this, /*scaled*/false, /*gIsData*/false))]); + // yBand.domain(data.map(dataConfig.datum2Location)); + + let + {height} = this.ranges.drawSize, + {x, y} = this.scales, + {gs, gsa} = this.dom, + dataConfig = this.dataConfig; + + x.domain([0, d3.max(data, dataConfig.rectWidth.bind(dataConfig, /*scaled*/false, /*gIsData*/false))]); let axisXa = gsa.append("g") @@ -277,63 +420,68 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) .attr("class", "axis axis--y"); axisYa.merge(gs.selectAll("g > g.axis--y")) .call(d3.axisLeft(y)) // .tickFormat(".2f") ? + // can option this out if !valueName .append("text") .attr("transform", "rotate(-90)") .attr("y", 6) .attr("dy", "0.71em") .attr("text-anchor", "end") - .text(valueName); - - data = data.sort((a,b) => middle(this.options.datum2Location(a)) - middle(this.options.datum2Location(b))); + .text(dataConfig.valueName); +}; +Chart1.prototype.data = function (data) +{ + let + dataConfig = this.dataConfig; + data = data.sort((a,b) => middle(dataConfig.datum2Location(a)) - middle(dataConfig.datum2Location(b))); this.drawContent(data); this.currentData = data; - }; +}; Chart1.prototype.bars = function (data) { let - options = this.options, + dataConfig = this.dataConfig, block = this.block, g = this.g; let rs = g // .select("g." + className + " > g") - .selectAll("rect." + options.barClassName) + .selectAll("rect." + dataConfig.barClassName) .data(data), re = rs.enter(), rx = rs.exit(); let ra = re .append("rect"); ra - .attr("class", options.barClassName) + .attr("class", dataConfig.barClassName) /** parent datum is currently 1, but could be this.block; * this.parentElement.parentElement.__data__ has the axis id (not the blockId), */ - .each(function (d) { configureHorizTickHover.apply(this, [d, block, options.hoverTextFn]); }); + .each(function (d) { configureHorizTickHover.apply(this, [d, block, dataConfig.hoverTextFn]); }); ra .merge(rs) .transition().duration(1500) .attr("x", 0) - .attr("y", (d) => { let li = this.datum2LocationScaled(d); return li.length ? li[0] : li; }) + .attr("y", (d) => { let li = dataConfig.datum2LocationScaled(d); return li.length ? li[0] : li; }) // yBand.bandwidth() - .attr("height", this.rectHeight.bind(this, /*gIsData*/false)) // equiv : (d, i, g) => this.rectHeight(false, d, i, g) - .attr("width", this.rectWidth.bind(this, /*scaled*/true, /*gIsData*/false)); + .attr("height", dataConfig.rectHeight.bind(dataConfig, /*gIsData*/false)) // equiv : (d, i, g) => dataConfig.rectHeight(false, d, i, g) + .attr("width", dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false)); rx.remove(); - console.log(gAxis.node(), rs.nodes(), re.nodes()); + console.log(rs.nodes(), re.nodes()); }; /** Calculate the height of rectangle to be used for this data point - * @param this is Chart1, not DOM element. + * @param this is DataConfig, not DOM element. * @param scaled true means apply scale (x) to the result * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. * gIsData will be true when called from d3.max(), and false for d3 attr functions. */ - Chart1.prototype.rectWidth = function (scaled, gIsData, d, i, g) + DataConfig.prototype.rectWidth = function (scaled, gIsData, d, i, g) { Ember.assert('rectWidth arguments.length === 5', arguments.length === 5); /** The scale is linear, so it is OK to scale before dividing by rectHeight. * Otherwise could use datum2Value() here and apply this.x() before the return. */ - let d2v = (scaled ? this.datum2ValueScaled : this.options.datum2Value), + let d2v = (scaled ? this.datum2ValueScaled : this.datum2Value), width = d2v(d); - if (this.options.valueIsArea) { + if (this.valueIsArea) { let h; width /= (h = this.rectHeight(gIsData, d, i, g)); // dLog('rectWidth', h, width, gIsData); @@ -341,11 +489,11 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) return width; }; /** Calculate the height of rectangle to be used for this data point - * @param this is Chart1, not DOM element. + * @param this is DataConfig, not DOM element. * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. * gIsData will be true when called from d3.max(), and false for d3 attr functions. */ - Chart1.prototype.rectHeight = function (gIsData, d, i, g) + DataConfig.prototype.rectHeight = function (gIsData, d, i, g) { Ember.assert('rectHeight arguments.length === 4', arguments.length === 4); let height, locationScaled; @@ -362,7 +510,10 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) * If this point is either end, simply double the other half-distance. */ if (! g.length) - height = this.yLine.range() / 10; + /* constant value OK - don't expect to be called if g.length is 0. + * this.scales.y.range() / 10; + */ + height = 10; else { let r = []; function gData(i) { let gi = g[i]; return gIsData ? gi : gi.__data__; }; @@ -384,16 +535,16 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) Chart1.prototype.scaleLinear = function (yRange, data) { // based on https://bl.ocks.org/mbostock/3883245 - if (! this.yLine) - this.yLine = d3.scaleLinear(); - let y = this.yLine; + if (! this.scales.yLine) + this.scales.yLine = d3.scaleLinear(); + let y = this.scales.yLine; y.rangeRound(yRange); let /** location may be an interval, so flatten the result. * Later Array.flat() can be used. */ yFlat = data - .map(this.options.datum2Location) + .map(this.dataConfig.datum2Location) .reduce((acc, val) => acc.concat(val), []); y.domain(d3.extent(yFlat)); console.log('scaleLinear domain', y.domain(), yFlat); @@ -401,24 +552,24 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) }; Chart1.prototype.line = function (data) { - let y = this.yLine, options = this.options; + let y = this.scales.yLine, dataConfig = this.dataConfig; - let datum2LocationScaled = scaleMaybeInterval(options.datum2Location, y); + let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, y); let line = d3.line() - .x(this.rectWidth.bind(this, /*scaled*/true, /*gIsData*/false)) + .x(dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false)) .y((d) => middle(datum2LocationScaled(d))); - console.log("line x domain", this.x.domain(), this.x.range()); + console.log("line x domain", this.scales.x.domain(), this.scales.x.range()); let g = this.g, ps = g - .selectAll("g > path." + options.barClassName) + .selectAll("g > path." + dataConfig.barClassName) .data([1]); ps .enter() .append("path") - .attr("class", options.barClassName + " line") + .attr("class", dataConfig.barClassName + " line") .datum(data) .attr("d", line) .merge(ps) @@ -445,8 +596,15 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) chartDraw.apply(this, [data]); }; +AxisCharts.prototype.commonFrame = function container() +{ + let axisID = this.axisID, + gAxis = this.dom.gAxis, + bbox = this.ranges.bbox; + /** datum is value in hash : {value : , description: } and with optional attribute description. */ + dLog('container', gAxis.node()); /** parent; contains a clipPath, g > rect, text.resizer. */ let gps = gAxis .selectAll("g." + className) @@ -483,33 +641,37 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) let g = gps.merge(gp).selectAll("g." + className+ " > g"); + if (! gp.empty() ) { + addParentClass(g); + /* .gc is ​​ + * .g (assigned later) is g.axis-chart + */ + this.dom.gc = g; + this.dom.gp = gp; + this.dom.gps = gps; + } +}; + +/* +class AxisChart { + bbox; + data; + gp; + gps; + }; +*/ +AxisCharts.prototype.configure = function configure(dataConfig) +{ + this.dataConfig = dataConfig; +}; + +AxisCharts.prototype.controls = function controls(chart1) +{ + let + bbox = this.ranges.bbox, + gp = this.dom.gp, + gps = this.dom.gps; - /* It is possible to recreate Chart1 for each call, but that leads to - * complexity in ensuring that the instance rendered by toggleBarsLineClosure() is - * the same one whose options.bbox.width is updated from axis-chart.width. - */ - let chart1 = axisChart.get("chart1"); - if (chart1) - { - chart1.options.bbox.width = bbox.width; - } - else - { - chart1 = new Chart1(g, - { - bbox : bbox, - barClassName : classNameSub, - dataTypeName : dataConfig.dataTypeName, - datum2Location : dataConfig.datum2Location, - datum2Value : dataConfig.datum2Value, - datum2Description : dataConfig.datum2Description, - hoverTextFn : dataConfig.hoverTextFn, - valueIsArea : dataConfig.valueIsArea - }); - chart1.block = axisChart.get('block'); - axisChart.set("chart1", chart1); - addParentClass(g); - } let b = chart1; // b for barChart function toggleBarsLineClosure(e) @@ -528,12 +690,10 @@ function layoutAndDrawChart(axisChart, chart, dataConfig) .attr("cy", bbox.height - 10) .classed("pushed", b.barsLine); b.chartTypeToggle = chartTypeToggle; - - b.draw(data); - } -; +}; /*----------------------------------------------------------------------------*/ -/* subsequent step will move layoutAndDrawChart into class Chart1, and export that instead. */ -export { className, layoutAndDrawChart /*Chart1*/ }; +/* layoutAndDrawChart() has been split into class methods of AxisCharts and Chart1, + * and replaced with a proxy which calls them, and can next be re-distributed into axis-chart. */ +export { layoutAndDrawChart, AxisCharts, /*AxisChart,*/ className, Chart1, DataConfig }; From b97729f359cfc9403474c1bfd24fc562ab2b51d7 Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 23 Jan 2020 14:51:34 +1100 Subject: [PATCH 07/57] auto re-indent, after previous function splits fa7d8c4 23038 Jan 23 14:44 frontend/app/utils/draw/chart1.js --- frontend/app/utils/draw/chart1.js | 860 +++++++++++++++--------------- 1 file changed, 430 insertions(+), 430 deletions(-) diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index 5308a6b65..ed59614a2 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -45,7 +45,7 @@ function middle(location) { : location; return result; } - + /** @return a function to map a chart datum to a y value or interval. */ function scaleMaybeInterval(datum2Location, yScale) { @@ -102,10 +102,10 @@ function addParentClass(g) { class DataConfig { /* - dataTypeName; - datum2Location; - datum2Value; - datum2Description; + dataTypeName; + datum2Location; + datum2Value; + datum2Description; */ constructor (properties) { if (properties) @@ -114,42 +114,42 @@ class DataConfig { }; - /** @param name is a feature or gene name */ - function name2Location(name, blockId) - { - /** @param ak1 axis name, (exists in axisIDs[]) - * @param d1 feature name, i.e. ak1:d1 - */ - return featureLocation(blockId, name); - } +/** @param name is a feature or gene name */ +function name2Location(name, blockId) +{ + /** @param ak1 axis name, (exists in axisIDs[]) + * @param d1 feature name, i.e. ak1:d1 + */ + return featureLocation(blockId, name); +} - /** Used for both blockData and parsedData. */ - function datum2LocationWithBlock(d, blockId) { return name2Location(d.name, blockId); } - function datum2Value(d) { return d.value; } - let parsedData = { - dataTypeName : 'parsedData', - // datum2LocationWithBlock assigned later, - datum2Value : datum2Value, - datum2Description : function(d) { return d.description; } - }, - blockData = { - dataTypeName : 'blockData', - // datum2LocationWithBlock assigned later, - /** The effects data is placed in .value[2] (the interval is in value[0..1]). - * Use the first effects value by default, but later will combine other values. - */ - datum2Value : function(d) { let v = d.value[2]; if (v.length) v = v[0]; return v; }, - datum2Description : function(d) { return JSON.stringify(d.value); } - }; +/** Used for both blockData and parsedData. */ +function datum2LocationWithBlock(d, blockId) { return name2Location(d.name, blockId); } +function datum2Value(d) { return d.value; } +let parsedData = { + dataTypeName : 'parsedData', + // datum2LocationWithBlock assigned later, + datum2Value : datum2Value, + datum2Description : function(d) { return d.description; } +}, +blockData = { + dataTypeName : 'blockData', + // datum2LocationWithBlock assigned later, + /** The effects data is placed in .value[2] (the interval is in value[0..1]). + * Use the first effects value by default, but later will combine other values. + */ + datum2Value : function(d) { let v = d.value[2]; if (v.length) v = v[0]; return v; }, + datum2Description : function(d) { return JSON.stringify(d.value); } +}; /*----------------------------------------------------------------------------*/ class AxisCharts { /* - dataConfig; // DataConfig (options) - ranges; // .bbox, .height - dom; // .gs, .gsa, + dataConfig; // DataConfig (options) + ranges; // .bbox, .height + dom; // .gs, .gsa, */ constructor(axisID, dataConfig) { @@ -160,7 +160,7 @@ class AxisCharts { // this will move to Chart1. this.scales = { /* x, y, yLine */ }; } - }; +}; /** * @param yAxisScale @@ -209,7 +209,7 @@ function layoutAndDrawChart(axisID, axisCharts, chart1, chartData, block, dataCo chart1.prepareScales(chartData, axisCharts.ranges.drawSize); chart1.g = - axisCharts.group(axisCharts.dom.gc, 'axis-chart'); + axisCharts.group(axisCharts.dom.gc, 'axis-chart'); if (showChartAxes) axisCharts.drawAxes(chartData); chart1.block = block; // used in Chart1:bars() for hover text. @@ -221,380 +221,380 @@ function layoutAndDrawChart(axisID, axisCharts, chart1, chartData, block, dataCo }; AxisCharts.prototype.selectParentContainer = function (axisID) - { +{ this.axisID = axisID; - // to support multiple split axes, identify axis by axisID (and possibly stack id) - // - // g.axis-outer#id - let gAxis = d3.select("g.axis-outer#id" + axisID + "> g.axis-use"); - if (! gAxis.node()) { /* probably because axisShowExtend() has not added the g.axis-use yet - will sort out the dependencies ... . */ - dLog('layoutAndDrawChart', d3.select("g.axis-outer#id" + axisID).node(), 'no g.axis-use', this, axisID); - } + // to support multiple split axes, identify axis by axisID (and possibly stack id) + // + // g.axis-outer#id + let gAxis = d3.select("g.axis-outer#id" + axisID + "> g.axis-use"); + if (! gAxis.node()) { /* probably because axisShowExtend() has not added the g.axis-use yet - will sort out the dependencies ... . */ + dLog('layoutAndDrawChart', d3.select("g.axis-outer#id" + axisID).node(), 'no g.axis-use', this, axisID); + } this.dom.gAxis = gAxis; return gAxis; }; AxisCharts.prototype.getBBox = function () - { - let axisID = this.axisID, - gAxis = this.dom.gAxis; - let +{ + let axisID = this.axisID, + gAxis = this.dom.gAxis; + let /** relative to the transform of parent g.axis-outer */ bbox = gAxis.node().getBBox(), - yrange = [bbox.y, bbox.height]; - if (bbox.x < 0) - { - console.log("x < 0", bbox); - bbox.x = 0; - } - this.ranges.bbox = bbox; - this.ranges.yrange = yrange; + yrange = [bbox.y, bbox.height]; + if (bbox.x < 0) + { + console.log("x < 0", bbox); + bbox.x = 0; + } + this.ranges.bbox = bbox; + this.ranges.yrange = yrange; }; AxisCharts.prototype.getRanges = function (chart, blockId, resizedWidth) { let - {yrange } = this.ranges, - {yAxis } = this.scales, - /** isBlockData is not used if dataConfig is defined. this can be moved out to the caller. */ - isBlockData = chart.length && (chart[0].description === undefined), - // axisID = gAxis.node().parentElement.__data__, - - yAxisDomain = yAxis.domain(), yDomain; - if (noDomain(yAxisDomain) && chart.length) { - yAxisDomain = [chart[0]._id.min, chart[chart.length-1]._id.max]; - yAxis.domain(yAxisDomain); - yDomain = yAxisDomain; - } - else - yDomain = [yAxis.invert(yrange[0]), yAxis.invert(yrange[1])]; - - let dataConfig = this.dataConfig; - if (! dataConfig) { - let dataConfigProperties = isBlockData ? blockData : parsedData; - dataConfigProperties.datum2Location = - (d) => datum2LocationWithBlock(d, blockId); - this.dataConfig = dataConfig = - new DataConfig(dataConfigProperties); - if (! dataConfig.hoverTextFn) - dataConfig.hoverTextFn = hoverTextFn; - if (dataConfig.valueIsArea === undefined) - dataConfig.valueIsArea = false; - } - if (dataConfig) { - if (! dataConfig.barClassName) - dataConfig.barClassName = classNameSub; - if (! dataConfig.valueName) - dataConfig.valueName = chart.valueName || "Values"; - } + {yrange } = this.ranges, + {yAxis } = this.scales, + /** isBlockData is not used if dataConfig is defined. this can be moved out to the caller. */ + isBlockData = chart.length && (chart[0].description === undefined), + // axisID = gAxis.node().parentElement.__data__, + + yAxisDomain = yAxis.domain(), yDomain; + if (noDomain(yAxisDomain) && chart.length) { + yAxisDomain = [chart[0]._id.min, chart[chart.length-1]._id.max]; + yAxis.domain(yAxisDomain); + yDomain = yAxisDomain; + } + else + yDomain = [yAxis.invert(yrange[0]), yAxis.invert(yrange[1])]; + + let dataConfig = this.dataConfig; + if (! dataConfig) { + let dataConfigProperties = isBlockData ? blockData : parsedData; + dataConfigProperties.datum2Location = + (d) => datum2LocationWithBlock(d, blockId); + this.dataConfig = dataConfig = + new DataConfig(dataConfigProperties); + if (! dataConfig.hoverTextFn) + dataConfig.hoverTextFn = hoverTextFn; + if (dataConfig.valueIsArea === undefined) + dataConfig.valueIsArea = false; + } + if (dataConfig) { + if (! dataConfig.barClassName) + dataConfig.barClassName = classNameSub; + if (! dataConfig.valueName) + dataConfig.valueName = chart.valueName || "Values"; + } - this.ranges.pxSize = (yDomain[1] - yDomain[0]) / this.ranges.bbox.height; + this.ranges.pxSize = (yDomain[1] - yDomain[0]) / this.ranges.bbox.height; }; AxisCharts.prototype.getRanges3 = function (chart, resizedWidth) { - let - {bbox} = this.ranges, - {yAxis} = this.scales, - yDomain = yAxis.domain(), - withinZoomRegion = (d) => { - return inRangeEither(this.dataConfig.datum2Location(d), yDomain); - }, - data = chart.filter(withinZoomRegion); - - console.log(resizedWidth, bbox, yDomain, data.length, (data.length == 0) || this.dataConfig.datum2Location(data[0])); - if (resizedWidth) - bbox.width = resizedWidth; + let + {bbox} = this.ranges, + {yAxis} = this.scales, + yDomain = yAxis.domain(), + withinZoomRegion = (d) => { + return inRangeEither(this.dataConfig.datum2Location(d), yDomain); + }, + data = chart.filter(withinZoomRegion); + + console.log(resizedWidth, bbox, yDomain, data.length, (data.length == 0) || this.dataConfig.datum2Location(data[0])); + if (resizedWidth) + bbox.width = resizedWidth; }; - - /* axis - * x .value - * y .name Location - */ - /** 1-dimensional chart, within an axis. */ - function Chart1(parentG, dataConfig) - { - this.parentG = parentG; - this.dataConfig = dataConfig; - } - Chart1.prototype.barsLine = true; - AxisCharts.prototype.getRanges2 = function () - { - // based on https://bl.ocks.org/mbostock/3885304, axes x & y swapped. - let - // parentG = this.parentG, - bbox = this.ranges.bbox, - margin = showChartAxes ? - {top: 10, right: 20, bottom: 40, left: 20} : - {top: 0, right: 0, bottom: 0, left: 0}, - // pp=parentG.node().parentElement, - parentW = bbox.width, // +pp.attr("width") - parentH = bbox.height, // +pp.attr("height") - width = parentW - margin.left - margin.right, - height = parentH - margin.top - margin.bottom; - this.ranges.drawSize = {width, height}; - dLog('getRanges2', parentW, parentH, this.ranges.drawSize); - }; - Chart1.prototype.prepareScales = function (data, drawSize) - { - /** The chart is perpendicular to the usual presentation. - * The names x & y (in {x,y}Range and yLine) match the orientation on the - * screen, rather than the role of abscissa / ordinate; the data maps - * .name (location) -> .value, which is named y -> x. - */ - let - dataConfig = this.dataConfig, - width = drawSize.width, - height = drawSize.height, - xRange = [0, width], - yRange = [0, height], - // yRange is used as range of yLine by this.scaleLinear(). - /* scaleBand would suit a data set with evenly spaced or ordinal / nominal y values. - * yBand = d3.scaleBand().rangeRound(yRange).padding(0.1), - */ - y = this.scaleLinear(yRange, data), - x = d3.scaleLinear().rangeRound(xRange); - // datum2LocationScaled() uses me.scales.x rather than the value in the closure in which it was created. - this.scales.x = x; - // Used by bars() - could be moved there, along with datum2LocationScaled(). - this.scales.y = y; - console.log("Chart1", xRange, yRange, dataConfig.dataTypeName); - - let me = this; - /* these can be renamed datum2{abscissa,ordinate}{,Scaled}() */ - /* apply y after scale applied by datum2Location */ - let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, me.scales.y); - /** related @see rectWidth(). */ - function datum2ValueScaled(d) { return me.scales.x(dataConfig.datum2Value(d)); } - dataConfig.datum2LocationScaled = datum2LocationScaled; - dataConfig.datum2ValueScaled = datum2ValueScaled; - }; + +/* axis + * x .value + * y .name Location + */ +/** 1-dimensional chart, within an axis. */ +function Chart1(parentG, dataConfig) +{ + this.parentG = parentG; + this.dataConfig = dataConfig; +} +Chart1.prototype.barsLine = true; +AxisCharts.prototype.getRanges2 = function () +{ + // based on https://bl.ocks.org/mbostock/3885304, axes x & y swapped. + let + // parentG = this.parentG, + bbox = this.ranges.bbox, + margin = showChartAxes ? + {top: 10, right: 20, bottom: 40, left: 20} : + {top: 0, right: 0, bottom: 0, left: 0}, + // pp=parentG.node().parentElement, + parentW = bbox.width, // +pp.attr("width") + parentH = bbox.height, // +pp.attr("height") + width = parentW - margin.left - margin.right, + height = parentH - margin.top - margin.bottom; + this.ranges.drawSize = {width, height}; + dLog('getRanges2', parentW, parentH, this.ranges.drawSize); +}; +Chart1.prototype.prepareScales = function (data, drawSize) +{ + /** The chart is perpendicular to the usual presentation. + * The names x & y (in {x,y}Range and yLine) match the orientation on the + * screen, rather than the role of abscissa / ordinate; the data maps + * .name (location) -> .value, which is named y -> x. + */ + let + dataConfig = this.dataConfig, + width = drawSize.width, + height = drawSize.height, + xRange = [0, width], + yRange = [0, height], + // yRange is used as range of yLine by this.scaleLinear(). + /* scaleBand would suit a data set with evenly spaced or ordinal / nominal y values. + * yBand = d3.scaleBand().rangeRound(yRange).padding(0.1), + */ + y = this.scaleLinear(yRange, data), + x = d3.scaleLinear().rangeRound(xRange); + // datum2LocationScaled() uses me.scales.x rather than the value in the closure in which it was created. + this.scales.x = x; + // Used by bars() - could be moved there, along with datum2LocationScaled(). + this.scales.y = y; + console.log("Chart1", xRange, yRange, dataConfig.dataTypeName); + + let me = this; + /* these can be renamed datum2{abscissa,ordinate}{,Scaled}() */ + /* apply y after scale applied by datum2Location */ + let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, me.scales.y); + /** related @see rectWidth(). */ + function datum2ValueScaled(d) { return me.scales.x(dataConfig.datum2Value(d)); } + dataConfig.datum2LocationScaled = datum2LocationScaled; + dataConfig.datum2ValueScaled = datum2ValueScaled; +}; AxisCharts.prototype.group = function (parentG, groupClassName) { - /** parentG is g.axis-use. add g.(groupClassName); - * within parentG there is also a sibling g.axis-html. */ - let data = parentG.data(), - gs = parentG - .selectAll("g > g." + groupClassName) - .data(data), // inherit g.datum(), or perhaps [groupClassName] - gsa = gs - .enter() - .append("g") // maybe drop this g, move margin calc to gp - // if drawing internal chart axes then move them inside the clip rect - // .attr("transform", "translate(" + margin.left + "," + margin.top + ")"), - .attr("class", groupClassName), - g = gsa.merge(gs); - dLog('group', this, parentG, g.node()); - this.dom.g = g; - this.dom.gs = gs; - this.dom.gsa = gsa; + /** parentG is g.axis-use. add g.(groupClassName); + * within parentG there is also a sibling g.axis-html. */ + let data = parentG.data(), + gs = parentG + .selectAll("g > g." + groupClassName) + .data(data), // inherit g.datum(), or perhaps [groupClassName] + gsa = gs + .enter() + .append("g") // maybe drop this g, move margin calc to gp + // if drawing internal chart axes then move them inside the clip rect + // .attr("transform", "translate(" + margin.left + "," + margin.top + ")"), + .attr("class", groupClassName), + g = gsa.merge(gs); + dLog('group', this, parentG, g.node()); + this.dom.g = g; + this.dom.gs = gs; + this.dom.gsa = gsa; return g; }; AxisCharts.prototype.drawAxes = function (data) { - /** first draft showed all data; subsequently adding : - * + select region from y domain - * - place data in tree for fast subset by region - * + alternate view : line - * + transition between views, zoom, stack - */ - // scaleBand() domain is a list of all y values. - // yBand.domain(data.map(dataConfig.datum2Location)); - - let - {height} = this.ranges.drawSize, - {x, y} = this.scales, - {gs, gsa} = this.dom, - dataConfig = this.dataConfig; - - x.domain([0, d3.max(data, dataConfig.rectWidth.bind(dataConfig, /*scaled*/false, /*gIsData*/false))]); - - let axisXa = - gsa.append("g") - // - handle .exit() for these 2 also - .attr("class", "axis axis--x"); - axisXa.merge(gs.selectAll("g > g.axis--x")) - .attr("transform", "translate(0," + height + ")") - .call(d3.axisBottom(x)); - - let axisYa = - gsa.append("g") - .attr("class", "axis axis--y"); - axisYa.merge(gs.selectAll("g > g.axis--y")) - .call(d3.axisLeft(y)) // .tickFormat(".2f") ? - // can option this out if !valueName - .append("text") - .attr("transform", "rotate(-90)") - .attr("y", 6) - .attr("dy", "0.71em") - .attr("text-anchor", "end") - .text(dataConfig.valueName); + /** first draft showed all data; subsequently adding : + * + select region from y domain + * - place data in tree for fast subset by region + * + alternate view : line + * + transition between views, zoom, stack + */ + // scaleBand() domain is a list of all y values. + // yBand.domain(data.map(dataConfig.datum2Location)); + + let + {height} = this.ranges.drawSize, + {x, y} = this.scales, + {gs, gsa} = this.dom, + dataConfig = this.dataConfig; + + x.domain([0, d3.max(data, dataConfig.rectWidth.bind(dataConfig, /*scaled*/false, /*gIsData*/false))]); + + let axisXa = + gsa.append("g") + // - handle .exit() for these 2 also + .attr("class", "axis axis--x"); + axisXa.merge(gs.selectAll("g > g.axis--x")) + .attr("transform", "translate(0," + height + ")") + .call(d3.axisBottom(x)); + + let axisYa = + gsa.append("g") + .attr("class", "axis axis--y"); + axisYa.merge(gs.selectAll("g > g.axis--y")) + .call(d3.axisLeft(y)) // .tickFormat(".2f") ? + // can option this out if !valueName + .append("text") + .attr("transform", "rotate(-90)") + .attr("y", 6) + .attr("dy", "0.71em") + .attr("text-anchor", "end") + .text(dataConfig.valueName); }; Chart1.prototype.data = function (data) { let - dataConfig = this.dataConfig; - data = data.sort((a,b) => middle(dataConfig.datum2Location(a)) - middle(dataConfig.datum2Location(b))); - this.drawContent(data); - this.currentData = data; + dataConfig = this.dataConfig; + data = data.sort((a,b) => middle(dataConfig.datum2Location(a)) - middle(dataConfig.datum2Location(b))); + this.drawContent(data); + this.currentData = data; }; - Chart1.prototype.bars = function (data) - { - let - dataConfig = this.dataConfig, - block = this.block, - g = this.g; - let - rs = g - // .select("g." + className + " > g") - .selectAll("rect." + dataConfig.barClassName) - .data(data), - re = rs.enter(), rx = rs.exit(); - let ra = re - .append("rect"); - ra - .attr("class", dataConfig.barClassName) - /** parent datum is currently 1, but could be this.block; - * this.parentElement.parentElement.__data__ has the axis id (not the blockId), - */ - .each(function (d) { configureHorizTickHover.apply(this, [d, block, dataConfig.hoverTextFn]); }); - ra - .merge(rs) - .transition().duration(1500) - .attr("x", 0) - .attr("y", (d) => { let li = dataConfig.datum2LocationScaled(d); return li.length ? li[0] : li; }) - // yBand.bandwidth() - .attr("height", dataConfig.rectHeight.bind(dataConfig, /*gIsData*/false)) // equiv : (d, i, g) => dataConfig.rectHeight(false, d, i, g) - .attr("width", dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false)); - rx.remove(); - console.log(rs.nodes(), re.nodes()); - }; - /** Calculate the height of rectangle to be used for this data point - * @param this is DataConfig, not DOM element. - * @param scaled true means apply scale (x) to the result - * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. - * gIsData will be true when called from d3.max(), and false for d3 attr functions. +Chart1.prototype.bars = function (data) +{ + let + dataConfig = this.dataConfig, + block = this.block, + g = this.g; + let + rs = g + // .select("g." + className + " > g") + .selectAll("rect." + dataConfig.barClassName) + .data(data), + re = rs.enter(), rx = rs.exit(); + let ra = re + .append("rect"); + ra + .attr("class", dataConfig.barClassName) + /** parent datum is currently 1, but could be this.block; + * this.parentElement.parentElement.__data__ has the axis id (not the blockId), + */ + .each(function (d) { configureHorizTickHover.apply(this, [d, block, dataConfig.hoverTextFn]); }); + ra + .merge(rs) + .transition().duration(1500) + .attr("x", 0) + .attr("y", (d) => { let li = dataConfig.datum2LocationScaled(d); return li.length ? li[0] : li; }) + // yBand.bandwidth() + .attr("height", dataConfig.rectHeight.bind(dataConfig, /*gIsData*/false)) // equiv : (d, i, g) => dataConfig.rectHeight(false, d, i, g) + .attr("width", dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false)); + rx.remove(); + console.log(rs.nodes(), re.nodes()); +}; +/** Calculate the height of rectangle to be used for this data point + * @param this is DataConfig, not DOM element. + * @param scaled true means apply scale (x) to the result + * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. + * gIsData will be true when called from d3.max(), and false for d3 attr functions. + */ +DataConfig.prototype.rectWidth = function (scaled, gIsData, d, i, g) +{ + Ember.assert('rectWidth arguments.length === 5', arguments.length === 5); + /** The scale is linear, so it is OK to scale before dividing by rectHeight. + * Otherwise could use datum2Value() here and apply this.x() before the return. + */ + let d2v = (scaled ? this.datum2ValueScaled : this.datum2Value), + width = d2v(d); + if (this.valueIsArea) { + let h; + width /= (h = this.rectHeight(gIsData, d, i, g)); + // dLog('rectWidth', h, width, gIsData); + } + return width; +}; +/** Calculate the height of rectangle to be used for this data point + * @param this is DataConfig, not DOM element. + * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. + * gIsData will be true when called from d3.max(), and false for d3 attr functions. + */ +DataConfig.prototype.rectHeight = function (gIsData, d, i, g) +{ + Ember.assert('rectHeight arguments.length === 4', arguments.length === 4); + let height, locationScaled; + /* if locationScaled is an interval, calculate height from it. + * Otherwise, use adjacent points to indicate height. + */ + if ((locationScaled = this.datum2LocationScaled(d)).length) { + height = Math.abs(locationScaled[1] - locationScaled[0]); + } + else { + /* the boundary between 2 adjacent points is midway between them. + * So sum that half-distance for the previous and next points. + * the y range distance from the previous point to the next point. + * If this point is either end, simply double the other half-distance. */ - DataConfig.prototype.rectWidth = function (scaled, gIsData, d, i, g) - { - Ember.assert('rectWidth arguments.length === 5', arguments.length === 5); - /** The scale is linear, so it is OK to scale before dividing by rectHeight. - * Otherwise could use datum2Value() here and apply this.x() before the return. + if (! g.length) + /* constant value OK - don't expect to be called if g.length is 0. + * this.scales.y.range() / 10; */ - let d2v = (scaled ? this.datum2ValueScaled : this.datum2Value), - width = d2v(d); - if (this.valueIsArea) { - let h; - width /= (h = this.rectHeight(gIsData, d, i, g)); - // dLog('rectWidth', h, width, gIsData); - } - return width; - }; - /** Calculate the height of rectangle to be used for this data point - * @param this is DataConfig, not DOM element. - * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. - * gIsData will be true when called from d3.max(), and false for d3 attr functions. + height = 10; + else { + let r = []; + function gData(i) { let gi = g[i]; return gIsData ? gi : gi.__data__; }; + if (i > 0) + r.push(gData(i)); + r.push(d); + if (i < g.length-1) + r.push(gData(i+1)); + let y = + r.map(this.datum2LocationScaled); + height = Math.abs(y[y.length-1] - y[0]) * 2 / (y.length-1); + dLog('rectHeight', gIsData, d, i, /*g,*/ r, y, height); + if (! height) + height = 1; + } + } + return height; +}; +Chart1.prototype.scaleLinear = function (yRange, data) +{ + // based on https://bl.ocks.org/mbostock/3883245 + if (! this.scales.yLine) + this.scales.yLine = d3.scaleLinear(); + let y = this.scales.yLine; + y.rangeRound(yRange); + let + /** location may be an interval, so flatten the result. + * Later Array.flat() can be used. */ - DataConfig.prototype.rectHeight = function (gIsData, d, i, g) - { - Ember.assert('rectHeight arguments.length === 4', arguments.length === 4); - let height, locationScaled; - /* if locationScaled is an interval, calculate height from it. - * Otherwise, use adjacent points to indicate height. - */ - if ((locationScaled = this.datum2LocationScaled(d)).length) { - height = Math.abs(locationScaled[1] - locationScaled[0]); - } - else { - /* the boundary between 2 adjacent points is midway between them. - * So sum that half-distance for the previous and next points. - * the y range distance from the previous point to the next point. - * If this point is either end, simply double the other half-distance. - */ - if (! g.length) - /* constant value OK - don't expect to be called if g.length is 0. - * this.scales.y.range() / 10; - */ - height = 10; - else { - let r = []; - function gData(i) { let gi = g[i]; return gIsData ? gi : gi.__data__; }; - if (i > 0) - r.push(gData(i)); - r.push(d); - if (i < g.length-1) - r.push(gData(i+1)); - let y = - r.map(this.datum2LocationScaled); - height = Math.abs(y[y.length-1] - y[0]) * 2 / (y.length-1); - dLog('rectHeight', gIsData, d, i, /*g,*/ r, y, height); - if (! height) - height = 1; - } - } - return height; - }; - Chart1.prototype.scaleLinear = function (yRange, data) - { - // based on https://bl.ocks.org/mbostock/3883245 - if (! this.scales.yLine) - this.scales.yLine = d3.scaleLinear(); - let y = this.scales.yLine; - y.rangeRound(yRange); - let - /** location may be an interval, so flatten the result. - * Later Array.flat() can be used. - */ - yFlat = data - .map(this.dataConfig.datum2Location) - .reduce((acc, val) => acc.concat(val), []); - y.domain(d3.extent(yFlat)); - console.log('scaleLinear domain', y.domain(), yFlat); - return y; - }; - Chart1.prototype.line = function (data) - { - let y = this.scales.yLine, dataConfig = this.dataConfig; - - let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, y); - let line = d3.line() - .x(dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false)) - .y((d) => middle(datum2LocationScaled(d))); - - console.log("line x domain", this.scales.x.domain(), this.scales.x.range()); - - let - g = this.g, - ps = g - .selectAll("g > path." + dataConfig.barClassName) - .data([1]); - ps - .enter() - .append("path") - .attr("class", dataConfig.barClassName + " line") - .datum(data) - .attr("d", line) - .merge(ps) - .datum(data) - .transition().duration(1500) - .attr("d", line); - // data length is constant 1, so .remove() is not needed - ps.exit().remove(); - }; - /** Alternate between bar chart and line chart */ - Chart1.prototype.toggleBarsLine = function () - { - console.log("toggleBarsLine", this); - d3.event.stopPropagation(); - this.barsLine = ! this.barsLine; - this.chartTypeToggle - .classed("pushed", this.barsLine); - this.g.selectAll("g > *").remove(); - this.drawContent(this.currentData); - }; - Chart1.prototype.drawContent = function(data) - { - let chartDraw = this.barsLine ? this.bars : this.line; - chartDraw.apply(this, [data]); - }; + yFlat = data + .map(this.dataConfig.datum2Location) + .reduce((acc, val) => acc.concat(val), []); + y.domain(d3.extent(yFlat)); + console.log('scaleLinear domain', y.domain(), yFlat); + return y; +}; +Chart1.prototype.line = function (data) +{ + let y = this.scales.yLine, dataConfig = this.dataConfig; + + let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, y); + let line = d3.line() + .x(dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false)) + .y((d) => middle(datum2LocationScaled(d))); + + console.log("line x domain", this.scales.x.domain(), this.scales.x.range()); + + let + g = this.g, + ps = g + .selectAll("g > path." + dataConfig.barClassName) + .data([1]); + ps + .enter() + .append("path") + .attr("class", dataConfig.barClassName + " line") + .datum(data) + .attr("d", line) + .merge(ps) + .datum(data) + .transition().duration(1500) + .attr("d", line); + // data length is constant 1, so .remove() is not needed + ps.exit().remove(); +}; +/** Alternate between bar chart and line chart */ +Chart1.prototype.toggleBarsLine = function () +{ + console.log("toggleBarsLine", this); + d3.event.stopPropagation(); + this.barsLine = ! this.barsLine; + this.chartTypeToggle + .classed("pushed", this.barsLine); + this.g.selectAll("g > *").remove(); + this.drawContent(this.currentData); +}; +Chart1.prototype.drawContent = function(data) +{ + let chartDraw = this.barsLine ? this.bars : this.line; + chartDraw.apply(this, [data]); +}; AxisCharts.prototype.commonFrame = function container() { @@ -602,18 +602,18 @@ AxisCharts.prototype.commonFrame = function container() gAxis = this.dom.gAxis, bbox = this.ranges.bbox; - /** datum is value in hash : {value : , description: } and with optional attribute description. */ - - dLog('container', gAxis.node()); - /** parent; contains a clipPath, g > rect, text.resizer. */ - let gps = gAxis - .selectAll("g." + className) - .data([axisID]), - gp = gps - .enter() - .insert("g", ":first-child") - .attr('class', className); - if (false) { // not completed. Can base resized() on axis-2d.js + /** datum is value in hash : {value : , description: } and with optional attribute description. */ + + dLog('container', gAxis.node()); + /** parent; contains a clipPath, g > rect, text.resizer. */ + let gps = gAxis + .selectAll("g." + className) + .data([axisID]), + gp = gps + .enter() + .insert("g", ":first-child") + .attr('class', className); + if (false) { // not completed. Can base resized() on axis-2d.js let text = gp .append("text") .attr('class', 'resizer') @@ -622,25 +622,25 @@ AxisCharts.prototype.commonFrame = function container() if (gp.size() > 0) eltWidthResizable("g.axis-use > g." + className + " > text.resizer", resized); } - /** datum is axisID, so id and clip-path could be functions. */ - let axisClipId = "axis-clip-" + axisID; - let gpa = + /** datum is axisID, so id and clip-path could be functions. */ + let axisClipId = "axis-clip-" + axisID; + let gpa = gp // define the clipPath - .append("clipPath") // define a clip path - .attr("id", axisClipId) // give the clipPath an ID - .append("rect"), // shape it as a rect - gprm = + .append("clipPath") // define a clip path + .attr("id", axisClipId) // give the clipPath an ID + .append("rect"), // shape it as a rect + gprm = gpa.merge(gps.selectAll("g > clipPath > rect")) - .attr("x", bbox.x) - .attr("y", bbox.y) - .attr("width", bbox.width) - .attr("height", bbox.height) - ; - gp.append("g") - .attr("clip-path", "url(#" + axisClipId + ")"); // clip with the rectangle - - let g = - gps.merge(gp).selectAll("g." + className+ " > g"); + .attr("x", bbox.x) + .attr("y", bbox.y) + .attr("width", bbox.width) + .attr("height", bbox.height) + ; + gp.append("g") + .attr("clip-path", "url(#" + axisClipId + ")"); // clip with the rectangle + + let g = + gps.merge(gp).selectAll("g." + className+ " > g"); if (! gp.empty() ) { addParentClass(g); /* .gc is ​​ @@ -653,13 +653,13 @@ AxisCharts.prototype.commonFrame = function container() }; /* -class AxisChart { - bbox; - data; - gp; - gps; + class AxisChart { + bbox; + data; + gp; + gps; }; -*/ + */ AxisCharts.prototype.configure = function configure(dataConfig) { this.dataConfig = dataConfig; @@ -672,24 +672,24 @@ AxisCharts.prototype.controls = function controls(chart1) gp = this.dom.gp, gps = this.dom.gps; - let b = chart1; // b for barChart + let b = chart1; // b for barChart - function toggleBarsLineClosure(e) - { - b.toggleBarsLine(); - } + function toggleBarsLineClosure(e) + { + b.toggleBarsLine(); + } - /** currently placed at g.chart, could be inside g.chart>g (clip-path=). */ - let chartTypeToggle = gp - .append("circle") - .attr("class", "radio toggle chartType") - .attr("r", 6) - .on("click", toggleBarsLineClosure); - chartTypeToggle.merge(gps.selectAll("g > circle")) - .attr("cx", bbox.x + bbox.width / 2) /* was o[p], but g.axis-outer translation does x offset of stack. */ - .attr("cy", bbox.height - 10) - .classed("pushed", b.barsLine); - b.chartTypeToggle = chartTypeToggle; + /** currently placed at g.chart, could be inside g.chart>g (clip-path=). */ + let chartTypeToggle = gp + .append("circle") + .attr("class", "radio toggle chartType") + .attr("r", 6) + .on("click", toggleBarsLineClosure); + chartTypeToggle.merge(gps.selectAll("g > circle")) + .attr("cx", bbox.x + bbox.width / 2) /* was o[p], but g.axis-outer translation does x offset of stack. */ + .attr("cy", bbox.height - 10) + .classed("pushed", b.barsLine); + b.chartTypeToggle = chartTypeToggle; }; /*----------------------------------------------------------------------------*/ From ab8f6b9253972b9df6078f23ee7bb20ec650bccf Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 24 Jan 2020 17:29:03 +1100 Subject: [PATCH 08/57] fix call which ensures block feature limits are loaded when adding a block. useTask() : call ensureFeatureLimits() from .block not this. db81a79 9466 Jan 24 17:11 frontend/app/controllers/mapview.js --- frontend/app/controllers/mapview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/controllers/mapview.js b/frontend/app/controllers/mapview.js index 543b98d01..a0ceece95 100644 --- a/frontend/app/controllers/mapview.js +++ b/frontend/app/controllers/mapview.js @@ -259,7 +259,7 @@ export default Ember.Controller.extend(Ember.Evented, ViewedBlocks, { * Also adding a similar request to updateModal (refreshDatasets) so by this * time that result should have been received. */ - this.ensureFeatureLimits(id); + this.get('block').ensureFeatureLimits(id); /** Before progressive loading this would load the data (features) of the block. */ const progressiveLoading = true; From 1e67720e43687385a510768069b073a8e17708dd Mon Sep 17 00:00:00 2001 From: Don Date: Sun, 26 Jan 2020 17:50:14 +1100 Subject: [PATCH 09/57] axis-chart : factor design to handle multiple lines per chart axis-2d.hbs : create axis-chart per axis, instead of per chartable block. axis-chart.js : factor countsChart to featureCountDataProperties, add dataConfigs to wrap the types, Add attributes : blocksData, charts Move CPs blockFeatures (with most of drawBlockFeatures0 included) & featuresCounts to form block-view; also move hbs content in parallel. Add chartTypes, chartTypesEffect. chart1.js : Split ChartLine out of Chart1. Pass dataTypeName instead of chart1 to .layoutAndDrawChart(). Split layoutAndDrawChart to form drawChart and setupChart Pass scaled to rectHeight(). Drop dataConfig from AxisChart; move getRanges() from AxisCharts to Chart1. drawAxes() : move to parent g, from g.axis-chart (gs,gsa) to g clip-path (gc,gca). Add 2-level .domain() - Chart1 and ChartLine, pass valueFn. Move drawContent() from Chart1 to ChartLine, call from added Chart1 .drawLine(), pass barsLine, not data. factor from drawBlockFeatures to form ensureBlockFeatures() in feature-lookup.js and ensureBlockFeatures() in block-view.js factor isBlockData check to form blockDataConfig() - likely no longer needed because DataConfig is set up based on the data source without needing to inspect the data, and passed in to setupChart(). frontend/app/ : c80fca6 9531 Jan 25 17:39 components/axis-chart.js 59b8adb 1778 Jan 23 09:11 templates/components/axis-2d.hbs 0fb0668 363 Jan 23 19:31 templates/components/axis-chart.hbs 10d2b76 28358 Jan 26 16:54 utils/draw/chart1.js 44d4b01 6547 Jan 23 19:25 utils/feature-lookup.js added : a399862 2693 Jan 24 22:02 components/draw/block-view.js 6af4c0b 111 Jan 23 09:11 templates/components/draw/block-view.hbs --- frontend/app/components/axis-chart.js | 178 +-- frontend/app/components/draw/block-view.js | 81 ++ frontend/app/templates/components/axis-2d.hbs | 4 +- .../app/templates/components/axis-chart.hbs | 9 +- .../templates/components/draw/block-view.hbs | 3 + frontend/app/utils/draw/chart1.js | 1058 +++++++++-------- frontend/app/utils/feature-lookup.js | 22 +- 7 files changed, 797 insertions(+), 558 deletions(-) create mode 100644 frontend/app/components/draw/block-view.js create mode 100644 frontend/app/templates/components/draw/block-view.hbs diff --git a/frontend/app/components/axis-chart.js b/frontend/app/components/axis-chart.js index 73a26500d..e76e6fd55 100644 --- a/frontend/app/components/axis-chart.js +++ b/frontend/app/components/axis-chart.js @@ -1,12 +1,45 @@ import Ember from 'ember'; const { inject: { service } } = Ember; -import { className, AxisCharts, layoutAndDrawChart, Chart1, DataConfig } from '../utils/draw/chart1'; +import { className, AxisCharts, setupChart, drawChart, Chart1, DataConfig, blockData, parsedData } from '../utils/draw/chart1'; /*----------------------------------------------------------------------------*/ const dLog = console.debug; +/*----------------------------------------------------------------------------*/ + +/** example element of array f : */ +const featureCountDataExample = + { + "_id": { + "min": 100, + "max": 160 + }, + "count": 109 + }; + +const featureCountDataProperties = { + dataTypeName : 'featureCountData', + datum2Location : function datum2Location(d) { return [d._id.min, d._id.max]; }, + datum2Value : function(d) { return d.count; }, + /** datum2Description() is not used; possibly intended for the same + * purpose as hoverTextFn(), so they could be assimilated. */ + datum2Description : function(d) { return JSON.stringify(d._id); }, + hoverTextFn : function (d, block) { + let valueText = '[' + d._id.min + ',' + d._id.max + '] : ' + d.count, + blockName = block.view && block.view.longName(); + return valueText + '\n' + blockName; + }, + valueIsArea : true +}; + +const dataConfigs = + [featureCountDataProperties, blockData, parsedData] + .reduce((result, properties) => { result[properties.dataTypeName] = new DataConfig(properties); return result; }, [] ); + + + /*----------------------------------------------------------------------------*/ @@ -26,13 +59,25 @@ const dLog = console.debug; * @param width resizedWidth *---------------- * data attributes created locally, not passed in : - * @param chart1 + * @param charts map of Chart1, indexed by typeName + * @param blocksData map of features, indexed by typeName, blockId, */ export default Ember.Component.extend({ blockService: service('data/block'), className : className, + /** blocks-view sets blocksData[blockId]. */ + blocksData : undefined, + /** {dataTypeName : Chart1, ... } */ + charts : undefined, + + init() { + this._super(...arguments); + this.set('blocksData', Ember.Object.create()); + this.set('charts', Ember.Object.create()); + }, + didRender() { console.log("components/axis-chart didRender()"); }, @@ -59,42 +104,43 @@ export default Ember.Component.extend({ }), axisCharts : Ember.computed(function () { - return new AxisCharts(); + return new AxisCharts(this.get('axisID')); }), - - - - blockFeatures : Ember.computed('block', 'block.features.[]', 'axis.axis1d.domainChanged', function () { - if (this.get('block.isChartable')) - this.drawBlockFeatures0(); + chartTypes : Ember.computed('blocksData.@each', function () { + let blocksData = this.get('blocksData'), + chartTypes = Object.keys(blocksData); + dLog('chartTypes', chartTypes); + return chartTypes; }), - featuresCounts : Ember.computed('block', 'block.featuresCounts.[]', 'axis.axis1d.domainChanged', function () { - let featuresCounts = this.get('block.featuresCounts'); - /* perhaps later draw both, for the moment just draw 1, and since all data - * blocks have featuresCounts, plot the features of those blocks which are - * chartable, so that we can see both capabilities are working. - */ - if (! this.get('block.isChartable')) - this.drawBlockFeaturesCounts(featuresCounts); - return featuresCounts; + + chartTypesEffect : Ember.computed('chartTypes.[]', function () { + let blocksData = this.get('blocksData'), + chartTypes = this.get('chartTypes'), + charts = this.get('charts'); + chartTypes.forEach((typeName) => { + if (! charts[typeName]) { + let data = blocksData.get(typeName), + /** same as Chart1.blockIds(). */ + blockIds = Object.keys(data), + /** may use firstBlock for isBlockData. */ + firstBlock = data[blockIds[0]], + dataConfig = dataConfigs[typeName], + parentG = this.get('axisCharts.dom.g'), // this.get('gAxis'), + chart = new Chart1(parentG, dataConfig); + charts[typeName] = chart; + let axisCharts = this.get('axisCharts'); + let blocks = this.get('blocks'); + setupChart( + this.get('axisID'), axisCharts, chart, data, + blocks, dataConfig, this.get('yAxisScale'), /*resizedWidth*/undefined); + drawChart(axisCharts, chart, data, blocks); + } + }); + return chartTypes; }), - drawBlockFeatures0 : function() { - let features = this.get('block.features'); - let domain = this.get('axis.axis1d.domainChanged'); - console.log('blockFeatures', features.length, domain); - if (features.length) // - should also handle drawing when .length changes to 0 - { - if (features.hasOwnProperty('promise')) - features = features.toArray(); - if (features[0] === undefined) - dLog('drawBlockFeatures0', features.length, domain); - else - this.drawBlockFeatures(features); - } - }, drawBlockFeaturesCounts : function(featuresCounts) { if (! featuresCounts) featuresCounts = this.get('featuresCounts'); @@ -102,58 +148,11 @@ export default Ember.Component.extend({ if (featuresCounts) { console.log('drawBlockFeaturesCounts', featuresCounts.length, domain, this.get('block.id')); - let countsChart = this.get('countsChart'); // pass alternate dataConfig to layoutAndDrawChart(), defining alternate functions for {datum2Value, datum2Location } - this.layoutAndDrawChart(featuresCounts, countsChart); + this.layoutAndDrawChart(featuresCounts, 'featureCountData'); } }, - countsChart : Ember.computed(function() { - /** example element of array f : */ - const dataExample = - { - "_id": { - "min": 100, - "max": 160 - }, - "count": 109 - }; - let - featureCountDataProperties = { - dataTypeName : 'featureCountData', - datum2Location : function datum2Location(d) { return [d._id.min, d._id.max]; }, - datum2Value : function(d) { return d.count; }, - /** datum2Description() is not used; possibly intended for the same - * purpose as hoverTextFn(), so they could be assimilated. */ - datum2Description : function(d) { return JSON.stringify(d._id); }, - hoverTextFn : function (d, block) { - let valueText = '[' + d._id.min + ',' + d._id.max + '] : ' + d.count, - blockName = block.view && block.view.longName(); - return valueText + '\n' + blockName; - }, - valueIsArea : true - }, - featureCountData = new DataConfig(featureCountDataProperties); - let countsChart = new Chart1(this.get('axisCharts.dom.gAxis'), featureCountData); - return countsChart; - }), - drawBlockFeatures : function(features) { - let f = features.toArray(), - fa = f.map(function (f0) { return f0._internalModel.__data;}); - - let axisID = this.get("axisID"), - oa = this.get('data'), - za = oa.z[axisID]; - /* if za has not been populated with features, it will have just .dataset - * and .scope, i.e. .length === 2 */ - if (Object.keys(za).length == 2) { - dLog('drawBlockFeatures()', axisID, za, fa); - // add features to za. - fa.forEach((f) => za[f.name] = f.value); - } - - this.layoutAndDrawChart(fa); - }, redraw : function(axisID, t) { let data = this.get(className), @@ -161,7 +160,7 @@ export default Ember.Component.extend({ if (data) { console.log("redraw", this, (data === undefined) || data.length, axisID, t); if (data) - layoutAndDrawChart.apply(this, [data]); + layoutAndDrawChart.apply(this, [data, undefined]); } else { // use block.features or block.featuresCounts when not using data parsed from table. if (this.get('block.isChartable')) @@ -235,15 +234,20 @@ export default Ember.Component.extend({ return result; }, - layoutAndDrawChart(chartData, chart1) { + layoutAndDrawChart(chartData, dataTypeName) { let axisID = this.get("axisID"), axisCharts = this.get('axisCharts'), - block = this.get('block'), + blocks = this.get('blocks'), yAxisScale = this.get('yAxisScale'), - resizedWidth = this.get('width'); - chart1 = layoutAndDrawChart( - axisID, axisCharts, chart1, chartData, block, /*dataConfig*/undefined, yAxisScale, resizedWidth); + dataConfig = dataConfigs[dataTypeName], + resizedWidth = this.get('width'), + chart1 = this.get('charts')[dataTypeName]; + chart1 = setupChart( + axisID, axisCharts, chart1, chartData, blocks, dataConfig, yAxisScale, resizedWidth); + drawChart(axisCharts, chart1, chartData, blocks); + if (! this.get('charts') && chart1) + this.set('charts', chart1); }, @@ -265,7 +269,7 @@ export default Ember.Component.extend({ let forTable = chart; chart.valueName = "values"; // add user config // ; draw chart. - layoutAndDrawChart.apply(this, [chart]); + layoutAndDrawChart.apply(this, [chart, 'parsedData']); this.set('data.chart', forTable); }, diff --git a/frontend/app/components/draw/block-view.js b/frontend/app/components/draw/block-view.js new file mode 100644 index 000000000..8b4a204f2 --- /dev/null +++ b/frontend/app/components/draw/block-view.js @@ -0,0 +1,81 @@ +import Ember from 'ember'; +const { inject: { service } } = Ember; + +import { ensureBlockFeatures } from '../../utils/feature-lookup'; + +/*----------------------------------------------------------------------------*/ + +const dLog = console.debug; + +/*----------------------------------------------------------------------------*/ + + +/** + * @param block a block returned by viewedChartable() + * @param axis axisComponent; parent axis-2d component + * @param axisID axisID + * @param featuresByBlock + * @param blocksData map to receive feature data + */ +export default Ember.Component.extend({ + blockService: service('data/block'), + + setBlockFeaturesData(dataTypeName, featuresData){ + let blocksData = this.get('blocksData'), + typeData = blocksData[dataTypeName] || (blocksData[dataTypeName] = {}), + blockId = this.get('block.id'); + typeData[blockId] = featuresData; + }, + + blockFeatures : Ember.computed('block', 'block.features.[]', 'axis.axis1d.domainChanged', function () { + if (this.get('block.isChartable')) { + let features = this.get('block.features'); + let domain = this.get('axis.axis1d.domainChanged'); + console.log('blockFeatures', features.length, domain); + if (features.length) // - should also handle drawing when .length changes to 0 + { + if (features.hasOwnProperty('promise')) + features = features.toArray(); + if (features[0] === undefined) + dLog('blockFeatures', features.length, domain); + else { + let + // f = features.toArray(), + featuresA = features.map(function (f0) { return f0._internalModel.__data;}); + this.ensureBlockFeatures(featuresA); + this.setBlockFeaturesData('blockData', featuresA); + // previous : .drawBlockFeatures0() -> drawBlockFeatures(); + } + } + } + }), + + featuresCounts : Ember.computed('block', 'block.featuresCounts.[]', 'axis.axis1d.domainChanged', function () { + let featuresCounts = this.get('block.featuresCounts'); + if (featuresCounts && featuresCounts.length) + this.setBlockFeaturesData('featureCountData', featuresCounts); + // previous : .drawBlockFeaturesCounts(featuresCounts); + + return featuresCounts; + }), + +// - results -> blocksData + +/** add features to featuresByBlock (oa.z) + * @param features array of just the feature attributes, without the relation to the parent block. + */ + ensureBlockFeatures : function(features) { + let + axisID = this.get("axisID"); + + ensureBlockFeatures(this.get('block.id'), features); + + // this.layoutAndDrawChart(fa, 'blockData'); + } + + + /*--------------------------------------------------------------------------*/ + + +}); + diff --git a/frontend/app/templates/components/axis-2d.hbs b/frontend/app/templates/components/axis-2d.hbs index 976e4ec28..0bf03c28d 100644 --- a/frontend/app/templates/components/axis-2d.hbs +++ b/frontend/app/templates/components/axis-2d.hbs @@ -35,9 +35,7 @@
axis-2d :{{this}}, {{axisID}}, {{targetEltId}}, {{subComponents.length}} :
subComponents : - {{#each viewedChartable as |chartBlock|}} - {{axis-chart data=data axis=this axisID=axisID block=chartBlock}} - {{/each}} + {{axis-chart data=data axis=this axisID=axisID blocks=viewedChartable}} {{log 'blockService' blockService 'viewedChartable' blockService.viewedChartable}} {{axis-tracks axis=this axisID=axisID trackBlocksR=dataBlocks}} {{#each subComponents as |subComponent|}} diff --git a/frontend/app/templates/components/axis-chart.hbs b/frontend/app/templates/components/axis-chart.hbs index bff59611d..3f02d65ad 100644 --- a/frontend/app/templates/components/axis-chart.hbs +++ b/frontend/app/templates/components/axis-chart.hbs @@ -1,7 +1,8 @@ -{{#if block}} - {{log 'blockFeatures' blockFeatures.length}} - {{blockFeatures.length}} - featuresCounts {{featuresCounts.length}} +{{#if blocks}} + {{#each blocks as |chartBlock|}} + {{draw/block-view axis=axis axisID=axisID block=chartBlock blocksData=blocksData}} + {{/each}} + {{log 'chartTypesEffect' chartTypesEffect }} {{else}}
{{content-editable diff --git a/frontend/app/templates/components/draw/block-view.hbs b/frontend/app/templates/components/draw/block-view.hbs new file mode 100644 index 000000000..c6afb5a7a --- /dev/null +++ b/frontend/app/templates/components/draw/block-view.hbs @@ -0,0 +1,3 @@ +{{log 'blockFeatures' blockFeatures.length}} +{{blockFeatures.length}} +featuresCounts {{featuresCounts.length}} diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index ed59614a2..ffcfd5ddc 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -45,7 +45,7 @@ function middle(location) { : location; return result; } - + /** @return a function to map a chart datum to a y value or interval. */ function scaleMaybeInterval(datum2Location, yScale) { @@ -102,10 +102,10 @@ function addParentClass(g) { class DataConfig { /* - dataTypeName; - datum2Location; - datum2Value; - datum2Description; + dataTypeName; + datum2Location; + datum2Value; + datum2Description; */ constructor (properties) { if (properties) @@ -113,85 +113,99 @@ class DataConfig { } }; +class ChartLine { + constructor(g, dataConfig, scales) { + this.g = g; + this.dataConfig = dataConfig; + this.scales = scales; + } +} -/** @param name is a feature or gene name */ -function name2Location(name, blockId) -{ - /** @param ak1 axis name, (exists in axisIDs[]) - * @param d1 feature name, i.e. ak1:d1 - */ - return featureLocation(blockId, name); +/*----------------------------------------------------------------------------*/ + + /** @param name is a feature or gene name */ + function name2Location(name, blockId) + { + /** @param ak1 axis name, (exists in axisIDs[]) + * @param d1 feature name, i.e. ak1:d1 + */ + return featureLocation(blockId, name); + } + + /** Used for both blockData and parsedData. */ + function datum2LocationWithBlock(d, blockId) { return name2Location(d.name, blockId); } + function datum2Value(d) { return d.value; } + let parsedData = { + dataTypeName : 'parsedData', + // datum2LocationWithBlock assigned later, + datum2Value : datum2Value, + datum2Description : function(d) { return d.description; } + }, + blockData = { + dataTypeName : 'blockData', + // datum2LocationWithBlock assigned later, + /** The effects data is placed in .value[2] (the interval is in value[0..1]). + * Use the first effects value by default, but later will combine other values. + */ + datum2Value : function(d) { let v = d.value[2]; if (v.length) v = v[0]; return v; }, + datum2Description : function(d) { return JSON.stringify(d.value); } + }; + +/** Determine the appropriate DataConfig for the given data. + */ +function blockDataConfig(chart) { + let + isBlockData = chart.length && (chart[0].description === undefined); + + let dataConfigProperties = isBlockData ? blockData : parsedData; + return dataConfigProperties; } -/** Used for both blockData and parsedData. */ -function datum2LocationWithBlock(d, blockId) { return name2Location(d.name, blockId); } -function datum2Value(d) { return d.value; } -let parsedData = { - dataTypeName : 'parsedData', - // datum2LocationWithBlock assigned later, - datum2Value : datum2Value, - datum2Description : function(d) { return d.description; } -}, -blockData = { - dataTypeName : 'blockData', - // datum2LocationWithBlock assigned later, - /** The effects data is placed in .value[2] (the interval is in value[0..1]). - * Use the first effects value by default, but later will combine other values. - */ - datum2Value : function(d) { let v = d.value[2]; if (v.length) v = v[0]; return v; }, - datum2Description : function(d) { return JSON.stringify(d.value); } -}; + +/*----------------------------------------------------------------------------*/ + /*----------------------------------------------------------------------------*/ class AxisCharts { /* - dataConfig; // DataConfig (options) - ranges; // .bbox, .height - dom; // .gs, .gsa, + ranges; // .bbox, .height + dom; // .gs, .gsa, */ - constructor(axisID, dataConfig) { + constructor(axisID) { this.axisID = axisID; - this.dataConfig = dataConfig; this.ranges = { }; this.dom = { }; // this will move to Chart1. this.scales = { /* x, y, yLine */ }; } + }; + +AxisCharts.prototype.setup = function(axisID, yAxisScale) { + this.selectParentContainer(axisID); + this.getBBox(); + this.scales.yAxis = yAxisScale; }; /** * @param yAxisScale */ -function layoutAndDrawChart(axisID, axisCharts, chart1, chartData, block, dataConfig, yAxisScale, resizedWidth) +function setupChart(axisID, axisCharts, chart1, chartData, blocks, dataConfig, yAxisScale, resizedWidth) { - - axisCharts.selectParentContainer(axisID); - axisCharts.getBBox(); - axisCharts.scales.yAxis = yAxisScale; - - if (! chart1) - chart1 = new Chart1(axisCharts.dom.g, dataConfig); + axisCharts.setup(axisID, yAxisScale); // Plan is for Axischarts to own .ranges and Chart1 to own .scales, but for now there is some overlap. - if (chart1 && ! chart1.ranges) { + if (! chart1.ranges) { chart1.ranges = axisCharts.ranges; chart1.scales = axisCharts.scales; + chart1.dom = axisCharts.dom; } - /* dataConfig may be set up (for featuresCounts) by countsChart(), - * or (for blockData and parsedData) by .getRanges(). - * In coming commits the latter will be factored out so that DataConfig it is - * uniformly passed in to Chart1(). This copying of .dataConfig is just provisional. - */ - if (! dataConfig && ! axisCharts.dataConfig && chart1 && chart1.dataConfig) - axisCharts.dataConfig = chart1.dataConfig; - axisCharts.getRanges(chartData, block.get('id'), resizedWidth); - if (! dataConfig && chart1 && ! chart1.dataConfig && axisCharts.dataConfig) - chart1.dataConfig = axisCharts.dataConfig; - axisCharts.getRanges3(chartData, resizedWidth); + chart1.getRanges(axisCharts.ranges, chartData); + + axisCharts.getRanges3(resizedWidth); // axisCharts.size(this.get('yAxisScale'), /*axisID, gAxis,*/ chartData, resizedWidth); // - split size/width and data/draw @@ -199,402 +213,522 @@ function layoutAndDrawChart(axisID, axisCharts, chart1, chartData, block, dataCo addParentClass(axisCharts.dom.gc); - if (dataConfig) - axisCharts.configure(dataConfig); axisCharts.controls(chart1); - // following are from b.draw(data) axisCharts.getRanges2(); - chart1.prepareScales(chartData, axisCharts.ranges.drawSize); - chart1.g = - axisCharts.group(axisCharts.dom.gc, 'axis-chart'); - if (showChartAxes) - axisCharts.drawAxes(chartData); - chart1.block = block; // used in Chart1:bars() for hover text. - chart1.data(chartData); + axisCharts.group(axisCharts.dom.gc, 'axis-chart'); + + return chart1; +}; +function drawChart(axisCharts, chart1, chartData, blocks) +{ + /** possibly don't (yet) have chartData for each of blocks[], + * i.e. blocksById may be a subset of blocks.mapBy('id'). + */ + let blockIds = Object.keys(chartData), + blocksById = blocks.reduce( + (result, block) => { result[block.get('id')] = block; return result; }, []); + blockIds.forEach((blockId) => { + let block = blocksById[blockId]; + chart1.data(blockId, block, chartData[blockId]); + }); + chart1.prepareScales(chartData, axisCharts.ranges.drawSize); + blockIds.forEach((blockId) => { + chart1.chartLines[blockId].scaledConfig(); } ); - return chart1; + if (showChartAxes) + chart1.drawAxes(chartData); + + chart1.drawContent(); }; AxisCharts.prototype.selectParentContainer = function (axisID) -{ + { this.axisID = axisID; - // to support multiple split axes, identify axis by axisID (and possibly stack id) - // - // g.axis-outer#id - let gAxis = d3.select("g.axis-outer#id" + axisID + "> g.axis-use"); - if (! gAxis.node()) { /* probably because axisShowExtend() has not added the g.axis-use yet - will sort out the dependencies ... . */ - dLog('layoutAndDrawChart', d3.select("g.axis-outer#id" + axisID).node(), 'no g.axis-use', this, axisID); - } + // to support multiple split axes, identify axis by axisID (and possibly stack id) + // + // g.axis-outer#id + let gAxis = d3.select("g.axis-outer#id" + axisID + "> g.axis-use"); + if (! gAxis.node()) { /* probably because axisShowExtend() has not added the g.axis-use yet - will sort out the dependencies ... . */ + dLog('layoutAndDrawChart', d3.select("g.axis-outer#id" + axisID).node(), 'no g.axis-use', this, axisID); + } this.dom.gAxis = gAxis; return gAxis; }; AxisCharts.prototype.getBBox = function () -{ - let axisID = this.axisID, - gAxis = this.dom.gAxis; - let + { + let + gAxis = this.dom.gAxis; + let /** relative to the transform of parent g.axis-outer */ bbox = gAxis.node().getBBox(), - yrange = [bbox.y, bbox.height]; - if (bbox.x < 0) - { - console.log("x < 0", bbox); - bbox.x = 0; - } - this.ranges.bbox = bbox; - this.ranges.yrange = yrange; + yrange = [bbox.y, bbox.height]; + if (bbox.x < 0) + { + console.log("x < 0", bbox); + bbox.x = 0; + } + this.ranges.bbox = bbox; + this.ranges.yrange = yrange; }; -AxisCharts.prototype.getRanges = function (chart, blockId, resizedWidth) { +Chart1.prototype.getRanges = function (ranges, chartData) { let - {yrange } = this.ranges, - {yAxis } = this.scales, - /** isBlockData is not used if dataConfig is defined. this can be moved out to the caller. */ - isBlockData = chart.length && (chart[0].description === undefined), - // axisID = gAxis.node().parentElement.__data__, - - yAxisDomain = yAxis.domain(), yDomain; - if (noDomain(yAxisDomain) && chart.length) { - yAxisDomain = [chart[0]._id.min, chart[chart.length-1]._id.max]; - yAxis.domain(yAxisDomain); - yDomain = yAxisDomain; - } - else - yDomain = [yAxis.invert(yrange[0]), yAxis.invert(yrange[1])]; + {yrange } = ranges, + {yAxis } = this.scales, + // if needed, use the first data array to calculate domain + chart = Object.values(chartData), + // axisID = gAxis.node().parentElement.__data__, + + yAxisDomain = yAxis.domain(), yDomain; + if (chart) + chart = chart[0]; + if (noDomain(yAxisDomain) && chart.length) { + // this assumes featureCountData; don't expect to need this. + yAxisDomain = [chart[0]._id.min, chart[chart.length-1]._id.max]; + yAxis.domain(yAxisDomain); + yDomain = yAxisDomain; + } + else + yDomain = [yAxis.invert(yrange[0]), yAxis.invert(yrange[1])]; - let dataConfig = this.dataConfig; - if (! dataConfig) { - let dataConfigProperties = isBlockData ? blockData : parsedData; - dataConfigProperties.datum2Location = + let dataConfig = this.dataConfig; + if (! dataConfig.hoverTextFn) + dataConfig.hoverTextFn = hoverTextFn; + if (dataConfig.valueIsArea === undefined) + dataConfig.valueIsArea = false; + + if (! dataConfig.barClassName) + dataConfig.barClassName = classNameSub; + if (! dataConfig.valueName) + dataConfig.valueName = chart.valueName || "Values"; + + ranges.pxSize = (yDomain[1] - yDomain[0]) / ranges.bbox.height; +}; +ChartLine.prototype.setup = function(blockId) { + /* Some types (blockData, parsedData) require a block to lookup the feature + * name for location. They are denoted by .datum2Location not being in their + * pre-defined config. + */ + if (! this.dataConfig.datum2Location) { + // copy dataConfig to give a custom value to this ChartLine. + let d = new DataConfig(this.dataConfig); + d.datum2Location = (d) => datum2LocationWithBlock(d, blockId); - this.dataConfig = dataConfig = - new DataConfig(dataConfigProperties); - if (! dataConfig.hoverTextFn) - dataConfig.hoverTextFn = hoverTextFn; - if (dataConfig.valueIsArea === undefined) - dataConfig.valueIsArea = false; + this.dataConfig = d; } - if (dataConfig) { - if (! dataConfig.barClassName) - dataConfig.barClassName = classNameSub; - if (! dataConfig.valueName) - dataConfig.valueName = chart.valueName || "Values"; - } - - this.ranges.pxSize = (yDomain[1] - yDomain[0]) / this.ranges.bbox.height; }; -AxisCharts.prototype.getRanges3 = function (chart, resizedWidth) { +/** Filter given data according to this.scales.yAxis.domain() + * and set .currentData + */ +ChartLine.prototype.filterToZoom = function(chart) { + let + {yAxis} = this.scales, + yDomain = yAxis.domain(), + withinZoomRegion = (d) => { + return inRangeEither(this.dataConfig.datum2Location(d), yDomain); + }, + data = chart.filter(withinZoomRegion); + this.currentData = data; + + dLog(yDomain, data.length, (data.length == 0) || this.dataConfig.datum2Location(data[0])); + return data; +}; - let - {bbox} = this.ranges, - {yAxis} = this.scales, - yDomain = yAxis.domain(), - withinZoomRegion = (d) => { - return inRangeEither(this.dataConfig.datum2Location(d), yDomain); - }, - data = chart.filter(withinZoomRegion); - - console.log(resizedWidth, bbox, yDomain, data.length, (data.length == 0) || this.dataConfig.datum2Location(data[0])); - if (resizedWidth) +AxisCharts.prototype.getRanges3 = function (resizedWidth) { + if (resizedWidth) { + let + {bbox} = this.ranges; bbox.width = resizedWidth; + dLog('resizedWidth', resizedWidth, bbox); + } }; - -/* axis - * x .value - * y .name Location - */ -/** 1-dimensional chart, within an axis. */ -function Chart1(parentG, dataConfig) -{ - this.parentG = parentG; - this.dataConfig = dataConfig; -} -Chart1.prototype.barsLine = true; -AxisCharts.prototype.getRanges2 = function () -{ - // based on https://bl.ocks.org/mbostock/3885304, axes x & y swapped. - let - // parentG = this.parentG, - bbox = this.ranges.bbox, - margin = showChartAxes ? - {top: 10, right: 20, bottom: 40, left: 20} : - {top: 0, right: 0, bottom: 0, left: 0}, - // pp=parentG.node().parentElement, - parentW = bbox.width, // +pp.attr("width") - parentH = bbox.height, // +pp.attr("height") - width = parentW - margin.left - margin.right, - height = parentH - margin.top - margin.bottom; - this.ranges.drawSize = {width, height}; - dLog('getRanges2', parentW, parentH, this.ranges.drawSize); -}; -Chart1.prototype.prepareScales = function (data, drawSize) -{ - /** The chart is perpendicular to the usual presentation. - * The names x & y (in {x,y}Range and yLine) match the orientation on the - * screen, rather than the role of abscissa / ordinate; the data maps - * .name (location) -> .value, which is named y -> x. - */ - let - dataConfig = this.dataConfig, - width = drawSize.width, - height = drawSize.height, - xRange = [0, width], - yRange = [0, height], - // yRange is used as range of yLine by this.scaleLinear(). - /* scaleBand would suit a data set with evenly spaced or ordinal / nominal y values. - * yBand = d3.scaleBand().rangeRound(yRange).padding(0.1), - */ - y = this.scaleLinear(yRange, data), - x = d3.scaleLinear().rangeRound(xRange); - // datum2LocationScaled() uses me.scales.x rather than the value in the closure in which it was created. - this.scales.x = x; - // Used by bars() - could be moved there, along with datum2LocationScaled(). - this.scales.y = y; - console.log("Chart1", xRange, yRange, dataConfig.dataTypeName); - - let me = this; - /* these can be renamed datum2{abscissa,ordinate}{,Scaled}() */ - /* apply y after scale applied by datum2Location */ - let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, me.scales.y); - /** related @see rectWidth(). */ - function datum2ValueScaled(d) { return me.scales.x(dataConfig.datum2Value(d)); } - dataConfig.datum2LocationScaled = datum2LocationScaled; - dataConfig.datum2ValueScaled = datum2ValueScaled; -}; + + /* axis + * x .value + * y .name Location + */ + /** 1-dimensional chart, within an axis. */ + function Chart1(parentG, dataConfig) + { + this.parentG = parentG; + this.dataConfig = dataConfig; + this.chartLines = {}; + } + Chart1.prototype.barsLine = true; + + + + AxisCharts.prototype.getRanges2 = function () + { + // based on https://bl.ocks.org/mbostock/3885304, axes x & y swapped. + let + // parentG = this.parentG, + bbox = this.ranges.bbox, + margin = showChartAxes ? + {top: 10, right: 20, bottom: 40, left: 20} : + {top: 0, right: 0, bottom: 0, left: 0}, + // pp=parentG.node().parentElement, + parentW = bbox.width, // +pp.attr("width") + parentH = bbox.height, // +pp.attr("height") + width = parentW - margin.left - margin.right, + height = parentH - margin.top - margin.bottom; + this.ranges.drawSize = {width, height}; + dLog('getRanges2', parentW, parentH, this.ranges.drawSize); + }; + Chart1.prototype.prepareScales = function (data, drawSize) + { + /** The chart is perpendicular to the usual presentation. + * The names x & y (in {x,y}Range and yLine) match the orientation on the + * screen, rather than the role of abscissa / ordinate; the data maps + * .name (location) -> .value, which is named y -> x. + */ + let + dataConfig = this.dataConfig, + width = drawSize.width, + height = drawSize.height, + xRange = [0, width], + yRange = [0, height], + // yRange is used as range of yLine by this.scaleLinear(). + /* scaleBand would suit a data set with evenly spaced or ordinal / nominal y values. + * yBand = d3.scaleBand().rangeRound(yRange).padding(0.1), + */ + y = this.scaleLinear(yRange, data), + x = d3.scaleLinear().rangeRound(xRange); + // datum2LocationScaled() uses me.scales.x rather than the value in the closure in which it was created. + this.scales.x = x; + // Used by bars() - could be moved there, along with datum2LocationScaled(). + this.scales.y = y; + console.log("Chart1", xRange, yRange, dataConfig.dataTypeName); + + let + valueWidthFn = dataConfig.rectWidth.bind(dataConfig, /*scaled*/false, /*gIsData*/true), + valueCombinedDomain = this.domain(valueWidthFn, data); + x.domain(valueCombinedDomain); + + }; + Chart1.prototype.drawContent = function () + { + Object.keys(this.chartLines).forEach((blockId) => { + let chartLine = this.chartLines[blockId]; + chartLine.drawContent(this.barsLine); + }); + }; + /** Enables use of the scales which are set up in .prepareScales(). + */ + ChartLine.prototype.scaledConfig = function () + { + let + dataConfig = this.dataConfig, + scales = this.scales; + + /* these can be renamed datum2{abscissa,ordinate}{,Scaled}() */ + /* apply y after scale applied by datum2Location */ + let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, scales.y); + /** related @see rectWidth(). */ + function datum2ValueScaled(d) { return scales.x(dataConfig.datum2Value(d)); } + dataConfig.datum2LocationScaled = datum2LocationScaled; + dataConfig.datum2ValueScaled = datum2ValueScaled; + }; AxisCharts.prototype.group = function (parentG, groupClassName) { - /** parentG is g.axis-use. add g.(groupClassName); - * within parentG there is also a sibling g.axis-html. */ - let data = parentG.data(), - gs = parentG - .selectAll("g > g." + groupClassName) - .data(data), // inherit g.datum(), or perhaps [groupClassName] - gsa = gs - .enter() - .append("g") // maybe drop this g, move margin calc to gp - // if drawing internal chart axes then move them inside the clip rect - // .attr("transform", "translate(" + margin.left + "," + margin.top + ")"), - .attr("class", groupClassName), - g = gsa.merge(gs); - dLog('group', this, parentG, g.node()); - this.dom.g = g; - this.dom.gs = gs; - this.dom.gsa = gsa; + /** parentG is g.axis-use. add g.(groupClassName); + * within parentG there is also a sibling g.axis-html. */ + let data = parentG.data(), + gs = parentG + .selectAll("g > g." + groupClassName) + .data(data), // inherit g.datum(), or perhaps [groupClassName] + gsa = gs + .enter() + .append("g") // maybe drop this g, move margin calc to gp + // if drawing internal chart axes then move them inside the clip rect + // .attr("transform", "translate(" + margin.left + "," + margin.top + ")"), + .attr("class", groupClassName), + g = gsa.merge(gs); + dLog('group', this, parentG, g.node()); + this.dom.g = g; + this.dom.gs = gs; + this.dom.gsa = gsa; return g; }; -AxisCharts.prototype.drawAxes = function (data) { - - /** first draft showed all data; subsequently adding : - * + select region from y domain - * - place data in tree for fast subset by region - * + alternate view : line - * + transition between views, zoom, stack - */ - // scaleBand() domain is a list of all y values. - // yBand.domain(data.map(dataConfig.datum2Location)); +Chart1.prototype.drawAxes = function (data) { - let - {height} = this.ranges.drawSize, - {x, y} = this.scales, - {gs, gsa} = this.dom, - dataConfig = this.dataConfig; - - x.domain([0, d3.max(data, dataConfig.rectWidth.bind(dataConfig, /*scaled*/false, /*gIsData*/false))]); - - let axisXa = - gsa.append("g") - // - handle .exit() for these 2 also - .attr("class", "axis axis--x"); - axisXa.merge(gs.selectAll("g > g.axis--x")) - .attr("transform", "translate(0," + height + ")") - .call(d3.axisBottom(x)); - - let axisYa = - gsa.append("g") - .attr("class", "axis axis--y"); - axisYa.merge(gs.selectAll("g > g.axis--y")) - .call(d3.axisLeft(y)) // .tickFormat(".2f") ? - // can option this out if !valueName - .append("text") - .attr("transform", "rotate(-90)") - .attr("y", 6) - .attr("dy", "0.71em") - .attr("text-anchor", "end") - .text(dataConfig.valueName); -}; -Chart1.prototype.data = function (data) -{ - let - dataConfig = this.dataConfig; - data = data.sort((a,b) => middle(dataConfig.datum2Location(a)) - middle(dataConfig.datum2Location(b))); - this.drawContent(data); - this.currentData = data; -}; -Chart1.prototype.bars = function (data) -{ - let - dataConfig = this.dataConfig, - block = this.block, - g = this.g; - let - rs = g - // .select("g." + className + " > g") - .selectAll("rect." + dataConfig.barClassName) - .data(data), - re = rs.enter(), rx = rs.exit(); - let ra = re - .append("rect"); - ra - .attr("class", dataConfig.barClassName) - /** parent datum is currently 1, but could be this.block; - * this.parentElement.parentElement.__data__ has the axis id (not the blockId), - */ - .each(function (d) { configureHorizTickHover.apply(this, [d, block, dataConfig.hoverTextFn]); }); - ra - .merge(rs) - .transition().duration(1500) - .attr("x", 0) - .attr("y", (d) => { let li = dataConfig.datum2LocationScaled(d); return li.length ? li[0] : li; }) - // yBand.bandwidth() - .attr("height", dataConfig.rectHeight.bind(dataConfig, /*gIsData*/false)) // equiv : (d, i, g) => dataConfig.rectHeight(false, d, i, g) - .attr("width", dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false)); - rx.remove(); - console.log(rs.nodes(), re.nodes()); + /** first draft showed all data; subsequently adding : + * + select region from y domain + * - place data in tree for fast subset by region + * + alternate view : line + * + transition between views, zoom, stack + */ + // scaleBand() domain is a list of all y values. + // yBand.domain(data.map(dataConfig.datum2Location)); + + let + {height} = this.ranges.drawSize, + {x, y} = this.scales, + dom = this.dom, + /** the axes were originally within the gs,gsa of .group(); hence the var names. + * gs selects the into which the axes will be inserted, and gpa is the + * .enter().append() of that selection. + */ + gs = dom.gc, + gsa = dom.gca, + dataConfig = this.dataConfig; + + let axisXa = + gsa.append("g") + // - handle .exit() for these 2 also + .attr("class", "axis axis--x"); + axisXa.merge(gs.selectAll("g > g.axis--x")) + .attr("transform", "translate(0," + height + ")") + .call(d3.axisBottom(x)); + + let axisYa = + gsa.append("g") + .attr("class", "axis axis--y"); + axisYa.merge(gs.selectAll("g > g.axis--y")) + .call(d3.axisLeft(y)) // .tickFormat(".2f") ? + // can option this out if !valueName + .append("text") + .attr("transform", "rotate(-90)") + .attr("y", 6) + .attr("dy", "0.71em") + .attr("text-anchor", "end") + .text(dataConfig.valueName); }; -/** Calculate the height of rectangle to be used for this data point - * @param this is DataConfig, not DOM element. - * @param scaled true means apply scale (x) to the result - * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. - * gIsData will be true when called from d3.max(), and false for d3 attr functions. +/** + * @param block hover + * used in Chart1:bars() for hover text. passed to hoverTextFn() for .longName() */ -DataConfig.prototype.rectWidth = function (scaled, gIsData, d, i, g) +Chart1.prototype.data = function (blockId, block, data) { - Ember.assert('rectWidth arguments.length === 5', arguments.length === 5); - /** The scale is linear, so it is OK to scale before dividing by rectHeight. - * Otherwise could use datum2Value() here and apply this.x() before the return. - */ - let d2v = (scaled ? this.datum2ValueScaled : this.datum2Value), - width = d2v(d); - if (this.valueIsArea) { - let h; - width /= (h = this.rectHeight(gIsData, d, i, g)); - // dLog('rectWidth', h, width, gIsData); + let chartLine = this.chartLines[blockId]; + if (! chartLine) + chartLine = this.chartLines[blockId] = new ChartLine(this.g, this.dataConfig, this.scales); + if (block) { + chartLine.block = block; + chartLine.setup(blockId); + // .setup() will copy dataConfig if need for custom config. } - return width; + function m(d) { return middle(chartLine.dataConfig.datum2Location(d)); } + data = data.sort((a,b) => m(a) - m(b)); + data = chartLine.filterToZoom(data); }; -/** Calculate the height of rectangle to be used for this data point - * @param this is DataConfig, not DOM element. - * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. - * gIsData will be true when called from d3.max(), and false for d3 attr functions. +/** Draw a single ChartLine of this chart. + * To draw all ChartLine of this chart, @see Chart1:drawContent() */ -DataConfig.prototype.rectHeight = function (gIsData, d, i, g) +Chart1.prototype.drawLine = function (blockId, block, data) { - Ember.assert('rectHeight arguments.length === 4', arguments.length === 4); - let height, locationScaled; - /* if locationScaled is an interval, calculate height from it. - * Otherwise, use adjacent points to indicate height. - */ - if ((locationScaled = this.datum2LocationScaled(d)).length) { - height = Math.abs(locationScaled[1] - locationScaled[0]); - } - else { - /* the boundary between 2 adjacent points is midway between them. - * So sum that half-distance for the previous and next points. - * the y range distance from the previous point to the next point. - * If this point is either end, simply double the other half-distance. + let chartLine = this.chartLines[blockId]; + chartLine.drawContent(this.barsLine); +}; + ChartLine.prototype.bars = function (data) + { + let + dataConfig = this.dataConfig, + block = this.block, + g = this.g; + let + rs = g + // .select("g." + className + " > g") + .selectAll("rect." + dataConfig.barClassName) + .data(data), + re = rs.enter(), rx = rs.exit(); + let ra = re + .append("rect"); + ra + .attr("class", dataConfig.barClassName) + /** parent datum is currently 1, but could be this.block; + * this.parentElement.parentElement.__data__ has the axis id (not the blockId), + */ + .each(function (d) { configureHorizTickHover.apply(this, [d, block, dataConfig.hoverTextFn]); }); + ra + .merge(rs) + .transition().duration(1500) + .attr("x", 0) + .attr("y", (d) => { let li = dataConfig.datum2LocationScaled(d); return li.length ? li[0] : li; }) + // yBand.bandwidth() + .attr("height", dataConfig.rectHeight.bind(dataConfig, /*scaled*/true, /*gIsData*/false)) // equiv : (d, i, g) => dataConfig.rectHeight(true, false, d, i, g) + .attr("width", dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false)); + rx.remove(); + console.log(rs.nodes(), re.nodes()); + }; + /** Calculate the height of rectangle to be used for this data point + * @param this is DataConfig, not DOM element. + * @param scaled true means apply scale (x) to the result + * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. + * gIsData will be true when called from d3.max(), and false for d3 attr functions. */ - if (! g.length) - /* constant value OK - don't expect to be called if g.length is 0. - * this.scales.y.range() / 10; + DataConfig.prototype.rectWidth = function (scaled, gIsData, d, i, g) + { + Ember.assert('rectWidth arguments.length === 5', arguments.length === 5); + /** The scale is linear, so it is OK to scale before dividing by rectHeight. + * Otherwise could use datum2Value() here and apply this.x() before the return. */ - height = 10; - else { - let r = []; - function gData(i) { let gi = g[i]; return gIsData ? gi : gi.__data__; }; - if (i > 0) - r.push(gData(i)); - r.push(d); - if (i < g.length-1) - r.push(gData(i+1)); - let y = - r.map(this.datum2LocationScaled); - height = Math.abs(y[y.length-1] - y[0]) * 2 / (y.length-1); - dLog('rectHeight', gIsData, d, i, /*g,*/ r, y, height); - if (! height) - height = 1; - } - } - return height; -}; -Chart1.prototype.scaleLinear = function (yRange, data) -{ - // based on https://bl.ocks.org/mbostock/3883245 - if (! this.scales.yLine) - this.scales.yLine = d3.scaleLinear(); - let y = this.scales.yLine; - y.rangeRound(yRange); - let - /** location may be an interval, so flatten the result. - * Later Array.flat() can be used. + let d2v = (scaled ? this.datum2ValueScaled : this.datum2Value), + width = d2v(d); + if (this.valueIsArea) { + let h; + width /= (h = this.rectHeight(scaled, gIsData, d, i, g)); + // dLog('rectWidth', h, width, gIsData); + } + return width; + }; + /** Calculate the height of rectangle to be used for this data point + * @param this is DataConfig, not DOM element. + * @param scaled true means apply scale (y) to the result + * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. + * gIsData will be true when called from d3.max(), and false for d3 attr functions. */ - yFlat = data - .map(this.dataConfig.datum2Location) - .reduce((acc, val) => acc.concat(val), []); - y.domain(d3.extent(yFlat)); - console.log('scaleLinear domain', y.domain(), yFlat); - return y; -}; -Chart1.prototype.line = function (data) -{ - let y = this.scales.yLine, dataConfig = this.dataConfig; - - let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, y); - let line = d3.line() - .x(dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false)) - .y((d) => middle(datum2LocationScaled(d))); - - console.log("line x domain", this.scales.x.domain(), this.scales.x.range()); - - let - g = this.g, - ps = g - .selectAll("g > path." + dataConfig.barClassName) - .data([1]); - ps - .enter() - .append("path") - .attr("class", dataConfig.barClassName + " line") - .datum(data) - .attr("d", line) - .merge(ps) - .datum(data) - .transition().duration(1500) - .attr("d", line); - // data length is constant 1, so .remove() is not needed - ps.exit().remove(); -}; -/** Alternate between bar chart and line chart */ -Chart1.prototype.toggleBarsLine = function () -{ - console.log("toggleBarsLine", this); - d3.event.stopPropagation(); - this.barsLine = ! this.barsLine; - this.chartTypeToggle - .classed("pushed", this.barsLine); - this.g.selectAll("g > *").remove(); - this.drawContent(this.currentData); -}; -Chart1.prototype.drawContent = function(data) -{ - let chartDraw = this.barsLine ? this.bars : this.line; - chartDraw.apply(this, [data]); -}; + DataConfig.prototype.rectHeight = function (scaled, gIsData, d, i, g) + { + Ember.assert('rectHeight arguments.length === 5', arguments.length === 5); + let height, + d2l = (scaled ? this.datum2LocationScaled : this.datum2Location), + location; + /* if location is an interval, calculate height from it. + * Otherwise, use adjacent points to indicate height. + */ + if ((location = d2l(d)).length) { + height = Math.abs(location[1] - location[0]); + } + else { + /* the boundary between 2 adjacent points is midway between them. + * So sum that half-distance for the previous and next points. + * the y range distance from the previous point to the next point. + * If this point is either end, simply double the other half-distance. + */ + if (! g.length) + /* constant value OK - don't expect to be called if g.length is 0. + * this.scales.y.range() / 10; + */ + height = 10; + else { + let r = []; + function gData(i) { let gi = g[i]; return gIsData ? gi : gi.__data__; }; + if (i > 0) + r.push(gData(i)); + r.push(d); + if (i < g.length-1) + r.push(gData(i+1)); + let y = + r.map(this.datum2LocationScaled); + height = Math.abs(y[y.length-1] - y[0]) * 2 / (y.length-1); + dLog('rectHeight', gIsData, d, i, /*g,*/ r, y, height); + if (! height) + height = 1; + } + } + return height; + }; + Chart1.prototype.scaleLinear = function (yRange, data) + { + // based on https://bl.ocks.org/mbostock/3883245 + if (! this.scales.yLine) + this.scales.yLine = d3.scaleLinear(); + let y = this.scales.yLine; + y.rangeRound(yRange); + let + d2l = this.dataConfig.datum2Location || Object.values(this.chartLines)[0].dataConfig.datum2Location; + let combinedDomain = this.domain(d2l, data); + y.domain(combinedDomain); + console.log('scaleLinear domain', combinedDomain); + return y; + }; + /** Combine the domains of each of the component ChartLine-s. + * @param valueFn e.g. datum2Location or datum2Value + */ + Chart1.prototype.domain = function (valueFn, blocksData) + { + let blockIds = Object.keys(blocksData), + domains = blockIds.map((blockId) => { + let + data = blocksData[blockId], + chartLine = this.chartLines[blockId]; + return chartLine.domain(valueFn, data); + }); + /** Union the domains. */ + let domain = domains + .reduce((acc, val) => acc.concat(val), []); + return domain; + }; + /** Calculate the domain of some function of the data, which may be the data value or location. + * This can be used in independent of axis-chart, and can be factored out + * for more general use, along with Chart1:domain(). + * @param valueFn e.g. datum2Location or datum2Value + * In axis-chart, value is x, location is y. + */ + ChartLine.prototype.domain = function (valueFn, data) + { + let + /** location may be an interval, so flatten the result. + * Later Array.flat() can be used. + * Value is not an interval (yet), so that .reduce is a no-op). + */ + yFlat = data + .map(valueFn) + .reduce((acc, val) => acc.concat(val), []); + let domain = d3.extent(yFlat); + console.log('ChartLine domain', domain, yFlat); + return domain; + }; + ChartLine.prototype.line = function (data) + { + let y = this.scales.yLine, dataConfig = this.dataConfig; + + let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, y); + let line = d3.line() + .x(dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false)) + .y((d) => middle(datum2LocationScaled(d))); + + console.log("line x domain", this.scales.x.domain(), this.scales.x.range()); + + let + g = this.g, + ps = g + .selectAll("g > path." + dataConfig.barClassName) + .data([1]); + ps + .enter() + .append("path") + .attr("class", dataConfig.barClassName + " line") + .datum(data) + .attr("d", line) + .merge(ps) + .datum(data) + .transition().duration(1500) + .attr("d", line); + // data length is constant 1, so .remove() is not needed + ps.exit().remove(); + }; + /** Alternate between bar chart and line chart */ + Chart1.prototype.toggleBarsLine = function () + { + console.log("toggleBarsLine", this); + d3.event.stopPropagation(); + this.barsLine = ! this.barsLine; + this.chartTypeToggle + .classed("pushed", this.barsLine); + this.g.selectAll("g > *").remove(); + Object.keys(this.chartLines).forEach((blockId) => { + let chartLine = this.chartLines[blockId]; + chartLine.drawContent(this.barsLine); + }); + }; + /** Draw, using .currentData, which is set by calling .filterToZoom(). + * @param barsLine if true, draw .bars, otherwise .line. + */ + ChartLine.prototype.drawContent = function(barsLine) + { + let + data = this.currentData; + let chartDraw = barsLine ? this.bars : this.line; + chartDraw.apply(this, [data]); + }; AxisCharts.prototype.commonFrame = function container() { @@ -602,18 +736,18 @@ AxisCharts.prototype.commonFrame = function container() gAxis = this.dom.gAxis, bbox = this.ranges.bbox; - /** datum is value in hash : {value : , description: } and with optional attribute description. */ - - dLog('container', gAxis.node()); - /** parent; contains a clipPath, g > rect, text.resizer. */ - let gps = gAxis - .selectAll("g." + className) - .data([axisID]), - gp = gps - .enter() - .insert("g", ":first-child") - .attr('class', className); - if (false) { // not completed. Can base resized() on axis-2d.js + /** datum is value in hash : {value : , description: } and with optional attribute description. */ + + dLog('container', gAxis.node()); + /** parent; contains a clipPath, g > rect, text.resizer. */ + let gps = gAxis + .selectAll("g." + className) + .data([axisID]), + gp = gps + .enter() + .insert("g", ":first-child") + .attr('class', className); + if (false) { // not completed. Can base resized() on axis-2d.js let text = gp .append("text") .attr('class', 'resizer') @@ -622,48 +756,46 @@ AxisCharts.prototype.commonFrame = function container() if (gp.size() > 0) eltWidthResizable("g.axis-use > g." + className + " > text.resizer", resized); } - /** datum is axisID, so id and clip-path could be functions. */ - let axisClipId = "axis-clip-" + axisID; - let gpa = + /** datum is axisID, so id and clip-path could be functions. */ + let axisClipId = "axis-clip-" + axisID; + let gpa = gp // define the clipPath - .append("clipPath") // define a clip path - .attr("id", axisClipId) // give the clipPath an ID - .append("rect"), // shape it as a rect - gprm = + .append("clipPath") // define a clip path + .attr("id", axisClipId) // give the clipPath an ID + .append("rect"), // shape it as a rect + gprm = gpa.merge(gps.selectAll("g > clipPath > rect")) - .attr("x", bbox.x) - .attr("y", bbox.y) - .attr("width", bbox.width) - .attr("height", bbox.height) - ; - gp.append("g") - .attr("clip-path", "url(#" + axisClipId + ")"); // clip with the rectangle - - let g = - gps.merge(gp).selectAll("g." + className+ " > g"); + .attr("x", bbox.x) + .attr("y", bbox.y) + .attr("width", bbox.width) + .attr("height", bbox.height) + ; + let gca = + gp.append("g") + .attr("clip-path", "url(#" + axisClipId + ")"); // clip with the rectangle + + let g = + gps.merge(gp).selectAll("g." + className+ " > g"); if (! gp.empty() ) { addParentClass(g); /* .gc is ​​ * .g (assigned later) is g.axis-chart */ this.dom.gc = g; + this.dom.gca = gca; this.dom.gp = gp; this.dom.gps = gps; } }; /* - class AxisChart { - bbox; - data; - gp; - gps; +class AxisChart { + bbox; + data; + gp; + gps; }; - */ -AxisCharts.prototype.configure = function configure(dataConfig) -{ - this.dataConfig = dataConfig; -}; +*/ AxisCharts.prototype.controls = function controls(chart1) { @@ -672,28 +804,28 @@ AxisCharts.prototype.controls = function controls(chart1) gp = this.dom.gp, gps = this.dom.gps; - let b = chart1; // b for barChart + let b = chart1; // b for barChart - function toggleBarsLineClosure(e) - { - b.toggleBarsLine(); - } + function toggleBarsLineClosure(e) + { + b.toggleBarsLine(); + } - /** currently placed at g.chart, could be inside g.chart>g (clip-path=). */ - let chartTypeToggle = gp - .append("circle") - .attr("class", "radio toggle chartType") - .attr("r", 6) - .on("click", toggleBarsLineClosure); - chartTypeToggle.merge(gps.selectAll("g > circle")) - .attr("cx", bbox.x + bbox.width / 2) /* was o[p], but g.axis-outer translation does x offset of stack. */ - .attr("cy", bbox.height - 10) - .classed("pushed", b.barsLine); - b.chartTypeToggle = chartTypeToggle; + /** currently placed at g.chart, could be inside g.chart>g (clip-path=). */ + let chartTypeToggle = gp + .append("circle") + .attr("class", "radio toggle chartType") + .attr("r", 6) + .on("click", toggleBarsLineClosure); + chartTypeToggle.merge(gps.selectAll("g > circle")) + .attr("cx", bbox.x + bbox.width / 2) /* was o[p], but g.axis-outer translation does x offset of stack. */ + .attr("cy", bbox.height - 10) + .classed("pushed", b.barsLine); + b.chartTypeToggle = chartTypeToggle; }; /*----------------------------------------------------------------------------*/ /* layoutAndDrawChart() has been split into class methods of AxisCharts and Chart1, * and replaced with a proxy which calls them, and can next be re-distributed into axis-chart. */ -export { layoutAndDrawChart, AxisCharts, /*AxisChart,*/ className, Chart1, DataConfig }; +export { setupChart, drawChart, AxisCharts, /*AxisChart,*/ className, Chart1, DataConfig, blockData, parsedData }; diff --git a/frontend/app/utils/feature-lookup.js b/frontend/app/utils/feature-lookup.js index 559281826..1e34c5156 100644 --- a/frontend/app/utils/feature-lookup.js +++ b/frontend/app/utils/feature-lookup.js @@ -169,4 +169,24 @@ function storeFeature(oa, flowsService, feature, f, axisID) { /*----------------------------------------------------------------------------*/ -export { featureChrs, name2Map, chrMap, objectSet, mapsOfFeature, storeFeature }; +import { stacks } from './stacks'; +/** @param features contains the data attributes of the features. + */ +function ensureBlockFeatures(blockId, features) { + // may also need ensureFeatureIndex(). + let + /** oa.z could be passed in as a parameter, but this will be replaced anyway. */ + oa = stacks.oa, + za = oa.z[blockId]; + /* if za has not been populated with features, it will have just .dataset + * and .scope, i.e. .length === 2 */ + if (Object.keys(za).length == 2) { + dLog('drawBlockFeatures()', blockId, za, features); + // add features to za. + features.forEach((f) => za[f.name] = f.value); + } +} + +/*----------------------------------------------------------------------------*/ + +export { featureChrs, name2Map, chrMap, objectSet, mapsOfFeature, storeFeature, ensureBlockFeatures }; From 5bdbd86b3658f835be731b51b32985b6c7377387 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 28 Jan 2020 17:26:20 +1100 Subject: [PATCH 10/57] handle multiple chart types within an axis, with multiple lines. Hold scales in Chart1 only, not AxisCharts, so chart types within an axis can have different scales. Insert g.(typeName) to wrap .axis-chart and the axes and toggle. Split addedDefaults() out of getRanges(). Split AxisCharts frame() out of commonFrame(). Just 1 toggle per chart type. frontend/app/ : b8d1de3 9890 Jan 28 15:31 components/axis-chart.js 55894e9 29602 Jan 28 17:08 utils/draw/chart1.js --- frontend/app/components/axis-chart.js | 27 +++++- frontend/app/utils/draw/chart1.js | 133 ++++++++++++++++---------- 2 files changed, 103 insertions(+), 57 deletions(-) diff --git a/frontend/app/components/axis-chart.js b/frontend/app/components/axis-chart.js index e76e6fd55..41f9f4ff6 100644 --- a/frontend/app/components/axis-chart.js +++ b/frontend/app/components/axis-chart.js @@ -1,7 +1,7 @@ import Ember from 'ember'; const { inject: { service } } = Ember; -import { className, AxisCharts, setupChart, drawChart, Chart1, DataConfig, blockData, parsedData } from '../utils/draw/chart1'; +import { className, AxisCharts, setupFrame, setupChart, drawChart, Chart1, DataConfig, blockData, parsedData } from '../utils/draw/chart1'; /*----------------------------------------------------------------------------*/ @@ -119,6 +119,8 @@ export default Ember.Component.extend({ let blocksData = this.get('blocksData'), chartTypes = this.get('chartTypes'), charts = this.get('charts'); + let axisCharts = this.get('axisCharts'); + chartTypes.forEach((typeName) => { if (! charts[typeName]) { let data = blocksData.get(typeName), @@ -126,15 +128,30 @@ export default Ember.Component.extend({ blockIds = Object.keys(data), /** may use firstBlock for isBlockData. */ firstBlock = data[blockIds[0]], + dataConfig = dataConfigs[typeName], parentG = this.get('axisCharts.dom.g'), // this.get('gAxis'), chart = new Chart1(parentG, dataConfig); charts[typeName] = chart; - let axisCharts = this.get('axisCharts'); + } + }); + setupFrame( + this.get('axisID'), axisCharts, + chartTypes, charts, /*resizedWidth*/undefined); + + + chartTypes.forEach((typeName) => { + let + chart = charts[typeName]; + if (! chart.ranges) { + let data = blocksData.get(typeName), + dataConfig = chart.dataConfig; let blocks = this.get('blocks'); + setupChart( - this.get('axisID'), axisCharts, chart, data, - blocks, dataConfig, this.get('yAxisScale'), /*resizedWidth*/undefined); + this.get('axisID'), axisCharts, chart, data, chartTypes, + dataConfig, this.get('yAxisScale'), /*resizedWidth*/undefined); + drawChart(axisCharts, chart, data, blocks); } }); @@ -244,7 +261,7 @@ export default Ember.Component.extend({ resizedWidth = this.get('width'), chart1 = this.get('charts')[dataTypeName]; chart1 = setupChart( - axisID, axisCharts, chart1, chartData, blocks, dataConfig, yAxisScale, resizedWidth); + axisID, axisCharts, chart1, chartData, this.get('chartTypes'), dataConfig, yAxisScale, resizedWidth); drawChart(axisCharts, chart1, chartData, blocks); if (! this.get('charts') && chart1) this.set('charts', chart1); diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index ffcfd5ddc..1dbbe2b4b 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -96,7 +96,7 @@ function addParentClass(g) { let axisUse=g.node().parentElement.parentElement, us=d3.select(axisUse); us.classed('hasChart', true); - console.log(us.node()); + console.log('addParentClass', us.node()); }; /*----------------------------------------------------------------------------*/ @@ -117,7 +117,8 @@ class ChartLine { constructor(g, dataConfig, scales) { this.g = g; this.dataConfig = dataConfig; - this.scales = scales; + /* the scales have the same .range() as the parent Chart1, but the .domain() varies. */ + this.scales = scales; // Object.assign({}, scales); } } @@ -178,32 +179,19 @@ class AxisCharts { this.axisID = axisID; this.ranges = { }; this.dom = { }; - // this will move to Chart1. - this.scales = { /* x, y, yLine */ }; } }; -AxisCharts.prototype.setup = function(axisID, yAxisScale) { +AxisCharts.prototype.setup = function(axisID) { this.selectParentContainer(axisID); this.getBBox(); - this.scales.yAxis = yAxisScale; }; /** - * @param yAxisScale */ -function setupChart(axisID, axisCharts, chart1, chartData, blocks, dataConfig, yAxisScale, resizedWidth) +function setupFrame(axisID, axisCharts, chartTypes, charts, resizedWidth) { - axisCharts.setup(axisID, yAxisScale); - - // Plan is for Axischarts to own .ranges and Chart1 to own .scales, but for now there is some overlap. - if (! chart1.ranges) { - chart1.ranges = axisCharts.ranges; - chart1.scales = axisCharts.scales; - chart1.dom = axisCharts.dom; - } - - chart1.getRanges(axisCharts.ranges, chartData); + axisCharts.setup(axisID); axisCharts.getRanges3(resizedWidth); @@ -211,14 +199,33 @@ function setupChart(axisID, axisCharts, chart1, chartData, blocks, dataConfig, y axisCharts.commonFrame(/*axisID, gAxis*/); - addParentClass(axisCharts.dom.gc); + // equivalent to addParentClass(); + axisCharts.dom.gAxis.classed('hasChart', true); + + axisCharts.frame(axisCharts.ranges.bbox, chartTypes); - axisCharts.controls(chart1); + axisCharts.controls(charts); axisCharts.getRanges2(); - chart1.g = - axisCharts.group(axisCharts.dom.gc, 'axis-chart'); + axisCharts.group(axisCharts.dom.gca, 'axis-chart', charts); +} +function setupChart(axisID, axisCharts, chart1, chartData, chartTypes, dataConfig, yAxisScale, resizedWidth) +{ + // Plan is for Axischarts to own .ranges, but for now there is some overlap. + if (! chart1.ranges) { + chart1.ranges = axisCharts.ranges; + chart1.dom = axisCharts.dom; + } + chart1.scales.yAxis = yAxisScale; + + + chart1.getRanges(axisCharts.ranges, chartData); + /* pasteProcess() may set .valueName, possibly provided by GUI; + * i.e. Object.values(chartData).mapBy('valueName').filter(n => n) + * and that can be passed to addedDefaults(). + */ + dataConfig.addedDefaults(); return chart1; }; @@ -295,19 +302,19 @@ Chart1.prototype.getRanges = function (ranges, chartData) { else yDomain = [yAxis.invert(yrange[0]), yAxis.invert(yrange[1])]; - let dataConfig = this.dataConfig; - if (! dataConfig.hoverTextFn) - dataConfig.hoverTextFn = hoverTextFn; - if (dataConfig.valueIsArea === undefined) - dataConfig.valueIsArea = false; - - if (! dataConfig.barClassName) - dataConfig.barClassName = classNameSub; - if (! dataConfig.valueName) - dataConfig.valueName = chart.valueName || "Values"; - ranges.pxSize = (yDomain[1] - yDomain[0]) / ranges.bbox.height; }; +DataConfig.prototype.addedDefaults = function(valueName) { + if (! this.hoverTextFn) + this.hoverTextFn = hoverTextFn; + if (this.valueIsArea === undefined) + this.valueIsArea = false; + + if (! this.barClassName) + this.barClassName = classNameSub; + if (! this.valueName) + this.valueName = valueName || "Values"; +}; ChartLine.prototype.setup = function(blockId) { /* Some types (blockData, parsedData) require a block to lookup the feature * name for location. They are denoted by .datum2Location not being in their @@ -358,6 +365,8 @@ AxisCharts.prototype.getRanges3 = function (resizedWidth) { this.parentG = parentG; this.dataConfig = dataConfig; this.chartLines = {}; + // yAxis is imported, x & y are calculated locally. yLine would be used if y is scaleBand. + this.scales = { /* yAxis, x, y, yLine */ }; } Chart1.prototype.barsLine = true; @@ -435,24 +444,28 @@ AxisCharts.prototype.getRanges3 = function (resizedWidth) { dataConfig.datum2ValueScaled = datum2ValueScaled; }; -AxisCharts.prototype.group = function (parentG, groupClassName) { +AxisCharts.prototype.group = function (parentG, groupClassName, charts) { /** parentG is g.axis-use. add g.(groupClassName); * within parentG there is also a sibling g.axis-html. */ - let data = parentG.data(), + let // data = parentG.data(), gs = parentG + /* .selectAll("g > g." + groupClassName) .data(data), // inherit g.datum(), or perhaps [groupClassName] + */, gsa = gs - .enter() +// .enter() .append("g") // maybe drop this g, move margin calc to gp // if drawing internal chart axes then move them inside the clip rect // .attr("transform", "translate(" + margin.left + "," + margin.top + ")"), .attr("class", groupClassName), - g = gsa.merge(gs); + g = parentG.selectAll("g > g." + groupClassName); // gsa.merge(gs); dLog('group', this, parentG, g.node()); this.dom.g = g; this.dom.gs = gs; this.dom.gsa = gsa; + // set ChartLine .g; used by ChartLine.{lines,bars}. + gsa.each(function(typeName, i) { charts[typeName].g = d3.select(this) ; } ); return g; }; @@ -756,8 +769,20 @@ AxisCharts.prototype.commonFrame = function container() if (gp.size() > 0) eltWidthResizable("g.axis-use > g." + className + " > text.resizer", resized); } - /** datum is axisID, so id and clip-path could be functions. */ - let axisClipId = "axis-clip-" + axisID; + this.dom.gps = gps; + this.dom.gp = gp; +}; + +AxisCharts.prototype.frame = function container(bbox, chartTypes) +{ + let + gps = this.dom.gps, + gp = this.dom.gp; + + /** datum is axisID, so id and clip-path can be functions. + * e.g. this.dom.gp.data() is [axisID] + */ + function axisClipId(axisID) { return "axis-clip-" + axisID; } let gpa = gp // define the clipPath .append("clipPath") // define a clip path @@ -772,7 +797,14 @@ AxisCharts.prototype.commonFrame = function container() ; let gca = gp.append("g") - .attr("clip-path", "url(#" + axisClipId + ")"); // clip with the rectangle + .attr("clip-path", (d) => "url(#" + axisClipId(d) + ")") // clip with the rectangle + .selectAll("g[clip-path]") + .data(chartTypes) + .enter() + .append("g") + .attr('class', (d) => d) + .attr("transform", (d, i) => "translate(" + (i * 30) + ", 0)") + ; let g = gps.merge(gp).selectAll("g." + className+ " > g"); @@ -780,11 +812,10 @@ AxisCharts.prototype.commonFrame = function container() addParentClass(g); /* .gc is ​​ * .g (assigned later) is g.axis-chart + * .gca contains a g for each chartType / dataTypeName, i.e. per Chart1. */ this.dom.gc = g; this.dom.gca = gca; - this.dom.gp = gp; - this.dom.gps = gps; } }; @@ -797,18 +828,16 @@ class AxisChart { }; */ -AxisCharts.prototype.controls = function controls(chart1) +AxisCharts.prototype.controls = function controls(charts) { let bbox = this.ranges.bbox, - gp = this.dom.gp, - gps = this.dom.gps; - - let b = chart1; // b for barChart + gp = this.dom.gca, + gps = this.dom.gc; - function toggleBarsLineClosure(e) + function toggleBarsLineClosure(typeName /*, i, g*/) { - b.toggleBarsLine(); + charts[typeName].toggleBarsLine(); } /** currently placed at g.chart, could be inside g.chart>g (clip-path=). */ @@ -820,12 +849,12 @@ AxisCharts.prototype.controls = function controls(chart1) chartTypeToggle.merge(gps.selectAll("g > circle")) .attr("cx", bbox.x + bbox.width / 2) /* was o[p], but g.axis-outer translation does x offset of stack. */ .attr("cy", bbox.height - 10) - .classed("pushed", b.barsLine); - b.chartTypeToggle = chartTypeToggle; + .classed("pushed", (typeName) => { let chart1 = charts[typeName]; return chart1.barsLine; }); + chartTypeToggle.each(function(typeName) { charts[typeName].chartTypeToggle = d3.select(this); } ); }; /*----------------------------------------------------------------------------*/ /* layoutAndDrawChart() has been split into class methods of AxisCharts and Chart1, * and replaced with a proxy which calls them, and can next be re-distributed into axis-chart. */ -export { setupChart, drawChart, AxisCharts, /*AxisChart,*/ className, Chart1, DataConfig, blockData, parsedData }; +export { setupFrame, setupChart, drawChart, AxisCharts, /*AxisChart,*/ className, Chart1, DataConfig, blockData, parsedData }; From 1a9269af5fe72b5018fef65696deeea6efea17c8 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 28 Jan 2020 23:13:58 +1100 Subject: [PATCH 11/57] just 1 copy of axes within each chart Change datum of g[clip-path] from typeName to Chart1. Move drawAxes() call from drawChart() to chartTypesEffect(). frontend/app/ : 7396c01 9932 Jan 28 18:55 components/axis-chart.js 9cfd35f 29773 Jan 28 23:02 utils/draw/chart1.js --- frontend/app/components/axis-chart.js | 10 ++-- frontend/app/utils/draw/chart1.js | 66 +++++++++++++++------------ 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/frontend/app/components/axis-chart.js b/frontend/app/components/axis-chart.js index 41f9f4ff6..7d41fab84 100644 --- a/frontend/app/components/axis-chart.js +++ b/frontend/app/components/axis-chart.js @@ -137,7 +137,7 @@ export default Ember.Component.extend({ }); setupFrame( this.get('axisID'), axisCharts, - chartTypes, charts, /*resizedWidth*/undefined); + charts, /*resizedWidth*/undefined); chartTypes.forEach((typeName) => { @@ -149,12 +149,16 @@ export default Ember.Component.extend({ let blocks = this.get('blocks'); setupChart( - this.get('axisID'), axisCharts, chart, data, chartTypes, + this.get('axisID'), axisCharts, chart, data, dataConfig, this.get('yAxisScale'), /*resizedWidth*/undefined); drawChart(axisCharts, chart, data, blocks); } }); + const showChartAxes = true; + if (showChartAxes) + axisCharts.drawAxes(charts); + return chartTypes; }), @@ -261,7 +265,7 @@ export default Ember.Component.extend({ resizedWidth = this.get('width'), chart1 = this.get('charts')[dataTypeName]; chart1 = setupChart( - axisID, axisCharts, chart1, chartData, this.get('chartTypes'), dataConfig, yAxisScale, resizedWidth); + axisID, axisCharts, chart1, chartData, dataConfig, yAxisScale, resizedWidth); drawChart(axisCharts, chart1, chartData, blocks); if (! this.get('charts') && chart1) this.set('charts', chart1); diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index 1dbbe2b4b..fd655fddc 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -90,7 +90,7 @@ function hoverTextFn (feature, block) { * Later can use e.g. axis-accordion to separate these horizontally; * for axis-chart the foreignObject is not used. * - * @param g parent of the chart. this is the with clip-path axis-clip. + * @param g parent of the chart. this is the with clip-path #axis-chart-clip. */ function addParentClass(g) { let axisUse=g.node().parentElement.parentElement, @@ -189,7 +189,7 @@ AxisCharts.prototype.setup = function(axisID) { /** */ -function setupFrame(axisID, axisCharts, chartTypes, charts, resizedWidth) +function setupFrame(axisID, axisCharts, charts, resizedWidth) { axisCharts.setup(axisID); @@ -202,15 +202,15 @@ function setupFrame(axisID, axisCharts, chartTypes, charts, resizedWidth) // equivalent to addParentClass(); axisCharts.dom.gAxis.classed('hasChart', true); - axisCharts.frame(axisCharts.ranges.bbox, chartTypes); + axisCharts.frame(axisCharts.ranges.bbox, charts); - axisCharts.controls(charts); + axisCharts.controls(); axisCharts.getRanges2(); axisCharts.group(axisCharts.dom.gca, 'axis-chart', charts); } -function setupChart(axisID, axisCharts, chart1, chartData, chartTypes, dataConfig, yAxisScale, resizedWidth) +function setupChart(axisID, axisCharts, chart1, chartData, dataConfig, yAxisScale, resizedWidth) { // Plan is for Axischarts to own .ranges, but for now there is some overlap. if (! chart1.ranges) { @@ -247,9 +247,6 @@ function drawChart(axisCharts, chart1, chartData, blocks) blockIds.forEach((blockId) => { chart1.chartLines[blockId].scaledConfig(); } ); - if (showChartAxes) - chart1.drawAxes(chartData); - chart1.drawContent(); }; @@ -464,12 +461,23 @@ AxisCharts.prototype.group = function (parentG, groupClassName, charts) { this.dom.g = g; this.dom.gs = gs; this.dom.gsa = gsa; - // set ChartLine .g; used by ChartLine.{lines,bars}. - gsa.each(function(typeName, i) { charts[typeName].g = d3.select(this) ; } ); + // set Chart1 .g; passed to ChartLine() and used by ChartLine.{lines,bars}. + gsa.each(function(chart, i) { chart.g = d3.select(this) ; } ); return g; }; -Chart1.prototype.drawAxes = function (data) { +AxisCharts.prototype.drawAxes = function (charts) { + let + dom = this.dom, + /** the axes were originally within the gs,gsa of .group(); hence the var names. + * gs selects the into which the axes will be inserted, and gpa is the + * .enter().append() of that selection. + */ + gs = dom.gc, + gsa = dom.gca; + gsa.each(Chart1.prototype.drawAxes); +}; +Chart1.prototype.drawAxes = function (chart, i, g) { /** first draft showed all data; subsequently adding : * + select region from y domain @@ -481,16 +489,14 @@ Chart1.prototype.drawAxes = function (data) { // yBand.domain(data.map(dataConfig.datum2Location)); let - {height} = this.ranges.drawSize, - {x, y} = this.scales, - dom = this.dom, - /** the axes were originally within the gs,gsa of .group(); hence the var names. - * gs selects the into which the axes will be inserted, and gpa is the - * .enter().append() of that selection. - */ + chart1 = this.__data__, + {height} = chart1.ranges.drawSize, + {x, y} = chart1.scales, + dom = chart1.dom, gs = dom.gc, - gsa = dom.gca, - dataConfig = this.dataConfig; + /** selection into which to append the axes. */ + gsa = d3.select(this), + dataConfig = chart1.dataConfig; let axisXa = gsa.append("g") @@ -773,7 +779,7 @@ AxisCharts.prototype.commonFrame = function container() this.dom.gp = gp; }; -AxisCharts.prototype.frame = function container(bbox, chartTypes) +AxisCharts.prototype.frame = function container(bbox, charts) { let gps = this.dom.gps, @@ -782,7 +788,7 @@ AxisCharts.prototype.frame = function container(bbox, chartTypes) /** datum is axisID, so id and clip-path can be functions. * e.g. this.dom.gp.data() is [axisID] */ - function axisClipId(axisID) { return "axis-clip-" + axisID; } + function axisClipId(axisID) { return "axis-chart-clip-" + axisID; } let gpa = gp // define the clipPath .append("clipPath") // define a clip path @@ -799,10 +805,10 @@ AxisCharts.prototype.frame = function container(bbox, chartTypes) gp.append("g") .attr("clip-path", (d) => "url(#" + axisClipId(d) + ")") // clip with the rectangle .selectAll("g[clip-path]") - .data(chartTypes) + .data(Object.values(charts)) .enter() .append("g") - .attr('class', (d) => d) + .attr('class', (d) => d.dataConfig.dataTypeName) .attr("transform", (d, i) => "translate(" + (i * 30) + ", 0)") ; @@ -810,7 +816,7 @@ AxisCharts.prototype.frame = function container(bbox, chartTypes) gps.merge(gp).selectAll("g." + className+ " > g"); if (! gp.empty() ) { addParentClass(g); - /* .gc is ​​ + /* .gc is ​​ * .g (assigned later) is g.axis-chart * .gca contains a g for each chartType / dataTypeName, i.e. per Chart1. */ @@ -828,16 +834,16 @@ class AxisChart { }; */ -AxisCharts.prototype.controls = function controls(charts) +AxisCharts.prototype.controls = function controls() { let bbox = this.ranges.bbox, gp = this.dom.gca, gps = this.dom.gc; - function toggleBarsLineClosure(typeName /*, i, g*/) + function toggleBarsLineClosure(chart /*, i, g*/) { - charts[typeName].toggleBarsLine(); + chart.toggleBarsLine(); } /** currently placed at g.chart, could be inside g.chart>g (clip-path=). */ @@ -849,8 +855,8 @@ AxisCharts.prototype.controls = function controls(charts) chartTypeToggle.merge(gps.selectAll("g > circle")) .attr("cx", bbox.x + bbox.width / 2) /* was o[p], but g.axis-outer translation does x offset of stack. */ .attr("cy", bbox.height - 10) - .classed("pushed", (typeName) => { let chart1 = charts[typeName]; return chart1.barsLine; }); - chartTypeToggle.each(function(typeName) { charts[typeName].chartTypeToggle = d3.select(this); } ); + .classed("pushed", (chart1) => { return chart1.barsLine; }); + chartTypeToggle.each(function(chart) { chart.chartTypeToggle = d3.select(this); } ); }; /*----------------------------------------------------------------------------*/ From 2fee08022f5cf027d198fb3704c70374e2d5c616 Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 29 Jan 2020 10:25:28 +1100 Subject: [PATCH 12/57] Use axis scale instead of local Y scale Add useLocalY. Move controls() to last so that toggle is accessible. f61e46f 30075 Jan 29 09:26 frontend/app/utils/draw/chart1.js --- frontend/app/utils/draw/chart1.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index fd655fddc..9a580946d 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -12,6 +12,7 @@ const className = "chart", classNameSub = "chartRow"; /** Enables per-chart axes; X axes will be often useful; Y axis might be used if * zooming into different regions of an axis. */ const showChartAxes = true; +const useLocalY = false; /* global d3 */ @@ -204,11 +205,11 @@ function setupFrame(axisID, axisCharts, charts, resizedWidth) axisCharts.frame(axisCharts.ranges.bbox, charts); - axisCharts.controls(); - axisCharts.getRanges2(); axisCharts.group(axisCharts.dom.gca, 'axis-chart', charts); + // place controls after the ChartLine-s group, so that the toggle is above the bars and can be accessed. + axisCharts.controls(); } function setupChart(axisID, axisCharts, chart1, chartData, dataConfig, yAxisScale, resizedWidth) { @@ -362,7 +363,10 @@ AxisCharts.prototype.getRanges3 = function (resizedWidth) { this.parentG = parentG; this.dataConfig = dataConfig; this.chartLines = {}; - // yAxis is imported, x & y are calculated locally. yLine would be used if y is scaleBand. + /* yAxis is imported, x & yLine are calculated locally. + * y is used for drawing - it refers to yAxis or yLine. + * yLine would be used for .line() if yBand / scaleBand was used for .bars(). + */ this.scales = { /* yAxis, x, y, yLine */ }; } Chart1.prototype.barsLine = true; @@ -403,7 +407,7 @@ AxisCharts.prototype.getRanges3 = function (resizedWidth) { /* scaleBand would suit a data set with evenly spaced or ordinal / nominal y values. * yBand = d3.scaleBand().rangeRound(yRange).padding(0.1), */ - y = this.scaleLinear(yRange, data), + y = useLocalY ? this.scaleLinear(yRange, data) : this.scales.yAxis, x = d3.scaleLinear().rangeRound(xRange); // datum2LocationScaled() uses me.scales.x rather than the value in the closure in which it was created. this.scales.x = x; @@ -506,6 +510,7 @@ Chart1.prototype.drawAxes = function (chart, i, g) { .attr("transform", "translate(0," + height + ")") .call(d3.axisBottom(x)); + if (useLocalY) { let axisYa = gsa.append("g") .attr("class", "axis axis--y"); @@ -518,6 +523,7 @@ Chart1.prototype.drawAxes = function (chart, i, g) { .attr("dy", "0.71em") .attr("text-anchor", "end") .text(dataConfig.valueName); + } }; /** * @param block hover @@ -697,7 +703,7 @@ Chart1.prototype.drawLine = function (blockId, block, data) }; ChartLine.prototype.line = function (data) { - let y = this.scales.yLine, dataConfig = this.dataConfig; + let y = this.scales.y, dataConfig = this.dataConfig; let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, y); let line = d3.line() From b2186093878f40cb5456b8336ba813e8cb5de28a Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 29 Jan 2020 11:27:32 +1100 Subject: [PATCH 13/57] axis-tracks : check for axis being closed. In axis1d CP, check if .isDestroying; planned change to axis-2d : axis1d to use store-based relations instead of stacks.js axes should make this check unnecessary and this can revert to a simple computed.alias. 4e42e90 25495 Jan 24 14:07 frontend/app/components/axis-tracks.js --- frontend/app/components/axis-tracks.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/app/components/axis-tracks.js b/frontend/app/components/axis-tracks.js index 8a04eaa66..ef2f1543e 100644 --- a/frontend/app/components/axis-tracks.js +++ b/frontend/app/components/axis-tracks.js @@ -184,7 +184,15 @@ export default InAxis.extend({ /*--------------------------------------------------------------------------*/ - axis1d : Ember.computed.alias('axis.axis1d'), + axis1d : Ember.computed('axis.axis1d', 'axis.axis1d.isDestroying', function () { + /** this CP could be simply a .alias, but it can get a reference to a axis1d + * which is being destroyed; probably need a small design change in the + * component relations. */ + let axis1d = this.get('axis.axis1d'); + if (axis1d.isDestroying) + axis1d = undefined; + return axis1d; + }), axisS : Ember.computed.alias('axis.axis'), currentPosition : Ember.computed.alias('axis1d.currentPosition'), yDomain : Ember.computed.alias('currentPosition.yDomain'), From bd35a3b8b539655c66dab96c0a91dbb8e1b49cca Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 31 Jan 2020 17:13:21 +1100 Subject: [PATCH 14/57] calculate allocatedWidths from childWidths in axis-2d axis-2d: compute allocatedWidths from childWidths; in hbs pass both to axis-chart and axis-tracks. Drop action contentWidth; will likely also drop function contentWidth() since allocatedWidths covers most of that functionality. axis-chart.js : use allocatedWidth. axis-tracks.js : set value on childWidths instead of sending action contentWidth to axis-2d. draw-map.js : when a block is unviewed, clear its brush (if any) in brushedRegions. in-axis.js : add allocatedWidth. chart1.js : rectWidth() : use rectHeight without scaled. Use startOffset from allocatedWidth for transform / translate. 4b6c469 13102 Jan 30 10:16 frontend/app/components/axis-2d.js 16d6889 9956 Jan 29 17:08 frontend/app/components/axis-chart.js 48ff7e3 25722 Jan 29 16:37 frontend/app/components/axis-tracks.js b9a620b 240437 Jan 29 19:01 frontend/app/components/draw-map.js 9216245 6530 Jan 31 16:55 frontend/app/components/in-axis.js dd68cc6 1890 Jan 31 16:55 frontend/app/templates/components/axis-2d.hbs f622b6c 30369 Jan 31 16:26 frontend/app/utils/draw/chart1.js --- frontend/app/components/axis-2d.js | 60 ++++++++++++++++--- frontend/app/components/axis-chart.js | 5 +- frontend/app/components/axis-tracks.js | 14 ++++- frontend/app/components/draw-map.js | 7 +++ frontend/app/components/in-axis.js | 15 ++++- frontend/app/templates/components/axis-2d.hbs | 4 +- frontend/app/utils/draw/chart1.js | 14 +++-- 7 files changed, 99 insertions(+), 20 deletions(-) diff --git a/frontend/app/components/axis-2d.js b/frontend/app/components/axis-2d.js index 8fb5ca4de..229abfbc0 100644 --- a/frontend/app/components/axis-2d.js +++ b/frontend/app/components/axis-2d.js @@ -6,6 +6,8 @@ import AxisEvents from '../utils/draw/axis-events'; /* global d3 */ +const dLog = console.debug; + const axisTransitionTime = 750; @@ -168,12 +170,6 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, { remove: function(){ this.remove(); console.log("components/axis-2d remove()"); - }, - /** - * @param componentName e.g. 'axis-tracks' - */ - contentWidth : function (componentName, axisID, width) { - this.contentWidth(componentName, axisID, width); } }, @@ -192,7 +188,53 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, { && (currentWidth = use_data[0]); return currentWidth; }, - childWidths : {}, + /** width of sub-components within this axis-2d. Indexed by componentName. + * For each sub-component, [min, max] : the minimum required width, and the + * maximum useful width, i.e. the maximum width that the component can fill + * with content. + * Measured nominally in pixels, but space may be allocated proportional to the width allocated to this axis-2d. + */ + childWidths : undefined, + /** Allocate the available width among the children listed in .childWidths + * @return [horizontal start offset, width] for each child. + * The key of the result is the same as the input .childWidths + */ + allocatedWidths : Ember.computed('childWidths.@each.1', 'childWidths.chart.1', function () { + let allocatedWidths, + childWidths = this.get('childWidths'), + groupNames = Object.keys(childWidths), + requested = + groupNames.reduce((result, groupName) => { + let cw = childWidths[groupName]; + result[0] += cw[0]; // min + result[1] += cw[1]; // max + }, [0, 0]); + /** Calculate the spare width after each child is assigned its requested + * minimum width, and apportion the spare width among them. + * If spare < 0 then each child will get < min, but not <0. + */ + let + startWidth = this.get('startWidth'), + available = (this.get('axisUse') && this.rectWidth()) || startWidth || 120, + /** spare and share may be -ve */ + spare = available - requested[0], + share = 0; + if (groupNames.length > 0) { + share = spare / groupNames.length; + } + /** horizontal offset to the start (left) of the child. */ + let offset = 0; + allocatedWidths = groupNames.map((groupName) => { + let w = childWidths[groupName][0] + share; + if (w < 0) + w = 0; + let allocated = [offset, w]; + offset += w; + return allocated; + }); + dLog('allocatedWidths', allocatedWidths, childWidths); + return allocatedWidths; + }), contentWidth : function (componentName, axisID, width) { let childWidths = this.get('childWidths'), @@ -215,6 +257,10 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, { Ember.run.later(call_setWidth); }, + init() { + this._super(...arguments); + this.set('childWidths', Ember.Object.create()); + }, didInsertElement() { let oa = this.get('data'), diff --git a/frontend/app/components/axis-chart.js b/frontend/app/components/axis-chart.js index 7d41fab84..cac4ea9ce 100644 --- a/frontend/app/components/axis-chart.js +++ b/frontend/app/components/axis-chart.js @@ -1,6 +1,7 @@ import Ember from 'ember'; const { inject: { service } } = Ember; +import InAxis from './in-axis'; import { className, AxisCharts, setupFrame, setupChart, drawChart, Chart1, DataConfig, blockData, parsedData } from '../utils/draw/chart1'; /*----------------------------------------------------------------------------*/ @@ -62,7 +63,7 @@ const dataConfigs = * @param charts map of Chart1, indexed by typeName * @param blocksData map of features, indexed by typeName, blockId, */ -export default Ember.Component.extend({ +export default InAxis.extend({ blockService: service('data/block'), className : className, @@ -137,7 +138,7 @@ export default Ember.Component.extend({ }); setupFrame( this.get('axisID'), axisCharts, - charts, /*resizedWidth*/undefined); + charts, this.get('allocatedWidth')); chartTypes.forEach((typeName) => { diff --git a/frontend/app/components/axis-tracks.js b/frontend/app/components/axis-tracks.js index ef2f1543e..fc5253bb3 100644 --- a/frontend/app/components/axis-tracks.js +++ b/frontend/app/components/axis-tracks.js @@ -19,6 +19,8 @@ const trackWidth = 10; /** for devel. ref comment in @see height() */ let trace_count_NaN = 10; +const dLog = console.debug; + /*------------------------------------------------------------------------*/ /* copied from draw-map.js - will import when that is split */ /** Setup hover info text over scaffold horizTick-s. @@ -172,10 +174,13 @@ export default InAxis.extend({ didInsertElement() { this._super(...arguments); - let parentView = this.get('parentView'), + let + childWidths = this.get('childWidths'), axisID = this.get('axisID'), width = this.get('layoutWidth'); - parentView.send('contentWidth', 'axis-tracks', axisID, width); + dLog('didInsertElement', axisID, width); + // [min, max] width + childWidths.set(this.get('className'), [width, width]); }, didRender() { @@ -189,8 +194,10 @@ export default InAxis.extend({ * which is being destroyed; probably need a small design change in the * component relations. */ let axis1d = this.get('axis.axis1d'); - if (axis1d.isDestroying) + if (axis1d.isDestroying) { + console.log('axis1d isDestroying', this); axis1d = undefined; + } return axis1d; }), axisS : Ember.computed.alias('axis.axis'), @@ -628,6 +635,7 @@ export default InAxis.extend({ width = 40 + blockIds.length * 2 * trackWidth + 20 + 50; console.log('layoutWidth', blockIds, width); + this.get('childWidths').set(this.get('className'), [width, width]); return width; }), showTrackBlocks: Ember.computed( diff --git a/frontend/app/components/draw-map.js b/frontend/app/components/draw-map.js index 6c14f02b4..c338d57d7 100644 --- a/frontend/app/components/draw-map.js +++ b/frontend/app/components/draw-map.js @@ -5719,6 +5719,13 @@ export default Ember.Component.extend(Ember.Evented, { .attr("viewBox", oa.vc.viewBox.bind(oa.vc)) .attr('height', graphDim.h /*"auto"*/); + /* axes removed via manage-view : removeBlock don't remove any brush of + * that axis from brushedRegions, so check for that here. + * This check could be done when services/data/blocks:viewed.[] updates. + */ + Object.keys(oa.brushedRegions).forEach( + (refBlockId) => { if (! oa.axes[refBlockId]) removeBrushExtent(refBlockId); } + ); /** As in zoom(), note the brushedDomains before the scale change, for * updating the brush selection position. * This could be passed to axisBrushShowSelection() and used in place of diff --git a/frontend/app/components/in-axis.js b/frontend/app/components/in-axis.js index 834d3330f..0304e936a 100644 --- a/frontend/app/components/in-axis.js +++ b/frontend/app/components/in-axis.js @@ -4,12 +4,15 @@ import { eltWidthResizable, noShiftKeyfilter } from '../utils/domElements'; /* global d3 */ +const dLog = console.debug; + export default Ember.Component.extend({ className : undefined, didInsertElement : function() { this._super(...arguments); + /* grandparent component - listen for resize and zoom events. * possibly these events will move from axis-2d to axis-accordion. * This event handling will move to in-axis, since it is shared by all children of axis-2d/axis-accordion. @@ -124,12 +127,13 @@ export default Ember.Component.extend({ .attr("clip-path", "url(#axis-clip)"); // clip the rectangle let + allocatedWidth = this.get('allocatedWidth'), margin = ranges.margin; let g = gps.merge(gp).selectAll("g." + className+ " > g"); g - .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + .attr("transform", "translate(" + (allocatedWidth[0] + margin.left) + "," + margin.top + ")"); return g; }, @@ -150,6 +154,15 @@ export default Ember.Component.extend({ /*--------------------------------------------------------------------------*/ + allocatedWidth : Ember.computed('allocatedWidths', function () { + let + allocatedWidths = this.get('allocatedWidths'), + allocatedWidth = this.get('allocatedWidths.' + this.get('className')); + dLog('allocatedWidth', allocatedWidth, allocatedWidths); + if (! allocatedWidth) + allocatedWidth = [12, 113]; + return allocatedWidth; + }), width : undefined, resized : function(prevSize, currentSize) { diff --git a/frontend/app/templates/components/axis-2d.hbs b/frontend/app/templates/components/axis-2d.hbs index 0bf03c28d..ba304527a 100644 --- a/frontend/app/templates/components/axis-2d.hbs +++ b/frontend/app/templates/components/axis-2d.hbs @@ -35,9 +35,9 @@
axis-2d :{{this}}, {{axisID}}, {{targetEltId}}, {{subComponents.length}} :
subComponents : - {{axis-chart data=data axis=this axisID=axisID blocks=viewedChartable}} + {{axis-chart data=data axis=this axisID=axisID childWidths=childWidths allocatedWidths=allocatedWidths blocks=viewedChartable}} {{log 'blockService' blockService 'viewedChartable' blockService.viewedChartable}} - {{axis-tracks axis=this axisID=axisID trackBlocksR=dataBlocks}} + {{axis-tracks axis=this axisID=axisID childWidths=childWidths allocatedWidths=allocatedWidths trackBlocksR=dataBlocks}} {{#each subComponents as |subComponent|}} {{subComponent}} {{/each}} diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index 9a580946d..3d3ea7a92 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -189,11 +189,13 @@ AxisCharts.prototype.setup = function(axisID) { }; /** + * @param allocatedWidth [horizontal start offset, width] */ -function setupFrame(axisID, axisCharts, charts, resizedWidth) +function setupFrame(axisID, axisCharts, charts, allocatedWidth) { axisCharts.setup(axisID); + let resizedWidth = allocatedWidth[1]; axisCharts.getRanges3(resizedWidth); // axisCharts.size(this.get('yAxisScale'), /*axisID, gAxis,*/ chartData, resizedWidth); // - split size/width and data/draw @@ -203,7 +205,7 @@ function setupFrame(axisID, axisCharts, charts, resizedWidth) // equivalent to addParentClass(); axisCharts.dom.gAxis.classed('hasChart', true); - axisCharts.frame(axisCharts.ranges.bbox, charts); + axisCharts.frame(axisCharts.ranges.bbox, charts, allocatedWidth); axisCharts.getRanges2(); @@ -598,7 +600,8 @@ Chart1.prototype.drawLine = function (blockId, block, data) width = d2v(d); if (this.valueIsArea) { let h; - width /= (h = this.rectHeight(scaled, gIsData, d, i, g)); + // Factor the width consistently by h, don't sometimes use scaled (i.e. pass scaled==false). + width /= (h = this.rectHeight(false, gIsData, d, i, g)); // dLog('rectWidth', h, width, gIsData); } return width; @@ -785,7 +788,7 @@ AxisCharts.prototype.commonFrame = function container() this.dom.gp = gp; }; -AxisCharts.prototype.frame = function container(bbox, charts) +AxisCharts.prototype.frame = function container(bbox, charts, allocatedWidth) { let gps = this.dom.gps, @@ -807,6 +810,7 @@ AxisCharts.prototype.frame = function container(bbox, charts) .attr("width", bbox.width) .attr("height", bbox.height) ; + let [startOffset, width] = allocatedWidth; let gca = gp.append("g") .attr("clip-path", (d) => "url(#" + axisClipId(d) + ")") // clip with the rectangle @@ -815,7 +819,7 @@ AxisCharts.prototype.frame = function container(bbox, charts) .enter() .append("g") .attr('class', (d) => d.dataConfig.dataTypeName) - .attr("transform", (d, i) => "translate(" + (i * 30) + ", 0)") + .attr("transform", (d, i) => "translate(" + (startOffset + (i * 30)) + ", 0)") ; let g = From 99f3e20d2400675e49dfebaa816610a038af7fc2 Mon Sep 17 00:00:00 2001 From: Don Date: Mon, 3 Feb 2020 16:29:56 +1100 Subject: [PATCH 15/57] Render from a list of axes from a CP, replacing the list generated by stacks library. axis-tracks.js : handle axis.axis1d being undefined. stacks-view.js : add axesBlocks, axesP (previous axesP is now _unused). axis-position.js : rename init() to _1, and use .on('init') to avoid any clash with class being mixed into. block.js : add log_Map_Map(), filterMap(), blocksByReferenceAndScope(), viewedBlocksByReferenceAndScope() dataset.js : add objectsMap(), datasetsByParent(), datasetsByName(). stacks.js : add getAxis1d() to handle sequence/timing variations between CP axesP and stacks axesP. frontend/app/ a06226e 25778 Feb 3 10:23 components/axis-tracks.js 33a5570 2650 Feb 3 00:34 components/draw/stacks-view.js cae51b3 2910 Feb 2 23:28 mixins/axis-position.js 01e1f91 33834 Feb 2 23:26 services/data/block.js 367159f 4280 Feb 2 12:01 services/data/dataset.js 149363a 72952 Feb 2 23:07 utils/stacks.js --- frontend/app/components/axis-tracks.js | 5 +- frontend/app/components/draw/stacks-view.js | 30 ++++- frontend/app/mixins/axis-position.js | 4 +- frontend/app/services/data/block.js | 132 ++++++++++++++++++++ frontend/app/services/data/dataset.js | 51 ++++++++ frontend/app/utils/stacks.js | 21 +++- 6 files changed, 232 insertions(+), 11 deletions(-) diff --git a/frontend/app/components/axis-tracks.js b/frontend/app/components/axis-tracks.js index fc5253bb3..fef0d1fa4 100644 --- a/frontend/app/components/axis-tracks.js +++ b/frontend/app/components/axis-tracks.js @@ -194,7 +194,7 @@ export default InAxis.extend({ * which is being destroyed; probably need a small design change in the * component relations. */ let axis1d = this.get('axis.axis1d'); - if (axis1d.isDestroying) { + if (axis1d && axis1d.isDestroying) { console.log('axis1d isDestroying', this); axis1d = undefined; } @@ -375,11 +375,12 @@ export default InAxis.extend({ let t = tracks.intervalTree[blockId], block = oa.stacks.blocks[blockId], axis = block.getAxis(), + zoomed = axis && axis.axis1d && axis.axis1d.zoomed, /** if zoomed in, tracks are not filtered by sizeThreshold. * The logic is : if the user is zooming in, they are interested in * features regardless of size, e.g. smaller than a pixel. */ - sizeThreshold = axis.axis1d.zoomed ? undefined : pxSize * 1/*5*/, + sizeThreshold = zoomed ? undefined : pxSize * 1/*5*/, tracksLayout = regionOfTree(t, yDomain, sizeThreshold), data = tracksLayout.intervals; if (false) // actually need to sum the .layoutWidth for all blockId-s, plus the block offsets which are calculated below diff --git a/frontend/app/components/draw/stacks-view.js b/frontend/app/components/draw/stacks-view.js index 6207f96c0..06b046ecd 100644 --- a/frontend/app/components/draw/stacks-view.js +++ b/frontend/app/components/draw/stacks-view.js @@ -11,9 +11,35 @@ export default Ember.Component.extend({ block: service('data/block'), previous : {}, - stacksCount : Ember.computed('block.stacksCount', 'block.viewed', 'axes2d.[]', function () { + /** Extract from viewedBlocksByReferenceAndScope(), the viewed blocks, mapped by the id of their reference block. + */ + axesBlocks : Ember.computed('block.viewedBlocksByReferenceAndScope.@each', function () { + let mapByDataset = this.get('block.viewedBlocksByReferenceAndScope'); + let mapByReferenceBlock = {}; + if (mapByDataset) + for (var [referenceName, mapByScope] of mapByDataset) { + for (var [scope, blocks] of mapByScope) { + mapByReferenceBlock[blocks[0].id] = blocks; + if (true /*trace*/ ) + dLog('axesBlocks', referenceName, scope, blocks.mapBy('_internalModel.__data')); + } + } + dLog('axesBlocks', mapByDataset, mapByReferenceBlock); + return mapByReferenceBlock; + }), + + axesP : Ember.computed('axesBlocks.@each', function () { + let axesBlocks = this.get('axesBlocks'), + axisIDs = Object.keys(axesBlocks), + axesP = axisIDs.map((axisID) => axesBlocks[axisID][0]); + dLog('axesP2', axesP, axisIDs); + return axesP; + }), + + stacksCount : Ember.computed('block.stacksCount', 'block.viewed', 'axes2d.[]', 'axesP.length', function () { let count; let previous = this.get('previous.stacks'); + let axesP = this.get('axesP'); count = this.get('block.stacksCount'); dLog('stacks', count, stacks); dLog(stacks, stacks.axesPCount, 'stacksCount', stacks.stacksCount); @@ -28,7 +54,7 @@ export default Ember.Component.extend({ }), /** @return blocks which correspond to axes, i.e. are not child blocks. */ - axesP : Ember.computed('block.viewed', function () { + axesP_unused : Ember.computed('block.viewed', function () { let blockService = this.get('block'); let viewedBlocks = blockService.get('viewed'); // dLog('viewedBlocks', viewedBlocks); diff --git a/frontend/app/mixins/axis-position.js b/frontend/app/mixins/axis-position.js index 9e44ac435..fd4ca9ec9 100644 --- a/frontend/app/mixins/axis-position.js +++ b/frontend/app/mixins/axis-position.js @@ -30,12 +30,12 @@ export default Mixin.create({ currentPosition : undefined, lastDrawn : undefined, - init() { + init_1 : function() { let store = this.get('store'); this.set('currentPosition', store.createRecord('vline-position')); this.set('lastDrawn', store.createRecord('vline-position')); this._super(...arguments); - }, + }.on('init'), /* updateDomain() and setDomain() moved here from utils/stacks.js diff --git a/frontend/app/services/data/block.js b/frontend/app/services/data/block.js index 178d375b2..be5409410 100644 --- a/frontend/app/services/data/block.js +++ b/frontend/app/services/data/block.js @@ -28,7 +28,35 @@ function log_Map(label, map) { console.log(label, block_text(key), blocks && blocks.map(block_text)); }); } +/** log a Map string -> Map (string -> [block]) + */ +function log_Map_Map(label, map) { + map.forEach(function (value, key) { + console.log(label, 'key'); + log_Map('', value); + }); +} +/** Filter the values of the given map. If the filter result is undefined, omit + * the value from the result map. + * This is a combination of map and filter. + * @param map a Map + * @return a Map, or undefined if the result Map would be empty. + */ +function filterMap(map, mapFilterFn) { + /* factored out of viewedBlocksByReferenceAndScope(); similar to : + * https://stackoverflow.com/questions/48707227/how-to-filter-a-javascript-map/53065133 */ + let result; + for (let [key, value] of map) { + let newValue = mapFilterFn(value); + if (newValue) { + if (! result) + result = new Map(); + result.set(key, newValue); + } + } + return result; +} /*----------------------------------------------------------------------------*/ @@ -595,6 +623,110 @@ export default Service.extend(Ember.Evented, { return map; }), + /** collate the blocks by the parent block they refer to, and by scope. + * + * Uses datasetsByParent(); this is more direct than blocksByReference() + * which uses Block.referenceBlock(), which does peekAll on blocks and filters + * on datasetId and scope. + * + * @return a Map, indexed by dataset name, each value being a further map + * indexed by block scope, and the value of that being an array of blocks, + * with the [0] position of the array reserved for the reference block. + * + * @desc + * Blocks without a parent / reference, will be mapped via their datasetId, + * and will be placed in [0] of the blocks array for their scope. + */ + blocksByReferenceAndScope : Ember.computed( + 'blockValues.[]', + function() { + const fnName = 'blocksByReferenceAndScope'; + let map = this.get('blockValues') + .reduce( + (map, block) => { + // related : manage-explorer : datasetsToBlocksByScope() but that applies a filter. + let id = block.id, + scope = block.get('scope'), + /** If block is a promise, block.get('datasetId.parent') will be a + * promise - non-null regardless of whether the dataset has a + * .parent, whereas .get of .parent.name will return undefined iff + * there is no parent. + */ + parentName = block.get('datasetId.parent.name'); + + function blocksOfDatasetAndScope(datasetId, scope) { + let mapByScope = map.get(datasetId); + if (! mapByScope) { + mapByScope = new Map(); + map.set(datasetId, mapByScope); + } + let blocks = mapByScope.get(scope); + if (! blocks) + mapByScope.set(scope, blocks = [undefined]); // [0] is reference block + return blocks; + } + + if (parentName) { + let blocks = blocksOfDatasetAndScope(parentName, scope); + blocks.push(block); + } else { + let datasetName = block.get('datasetId.name'); + let blocks = blocksOfDatasetAndScope(datasetName, scope); + if (blocks[0]) { + // console.log(fnName, '() >1 reference blocks for scope', scope, blocks, block, map); + /* Reference chromosome assemblies, i.e. physical maps, define a + * unique (reference) block for each scope, whereas Genetic Maps + * are their own reference, and each block in a GM is both a + * data block and a reference block, and a GM may define + * multiple blocks with the same scope. + * As a provisional measure, if there is a name clash (multiple + * blocks with no parent and the same scope), append the + * datasetName name to the scope to make it unique. + */ + blocks = blocksOfDatasetAndScope(datasetName, scope + '_' + datasetName); + } + blocks[0] = block; + } + return map; + }, + new Map()); + + if (trace_block > 1) + log_Map_Map(fnName, map); + return map; + }), + + /** filter blocksByReferenceAndScope() for viewed blocks, + * @return Map, which may be empty + */ + viewedBlocksByReferenceAndScope : Ember.computed('blocksByReferenceAndScope', 'viewed.[]', function () { + const fnName = 'viewedBlocksByReferenceAndScope'; + let viewed = this.get('viewed'); + let map = this.get('blocksByReferenceAndScope'), + resultMap = filterMap(map, referencesMapFilter); + function referencesMapFilter(mapByScope) { + let resultMap = filterMap(mapByScope, scopesMapFilter); + if (trace_block > 2) + dLog('referencesMapFilter', mapByScope, resultMap); + return resultMap; + }; + function scopesMapFilter(blocks) { + // axis : blocks : [0] is included if any blocks[*] are viewed, ... + // && .isLoaded ? + let blocksOut = blocks.filter((b, i) => ((i===0) || b.get('isViewed'))); + // expect that blocksOut.length != 0 here. + if ((blocksOut.length === 1) && ! blocks[0].get('isViewed')) + blocksOut = undefined; + if (trace_block > 3) + dLog('scopesMapFilter', blocks, blocksOut); + return blocksOut; + }; + + if (trace_block && resultMap) + log_Map_Map(fnName, resultMap); + return resultMap; + }), + /** Search for the named features, and return also their blocks and datasets. * @return features (store object references) diff --git a/frontend/app/services/data/dataset.js b/frontend/app/services/data/dataset.js index af168d3bf..7a19da81c 100644 --- a/frontend/app/services/data/dataset.js +++ b/frontend/app/services/data/dataset.js @@ -4,6 +4,8 @@ import { task } from 'ember-concurrency'; const { inject: { service } } = Ember; +const dLog = console.debug; + // based on ./block.js export default Service.extend(Ember.Evented, { @@ -80,5 +82,54 @@ export default Service.extend(Ember.Evented, { console.log('values', records); return records; }) + , + /** Map the given array of objects by the result of the keyFn. + * @return Map of key values to objects. + */ + objectsMap : function (objects, keyFn) { + let + objectsMap = objects.reduce( + (map, o) => { + let block = keyFn(o), + axis = block.get('axis'), + stack = axis && axis.getStack(); + if (stack) { + let axes = map.get(stack); + if (! axes) + map.set(stack, axes = []); + axes.push(axis); + } + return map; }, + new Map() + ); + return objectsMap; + }, + /** Collate all loaded datasets by their parent name. + * Those without a parent name are mapped by the value null. + * This does not check if the named parent exists - use datasetsByName() for that. + * + * This will recalculate if the user uploads an additional dataset, which will + * not be frequent, so it is efficient to calculate this in advance. + * We could also reload when other users or system admins add datasets to the + * database, but again that is not expected to be frequent, and it is + * likely that this user will not need them in this session. + */ + datasetsByParent : Ember.computed('values.[]', function () { + let values = this.get('values'), + map = this.objectsMap(values, (d) => d.get('parent')); + dLog('datasetsByParent', map); + return map; + }), + datasetsByName : Ember.computed('values.[]', function () { + /** currently dataset.name is used as DB ID, so name lookup of datasets can + * also be done via peekRecord('dataset', name). + */ + let values = this.get('values'), + map = this.objectsMap(values, (d) => d.get('name')); + dLog('datasetsByName', map); + return map; + }), + + }); diff --git a/frontend/app/utils/stacks.js b/frontend/app/utils/stacks.js index 6e0f520f1..b610a36a8 100644 --- a/frontend/app/utils/stacks.js +++ b/frontend/app/utils/stacks.js @@ -302,6 +302,15 @@ Stacked.prototype.getAxis = function() Stacked.axis1dAdd = function (axisName, axis1dComponent) { axes1d[axisName] = axis1dComponent; }; +Stacked.prototype.getAxis1d = function () { + let axis1d = this.axis1d || (this.axis1d = axes1d[this.axisName]); + if (axis1d && (axis1d.isDestroying || axis1d.isDestroying)) { + dLog('getAxis1d() isDestroying', axis1d, this); + axis1d = this.axis1d = undefined; + delete axes1d[this.axisName]; + } + return axis1d; +} function positionToString(p) { return (p === undefined) ? "" @@ -2104,12 +2113,13 @@ Stacked.prototype.axisDimensions = function () /** y scale of this axis */ y = this.getY(), domain = this.y.domain(), - axis1d = this.axis1d, - dim = { domain, range : this.yRange(), zoomed : axis1d.zoomed}; + axis1d = this.getAxis1d(), + zoomed = axis1d && axis1d.zoomed, + dim = { domain, range : this.yRange(), zoomed}; let currentPosition = axis1d && axis1d.get('currentPosition'); if (! isEqual(domain, currentPosition.yDomain)) - dLog('axisDimensions', domain, currentPosition.yDomain, axis1d.zoomed, currentPosition); + dLog('axisDimensions', domain, currentPosition.yDomain, zoomed, currentPosition); return dim; }; /** Set the domain of the current position to the given domain @@ -2126,11 +2136,12 @@ Stacked.prototype.setDomain = function (domain) */ Stacked.prototype.setZoomed = function (zoomed) { - let axis1d = this.axis1d; + let axis1d = this.getAxis1d(); // later .zoomed may move into axis1d.currentPosition // if (! axisPosition) // dLog('setZoomed', this, 'zoomed', axis1d.zoomed, '->', zoomed, axis1d); - axis1d.setZoomed(zoomed); + if (axis1d) + axis1d.setZoomed(zoomed); }; Stacked.prototype.unviewBlocks = function () From 07cb47cb0d04c1667ff969069ecb78dad88f0881 Mon Sep 17 00:00:00 2001 From: Don Date: Mon, 3 Feb 2020 16:30:31 +1100 Subject: [PATCH 16/57] Add heatmap as a variation of bar chart axis-chart.js : add barAsHeatmap to featureCountDataProperties. app.scss : .featureCountData colour is (currently) heatmap, so limit fill:aqua (for rect.chartRow) to .blockData. chart1.js : add scales.xColour, renaming .x to .xWidth. switching scales to use rectWidth() for heatmap colour - works but some refactor seems called for. frontend/app/ 4dec1fe 9980 Feb 3 08:53 components/axis-chart.js f2ada59 30220 Feb 3 09:25 styles/app.scss 684cdf8 30996 Feb 3 10:48 utils/draw/chart1.js --- frontend/app/components/axis-chart.js | 3 ++- frontend/app/styles/app.scss | 5 +++- frontend/app/utils/draw/chart1.js | 35 +++++++++++++++++++++------ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/frontend/app/components/axis-chart.js b/frontend/app/components/axis-chart.js index cac4ea9ce..6d40aa5b6 100644 --- a/frontend/app/components/axis-chart.js +++ b/frontend/app/components/axis-chart.js @@ -32,7 +32,8 @@ const featureCountDataProperties = { blockName = block.view && block.view.longName(); return valueText + '\n' + blockName; }, - valueIsArea : true + valueIsArea : false, + barAsHeatmap : true }; const dataConfigs = diff --git a/frontend/app/styles/app.scss b/frontend/app/styles/app.scss index 905b208a4..ddc8fc757 100644 --- a/frontend/app/styles/app.scss +++ b/frontend/app/styles/app.scss @@ -245,10 +245,13 @@ g.axis-use > rect.chartRow */ rect.chartRow { - fill: aqua; opacity : 0.5; stroke: blue; } +/* .featureCountData colour is heatmap **/ +.blockData rect.chartRow { + fill: aqua; +} /* ---------------------------------- */ diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index 3d3ea7a92..8123a2632 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -401,6 +401,7 @@ AxisCharts.prototype.getRanges3 = function (resizedWidth) { */ let dataConfig = this.dataConfig, + scales = this.scales, width = drawSize.width, height = drawSize.height, xRange = [0, width], @@ -409,18 +410,25 @@ AxisCharts.prototype.getRanges3 = function (resizedWidth) { /* scaleBand would suit a data set with evenly spaced or ordinal / nominal y values. * yBand = d3.scaleBand().rangeRound(yRange).padding(0.1), */ - y = useLocalY ? this.scaleLinear(yRange, data) : this.scales.yAxis, - x = d3.scaleLinear().rangeRound(xRange); + y = useLocalY ? this.scaleLinear(yRange, data) : scales.yAxis; + scales.xWidth = + d3.scaleLinear().rangeRound(xRange); + if (dataConfig.barAsHeatmap) { + scales.xColour = d3.scaleOrdinal().range(d3.schemeCategory20); + } // datum2LocationScaled() uses me.scales.x rather than the value in the closure in which it was created. - this.scales.x = x; + scales.x = dataConfig.barAsHeatmap ? scales.xColour : scales.xWidth; + // Used by bars() - could be moved there, along with datum2LocationScaled(). - this.scales.y = y; + scales.y = y; console.log("Chart1", xRange, yRange, dataConfig.dataTypeName); let valueWidthFn = dataConfig.rectWidth.bind(dataConfig, /*scaled*/false, /*gIsData*/true), valueCombinedDomain = this.domain(valueWidthFn, data); - x.domain(valueCombinedDomain); + scales.xWidth.domain(valueCombinedDomain); + if (scales.xColour) + scales.xColour.domain(valueCombinedDomain); }; Chart1.prototype.drawContent = function () @@ -509,7 +517,9 @@ Chart1.prototype.drawAxes = function (chart, i, g) { // - handle .exit() for these 2 also .attr("class", "axis axis--x"); axisXa.merge(gs.selectAll("g > g.axis--x")) - .attr("transform", "translate(0," + height + ")") + .attr("transform", "translate(0," + height + ")"); + if (! dataConfig.barAsHeatmap) + axisXa .call(d3.axisBottom(x)); if (useLocalY) { @@ -559,6 +569,8 @@ Chart1.prototype.drawLine = function (blockId, block, data) dataConfig = this.dataConfig, block = this.block, g = this.g; + if (dataConfig.barAsHeatmap) + this.scales.x = this.scales.xColour; let rs = g // .select("g." + className + " > g") @@ -579,8 +591,14 @@ Chart1.prototype.drawLine = function (blockId, block, data) .attr("x", 0) .attr("y", (d) => { let li = dataConfig.datum2LocationScaled(d); return li.length ? li[0] : li; }) // yBand.bandwidth() - .attr("height", dataConfig.rectHeight.bind(dataConfig, /*scaled*/true, /*gIsData*/false)) // equiv : (d, i, g) => dataConfig.rectHeight(true, false, d, i, g) - .attr("width", dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false)); + .attr("height", dataConfig.rectHeight.bind(dataConfig, /*scaled*/true, /*gIsData*/false)) // equiv : (d, i, g) => dataConfig.rectHeight(true, false, d, i, g); + ; + let barWidth = dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false); + ra + .attr("width", dataConfig.barAsHeatmap ? 20 : barWidth); + if (dataConfig.barAsHeatmap) + ra + .attr('fill', barWidth); rx.remove(); console.log(rs.nodes(), re.nodes()); }; @@ -707,6 +725,7 @@ Chart1.prototype.drawLine = function (blockId, block, data) ChartLine.prototype.line = function (data) { let y = this.scales.y, dataConfig = this.dataConfig; + this.scales.x = this.scales.xWidth; let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, y); let line = d3.line() From 1007fde3a09be7f2f09d860b023a4049c3ff2852 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 4 Feb 2020 08:55:00 +1100 Subject: [PATCH 17/57] Add horizontal lines for effects probability data. block.js : scopesMapFilter() : filter out data blocks without a reference block for the corresponding scope. chart1.js : add linebars() (based on bars(), about 1/2 can be factored with bars), isEffectsData (recognise effects probabilities data based on data shape; this is provisional for development - we will add tags in the dataset or block meta to indicate data type and preferred display chart type, and user controls to vary that). frontend/app/ : 4fec0b6 1406 Feb 3 16:39 services/data/axis-brush.js 5297c4f 34298 Feb 3 20:46 services/data/block.js 5358ae3 32784 Feb 3 22:57 utils/draw/chart1.js e15fe83 72977 Feb 3 21:11 utils/stacks.js --- frontend/app/services/data/axis-brush.js | 4 +- frontend/app/services/data/block.js | 11 ++++- frontend/app/utils/draw/chart1.js | 52 +++++++++++++++++++++++- frontend/app/utils/stacks.js | 3 +- 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/frontend/app/services/data/axis-brush.js b/frontend/app/services/data/axis-brush.js index 526c5de9e..6e1bd4982 100644 --- a/frontend/app/services/data/axis-brush.js +++ b/frontend/app/services/data/axis-brush.js @@ -34,8 +34,8 @@ export default Service.extend(Ember.Evented, { dLog('brushedAxes', this); const fnName = 'brushedAxes'; let records = this.get('all'); - // if (trace_axisBrush > 1) - records.map(function (a) { console.log(fnName, a, a.get('brushedDomain')); } ); + if (trace_axisBrush > 1) + records.map(function (a) { dLog(fnName, a, a.get('brushedDomain')); } ); if (trace_axisBrush) dLog('viewed Blocks', records); let blocks = records.filter(function (ab) { diff --git a/frontend/app/services/data/block.js b/frontend/app/services/data/block.js index be5409410..8200d07ec 100644 --- a/frontend/app/services/data/block.js +++ b/frontend/app/services/data/block.js @@ -575,7 +575,7 @@ export default Service.extend(Ember.Evented, { * These are suited to be rendered by axis-chart. */ viewedChartable: Ember.computed( - 'viewed.[]', + 'viewed.@each.{featuresCounts,isChartable}', function() { let records = this.get('viewed') @@ -715,7 +715,14 @@ export default Service.extend(Ember.Evented, { // && .isLoaded ? let blocksOut = blocks.filter((b, i) => ((i===0) || b.get('isViewed'))); // expect that blocksOut.length != 0 here. - if ((blocksOut.length === 1) && ! blocks[0].get('isViewed')) + /* If a data block's dataset's parent does not have a reference block for + * the corresponding scope, then blocks[0] will be undefined, which is + * filtered out here. + * Some options for conveying the error to the user : perhaps display in + * the dataset explorer with an error status for the data block, e.g. the + * add button for the data block could be insensitive. + */ + if ((blocksOut.length === 1) && (! blocks[0] || ! blocks[0].get('isViewed'))) blocksOut = undefined; if (trace_block > 3) dLog('scopesMapFilter', blocks, blocksOut); diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index 8123a2632..1ee32bc82 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -602,6 +602,53 @@ Chart1.prototype.drawLine = function (blockId, block, data) rx.remove(); console.log(rs.nodes(), re.nodes()); }; + /** A single horizontal line for each data point. + * Position is similar to the rectangle which would be drawn by bars(): + * X limits are the same as the rectangle limits (left, right) + * and Y position is at the middle of the equivalent rectangle. + */ + ChartLine.prototype.linebars = function (data) + { + let + dataConfig = this.dataConfig, + block = this.block, + scales = this.scales, + g = this.g; + let + rs = g + // .select("g." + className + " > g") + .selectAll("path." + dataConfig.barClassName) + .data(data), + re = rs.enter(), rx = rs.exit(); + let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, scales.y); + let line = d3.line(); + + function horizLine(d, i, g) { + let barWidth = dataConfig.rectWidth(/*scaled*/true, /*gIsData*/false, d, i, g); + let y = middle(datum2LocationScaled(d)), + l = line([ + [0, y], + [barWidth, y]]); + return [l]; + } + + let ra = re + .append("path"); + ra + .attr("class", dataConfig.barClassName) + // same comment re parent datum as for bars() + .each(function (d) { configureHorizTickHover.apply(this, [d, block, dataConfig.hoverTextFn]); }); + ra + .merge(rs) + .transition().duration(1500) + .attr('d', horizLine) + .attr("stroke", 'red') + ; + + rx.remove(); + console.log(rs.nodes(), re.nodes()); + }; + /** Calculate the height of rectangle to be used for this data point * @param this is DataConfig, not DOM element. * @param scaled true means apply scale (x) to the result @@ -773,7 +820,10 @@ Chart1.prototype.drawLine = function (blockId, block, data) { let data = this.currentData; - let chartDraw = barsLine ? this.bars : this.line; + /** The effects data takes the form of an array of 5 probabilities, in the 3rd element of feature.value */ + let isEffectsData = data.length && data[0].name && data[0].value && (data[0].value.length === 3) && (data[0].value[2].length === 6); + let bars = isEffectsData ? this.linebars : this.bars; + let chartDraw = barsLine ? bars : this.line; chartDraw.apply(this, [data]); }; diff --git a/frontend/app/utils/stacks.js b/frontend/app/utils/stacks.js index b610a36a8..6abe9519e 100644 --- a/frontend/app/utils/stacks.js +++ b/frontend/app/utils/stacks.js @@ -646,7 +646,8 @@ Stacked.prototype.getDomain = function () * Also this.domain above is not recalculated after additional features are received, * whereas blocksDomain has the necessary dependency. */ - let blocksDomain = this.axis1d && this.axis1d.get('blocksDomain'); + let axis1d = this.getAxis1d(); + let blocksDomain = axis1d && axis1d.get('blocksDomain'); if (blocksDomain && blocksDomain.length) { dLog('getDomain()', this.axisName, domain, blocksDomain); domain = d3.extent(domain.concat(blocksDomain)); From f0108d9268d0dc76983404a74f65dd4a18fd8ee3 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 4 Feb 2020 17:07:17 +1100 Subject: [PATCH 18/57] setup data to enable per-block layout and colour shift param blocks from drawChart() to setupChart(), and also move calls : createLine() and group(). split createLine() out of data(). group() : place each chart-line in a separate g; g.axis-chart is renamed to .chart-line, given block id, and datum is chartLine. frontend/app/ : 0c051be 9980 Feb 4 16:14 components/axis-chart.js dee88dd 33535 Feb 4 16:47 utils/draw/chart1.js --- frontend/app/components/axis-chart.js | 8 +-- frontend/app/utils/draw/chart1.js | 81 +++++++++++++++++---------- 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/frontend/app/components/axis-chart.js b/frontend/app/components/axis-chart.js index 6d40aa5b6..9bb9721d0 100644 --- a/frontend/app/components/axis-chart.js +++ b/frontend/app/components/axis-chart.js @@ -151,10 +151,10 @@ export default InAxis.extend({ let blocks = this.get('blocks'); setupChart( - this.get('axisID'), axisCharts, chart, data, + this.get('axisID'), axisCharts, chart, data, blocks, dataConfig, this.get('yAxisScale'), /*resizedWidth*/undefined); - drawChart(axisCharts, chart, data, blocks); + drawChart(axisCharts, chart, data); } }); const showChartAxes = true; @@ -267,8 +267,8 @@ export default InAxis.extend({ resizedWidth = this.get('width'), chart1 = this.get('charts')[dataTypeName]; chart1 = setupChart( - axisID, axisCharts, chart1, chartData, dataConfig, yAxisScale, resizedWidth); - drawChart(axisCharts, chart1, chartData, blocks); + axisID, axisCharts, chart1, chartData, blocks, dataConfig, yAxisScale, resizedWidth); + drawChart(axisCharts, chart1, chartData); if (! this.get('charts') && chart1) this.set('charts', chart1); }, diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index 1ee32bc82..b52636497 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -115,8 +115,7 @@ class DataConfig { }; class ChartLine { - constructor(g, dataConfig, scales) { - this.g = g; + constructor(dataConfig, scales) { this.dataConfig = dataConfig; /* the scales have the same .range() as the parent Chart1, but the .domain() varies. */ this.scales = scales; // Object.assign({}, scales); @@ -208,12 +207,8 @@ function setupFrame(axisID, axisCharts, charts, allocatedWidth) axisCharts.frame(axisCharts.ranges.bbox, charts, allocatedWidth); axisCharts.getRanges2(); - - axisCharts.group(axisCharts.dom.gca, 'axis-chart', charts); - // place controls after the ChartLine-s group, so that the toggle is above the bars and can be accessed. - axisCharts.controls(); } -function setupChart(axisID, axisCharts, chart1, chartData, dataConfig, yAxisScale, resizedWidth) +function setupChart(axisID, axisCharts, chart1, chartData, blocks, dataConfig, yAxisScale, resizedWidth) { // Plan is for Axischarts to own .ranges, but for now there is some overlap. if (! chart1.ranges) { @@ -222,28 +217,46 @@ function setupChart(axisID, axisCharts, chart1, chartData, dataConfig, yAxisScal } chart1.scales.yAxis = yAxisScale; + //---------------------------------------------------------------------------- - chart1.getRanges(axisCharts.ranges, chartData); + /* ChartLine:setup() makes a copy of Chart1's .dataConfig, so augment it + * before then (called in createLine()). + */ /* pasteProcess() may set .valueName, possibly provided by GUI; * i.e. Object.values(chartData).mapBy('valueName').filter(n => n) * and that can be passed to addedDefaults(). */ dataConfig.addedDefaults(); + //---------------------------------------------------------------------------- + let + blocksById = blocks.reduce( + (result, block) => { result[block.get('id')] = block; return result; }, []), + blockIds = Object.keys(chartData); + blockIds.forEach((blockId) => { + let block = blocksById[blockId]; + chart1.createLine(blockId, block); + }); + chart1.group(axisCharts.dom.gca, 'chart-line'); + // place controls after the ChartLine-s group, so that the toggle is above the bars and can be accessed. + axisCharts.controls(); + //---------------------------------------------------------------------------- + + + + chart1.getRanges(axisCharts.ranges, chartData); + return chart1; }; -function drawChart(axisCharts, chart1, chartData, blocks) +function drawChart(axisCharts, chart1, chartData) { /** possibly don't (yet) have chartData for each of blocks[], * i.e. blocksById may be a subset of blocks.mapBy('id'). */ - let blockIds = Object.keys(chartData), - blocksById = blocks.reduce( - (result, block) => { result[block.get('id')] = block; return result; }, []); + let blockIds = Object.keys(chartData); blockIds.forEach((blockId) => { - let block = blocksById[blockId]; - chart1.data(blockId, block, chartData[blockId]); + chart1.data(blockId, chartData[blockId]); }); chart1.prepareScales(chartData, axisCharts.ranges.drawSize); @@ -455,28 +468,31 @@ AxisCharts.prototype.getRanges3 = function (resizedWidth) { dataConfig.datum2ValueScaled = datum2ValueScaled; }; -AxisCharts.prototype.group = function (parentG, groupClassName, charts) { - /** parentG is g.axis-use. add g.(groupClassName); - * within parentG there is also a sibling g.axis-html. */ +Chart1.prototype.group = function (parentG, groupClassName) { + /** parentG is g.{{dataTypeName}}, within : g.axis-use > g.chart > g[clip-path] > g.{{dataTypeName}}. + * add g.(groupClassName); + * within g.axis-use there is also a sibling g.axis-html. */ let // data = parentG.data(), gs = parentG - /* .selectAll("g > g." + groupClassName) - .data(data), // inherit g.datum(), or perhaps [groupClassName] - */, + .data((chart) => Object.values(chart.chartLines)), gsa = gs -// .enter() + .enter() .append("g") // maybe drop this g, move margin calc to gp // if drawing internal chart axes then move them inside the clip rect // .attr("transform", "translate(" + margin.left + "," + margin.top + ")"), - .attr("class", groupClassName), - g = parentG.selectAll("g > g." + groupClassName); // gsa.merge(gs); - dLog('group', this, parentG, g.node()); + .attr("class", (chartLine) => groupClassName) + .attr('id', (chartLine) => groupClassName + '-' + chartLine.block.id) + // .data((chartLine) => chartLine.currentData) + , + // parentG.selectAll("g > g." + groupClassName); // + g = gsa.merge(gs); + dLog('group', this, parentG.node(), parentG, g.node()); this.dom.g = g; this.dom.gs = gs; this.dom.gsa = gsa; - // set Chart1 .g; passed to ChartLine() and used by ChartLine.{lines,bars}. - gsa.each(function(chart, i) { chart.g = d3.select(this) ; } ); + // set ChartLine .g; used by ChartLine.{lines,bars}. + gsa.each(function(chartLine, i) { chartLine.g = d3.select(this) ; } ); return g; }; @@ -541,16 +557,22 @@ Chart1.prototype.drawAxes = function (chart, i, g) { * @param block hover * used in Chart1:bars() for hover text. passed to hoverTextFn() for .longName() */ -Chart1.prototype.data = function (blockId, block, data) + +Chart1.prototype.createLine = function (blockId, block) { let chartLine = this.chartLines[blockId]; - if (! chartLine) - chartLine = this.chartLines[blockId] = new ChartLine(this.g, this.dataConfig, this.scales); + if (! chartLine) + chartLine = this.chartLines[blockId] = new ChartLine(this.dataConfig, this.scales); if (block) { chartLine.block = block; chartLine.setup(blockId); // .setup() will copy dataConfig if need for custom config. } +}; +Chart1.prototype.data = function (blockId, data) +{ + let chartLine = this.chartLines[blockId]; + function m(d) { return middle(chartLine.dataConfig.datum2Location(d)); } data = data.sort((a,b) => m(a) - m(b)); data = chartLine.filterToZoom(data); @@ -897,6 +919,7 @@ AxisCharts.prototype.frame = function container(bbox, charts, allocatedWidth) addParentClass(g); /* .gc is ​​ * .g (assigned later) is g.axis-chart +.chart-line ? * .gca contains a g for each chartType / dataTypeName, i.e. per Chart1. */ this.dom.gc = g; From edc4952aa5c7a906e5d85d7057680db5a2b7d7f0 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 4 Feb 2020 22:39:53 +1100 Subject: [PATCH 19/57] block colours and zoom update. redraw(): use drawContent() (previous version renamed, drop drawBlockFeaturesCounts()), add chartHandlesFromDom(). Revert featureCountData to bars (used to test heatmap in previous versions). chart1.js : add blockColour(). frontend/app/ : 8e10574 10146 Feb 4 22:27 components/axis-chart.js 4c80091 30226 Feb 4 18:50 styles/app.scss 6782797 33945 Feb 4 18:45 utils/draw/chart1.js --- frontend/app/components/axis-chart.js | 44 +++++++++++++++------------ frontend/app/styles/app.scss | 2 +- frontend/app/utils/draw/chart1.js | 17 +++++++++-- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/frontend/app/components/axis-chart.js b/frontend/app/components/axis-chart.js index 9bb9721d0..4189e03f3 100644 --- a/frontend/app/components/axis-chart.js +++ b/frontend/app/components/axis-chart.js @@ -32,8 +32,7 @@ const featureCountDataProperties = { blockName = block.view && block.view.longName(); return valueText + '\n' + blockName; }, - valueIsArea : false, - barAsHeatmap : true + valueIsArea : true }; const dataConfigs = @@ -121,7 +120,7 @@ export default InAxis.extend({ let blocksData = this.get('blocksData'), chartTypes = this.get('chartTypes'), charts = this.get('charts'); - let axisCharts = this.get('axisCharts'); + let axisCharts = this.get('axisCharts'); chartTypes.forEach((typeName) => { if (! charts[typeName]) { @@ -147,7 +146,7 @@ export default InAxis.extend({ chart = charts[typeName]; if (! chart.ranges) { let data = blocksData.get(typeName), - dataConfig = chart.dataConfig; + dataConfig = chart.dataConfig; let blocks = this.get('blocks'); setupChart( @@ -164,20 +163,31 @@ export default InAxis.extend({ return chartTypes; }), - drawBlockFeaturesCounts : function(featuresCounts) { - if (! featuresCounts) - featuresCounts = this.get('featuresCounts'); - let domain = this.get('axis.axis1d.domainChanged'); - if (featuresCounts) { - console.log('drawBlockFeaturesCounts', featuresCounts.length, domain, this.get('block.id')); - // pass alternate dataConfig to layoutAndDrawChart(), defining alternate functions for {datum2Value, datum2Location } - this.layoutAndDrawChart(featuresCounts, 'featureCountData'); - } + /** Retrieve charts handles from the DOM. + * This could be used as verification - the result should be the same as + * this.get('charts'). + */ + chartHandlesFromDom () { + let axisUse = this.get('axis.axisUse'), + g = axisUse.selectAll('g.chart > g[clip-path] > g'), + charts = g.data(); + return charts; }, - + /** Called via in-axis:{zoomed or resized}() -> redrawDebounced() -> redrawOnce() + * Redraw the chart content. + * In the case of resize, the chart frame will need to be resized (not yet + * done; the setup functions are designed to be called again - in that case + * they will update sizes rather than add new elements). + */ redraw : function(axisID, t) { + let charts = this.get('charts'); + /* y axis has been updated, so redrawing the content will update y positions. */ + Object.values(charts).forEach((chart) => chart.drawContent()); + }, + /** for use with @see pasteProcess() */ + redraw_from_paste : function(axisID, t) { let data = this.get(className), layoutAndDrawChart = this.get('layoutAndDrawChart'); if (data) { @@ -185,12 +195,6 @@ export default InAxis.extend({ if (data) layoutAndDrawChart.apply(this, [data, undefined]); } - else { // use block.features or block.featuresCounts when not using data parsed from table. - if (this.get('block.isChartable')) - this.drawBlockFeatures0(); - else - this.drawBlockFeaturesCounts(); - } }, /*--------------------------------------------------------------------------*/ diff --git a/frontend/app/styles/app.scss b/frontend/app/styles/app.scss index ddc8fc757..af9539361 100644 --- a/frontend/app/styles/app.scss +++ b/frontend/app/styles/app.scss @@ -258,7 +258,7 @@ rect.chartRow { path.chartRow.line { fill : none; - stroke : magenta; + /* stroke : magenta; */ stroke-linejoin : round; stroke-linecap : round; stroke-width : 1.5; diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index b52636497..a9cc764d0 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -585,6 +585,15 @@ Chart1.prototype.drawLine = function (blockId, block, data) let chartLine = this.chartLines[blockId]; chartLine.drawContent(this.barsLine); }; + +ChartLine.prototype.blockColour = function () +{ + let blockS = this.block && this.block.get('view'), + /* For axes without a reference, i.e. GMs, there is a single data block with colour===undefined. */ + colour = (blockS && blockS.axisTitleColour()) || 'red'; + return colour; +}; + ChartLine.prototype.bars = function (data) { let @@ -603,6 +612,7 @@ Chart1.prototype.drawLine = function (blockId, block, data) .append("rect"); ra .attr("class", dataConfig.barClassName) + .attr("fill", (d) => this.blockColour()) /** parent datum is currently 1, but could be this.block; * this.parentElement.parentElement.__data__ has the axis id (not the blockId), */ @@ -664,7 +674,7 @@ Chart1.prototype.drawLine = function (blockId, block, data) .merge(rs) .transition().duration(1500) .attr('d', horizLine) - .attr("stroke", 'red') + .attr("stroke", (d) => this.blockColour()) ; rx.remove(); @@ -807,11 +817,12 @@ Chart1.prototype.drawLine = function (blockId, block, data) g = this.g, ps = g .selectAll("g > path." + dataConfig.barClassName) - .data([1]); + .data([this]); ps .enter() .append("path") .attr("class", dataConfig.barClassName + " line") + .attr("stroke", (d) => this.blockColour()) .datum(data) .attr("d", line) .merge(ps) @@ -829,7 +840,7 @@ Chart1.prototype.drawLine = function (blockId, block, data) this.barsLine = ! this.barsLine; this.chartTypeToggle .classed("pushed", this.barsLine); - this.g.selectAll("g > *").remove(); + this.dom.g.selectAll("g > *").remove(); Object.keys(this.chartLines).forEach((blockId) => { let chartLine = this.chartLines[blockId]; chartLine.drawContent(this.barsLine); From d89def54c325765567297be757b8ac001569e7e7 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 4 Feb 2020 23:01:34 +1100 Subject: [PATCH 20/57] setup for splitting axis-chart{,s}.js rename axis-chart{,s}.js since most of the content will be in axis-charts.js --- frontend/app/components/{axis-chart.js => axis-charts.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/app/components/{axis-chart.js => axis-charts.js} (100%) diff --git a/frontend/app/components/axis-chart.js b/frontend/app/components/axis-charts.js similarity index 100% rename from frontend/app/components/axis-chart.js rename to frontend/app/components/axis-charts.js From 3a0b7795e9b3bcc532de996c5a1f95e66e788eee Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 4 Feb 2020 23:33:46 +1100 Subject: [PATCH 21/57] for split, also rename axis-chart{,s}.hbs as commented in previous commit. --- .../app/templates/components/{axis-chart.hbs => axis-charts.hbs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/app/templates/components/{axis-chart.hbs => axis-charts.hbs} (100%) diff --git a/frontend/app/templates/components/axis-chart.hbs b/frontend/app/templates/components/axis-charts.hbs similarity index 100% rename from frontend/app/templates/components/axis-chart.hbs rename to frontend/app/templates/components/axis-charts.hbs From 3b910cf498ef7a3d3abbe08588eb95d1c69d9634 Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 6 Feb 2020 18:10:59 +1100 Subject: [PATCH 22/57] connect resizeEffect in split axis axis-charts.js : use zoomedDomainEffect, allocatedWidth. chartTypesEffect -> chartsArray, draw(). split out (singular) axis-chart, with each chartsArray in hbs chart1.js: Adopt setupFrame() as a method of AxisCharts; ditto setupChart() and drawChart() -> Chart1. setupChart() : split overlap() out, and move call to controls() out. in-axis.js : make available : scaleChanged, domainChanged, zoomedDomain. split out to form data-types.js : from axis-charts.js: featureCountDataProperties, dataConfigs, from chart1: featureLocation(), middle(), scaleMaybeInterval(), hoverTextFn(), DataConfig, name2Location(), datum2LocationWithBlock(), datum2Value(), parsedData, blockData, blockDataConfig(). frontend/app/ : 35b5dee 13330 Feb 5 06:34 components/axis-2d.js d4fcd13 607 Feb 5 12:08 components/axis-chart.js 3025f5a 11149 Feb 6 18:02 components/axis-charts.js f265212 7010 Feb 5 11:23 components/in-axis.js 9d1b020 1943 Feb 5 06:34 templates/components/axis-2d.hbs f123624 524 Feb 5 11:10 templates/components/axis-charts.hbs a9c6323 5302 Feb 6 15:11 utils/data-types.js dc1d912 30582 Feb 6 14:33 utils/draw/chart1.js --- frontend/app/components/axis-2d.js | 6 + frontend/app/components/axis-chart.js | 30 +++ frontend/app/components/axis-charts.js | 176 ++++++++++-------- frontend/app/components/in-axis.js | 10 + frontend/app/templates/components/axis-2d.hbs | 4 +- .../app/templates/components/axis-charts.hbs | 7 +- frontend/app/utils/data-types.js | 168 +++++++++++++++++ frontend/app/utils/draw/chart1.js | 170 ++++------------- 8 files changed, 352 insertions(+), 219 deletions(-) create mode 100644 frontend/app/components/axis-chart.js create mode 100644 frontend/app/utils/data-types.js diff --git a/frontend/app/components/axis-2d.js b/frontend/app/components/axis-2d.js index 229abfbc0..6e71d25b4 100644 --- a/frontend/app/components/axis-2d.js +++ b/frontend/app/components/axis-2d.js @@ -262,6 +262,12 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, { this.set('childWidths', Ember.Object.create()); }, + /*--------------------------------------------------------------------------*/ + + resizeEffect : Ember.computed.alias('drawMap.resizeEffect'), + + /*--------------------------------------------------------------------------*/ + didInsertElement() { let oa = this.get('data'), axisUse = oa.svgContainer.selectAll("g.axis-outer#id"+this.get('axisID')), diff --git a/frontend/app/components/axis-chart.js b/frontend/app/components/axis-chart.js new file mode 100644 index 000000000..782658399 --- /dev/null +++ b/frontend/app/components/axis-chart.js @@ -0,0 +1,30 @@ +import Ember from 'ember'; +const { inject: { service } } = Ember; + +import InAxis from './in-axis'; +import { className, AxisCharts, setupFrame, setupChart, drawChart, Chart1, blockData, parsedData } from '../utils/draw/chart1'; +import { DataConfig, dataConfigs } from '../utils/data-types'; + +/*----------------------------------------------------------------------------*/ + +const dLog = console.debug; + + +/*----------------------------------------------------------------------------*/ + + +/* global d3 */ + +export default Ember.Component.extend({ + + didRender() { + this.draw(); + }, + draw() { + + }, + + + + +}); diff --git a/frontend/app/components/axis-charts.js b/frontend/app/components/axis-charts.js index 4189e03f3..f9345f97e 100644 --- a/frontend/app/components/axis-charts.js +++ b/frontend/app/components/axis-charts.js @@ -2,44 +2,13 @@ import Ember from 'ember'; const { inject: { service } } = Ember; import InAxis from './in-axis'; -import { className, AxisCharts, setupFrame, setupChart, drawChart, Chart1, DataConfig, blockData, parsedData } from '../utils/draw/chart1'; +import { className, AxisCharts, Chart1 } from '../utils/draw/chart1'; +import { DataConfig, dataConfigs, blockData, parsedData } from '../utils/data-types'; /*----------------------------------------------------------------------------*/ const dLog = console.debug; -/*----------------------------------------------------------------------------*/ - -/** example element of array f : */ -const featureCountDataExample = - { - "_id": { - "min": 100, - "max": 160 - }, - "count": 109 - }; - -const featureCountDataProperties = { - dataTypeName : 'featureCountData', - datum2Location : function datum2Location(d) { return [d._id.min, d._id.max]; }, - datum2Value : function(d) { return d.count; }, - /** datum2Description() is not used; possibly intended for the same - * purpose as hoverTextFn(), so they could be assimilated. */ - datum2Description : function(d) { return JSON.stringify(d._id); }, - hoverTextFn : function (d, block) { - let valueText = '[' + d._id.min + ',' + d._id.max + '] : ' + d.count, - blockName = block.view && block.view.longName(); - return valueText + '\n' + blockName; - }, - valueIsArea : true -}; - -const dataConfigs = - [featureCountDataProperties, blockData, parsedData] - .reduce((result, properties) => { result[properties.dataTypeName] = new DataConfig(properties); return result; }, [] ); - - /*----------------------------------------------------------------------------*/ @@ -75,12 +44,14 @@ export default InAxis.extend({ init() { this._super(...arguments); + this.set('blocksData', Ember.Object.create()); this.set('charts', Ember.Object.create()); }, didRender() { console.log("components/axis-chart didRender()"); + this.draw(); }, axisID : Ember.computed.alias('axis.axisID'), @@ -105,6 +76,8 @@ export default InAxis.extend({ }), axisCharts : Ember.computed(function () { + /* axisID isn't planned to change for this component; could set this up in + * init(); using a CP has the benefit of lazy-evaluation. */ return new AxisCharts(this.get('axisID')); }), @@ -115,64 +88,102 @@ export default InAxis.extend({ dLog('chartTypes', chartTypes); return chartTypes; }), - - chartTypesEffect : Ember.computed('chartTypes.[]', function () { - let blocksData = this.get('blocksData'), - chartTypes = this.get('chartTypes'), - charts = this.get('charts'); - let axisCharts = this.get('axisCharts'); - - chartTypes.forEach((typeName) => { - if (! charts[typeName]) { - let data = blocksData.get(typeName), - /** same as Chart1.blockIds(). */ - blockIds = Object.keys(data), - /** may use firstBlock for isBlockData. */ - firstBlock = data[blockIds[0]], - - dataConfig = dataConfigs[typeName], - parentG = this.get('axisCharts.dom.g'), // this.get('gAxis'), - chart = new Chart1(parentG, dataConfig); - charts[typeName] = chart; + chartsArray : Ember.computed('chartTypes.[]', function () { + /* The result is roughly equivalent to Object.values(this.get('charts')), + * but for any chartType which doesn't have an element in .charts, add + * it. */ + let + chartTypes = this.get('chartTypes'), + charts = chartTypes.map((typeName) => { + let chart = this.charts[typeName]; + if (! chart) { + let + dataConfig = dataConfigs[typeName], + parentG = this.get('axisCharts.dom.g'); // this.get('gAxis'), + chart = this.charts[typeName] = new Chart1(parentG, dataConfig); + let axisCharts = this.get('axisCharts'); + chart.overlap(axisCharts); } + return chart; }); - setupFrame( - this.get('axisID'), axisCharts, - charts, this.get('allocatedWidth')); + return charts; + }), + + /** Retrieve charts handles from the DOM. + * This could be used as verification - the result should be the same as + * this.get('chartsArray'). + */ + chartHandlesFromDom () { + /** this value is currently the g.axis-outer, which is 2 + * levels out from the g.axis-use, so this is a misnomer - + * will change either the name or the value. + * The result is the same because there is just 1 g.chart inside 'g.axis-outer > g.axis-all > g.axis-use'. + */ + let axisUse = this.get('axis.axisUse'), + g = axisUse.selectAll('g.chart > g[clip-path] > g'), + charts = g.data(); + return charts; + }, + + resizeEffectHere : Ember.computed('resizeEffect', function () { + dLog('resizeEffectHere in axis-charts', this.get('axisID')); + }), + zoomedDomainEffect : Ember.computed('zoomedDomain', function () { + dLog('zoomedDomainEffect in axis-charts', this.get('axisID')); + this.drawContent(); + }), + + draw() { + // probably this function can be factored out as AxisCharts:draw() + let axisCharts = this.get('axisCharts'), + charts = this.get('charts'), + allocatedWidth = this.get('allocatedWidth'); + axisCharts.setupFrame( + this.get('axisID'), + charts, allocatedWidth); + + let + chartTypes = this.get('chartTypes'), + // equiv : charts && Object.keys(charts).length, + nCharts = chartTypes && chartTypes.length; + if (nCharts) + allocatedWidth /= nCharts; chartTypes.forEach((typeName) => { + // this function could be factored out as axis-chart:draw() let chart = charts[typeName]; - if (! chart.ranges) { - let data = blocksData.get(typeName), + /*if (! chart.ranges)*/ { + let + blocksData = this.get('blocksData'), + data = blocksData.get(typeName), dataConfig = chart.dataConfig; let blocks = this.get('blocks'); - setupChart( - this.get('axisID'), axisCharts, chart, data, blocks, - dataConfig, this.get('yAxisScale'), /*resizedWidth*/undefined); + chart.setupChart( + this.get('axisID'), axisCharts, data, blocks, + dataConfig, this.get('yAxisScale'), allocatedWidth); - drawChart(axisCharts, chart, data); + chart.drawChart(axisCharts, data); } }); + + /** drawAxes() uses the x scale updated in drawChart() -> prepareScales(), called above. */ const showChartAxes = true; if (showChartAxes) axisCharts.drawAxes(charts); - return chartTypes; - }), + // place controls after the ChartLine-s group, so that the toggle is above the bars and can be accessed. + axisCharts.controls(); + }, - /** Retrieve charts handles from the DOM. - * This could be used as verification - the result should be the same as - * this.get('charts'). - */ - chartHandlesFromDom () { - let axisUse = this.get('axis.axisUse'), - g = axisUse.selectAll('g.chart > g[clip-path] > g'), - charts = g.data(); - return charts; + drawContent() { + let charts = this.get('chartsArray'); + /* y axis has been updated, so redrawing the content will update y positions. */ + if (charts) + charts.forEach((chart) => chart.drawContent()); }, /** Called via in-axis:{zoomed or resized}() -> redrawDebounced() -> redrawOnce() @@ -182,9 +193,7 @@ export default InAxis.extend({ * they will update sizes rather than add new elements). */ redraw : function(axisID, t) { - let charts = this.get('charts'); - /* y axis has been updated, so redrawing the content will update y positions. */ - Object.values(charts).forEach((chart) => chart.drawContent()); + this.drawContent(); }, /** for use with @see pasteProcess() */ redraw_from_paste : function(axisID, t) { @@ -261,6 +270,11 @@ export default InAxis.extend({ return result; }, + /** this function was used in all cases in the original development, but is + * now restricted to pasteProcess() / redraw_from_paste(); the paste + * functionality is not a current focus, so this is not up to date with some + * changes. + */ layoutAndDrawChart(chartData, dataTypeName) { let axisID = this.get("axisID"), @@ -270,9 +284,15 @@ export default InAxis.extend({ dataConfig = dataConfigs[dataTypeName], resizedWidth = this.get('width'), chart1 = this.get('charts')[dataTypeName]; - chart1 = setupChart( - axisID, axisCharts, chart1, chartData, blocks, dataConfig, yAxisScale, resizedWidth); - drawChart(axisCharts, chart1, chartData); + /* These have been split out of setupChart() and hence will need to be added as calls here : + */ + chart1.overlap(axisCharts); + axisCharts.controls(); + /* setupFrame() is now a method of AxisCharts; setupChart() and drawChart() are now methods of Chart1. + */ + chart1 = chart1.setupChart( + axisID, axisCharts, chartData, blocks, dataConfig, yAxisScale, resizedWidth); + chart1.drawChart(axisCharts, chartData); if (! this.get('charts') && chart1) this.set('charts', chart1); }, diff --git a/frontend/app/components/in-axis.js b/frontend/app/components/in-axis.js index 0304e936a..17d070065 100644 --- a/frontend/app/components/in-axis.js +++ b/frontend/app/components/in-axis.js @@ -53,6 +53,16 @@ export default Ember.Component.extend({ /*--------------------------------------------------------------------------*/ + /** axis is the axis-2d; this is passed into axis-tracks and axis-charts; + * can be passed into subComponents also. */ + axis1d : Ember.computed.alias('axis.axis1d'), + /** y scale of axis has changed */ + scaleChanged : Ember.computed.alias('axis1d.scaleChanged'), + domainChanged : Ember.computed.alias('axis1d.domainChanged'), + zoomedDomain : Ember.computed.alias('axis1d.zoomedDomain'), + + /*--------------------------------------------------------------------------*/ + getRanges(margin) { // initial version supports only 1 split axis; next identify axis by axisID (and possibly stack id) diff --git a/frontend/app/templates/components/axis-2d.hbs b/frontend/app/templates/components/axis-2d.hbs index ba304527a..67e408e91 100644 --- a/frontend/app/templates/components/axis-2d.hbs +++ b/frontend/app/templates/components/axis-2d.hbs @@ -35,9 +35,9 @@
axis-2d :{{this}}, {{axisID}}, {{targetEltId}}, {{subComponents.length}} :
subComponents : - {{axis-chart data=data axis=this axisID=axisID childWidths=childWidths allocatedWidths=allocatedWidths blocks=viewedChartable}} + {{axis-charts data=data axis=this axisID=axisID childWidths=childWidths allocatedWidths=allocatedWidths blocks=viewedChartable resizeEffect=resizeEffect}} {{log 'blockService' blockService 'viewedChartable' blockService.viewedChartable}} - {{axis-tracks axis=this axisID=axisID childWidths=childWidths allocatedWidths=allocatedWidths trackBlocksR=dataBlocks}} + {{axis-tracks axis=this axisID=axisID childWidths=childWidths allocatedWidths=allocatedWidths trackBlocksR=dataBlocks resizeEffect=resizeEffect}} {{#each subComponents as |subComponent|}} {{subComponent}} {{/each}} diff --git a/frontend/app/templates/components/axis-charts.hbs b/frontend/app/templates/components/axis-charts.hbs index 3f02d65ad..07c959a20 100644 --- a/frontend/app/templates/components/axis-charts.hbs +++ b/frontend/app/templates/components/axis-charts.hbs @@ -2,7 +2,12 @@ {{#each blocks as |chartBlock|}} {{draw/block-view axis=axis axisID=axisID block=chartBlock blocksData=blocksData}} {{/each}} - {{log 'chartTypesEffect' chartTypesEffect }} + {{#each chartsArray as |chart|}} + {{axis-chart axis=axis axisID=axisID blocks=blocks blocksData=blocksData axisCharts=this chart=chart }} + {{/each}} + {{ resizeEffectHere }} + {{ zoomedDomainEffect }} + {{else}}
{{content-editable diff --git a/frontend/app/utils/data-types.js b/frontend/app/utils/data-types.js new file mode 100644 index 000000000..39d900a2d --- /dev/null +++ b/frontend/app/utils/data-types.js @@ -0,0 +1,168 @@ +import { getAttrOrCP } from './ember-devel'; + + +/*----------------------------------------------------------------------------*/ + +class DataConfig { + /* + dataTypeName; + datum2Location; + datum2Value; + datum2Description; + */ + constructor (properties) { + if (properties) + Object.assign(this, properties); + } +}; + +/*----------------------------------------------------------------------------*/ + + +/** @param name is a feature or gene name */ +function name2Location(name, blockId) +{ + /** @param ak1 axis name, (exists in axisIDs[]) + * @param d1 feature name, i.e. ak1:d1 + */ + return featureLocation(blockId, name); +} + +/** Used for both blockData and parsedData. */ +function datum2LocationWithBlock(d, blockId) { return name2Location(d.name, blockId); } +function datum2Value(d) { return d.value; } +let parsedData = { + dataTypeName : 'parsedData', + // datum2LocationWithBlock assigned later, + datum2Value : datum2Value, + datum2Description : function(d) { return d.description; } +}, +blockData = { + dataTypeName : 'blockData', + // datum2LocationWithBlock assigned later, + /** The effects data is placed in .value[2] (the interval is in value[0..1]). + * Use the first effects value by default, but later will combine other values. + */ + datum2Value : function(d) { let v = d.value[2]; if (v.length) v = v[0]; return v; }, + datum2Description : function(d) { return JSON.stringify(d.value); } +}; + +/** Determine the appropriate DataConfig for the given data. + */ +function blockDataConfig(chart) { + let + isBlockData = chart.length && (chart[0].description === undefined); + + let dataConfigProperties = isBlockData ? blockData : parsedData; + return dataConfigProperties; +} + + +/*----------------------------------------------------------------------------*/ + + +/** example element of array f : */ +const featureCountDataExample = + { + "_id": { + "min": 100, + "max": 160 + }, + "count": 109 + }; + +const featureCountDataProperties = { + dataTypeName : 'featureCountData', + datum2Location : function datum2Location(d) { return [d._id.min, d._id.max]; }, + datum2Value : function(d) { return d.count; }, + /** datum2Description() is not used; possibly intended for the same + * purpose as hoverTextFn(), so they could be assimilated. */ + datum2Description : function(d) { return JSON.stringify(d._id); }, + hoverTextFn : function (d, block) { + let valueText = '[' + d._id.min + ',' + d._id.max + '] : ' + d.count, + blockName = block.view && block.view.longName(); + return valueText + '\n' + blockName; + }, + valueIsArea : true +}; + +const dataConfigs = + [featureCountDataProperties, blockData, parsedData] + .reduce((result, properties) => { result[properties.dataTypeName] = new DataConfig(properties); return result; }, [] ); + + + +/*----------------------------------------------------------------------------*/ +/* Copied from draw-map.js */ + +import { stacks } from './stacks'; // just for oa.z and .y; this will be replaced. +let blockFeatures = stacks.oa.z; + +function featureLocation(blockId, d) +{ + let feature = blockFeatures[blockId][d]; + if (feature === undefined) + { + console.log("axis-chart featureY_", blockId, blockFeatures[blockId], "does not contain feature", d); + } + let location = feature && feature.location; + return location; +} + +/** If the given value is an interval, convert it to a single value by calculating the middle of the interval. + * @param location is a single value or an array [from, to] + * @return the middle of an interval + */ +function middle(location) { + let result = location.length ? + location.reduce((sum, val) => sum + val, 0) / location.length + : location; + return result; +} + +/** @return a function to map a chart datum to a y value or interval. + */ +function scaleMaybeInterval(datum2Location, yScale) { + /* In both uses in this file, the result is passed to middle(), so an argument + * could be added to scaleMaybeInterval() to indicate the result should be a + * single value (using mid-point if datum location is an interval). + */ + + function datum2LocationScaled(d) { + /** location may be an interval [from, to] or a single value. */ + let l = datum2Location(d); + return l.length ? l.map((li) => yScale(li)) : yScale(l); }; + return datum2LocationScaled; +} + +/*----------------------------------------------------------------------------*/ +/* based on axis-1d.js: hoverTextFn() and setupHover() */ + +/** eg: "ChrA_283:A:283" */ +function hoverTextFn (feature, block) { + let + value = getAttrOrCP(feature, 'value'), + /** undefined values are filtered out below. */ + valueText = value && (value.length ? ('' + value[0] + ' - ' + value[1]) : value), + + /** block.view is the Stacks Block. */ + blockName = block.view && block.view.longName(), + featureName = getAttrOrCP(feature, 'name'), + /** common with dataConfig.datum2Description */ + description = value && JSON.stringify(value), + + text = [featureName, valueText, description, blockName] + .filter(function (x) { return x; }) + .join(" : "); + return text; +}; + + + + + +/*----------------------------------------------------------------------------*/ + + + +export { featureCountDataProperties, dataConfigs, DataConfig, blockDataConfig, blockData, parsedData, hoverTextFn, middle, scaleMaybeInterval, datum2LocationWithBlock }; diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index a9cc764d0..91758afff 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -6,6 +6,7 @@ import { eltWidthResizable, noShiftKeyfilter } from '../domElements'; import { noDomain } from '../draw/axis'; import { stacks } from '../stacks'; // just for oa.z and .y, don't commit this. import { inRangeEither } from './zoomPanCalcs'; +import { featureCountDataProperties, dataConfigs, DataConfig, blockDataConfig, hoverTextFn, middle, scaleMaybeInterval, datum2LocationWithBlock } from '../data-types'; const className = "chart", classNameSub = "chartRow"; @@ -20,70 +21,6 @@ const useLocalY = false; const dLog = console.debug; -/*----------------------------------------------------------------------------*/ -/* Copied from draw-map.js */ - -let blockFeatures = stacks.oa.z; - -function featureLocation(blockId, d) -{ - let feature = blockFeatures[blockId][d]; - if (feature === undefined) - { - console.log("axis-chart featureY_", blockId, blockFeatures[blockId], "does not contain feature", d); - } - let location = feature && feature.location; - return location; -} - -/** If the given value is an interval, convert it to a single value by calculating the middle of the interval. - * @param location is a single value or an array [from, to] - * @return the middle of an interval - */ -function middle(location) { - let result = location.length ? - location.reduce((sum, val) => sum + val, 0) / location.length - : location; - return result; -} - -/** @return a function to map a chart datum to a y value or interval. - */ -function scaleMaybeInterval(datum2Location, yScale) { - /* In both uses in this file, the result is passed to middle(), so an argument - * could be added to scaleMaybeInterval() to indicate the result should be a - * single value (using mid-point if datum location is an interval). - */ - - function datum2LocationScaled(d) { - /** location may be an interval [from, to] or a single value. */ - let l = datum2Location(d); - return l.length ? l.map((li) => yScale(li)) : yScale(l); }; - return datum2LocationScaled; -} - -/*----------------------------------------------------------------------------*/ -/* based on axis-1d.js: hoverTextFn() and setupHover() */ - -/** eg: "ChrA_283:A:283" */ -function hoverTextFn (feature, block) { - let - value = getAttrOrCP(feature, 'value'), - /** undefined values are filtered out below. */ - valueText = value && (value.length ? ('' + value[0] + ' - ' + value[1]) : value), - - /** block.view is the Stacks Block. */ - blockName = block.view && block.view.longName(), - featureName = getAttrOrCP(feature, 'name'), - /** common with dataConfig.datum2Description */ - description = value && JSON.stringify(value), - - text = [featureName, valueText, description, blockName] - .filter(function (x) { return x; }) - .join(" : "); - return text; -}; - /** Add a .hasChart class to the which contains this chart. * Currently this is used to hide the so that hover events are @@ -101,19 +38,6 @@ function addParentClass(g) { }; /*----------------------------------------------------------------------------*/ -class DataConfig { - /* - dataTypeName; - datum2Location; - datum2Value; - datum2Description; - */ - constructor (properties) { - if (properties) - Object.assign(this, properties); - } -}; - class ChartLine { constructor(dataConfig, scales) { this.dataConfig = dataConfig; @@ -122,51 +46,6 @@ class ChartLine { } } -/*----------------------------------------------------------------------------*/ - - /** @param name is a feature or gene name */ - function name2Location(name, blockId) - { - /** @param ak1 axis name, (exists in axisIDs[]) - * @param d1 feature name, i.e. ak1:d1 - */ - return featureLocation(blockId, name); - } - - /** Used for both blockData and parsedData. */ - function datum2LocationWithBlock(d, blockId) { return name2Location(d.name, blockId); } - function datum2Value(d) { return d.value; } - let parsedData = { - dataTypeName : 'parsedData', - // datum2LocationWithBlock assigned later, - datum2Value : datum2Value, - datum2Description : function(d) { return d.description; } - }, - blockData = { - dataTypeName : 'blockData', - // datum2LocationWithBlock assigned later, - /** The effects data is placed in .value[2] (the interval is in value[0..1]). - * Use the first effects value by default, but later will combine other values. - */ - datum2Value : function(d) { let v = d.value[2]; if (v.length) v = v[0]; return v; }, - datum2Description : function(d) { return JSON.stringify(d.value); } - }; - -/** Determine the appropriate DataConfig for the given data. - */ -function blockDataConfig(chart) { - let - isBlockData = chart.length && (chart[0].description === undefined); - - let dataConfigProperties = isBlockData ? blockData : parsedData; - return dataConfigProperties; -} - - - - -/*----------------------------------------------------------------------------*/ - /*----------------------------------------------------------------------------*/ class AxisCharts { @@ -190,8 +69,9 @@ AxisCharts.prototype.setup = function(axisID) { /** * @param allocatedWidth [horizontal start offset, width] */ -function setupFrame(axisID, axisCharts, charts, allocatedWidth) +AxisCharts.prototype.setupFrame = function(axisID, charts, allocatedWidth) { + let axisCharts = this; axisCharts.setup(axisID); let resizedWidth = allocatedWidth[1]; @@ -207,14 +87,21 @@ function setupFrame(axisID, axisCharts, charts, allocatedWidth) axisCharts.frame(axisCharts.ranges.bbox, charts, allocatedWidth); axisCharts.getRanges2(); -} -function setupChart(axisID, axisCharts, chart1, chartData, blocks, dataConfig, yAxisScale, resizedWidth) -{ +}; + + + +Chart1.prototype.overlap = function(axisCharts) { + let chart1 = this; // Plan is for Axischarts to own .ranges, but for now there is some overlap. if (! chart1.ranges) { chart1.ranges = axisCharts.ranges; chart1.dom = axisCharts.dom; } +}; +Chart1.prototype.setupChart = function(axisID, axisCharts, chartData, blocks, dataConfig, yAxisScale, resizedWidth) +{ + let chart1 = this; chart1.scales.yAxis = yAxisScale; //---------------------------------------------------------------------------- @@ -229,6 +116,7 @@ function setupChart(axisID, axisCharts, chart1, chartData, blocks, dataConfig, y dataConfig.addedDefaults(); //---------------------------------------------------------------------------- + let blocksById = blocks.reduce( (result, block) => { result[block.get('id')] = block; return result; }, []), @@ -237,20 +125,18 @@ function setupChart(axisID, axisCharts, chart1, chartData, blocks, dataConfig, y let block = blocksById[blockId]; chart1.createLine(blockId, block); }); - chart1.group(axisCharts.dom.gca, 'chart-line'); - // place controls after the ChartLine-s group, so that the toggle is above the bars and can be accessed. - axisCharts.controls(); - //---------------------------------------------------------------------------- - + chart1.group(chart1.dom.gca, 'chart-line'); + //---------------------------------------------------------------------------- - chart1.getRanges(axisCharts.ranges, chartData); + chart1.getRanges(chart1.ranges, chartData); return chart1; }; -function drawChart(axisCharts, chart1, chartData) +Chart1.prototype.drawChart = function(axisCharts, chartData) { + let chart1 = this; /** possibly don't (yet) have chartData for each of blocks[], * i.e. blocksById may be a subset of blocks.mapBy('id'). */ @@ -282,10 +168,13 @@ AxisCharts.prototype.selectParentContainer = function (axisID) AxisCharts.prototype.getBBox = function () { let - gAxis = this.dom.gAxis; + gAxis = this.dom.gAxis, + gAxisElt = gAxis.node(); + /* If the selection is empty, nothing will be drawn. */ + if (gAxisElt) { let - /** relative to the transform of parent g.axis-outer */ - bbox = gAxis.node().getBBox(), + /** relative to the transform of parent g.axis-outer */ + bbox = gAxisElt.getBBox(), yrange = [bbox.y, bbox.height]; if (bbox.x < 0) { @@ -294,6 +183,7 @@ AxisCharts.prototype.getBBox = function () } this.ranges.bbox = bbox; this.ranges.yrange = yrange; + } }; Chart1.prototype.getRanges = function (ranges, chartData) { let @@ -853,7 +743,11 @@ ChartLine.prototype.blockColour = function () { let data = this.currentData; - /** The effects data takes the form of an array of 5 probabilities, in the 3rd element of feature.value */ + /** The Effects probabilities data is keyed by a location which is a SNP, so linebars() is a sensible representaion. + * This uses the data shape to recognise Effects data; this is provisional + * - we can probably lookup the tag 'EffectsPlus' in dataset tags (refn resources/tools/dev/effects2Dataset.pl). + * The effects data takes the form of an array of 5 probabilities, in the 3rd element of feature.value. + */ let isEffectsData = data.length && data[0].name && data[0].value && (data[0].value.length === 3) && (data[0].value[2].length === 6); let bars = isEffectsData ? this.linebars : this.bars; let chartDraw = barsLine ? bars : this.line; @@ -976,4 +870,4 @@ AxisCharts.prototype.controls = function controls() /* layoutAndDrawChart() has been split into class methods of AxisCharts and Chart1, * and replaced with a proxy which calls them, and can next be re-distributed into axis-chart. */ -export { setupFrame, setupChart, drawChart, AxisCharts, /*AxisChart,*/ className, Chart1, DataConfig, blockData, parsedData }; +export { AxisCharts, /*AxisChart,*/ className, Chart1 }; From 24dc12fcb98eb2aea34627574bc33639fe9dcb68 Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 6 Feb 2020 21:35:35 +1100 Subject: [PATCH 23/57] group class methods into their classes ae755af 30931 Feb 6 21:34 frontend/app/utils/draw/chart1.js --- frontend/app/utils/draw/chart1.js | 749 ++++++++++++++++-------------- 1 file changed, 389 insertions(+), 360 deletions(-) diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index 91758afff..06b3174c0 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -38,6 +38,26 @@ function addParentClass(g) { }; /*----------------------------------------------------------------------------*/ + + /* axis + * x .value + * y .name Location + */ + /** 1-dimensional chart, within an axis. */ + function Chart1(parentG, dataConfig) + { + this.parentG = parentG; + this.dataConfig = dataConfig; + this.chartLines = {}; + /* yAxis is imported, x & yLine are calculated locally. + * y is used for drawing - it refers to yAxis or yLine. + * yLine would be used for .line() if yBand / scaleBand was used for .bars(). + */ + this.scales = { /* yAxis, x, y, yLine */ }; + } + Chart1.prototype.barsLine = true; + + class ChartLine { constructor(dataConfig, scales) { this.dataConfig = dataConfig; @@ -90,6 +110,197 @@ AxisCharts.prototype.setupFrame = function(axisID, charts, allocatedWidth) }; +AxisCharts.prototype.selectParentContainer = function (axisID) + { + this.axisID = axisID; + // to support multiple split axes, identify axis by axisID (and possibly stack id) + // + // g.axis-outer#id + let gAxis = d3.select("g.axis-outer#id" + axisID + "> g.axis-use"); + if (! gAxis.node()) { /* probably because axisShowExtend() has not added the g.axis-use yet - will sort out the dependencies ... . */ + dLog('layoutAndDrawChart', d3.select("g.axis-outer#id" + axisID).node(), 'no g.axis-use', this, axisID); + } + this.dom.gAxis = gAxis; + return gAxis; +}; + +AxisCharts.prototype.getBBox = function () + { + let + gAxis = this.dom.gAxis, + gAxisElt = gAxis.node(); + /* If the selection is empty, nothing will be drawn. */ + if (gAxisElt) { + let + /** relative to the transform of parent g.axis-outer */ + bbox = gAxisElt.getBBox(), + yrange = [bbox.y, bbox.height]; + if (bbox.x < 0) + { + console.log("x < 0", bbox); + bbox.x = 0; + } + this.ranges.bbox = bbox; + this.ranges.yrange = yrange; + } +}; + +AxisCharts.prototype.getRanges3 = function (resizedWidth) { + if (resizedWidth) { + let + {bbox} = this.ranges; + bbox.width = resizedWidth; + dLog('resizedWidth', resizedWidth, bbox); + } +}; + + AxisCharts.prototype.getRanges2 = function () + { + // based on https://bl.ocks.org/mbostock/3885304, axes x & y swapped. + let + // parentG = this.parentG, + bbox = this.ranges.bbox, + margin = showChartAxes ? + {top: 10, right: 20, bottom: 40, left: 20} : + {top: 0, right: 0, bottom: 0, left: 0}, + // pp=parentG.node().parentElement, + parentW = bbox.width, // +pp.attr("width") + parentH = bbox.height, // +pp.attr("height") + width = parentW - margin.left - margin.right, + height = parentH - margin.top - margin.bottom; + this.ranges.drawSize = {width, height}; + dLog('getRanges2', parentW, parentH, this.ranges.drawSize); + }; + +AxisCharts.prototype.drawAxes = function (charts) { + let + dom = this.dom, + /** the axes were originally within the gs,gsa of .group(); hence the var names. + * gs selects the into which the axes will be inserted, and gpa is the + * .enter().append() of that selection. + */ + gs = dom.gc, + gsa = dom.gca; + gsa.each(Chart1.prototype.drawAxes); +}; + +AxisCharts.prototype.commonFrame = function container() +{ + let axisID = this.axisID, + gAxis = this.dom.gAxis, + bbox = this.ranges.bbox; + + /** datum is value in hash : {value : , description: } and with optional attribute description. */ + + dLog('container', gAxis.node()); + /** parent; contains a clipPath, g > rect, text.resizer. */ + let gps = gAxis + .selectAll("g." + className) + .data([axisID]), + gp = gps + .enter() + .insert("g", ":first-child") + .attr('class', className); + if (false) { // not completed. Can base resized() on axis-2d.js + let text = gp + .append("text") + .attr('class', 'resizer') + .html("⇹") + .attr("x", bbox.width-10); + if (gp.size() > 0) + eltWidthResizable("g.axis-use > g." + className + " > text.resizer", resized); + } + this.dom.gps = gps; + this.dom.gp = gp; +}; + +AxisCharts.prototype.frame = function container(bbox, charts, allocatedWidth) +{ + let + gps = this.dom.gps, + gp = this.dom.gp; + + /** datum is axisID, so id and clip-path can be functions. + * e.g. this.dom.gp.data() is [axisID] + */ + function axisClipId(axisID) { return "axis-chart-clip-" + axisID; } + let gpa = + gp // define the clipPath + .append("clipPath") // define a clip path + .attr("id", axisClipId) // give the clipPath an ID + .append("rect"), // shape it as a rect + gprm = + gpa.merge(gps.selectAll("g > clipPath > rect")) + .attr("x", bbox.x) + .attr("y", bbox.y) + .attr("width", bbox.width) + .attr("height", bbox.height) + ; + let [startOffset, width] = allocatedWidth; + let gca = + gp.append("g") + .attr("clip-path", (d) => "url(#" + axisClipId(d) + ")") // clip with the rectangle + .selectAll("g[clip-path]") + .data(Object.values(charts)) + .enter() + .append("g") + .attr('class', (d) => d.dataConfig.dataTypeName) + .attr("transform", (d, i) => "translate(" + (startOffset + (i * 30)) + ", 0)") + ; + + let g = + gps.merge(gp).selectAll("g." + className+ " > g"); + if (! gp.empty() ) { + addParentClass(g); + /* .gc is ​​ + * .g (assigned later) is g.axis-chart +.chart-line ? + * .gca contains a g for each chartType / dataTypeName, i.e. per Chart1. + */ + this.dom.gc = g; + this.dom.gca = gca; + } +}; + + +AxisCharts.prototype.controls = function controls() +{ + let + bbox = this.ranges.bbox, + gp = this.dom.gca, + gps = this.dom.gc; + + function toggleBarsLineClosure(chart /*, i, g*/) + { + chart.toggleBarsLine(); + } + + /** currently placed at g.chart, could be inside g.chart>g (clip-path=). */ + let chartTypeToggle = gp + .append("circle") + .attr("class", "radio toggle chartType") + .attr("r", 6) + .on("click", toggleBarsLineClosure); + chartTypeToggle.merge(gps.selectAll("g > circle")) + .attr("cx", bbox.x + bbox.width / 2) /* was o[p], but g.axis-outer translation does x offset of stack. */ + .attr("cy", bbox.height - 10) + .classed("pushed", (chart1) => { return chart1.barsLine; }); + chartTypeToggle.each(function(chart) { chart.chartTypeToggle = d3.select(this); } ); +}; + +/*----------------------------------------------------------------------------*/ + +/* +class AxisChart { + bbox; + data; + gp; + gps; + }; +*/ + +/*----------------------------------------------------------------------------*/ + Chart1.prototype.overlap = function(axisCharts) { let chart1 = this; @@ -152,39 +363,6 @@ Chart1.prototype.drawChart = function(axisCharts, chartData) chart1.drawContent(); }; -AxisCharts.prototype.selectParentContainer = function (axisID) - { - this.axisID = axisID; - // to support multiple split axes, identify axis by axisID (and possibly stack id) - // - // g.axis-outer#id - let gAxis = d3.select("g.axis-outer#id" + axisID + "> g.axis-use"); - if (! gAxis.node()) { /* probably because axisShowExtend() has not added the g.axis-use yet - will sort out the dependencies ... . */ - dLog('layoutAndDrawChart', d3.select("g.axis-outer#id" + axisID).node(), 'no g.axis-use', this, axisID); - } - this.dom.gAxis = gAxis; - return gAxis; -}; -AxisCharts.prototype.getBBox = function () - { - let - gAxis = this.dom.gAxis, - gAxisElt = gAxis.node(); - /* If the selection is empty, nothing will be drawn. */ - if (gAxisElt) { - let - /** relative to the transform of parent g.axis-outer */ - bbox = gAxisElt.getBBox(), - yrange = [bbox.y, bbox.height]; - if (bbox.x < 0) - { - console.log("x < 0", bbox); - bbox.x = 0; - } - this.ranges.bbox = bbox; - this.ranges.yrange = yrange; - } -}; Chart1.prototype.getRanges = function (ranges, chartData) { let {yrange } = ranges, @@ -207,94 +385,10 @@ Chart1.prototype.getRanges = function (ranges, chartData) { ranges.pxSize = (yDomain[1] - yDomain[0]) / ranges.bbox.height; }; -DataConfig.prototype.addedDefaults = function(valueName) { - if (! this.hoverTextFn) - this.hoverTextFn = hoverTextFn; - if (this.valueIsArea === undefined) - this.valueIsArea = false; - if (! this.barClassName) - this.barClassName = classNameSub; - if (! this.valueName) - this.valueName = valueName || "Values"; -}; -ChartLine.prototype.setup = function(blockId) { - /* Some types (blockData, parsedData) require a block to lookup the feature - * name for location. They are denoted by .datum2Location not being in their - * pre-defined config. - */ - if (! this.dataConfig.datum2Location) { - // copy dataConfig to give a custom value to this ChartLine. - let d = new DataConfig(this.dataConfig); - d.datum2Location = - (d) => datum2LocationWithBlock(d, blockId); - this.dataConfig = d; - } -}; -/** Filter given data according to this.scales.yAxis.domain() - * and set .currentData - */ -ChartLine.prototype.filterToZoom = function(chart) { - let - {yAxis} = this.scales, - yDomain = yAxis.domain(), - withinZoomRegion = (d) => { - return inRangeEither(this.dataConfig.datum2Location(d), yDomain); - }, - data = chart.filter(withinZoomRegion); - this.currentData = data; - - dLog(yDomain, data.length, (data.length == 0) || this.dataConfig.datum2Location(data[0])); - return data; -}; - -AxisCharts.prototype.getRanges3 = function (resizedWidth) { - if (resizedWidth) { - let - {bbox} = this.ranges; - bbox.width = resizedWidth; - dLog('resizedWidth', resizedWidth, bbox); - } -}; - - - /* axis - * x .value - * y .name Location - */ - /** 1-dimensional chart, within an axis. */ - function Chart1(parentG, dataConfig) - { - this.parentG = parentG; - this.dataConfig = dataConfig; - this.chartLines = {}; - /* yAxis is imported, x & yLine are calculated locally. - * y is used for drawing - it refers to yAxis or yLine. - * yLine would be used for .line() if yBand / scaleBand was used for .bars(). - */ - this.scales = { /* yAxis, x, y, yLine */ }; - } - Chart1.prototype.barsLine = true; - AxisCharts.prototype.getRanges2 = function () - { - // based on https://bl.ocks.org/mbostock/3885304, axes x & y swapped. - let - // parentG = this.parentG, - bbox = this.ranges.bbox, - margin = showChartAxes ? - {top: 10, right: 20, bottom: 40, left: 20} : - {top: 0, right: 0, bottom: 0, left: 0}, - // pp=parentG.node().parentElement, - parentW = bbox.width, // +pp.attr("width") - parentH = bbox.height, // +pp.attr("height") - width = parentW - margin.left - margin.right, - height = parentH - margin.top - margin.bottom; - this.ranges.drawSize = {width, height}; - dLog('getRanges2', parentW, parentH, this.ranges.drawSize); - }; Chart1.prototype.prepareScales = function (data, drawSize) { /** The chart is perpendicular to the usual presentation. @@ -341,22 +435,6 @@ AxisCharts.prototype.getRanges3 = function (resizedWidth) { chartLine.drawContent(this.barsLine); }); }; - /** Enables use of the scales which are set up in .prepareScales(). - */ - ChartLine.prototype.scaledConfig = function () - { - let - dataConfig = this.dataConfig, - scales = this.scales; - - /* these can be renamed datum2{abscissa,ordinate}{,Scaled}() */ - /* apply y after scale applied by datum2Location */ - let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, scales.y); - /** related @see rectWidth(). */ - function datum2ValueScaled(d) { return scales.x(dataConfig.datum2Value(d)); } - dataConfig.datum2LocationScaled = datum2LocationScaled; - dataConfig.datum2ValueScaled = datum2ValueScaled; - }; Chart1.prototype.group = function (parentG, groupClassName) { /** parentG is g.{{dataTypeName}}, within : g.axis-use > g.chart > g[clip-path] > g.{{dataTypeName}}. @@ -386,17 +464,6 @@ Chart1.prototype.group = function (parentG, groupClassName) { return g; }; -AxisCharts.prototype.drawAxes = function (charts) { - let - dom = this.dom, - /** the axes were originally within the gs,gsa of .group(); hence the var names. - * gs selects the into which the axes will be inserted, and gpa is the - * .enter().append() of that selection. - */ - gs = dom.gc, - gsa = dom.gca; - gsa.each(Chart1.prototype.drawAxes); -}; Chart1.prototype.drawAxes = function (chart, i, g) { /** first draft showed all data; subsequently adding : @@ -476,6 +543,101 @@ Chart1.prototype.drawLine = function (blockId, block, data) chartLine.drawContent(this.barsLine); }; + Chart1.prototype.scaleLinear = function (yRange, data) + { + // based on https://bl.ocks.org/mbostock/3883245 + if (! this.scales.yLine) + this.scales.yLine = d3.scaleLinear(); + let y = this.scales.yLine; + y.rangeRound(yRange); + let + d2l = this.dataConfig.datum2Location || Object.values(this.chartLines)[0].dataConfig.datum2Location; + let combinedDomain = this.domain(d2l, data); + y.domain(combinedDomain); + console.log('scaleLinear domain', combinedDomain); + return y; + }; + /** Combine the domains of each of the component ChartLine-s. + * @param valueFn e.g. datum2Location or datum2Value + */ + Chart1.prototype.domain = function (valueFn, blocksData) + { + let blockIds = Object.keys(blocksData), + domains = blockIds.map((blockId) => { + let + data = blocksData[blockId], + chartLine = this.chartLines[blockId]; + return chartLine.domain(valueFn, data); + }); + /** Union the domains. */ + let domain = domains + .reduce((acc, val) => acc.concat(val), []); + return domain; + }; + /** Alternate between bar chart and line chart */ + Chart1.prototype.toggleBarsLine = function () + { + console.log("toggleBarsLine", this); + d3.event.stopPropagation(); + this.barsLine = ! this.barsLine; + this.chartTypeToggle + .classed("pushed", this.barsLine); + this.dom.g.selectAll("g > *").remove(); + Object.keys(this.chartLines).forEach((blockId) => { + let chartLine = this.chartLines[blockId]; + chartLine.drawContent(this.barsLine); + }); + }; + +/*----------------------------------------------------------------------------*/ + +ChartLine.prototype.setup = function(blockId) { + /* Some types (blockData, parsedData) require a block to lookup the feature + * name for location. They are denoted by .datum2Location not being in their + * pre-defined config. + */ + if (! this.dataConfig.datum2Location) { + // copy dataConfig to give a custom value to this ChartLine. + let d = new DataConfig(this.dataConfig); + d.datum2Location = + (d) => datum2LocationWithBlock(d, blockId); + this.dataConfig = d; + } +}; +/** Filter given data according to this.scales.yAxis.domain() + * and set .currentData + */ +ChartLine.prototype.filterToZoom = function(chart) { + let + {yAxis} = this.scales, + yDomain = yAxis.domain(), + withinZoomRegion = (d) => { + return inRangeEither(this.dataConfig.datum2Location(d), yDomain); + }, + data = chart.filter(withinZoomRegion); + this.currentData = data; + + dLog(yDomain, data.length, (data.length == 0) || this.dataConfig.datum2Location(data[0])); + return data; +}; + + /** Enables use of the scales which are set up in .prepareScales(). + */ + ChartLine.prototype.scaledConfig = function () + { + let + dataConfig = this.dataConfig, + scales = this.scales; + + /* these can be renamed datum2{abscissa,ordinate}{,Scaled}() */ + /* apply y after scale applied by datum2Location */ + let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, scales.y); + /** related @see rectWidth(). */ + function datum2ValueScaled(d) { return scales.x(dataConfig.datum2Value(d)); } + dataConfig.datum2LocationScaled = datum2LocationScaled; + dataConfig.datum2ValueScaled = datum2ValueScaled; + }; + ChartLine.prototype.blockColour = function () { let blockS = this.block && this.block.get('view'), @@ -524,6 +686,7 @@ ChartLine.prototype.blockColour = function () rx.remove(); console.log(rs.nodes(), re.nodes()); }; + /** A single horizontal line for each data point. * Position is similar to the rectangle which would be drawn by bars(): * X limits are the same as the rectangle limits (left, right) @@ -571,106 +734,6 @@ ChartLine.prototype.blockColour = function () console.log(rs.nodes(), re.nodes()); }; - /** Calculate the height of rectangle to be used for this data point - * @param this is DataConfig, not DOM element. - * @param scaled true means apply scale (x) to the result - * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. - * gIsData will be true when called from d3.max(), and false for d3 attr functions. - */ - DataConfig.prototype.rectWidth = function (scaled, gIsData, d, i, g) - { - Ember.assert('rectWidth arguments.length === 5', arguments.length === 5); - /** The scale is linear, so it is OK to scale before dividing by rectHeight. - * Otherwise could use datum2Value() here and apply this.x() before the return. - */ - let d2v = (scaled ? this.datum2ValueScaled : this.datum2Value), - width = d2v(d); - if (this.valueIsArea) { - let h; - // Factor the width consistently by h, don't sometimes use scaled (i.e. pass scaled==false). - width /= (h = this.rectHeight(false, gIsData, d, i, g)); - // dLog('rectWidth', h, width, gIsData); - } - return width; - }; - /** Calculate the height of rectangle to be used for this data point - * @param this is DataConfig, not DOM element. - * @param scaled true means apply scale (y) to the result - * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. - * gIsData will be true when called from d3.max(), and false for d3 attr functions. - */ - DataConfig.prototype.rectHeight = function (scaled, gIsData, d, i, g) - { - Ember.assert('rectHeight arguments.length === 5', arguments.length === 5); - let height, - d2l = (scaled ? this.datum2LocationScaled : this.datum2Location), - location; - /* if location is an interval, calculate height from it. - * Otherwise, use adjacent points to indicate height. - */ - if ((location = d2l(d)).length) { - height = Math.abs(location[1] - location[0]); - } - else { - /* the boundary between 2 adjacent points is midway between them. - * So sum that half-distance for the previous and next points. - * the y range distance from the previous point to the next point. - * If this point is either end, simply double the other half-distance. - */ - if (! g.length) - /* constant value OK - don't expect to be called if g.length is 0. - * this.scales.y.range() / 10; - */ - height = 10; - else { - let r = []; - function gData(i) { let gi = g[i]; return gIsData ? gi : gi.__data__; }; - if (i > 0) - r.push(gData(i)); - r.push(d); - if (i < g.length-1) - r.push(gData(i+1)); - let y = - r.map(this.datum2LocationScaled); - height = Math.abs(y[y.length-1] - y[0]) * 2 / (y.length-1); - dLog('rectHeight', gIsData, d, i, /*g,*/ r, y, height); - if (! height) - height = 1; - } - } - return height; - }; - Chart1.prototype.scaleLinear = function (yRange, data) - { - // based on https://bl.ocks.org/mbostock/3883245 - if (! this.scales.yLine) - this.scales.yLine = d3.scaleLinear(); - let y = this.scales.yLine; - y.rangeRound(yRange); - let - d2l = this.dataConfig.datum2Location || Object.values(this.chartLines)[0].dataConfig.datum2Location; - let combinedDomain = this.domain(d2l, data); - y.domain(combinedDomain); - console.log('scaleLinear domain', combinedDomain); - return y; - }; - /** Combine the domains of each of the component ChartLine-s. - * @param valueFn e.g. datum2Location or datum2Value - */ - Chart1.prototype.domain = function (valueFn, blocksData) - { - let blockIds = Object.keys(blocksData), - domains = blockIds.map((blockId) => { - let - data = blocksData[blockId], - chartLine = this.chartLines[blockId]; - return chartLine.domain(valueFn, data); - }); - /** Union the domains. */ - let domain = domains - .reduce((acc, val) => acc.concat(val), []); - return domain; - }; /** Calculate the domain of some function of the data, which may be the data value or location. * This can be used in independent of axis-chart, and can be factored out * for more general use, along with Chart1:domain(). @@ -691,6 +754,7 @@ ChartLine.prototype.blockColour = function () console.log('ChartLine domain', domain, yFlat); return domain; }; + ChartLine.prototype.line = function (data) { let y = this.scales.y, dataConfig = this.dataConfig; @@ -722,20 +786,9 @@ ChartLine.prototype.blockColour = function () // data length is constant 1, so .remove() is not needed ps.exit().remove(); }; - /** Alternate between bar chart and line chart */ - Chart1.prototype.toggleBarsLine = function () - { - console.log("toggleBarsLine", this); - d3.event.stopPropagation(); - this.barsLine = ! this.barsLine; - this.chartTypeToggle - .classed("pushed", this.barsLine); - this.dom.g.selectAll("g > *").remove(); - Object.keys(this.chartLines).forEach((blockId) => { - let chartLine = this.chartLines[blockId]; - chartLine.drawContent(this.barsLine); - }); - }; + + + /** Draw, using .currentData, which is set by calling .filterToZoom(). * @param barsLine if true, draw .bars, otherwise .line. */ @@ -754,120 +807,96 @@ ChartLine.prototype.blockColour = function () chartDraw.apply(this, [data]); }; -AxisCharts.prototype.commonFrame = function container() -{ - let axisID = this.axisID, - gAxis = this.dom.gAxis, - bbox = this.ranges.bbox; - /** datum is value in hash : {value : , description: } and with optional attribute description. */ - dLog('container', gAxis.node()); - /** parent; contains a clipPath, g > rect, text.resizer. */ - let gps = gAxis - .selectAll("g." + className) - .data([axisID]), - gp = gps - .enter() - .insert("g", ":first-child") - .attr('class', className); - if (false) { // not completed. Can base resized() on axis-2d.js - let text = gp - .append("text") - .attr('class', 'resizer') - .html("⇹") - .attr("x", bbox.width-10); - if (gp.size() > 0) - eltWidthResizable("g.axis-use > g." + className + " > text.resizer", resized); - } - this.dom.gps = gps; - this.dom.gp = gp; -}; -AxisCharts.prototype.frame = function container(bbox, charts, allocatedWidth) -{ - let - gps = this.dom.gps, - gp = this.dom.gp; +/*----------------------------------------------------------------------------*/ - /** datum is axisID, so id and clip-path can be functions. - * e.g. this.dom.gp.data() is [axisID] + /** Calculate the height of rectangle to be used for this data point + * @param this is DataConfig, not DOM element. + * @param scaled true means apply scale (x) to the result + * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. + * gIsData will be true when called from d3.max(), and false for d3 attr functions. */ - function axisClipId(axisID) { return "axis-chart-clip-" + axisID; } - let gpa = - gp // define the clipPath - .append("clipPath") // define a clip path - .attr("id", axisClipId) // give the clipPath an ID - .append("rect"), // shape it as a rect - gprm = - gpa.merge(gps.selectAll("g > clipPath > rect")) - .attr("x", bbox.x) - .attr("y", bbox.y) - .attr("width", bbox.width) - .attr("height", bbox.height) - ; - let [startOffset, width] = allocatedWidth; - let gca = - gp.append("g") - .attr("clip-path", (d) => "url(#" + axisClipId(d) + ")") // clip with the rectangle - .selectAll("g[clip-path]") - .data(Object.values(charts)) - .enter() - .append("g") - .attr('class', (d) => d.dataConfig.dataTypeName) - .attr("transform", (d, i) => "translate(" + (startOffset + (i * 30)) + ", 0)") - ; - - let g = - gps.merge(gp).selectAll("g." + className+ " > g"); - if (! gp.empty() ) { - addParentClass(g); - /* .gc is ​​ - * .g (assigned later) is g.axis-chart -.chart-line ? - * .gca contains a g for each chartType / dataTypeName, i.e. per Chart1. + DataConfig.prototype.rectWidth = function (scaled, gIsData, d, i, g) + { + Ember.assert('rectWidth arguments.length === 5', arguments.length === 5); + /** The scale is linear, so it is OK to scale before dividing by rectHeight. + * Otherwise could use datum2Value() here and apply this.x() before the return. + */ + let d2v = (scaled ? this.datum2ValueScaled : this.datum2Value), + width = d2v(d); + if (this.valueIsArea) { + let h; + // Factor the width consistently by h, don't sometimes use scaled (i.e. pass scaled==false). + width /= (h = this.rectHeight(false, gIsData, d, i, g)); + // dLog('rectWidth', h, width, gIsData); + } + return width; + }; + /** Calculate the height of rectangle to be used for this data point + * @param this is DataConfig, not DOM element. + * @param scaled true means apply scale (y) to the result + * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. + * gIsData will be true when called from d3.max(), and false for d3 attr functions. */ - this.dom.gc = g; - this.dom.gca = gca; - } -}; - -/* -class AxisChart { - bbox; - data; - gp; - gps; - }; -*/ - -AxisCharts.prototype.controls = function controls() -{ - let - bbox = this.ranges.bbox, - gp = this.dom.gca, - gps = this.dom.gc; - - function toggleBarsLineClosure(chart /*, i, g*/) + DataConfig.prototype.rectHeight = function (scaled, gIsData, d, i, g) { - chart.toggleBarsLine(); - } + Ember.assert('rectHeight arguments.length === 5', arguments.length === 5); + let height, + d2l = (scaled ? this.datum2LocationScaled : this.datum2Location), + location; + /* if location is an interval, calculate height from it. + * Otherwise, use adjacent points to indicate height. + */ + if ((location = d2l(d)).length) { + height = Math.abs(location[1] - location[0]); + } + else { + /* the boundary between 2 adjacent points is midway between them. + * So sum that half-distance for the previous and next points. + * the y range distance from the previous point to the next point. + * If this point is either end, simply double the other half-distance. + */ + if (! g.length) + /* constant value OK - don't expect to be called if g.length is 0. + * this.scales.y.range() / 10; + */ + height = 10; + else { + let r = []; + function gData(i) { let gi = g[i]; return gIsData ? gi : gi.__data__; }; + if (i > 0) + r.push(gData(i)); + r.push(d); + if (i < g.length-1) + r.push(gData(i+1)); + let y = + r.map(this.datum2LocationScaled); + height = Math.abs(y[y.length-1] - y[0]) * 2 / (y.length-1); + dLog('rectHeight', gIsData, d, i, /*g,*/ r, y, height); + if (! height) + height = 1; + } + } + return height; + }; - /** currently placed at g.chart, could be inside g.chart>g (clip-path=). */ - let chartTypeToggle = gp - .append("circle") - .attr("class", "radio toggle chartType") - .attr("r", 6) - .on("click", toggleBarsLineClosure); - chartTypeToggle.merge(gps.selectAll("g > circle")) - .attr("cx", bbox.x + bbox.width / 2) /* was o[p], but g.axis-outer translation does x offset of stack. */ - .attr("cy", bbox.height - 10) - .classed("pushed", (chart1) => { return chart1.barsLine; }); - chartTypeToggle.each(function(chart) { chart.chartTypeToggle = d3.select(this); } ); +DataConfig.prototype.addedDefaults = function(valueName) { + if (! this.hoverTextFn) + this.hoverTextFn = hoverTextFn; + if (this.valueIsArea === undefined) + this.valueIsArea = false; + + if (! this.barClassName) + this.barClassName = classNameSub; + if (! this.valueName) + this.valueName = valueName || "Values"; }; /*----------------------------------------------------------------------------*/ + /* layoutAndDrawChart() has been split into class methods of AxisCharts and Chart1, * and replaced with a proxy which calls them, and can next be re-distributed into axis-chart. */ export { AxisCharts, /*AxisChart,*/ className, Chart1 }; From d8fbe2d3320edebff43b5c48ce6fc997ba69d9e7 Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 6 Feb 2020 21:55:43 +1100 Subject: [PATCH 24/57] re-indent frontend/app/ : 847839b 29133 Feb 6 21:50 utils/draw/chart1.js 76cf49c 11196 Feb 6 21:53 components/axis-charts.js --- frontend/app/components/axis-charts.js | 42 +- frontend/app/utils/draw/chart1.js | 1068 ++++++++++++------------ 2 files changed, 555 insertions(+), 555 deletions(-) diff --git a/frontend/app/components/axis-charts.js b/frontend/app/components/axis-charts.js index f9345f97e..c298965df 100644 --- a/frontend/app/components/axis-charts.js +++ b/frontend/app/components/axis-charts.js @@ -200,9 +200,9 @@ export default InAxis.extend({ let data = this.get(className), layoutAndDrawChart = this.get('layoutAndDrawChart'); if (data) { - console.log("redraw", this, (data === undefined) || data.length, axisID, t); - if (data) - layoutAndDrawChart.apply(this, [data, undefined]); + console.log("redraw", this, (data === undefined) || data.length, axisID, t); + if (data) + layoutAndDrawChart.apply(this, [data, undefined]); } }, @@ -213,8 +213,8 @@ export default InAxis.extend({ * have some use. */ - /** Convert input text to an array. - * Used to process text from clip-board, by @see pasteProcess(). + /** Convert input text to an array. + * Used to process text from clip-board, by @see pasteProcess(). * @param tableText text string, TSV, rows separated by \n and/or \r. * First row may contain a header with column names, indicated by leading #. * Column names "name", "value" and "description" indicate the columns containing those values, @@ -226,7 +226,7 @@ export default InAxis.extend({ /* can replace most of this function with d3.tsv; * It currently implements the feature that header is optional; to replicate that can use * dsv.parse() when header is input and dsv.parseRows() otherwise. - */ + */ let values = []; let rows = tableText.split(/[\n\r]+/); let colIdx = {name : 0, value : 1, description : 2}; @@ -246,22 +246,22 @@ export default InAxis.extend({ } else { - let - rowValue = { - name : col[colIdx["name"]], - value : col[colIdx["value"]] - }, - description = col[colIdx["description"]]; - for (let ic=0; ic - // g.axis-outer#id - let gAxis = d3.select("g.axis-outer#id" + axisID + "> g.axis-use"); - if (! gAxis.node()) { /* probably because axisShowExtend() has not added the g.axis-use yet - will sort out the dependencies ... . */ - dLog('layoutAndDrawChart', d3.select("g.axis-outer#id" + axisID).node(), 'no g.axis-use', this, axisID); - } + // to support multiple split axes, identify axis by axisID (and possibly stack id) + // + // g.axis-outer#id + let gAxis = d3.select("g.axis-outer#id" + axisID + "> g.axis-use"); + if (! gAxis.node()) { /* probably because axisShowExtend() has not added the g.axis-use yet - will sort out the dependencies ... . */ + dLog('layoutAndDrawChart', d3.select("g.axis-outer#id" + axisID).node(), 'no g.axis-use', this, axisID); + } this.dom.gAxis = gAxis; return gAxis; }; AxisCharts.prototype.getBBox = function () - { - let - gAxis = this.dom.gAxis, +{ + let + gAxis = this.dom.gAxis, gAxisElt = gAxis.node(); /* If the selection is empty, nothing will be drawn. */ if (gAxisElt) { @@ -154,32 +154,32 @@ AxisCharts.prototype.getRanges3 = function (resizedWidth) { } }; - AxisCharts.prototype.getRanges2 = function () - { - // based on https://bl.ocks.org/mbostock/3885304, axes x & y swapped. - let - // parentG = this.parentG, - bbox = this.ranges.bbox, - margin = showChartAxes ? - {top: 10, right: 20, bottom: 40, left: 20} : - {top: 0, right: 0, bottom: 0, left: 0}, - // pp=parentG.node().parentElement, - parentW = bbox.width, // +pp.attr("width") - parentH = bbox.height, // +pp.attr("height") - width = parentW - margin.left - margin.right, - height = parentH - margin.top - margin.bottom; - this.ranges.drawSize = {width, height}; - dLog('getRanges2', parentW, parentH, this.ranges.drawSize); - }; +AxisCharts.prototype.getRanges2 = function () +{ + // based on https://bl.ocks.org/mbostock/3885304, axes x & y swapped. + let + // parentG = this.parentG, + bbox = this.ranges.bbox, + margin = showChartAxes ? + {top: 10, right: 20, bottom: 40, left: 20} : + {top: 0, right: 0, bottom: 0, left: 0}, + // pp=parentG.node().parentElement, + parentW = bbox.width, // +pp.attr("width") + parentH = bbox.height, // +pp.attr("height") + width = parentW - margin.left - margin.right, + height = parentH - margin.top - margin.bottom; + this.ranges.drawSize = {width, height}; + dLog('getRanges2', parentW, parentH, this.ranges.drawSize); +}; AxisCharts.prototype.drawAxes = function (charts) { let - dom = this.dom, - /** the axes were originally within the gs,gsa of .group(); hence the var names. - * gs selects the into which the axes will be inserted, and gpa is the - * .enter().append() of that selection. - */ - gs = dom.gc, + dom = this.dom, + /** the axes were originally within the gs,gsa of .group(); hence the var names. + * gs selects the into which the axes will be inserted, and gpa is the + * .enter().append() of that selection. + */ + gs = dom.gc, gsa = dom.gca; gsa.each(Chart1.prototype.drawAxes); }; @@ -190,18 +190,18 @@ AxisCharts.prototype.commonFrame = function container() gAxis = this.dom.gAxis, bbox = this.ranges.bbox; - /** datum is value in hash : {value : , description: } and with optional attribute description. */ - - dLog('container', gAxis.node()); - /** parent; contains a clipPath, g > rect, text.resizer. */ - let gps = gAxis - .selectAll("g." + className) - .data([axisID]), - gp = gps - .enter() - .insert("g", ":first-child") - .attr('class', className); - if (false) { // not completed. Can base resized() on axis-2d.js + /** datum is value in hash : {value : , description: } and with optional attribute description. */ + + dLog('container', gAxis.node()); + /** parent; contains a clipPath, g > rect, text.resizer. */ + let gps = gAxis + .selectAll("g." + className) + .data([axisID]), + gp = gps + .enter() + .insert("g", ":first-child") + .attr('class', className); + if (false) { // not completed. Can base resized() on axis-2d.js let text = gp .append("text") .attr('class', 'resizer') @@ -217,44 +217,44 @@ AxisCharts.prototype.commonFrame = function container() AxisCharts.prototype.frame = function container(bbox, charts, allocatedWidth) { let - gps = this.dom.gps, + gps = this.dom.gps, gp = this.dom.gp; - /** datum is axisID, so id and clip-path can be functions. - * e.g. this.dom.gp.data() is [axisID] - */ - function axisClipId(axisID) { return "axis-chart-clip-" + axisID; } - let gpa = + /** datum is axisID, so id and clip-path can be functions. + * e.g. this.dom.gp.data() is [axisID] + */ + function axisClipId(axisID) { return "axis-chart-clip-" + axisID; } + let gpa = gp // define the clipPath - .append("clipPath") // define a clip path - .attr("id", axisClipId) // give the clipPath an ID - .append("rect"), // shape it as a rect - gprm = + .append("clipPath") // define a clip path + .attr("id", axisClipId) // give the clipPath an ID + .append("rect"), // shape it as a rect + gprm = gpa.merge(gps.selectAll("g > clipPath > rect")) - .attr("x", bbox.x) - .attr("y", bbox.y) - .attr("width", bbox.width) - .attr("height", bbox.height) - ; + .attr("x", bbox.x) + .attr("y", bbox.y) + .attr("width", bbox.width) + .attr("height", bbox.height) + ; let [startOffset, width] = allocatedWidth; let gca = gp.append("g") - .attr("clip-path", (d) => "url(#" + axisClipId(d) + ")") // clip with the rectangle - .selectAll("g[clip-path]") - .data(Object.values(charts)) - .enter() - .append("g") - .attr('class', (d) => d.dataConfig.dataTypeName) - .attr("transform", (d, i) => "translate(" + (startOffset + (i * 30)) + ", 0)") + .attr("clip-path", (d) => "url(#" + axisClipId(d) + ")") // clip with the rectangle + .selectAll("g[clip-path]") + .data(Object.values(charts)) + .enter() + .append("g") + .attr('class', (d) => d.dataConfig.dataTypeName) + .attr("transform", (d, i) => "translate(" + (startOffset + (i * 30)) + ", 0)") ; - let g = - gps.merge(gp).selectAll("g." + className+ " > g"); + let g = + gps.merge(gp).selectAll("g." + className+ " > g"); if (! gp.empty() ) { addParentClass(g); /* .gc is ​​ * .g (assigned later) is g.axis-chart -.chart-line ? + .chart-line ? * .gca contains a g for each chartType / dataTypeName, i.e. per Chart1. */ this.dom.gc = g; @@ -270,20 +270,20 @@ AxisCharts.prototype.controls = function controls() gp = this.dom.gca, gps = this.dom.gc; - function toggleBarsLineClosure(chart /*, i, g*/) - { - chart.toggleBarsLine(); - } + function toggleBarsLineClosure(chart /*, i, g*/) + { + chart.toggleBarsLine(); + } - /** currently placed at g.chart, could be inside g.chart>g (clip-path=). */ - let chartTypeToggle = gp - .append("circle") - .attr("class", "radio toggle chartType") - .attr("r", 6) - .on("click", toggleBarsLineClosure); - chartTypeToggle.merge(gps.selectAll("g > circle")) - .attr("cx", bbox.x + bbox.width / 2) /* was o[p], but g.axis-outer translation does x offset of stack. */ - .attr("cy", bbox.height - 10) + /** currently placed at g.chart, could be inside g.chart>g (clip-path=). */ + let chartTypeToggle = gp + .append("circle") + .attr("class", "radio toggle chartType") + .attr("r", 6) + .on("click", toggleBarsLineClosure); + chartTypeToggle.merge(gps.selectAll("g > circle")) + .attr("cx", bbox.x + bbox.width / 2) /* was o[p], but g.axis-outer translation does x offset of stack. */ + .attr("cy", bbox.height - 10) .classed("pushed", (chart1) => { return chart1.barsLine; }); chartTypeToggle.each(function(chart) { chart.chartTypeToggle = d3.select(this); } ); }; @@ -291,13 +291,13 @@ AxisCharts.prototype.controls = function controls() /*----------------------------------------------------------------------------*/ /* -class AxisChart { - bbox; - data; - gp; - gps; + class AxisChart { + bbox; + data; + gp; + gps; }; -*/ + */ /*----------------------------------------------------------------------------*/ @@ -329,8 +329,8 @@ Chart1.prototype.setupChart = function(axisID, axisCharts, chartData, blocks, da //---------------------------------------------------------------------------- let - blocksById = blocks.reduce( - (result, block) => { result[block.get('id')] = block; return result; }, []), + blocksById = blocks.reduce( + (result, block) => { result[block.get('id')] = block; return result; }, []), blockIds = Object.keys(chartData); blockIds.forEach((blockId) => { let block = blocksById[blockId]; @@ -350,7 +350,7 @@ Chart1.prototype.drawChart = function(axisCharts, chartData) let chart1 = this; /** possibly don't (yet) have chartData for each of blocks[], * i.e. blocksById may be a subset of blocks.mapBy('id'). - */ + */ let blockIds = Object.keys(chartData); blockIds.forEach((blockId) => { chart1.data(blockId, chartData[blockId]); @@ -365,100 +365,100 @@ Chart1.prototype.drawChart = function(axisCharts, chartData) Chart1.prototype.getRanges = function (ranges, chartData) { let - {yrange } = ranges, - {yAxis } = this.scales, - // if needed, use the first data array to calculate domain - chart = Object.values(chartData), - // axisID = gAxis.node().parentElement.__data__, - - yAxisDomain = yAxis.domain(), yDomain; - if (chart) - chart = chart[0]; - if (noDomain(yAxisDomain) && chart.length) { - // this assumes featureCountData; don't expect to need this. - yAxisDomain = [chart[0]._id.min, chart[chart.length-1]._id.max]; - yAxis.domain(yAxisDomain); - yDomain = yAxisDomain; - } - else - yDomain = [yAxis.invert(yrange[0]), yAxis.invert(yrange[1])]; + {yrange } = ranges, + {yAxis } = this.scales, + // if needed, use the first data array to calculate domain + chart = Object.values(chartData), + // axisID = gAxis.node().parentElement.__data__, + + yAxisDomain = yAxis.domain(), yDomain; + if (chart) + chart = chart[0]; + if (noDomain(yAxisDomain) && chart.length) { + // this assumes featureCountData; don't expect to need this. + yAxisDomain = [chart[0]._id.min, chart[chart.length-1]._id.max]; + yAxis.domain(yAxisDomain); + yDomain = yAxisDomain; + } + else + yDomain = [yAxis.invert(yrange[0]), yAxis.invert(yrange[1])]; - ranges.pxSize = (yDomain[1] - yDomain[0]) / ranges.bbox.height; + ranges.pxSize = (yDomain[1] - yDomain[0]) / ranges.bbox.height; }; - Chart1.prototype.prepareScales = function (data, drawSize) - { - /** The chart is perpendicular to the usual presentation. - * The names x & y (in {x,y}Range and yLine) match the orientation on the - * screen, rather than the role of abscissa / ordinate; the data maps - * .name (location) -> .value, which is named y -> x. - */ - let - dataConfig = this.dataConfig, - scales = this.scales, - width = drawSize.width, - height = drawSize.height, - xRange = [0, width], - yRange = [0, height], - // yRange is used as range of yLine by this.scaleLinear(). - /* scaleBand would suit a data set with evenly spaced or ordinal / nominal y values. - * yBand = d3.scaleBand().rangeRound(yRange).padding(0.1), - */ - y = useLocalY ? this.scaleLinear(yRange, data) : scales.yAxis; - scales.xWidth = - d3.scaleLinear().rangeRound(xRange); - if (dataConfig.barAsHeatmap) { - scales.xColour = d3.scaleOrdinal().range(d3.schemeCategory20); - } - // datum2LocationScaled() uses me.scales.x rather than the value in the closure in which it was created. - scales.x = dataConfig.barAsHeatmap ? scales.xColour : scales.xWidth; - - // Used by bars() - could be moved there, along with datum2LocationScaled(). - scales.y = y; - console.log("Chart1", xRange, yRange, dataConfig.dataTypeName); - - let - valueWidthFn = dataConfig.rectWidth.bind(dataConfig, /*scaled*/false, /*gIsData*/true), - valueCombinedDomain = this.domain(valueWidthFn, data); - scales.xWidth.domain(valueCombinedDomain); - if (scales.xColour) - scales.xColour.domain(valueCombinedDomain); - - }; - Chart1.prototype.drawContent = function () - { - Object.keys(this.chartLines).forEach((blockId) => { - let chartLine = this.chartLines[blockId]; - chartLine.drawContent(this.barsLine); - }); - }; +Chart1.prototype.prepareScales = function (data, drawSize) +{ + /** The chart is perpendicular to the usual presentation. + * The names x & y (in {x,y}Range and yLine) match the orientation on the + * screen, rather than the role of abscissa / ordinate; the data maps + * .name (location) -> .value, which is named y -> x. + */ + let + dataConfig = this.dataConfig, + scales = this.scales, + width = drawSize.width, + height = drawSize.height, + xRange = [0, width], + yRange = [0, height], + // yRange is used as range of yLine by this.scaleLinear(). + /* scaleBand would suit a data set with evenly spaced or ordinal / nominal y values. + * yBand = d3.scaleBand().rangeRound(yRange).padding(0.1), + */ + y = useLocalY ? this.scaleLinear(yRange, data) : scales.yAxis; + scales.xWidth = + d3.scaleLinear().rangeRound(xRange); + if (dataConfig.barAsHeatmap) { + scales.xColour = d3.scaleOrdinal().range(d3.schemeCategory20); + } + // datum2LocationScaled() uses me.scales.x rather than the value in the closure in which it was created. + scales.x = dataConfig.barAsHeatmap ? scales.xColour : scales.xWidth; + + // Used by bars() - could be moved there, along with datum2LocationScaled(). + scales.y = y; + console.log("Chart1", xRange, yRange, dataConfig.dataTypeName); + + let + valueWidthFn = dataConfig.rectWidth.bind(dataConfig, /*scaled*/false, /*gIsData*/true), + valueCombinedDomain = this.domain(valueWidthFn, data); + scales.xWidth.domain(valueCombinedDomain); + if (scales.xColour) + scales.xColour.domain(valueCombinedDomain); + +}; +Chart1.prototype.drawContent = function () +{ + Object.keys(this.chartLines).forEach((blockId) => { + let chartLine = this.chartLines[blockId]; + chartLine.drawContent(this.barsLine); + }); +}; Chart1.prototype.group = function (parentG, groupClassName) { - /** parentG is g.{{dataTypeName}}, within : g.axis-use > g.chart > g[clip-path] > g.{{dataTypeName}}. - * add g.(groupClassName); - * within g.axis-use there is also a sibling g.axis-html. */ - let // data = parentG.data(), - gs = parentG - .selectAll("g > g." + groupClassName) - .data((chart) => Object.values(chart.chartLines)), - gsa = gs - .enter() - .append("g") // maybe drop this g, move margin calc to gp - // if drawing internal chart axes then move them inside the clip rect - // .attr("transform", "translate(" + margin.left + "," + margin.top + ")"), + /** parentG is g.{{dataTypeName}}, within : g.axis-use > g.chart > g[clip-path] > g.{{dataTypeName}}. + * add g.(groupClassName); + * within g.axis-use there is also a sibling g.axis-html. */ + let // data = parentG.data(), + gs = parentG + .selectAll("g > g." + groupClassName) + .data((chart) => Object.values(chart.chartLines)), + gsa = gs + .enter() + .append("g") // maybe drop this g, move margin calc to gp + // if drawing internal chart axes then move them inside the clip rect + // .attr("transform", "translate(" + margin.left + "," + margin.top + ")"), .attr("class", (chartLine) => groupClassName) .attr('id', (chartLine) => groupClassName + '-' + chartLine.block.id) - // .data((chartLine) => chartLine.currentData) + // .data((chartLine) => chartLine.currentData) , // parentG.selectAll("g > g." + groupClassName); // g = gsa.merge(gs); dLog('group', this, parentG.node(), parentG, g.node()); - this.dom.g = g; - this.dom.gs = gs; - this.dom.gsa = gsa; + this.dom.g = g; + this.dom.gs = gs; + this.dom.gsa = gsa; // set ChartLine .g; used by ChartLine.{lines,bars}. gsa.each(function(chartLine, i) { chartLine.g = d3.select(this) ; } ); return g; @@ -466,48 +466,48 @@ Chart1.prototype.group = function (parentG, groupClassName) { Chart1.prototype.drawAxes = function (chart, i, g) { - /** first draft showed all data; subsequently adding : - * + select region from y domain - * - place data in tree for fast subset by region - * + alternate view : line - * + transition between views, zoom, stack - */ - // scaleBand() domain is a list of all y values. - // yBand.domain(data.map(dataConfig.datum2Location)); - - let - chart1 = this.__data__, - {height} = chart1.ranges.drawSize, - {x, y} = chart1.scales, - dom = chart1.dom, - gs = dom.gc, - /** selection into which to append the axes. */ - gsa = d3.select(this), - dataConfig = chart1.dataConfig; - - let axisXa = - gsa.append("g") - // - handle .exit() for these 2 also - .attr("class", "axis axis--x"); - axisXa.merge(gs.selectAll("g > g.axis--x")) - .attr("transform", "translate(0," + height + ")"); - if (! dataConfig.barAsHeatmap) - axisXa - .call(d3.axisBottom(x)); + /** first draft showed all data; subsequently adding : + * + select region from y domain + * - place data in tree for fast subset by region + * + alternate view : line + * + transition between views, zoom, stack + */ + // scaleBand() domain is a list of all y values. + // yBand.domain(data.map(dataConfig.datum2Location)); + + let + chart1 = this.__data__, + {height} = chart1.ranges.drawSize, + {x, y} = chart1.scales, + dom = chart1.dom, + gs = dom.gc, + /** selection into which to append the axes. */ + gsa = d3.select(this), + dataConfig = chart1.dataConfig; + + let axisXa = + gsa.append("g") + // - handle .exit() for these 2 also + .attr("class", "axis axis--x"); + axisXa.merge(gs.selectAll("g > g.axis--x")) + .attr("transform", "translate(0," + height + ")"); + if (! dataConfig.barAsHeatmap) + axisXa + .call(d3.axisBottom(x)); if (useLocalY) { - let axisYa = + let axisYa = gsa.append("g") - .attr("class", "axis axis--y"); - axisYa.merge(gs.selectAll("g > g.axis--y")) - .call(d3.axisLeft(y)) // .tickFormat(".2f") ? - // can option this out if !valueName - .append("text") - .attr("transform", "rotate(-90)") - .attr("y", 6) - .attr("dy", "0.71em") - .attr("text-anchor", "end") - .text(dataConfig.valueName); + .attr("class", "axis axis--y"); + axisYa.merge(gs.selectAll("g > g.axis--y")) + .call(d3.axisLeft(y)) // .tickFormat(".2f") ? + // can option this out if !valueName + .append("text") + .attr("transform", "rotate(-90)") + .attr("y", 6) + .attr("dy", "0.71em") + .attr("text-anchor", "end") + .text(dataConfig.valueName); } }; /** @@ -543,51 +543,51 @@ Chart1.prototype.drawLine = function (blockId, block, data) chartLine.drawContent(this.barsLine); }; - Chart1.prototype.scaleLinear = function (yRange, data) - { - // based on https://bl.ocks.org/mbostock/3883245 - if (! this.scales.yLine) - this.scales.yLine = d3.scaleLinear(); - let y = this.scales.yLine; - y.rangeRound(yRange); - let - d2l = this.dataConfig.datum2Location || Object.values(this.chartLines)[0].dataConfig.datum2Location; - let combinedDomain = this.domain(d2l, data); - y.domain(combinedDomain); - console.log('scaleLinear domain', combinedDomain); - return y; - }; - /** Combine the domains of each of the component ChartLine-s. - * @param valueFn e.g. datum2Location or datum2Value - */ - Chart1.prototype.domain = function (valueFn, blocksData) - { - let blockIds = Object.keys(blocksData), - domains = blockIds.map((blockId) => { - let - data = blocksData[blockId], - chartLine = this.chartLines[blockId]; - return chartLine.domain(valueFn, data); - }); - /** Union the domains. */ - let domain = domains - .reduce((acc, val) => acc.concat(val), []); - return domain; - }; - /** Alternate between bar chart and line chart */ - Chart1.prototype.toggleBarsLine = function () - { - console.log("toggleBarsLine", this); - d3.event.stopPropagation(); - this.barsLine = ! this.barsLine; - this.chartTypeToggle - .classed("pushed", this.barsLine); - this.dom.g.selectAll("g > *").remove(); - Object.keys(this.chartLines).forEach((blockId) => { - let chartLine = this.chartLines[blockId]; - chartLine.drawContent(this.barsLine); - }); - }; +Chart1.prototype.scaleLinear = function (yRange, data) +{ + // based on https://bl.ocks.org/mbostock/3883245 + if (! this.scales.yLine) + this.scales.yLine = d3.scaleLinear(); + let y = this.scales.yLine; + y.rangeRound(yRange); + let + d2l = this.dataConfig.datum2Location || Object.values(this.chartLines)[0].dataConfig.datum2Location; + let combinedDomain = this.domain(d2l, data); + y.domain(combinedDomain); + console.log('scaleLinear domain', combinedDomain); + return y; +}; +/** Combine the domains of each of the component ChartLine-s. + * @param valueFn e.g. datum2Location or datum2Value + */ +Chart1.prototype.domain = function (valueFn, blocksData) +{ + let blockIds = Object.keys(blocksData), + domains = blockIds.map((blockId) => { + let + data = blocksData[blockId], + chartLine = this.chartLines[blockId]; + return chartLine.domain(valueFn, data); + }); + /** Union the domains. */ + let domain = domains + .reduce((acc, val) => acc.concat(val), []); + return domain; +}; +/** Alternate between bar chart and line chart */ +Chart1.prototype.toggleBarsLine = function () +{ + console.log("toggleBarsLine", this); + d3.event.stopPropagation(); + this.barsLine = ! this.barsLine; + this.chartTypeToggle + .classed("pushed", this.barsLine); + this.dom.g.selectAll("g > *").remove(); + Object.keys(this.chartLines).forEach((blockId) => { + let chartLine = this.chartLines[blockId]; + chartLine.drawContent(this.barsLine); + }); +}; /*----------------------------------------------------------------------------*/ @@ -599,7 +599,7 @@ ChartLine.prototype.setup = function(blockId) { if (! this.dataConfig.datum2Location) { // copy dataConfig to give a custom value to this ChartLine. let d = new DataConfig(this.dataConfig); - d.datum2Location = + d.datum2Location = (d) => datum2LocationWithBlock(d, blockId); this.dataConfig = d; } @@ -608,35 +608,35 @@ ChartLine.prototype.setup = function(blockId) { * and set .currentData */ ChartLine.prototype.filterToZoom = function(chart) { - let - {yAxis} = this.scales, - yDomain = yAxis.domain(), - withinZoomRegion = (d) => { - return inRangeEither(this.dataConfig.datum2Location(d), yDomain); - }, - data = chart.filter(withinZoomRegion); - this.currentData = data; - - dLog(yDomain, data.length, (data.length == 0) || this.dataConfig.datum2Location(data[0])); + let + {yAxis} = this.scales, + yDomain = yAxis.domain(), + withinZoomRegion = (d) => { + return inRangeEither(this.dataConfig.datum2Location(d), yDomain); + }, + data = chart.filter(withinZoomRegion); + this.currentData = data; + + dLog(yDomain, data.length, (data.length == 0) || this.dataConfig.datum2Location(data[0])); return data; }; - /** Enables use of the scales which are set up in .prepareScales(). - */ - ChartLine.prototype.scaledConfig = function () - { - let - dataConfig = this.dataConfig, - scales = this.scales; - - /* these can be renamed datum2{abscissa,ordinate}{,Scaled}() */ - /* apply y after scale applied by datum2Location */ - let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, scales.y); - /** related @see rectWidth(). */ - function datum2ValueScaled(d) { return scales.x(dataConfig.datum2Value(d)); } - dataConfig.datum2LocationScaled = datum2LocationScaled; - dataConfig.datum2ValueScaled = datum2ValueScaled; - }; +/** Enables use of the scales which are set up in .prepareScales(). + */ +ChartLine.prototype.scaledConfig = function () +{ + let + dataConfig = this.dataConfig, + scales = this.scales; + + /* these can be renamed datum2{abscissa,ordinate}{,Scaled}() */ + /* apply y after scale applied by datum2Location */ + let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, scales.y); + /** related @see rectWidth(). */ + function datum2ValueScaled(d) { return scales.x(dataConfig.datum2Value(d)); } + dataConfig.datum2LocationScaled = datum2LocationScaled; + dataConfig.datum2ValueScaled = datum2ValueScaled; +}; ChartLine.prototype.blockColour = function () { @@ -646,241 +646,241 @@ ChartLine.prototype.blockColour = function () return colour; }; - ChartLine.prototype.bars = function (data) - { - let - dataConfig = this.dataConfig, - block = this.block, - g = this.g; - if (dataConfig.barAsHeatmap) - this.scales.x = this.scales.xColour; - let - rs = g - // .select("g." + className + " > g") - .selectAll("rect." + dataConfig.barClassName) - .data(data), - re = rs.enter(), rx = rs.exit(); - let ra = re - .append("rect"); - ra - .attr("class", dataConfig.barClassName) - .attr("fill", (d) => this.blockColour()) - /** parent datum is currently 1, but could be this.block; - * this.parentElement.parentElement.__data__ has the axis id (not the blockId), - */ - .each(function (d) { configureHorizTickHover.apply(this, [d, block, dataConfig.hoverTextFn]); }); - ra - .merge(rs) - .transition().duration(1500) - .attr("x", 0) - .attr("y", (d) => { let li = dataConfig.datum2LocationScaled(d); return li.length ? li[0] : li; }) - // yBand.bandwidth() - .attr("height", dataConfig.rectHeight.bind(dataConfig, /*scaled*/true, /*gIsData*/false)) // equiv : (d, i, g) => dataConfig.rectHeight(true, false, d, i, g); - ; - let barWidth = dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false); - ra - .attr("width", dataConfig.barAsHeatmap ? 20 : barWidth); - if (dataConfig.barAsHeatmap) - ra - .attr('fill', barWidth); - rx.remove(); - console.log(rs.nodes(), re.nodes()); - }; - - /** A single horizontal line for each data point. - * Position is similar to the rectangle which would be drawn by bars(): - * X limits are the same as the rectangle limits (left, right) - * and Y position is at the middle of the equivalent rectangle. - */ - ChartLine.prototype.linebars = function (data) - { - let - dataConfig = this.dataConfig, - block = this.block, - scales = this.scales, - g = this.g; - let - rs = g - // .select("g." + className + " > g") - .selectAll("path." + dataConfig.barClassName) - .data(data), - re = rs.enter(), rx = rs.exit(); - let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, scales.y); - let line = d3.line(); - - function horizLine(d, i, g) { - let barWidth = dataConfig.rectWidth(/*scaled*/true, /*gIsData*/false, d, i, g); - let y = middle(datum2LocationScaled(d)), - l = line([ - [0, y], - [barWidth, y]]); - return [l]; - } - - let ra = re - .append("path"); - ra - .attr("class", dataConfig.barClassName) - // same comment re parent datum as for bars() - .each(function (d) { configureHorizTickHover.apply(this, [d, block, dataConfig.hoverTextFn]); }); - ra - .merge(rs) - .transition().duration(1500) - .attr('d', horizLine) - .attr("stroke", (d) => this.blockColour()) - ; - - rx.remove(); - console.log(rs.nodes(), re.nodes()); - }; - - /** Calculate the domain of some function of the data, which may be the data value or location. - * This can be used in independent of axis-chart, and can be factored out - * for more general use, along with Chart1:domain(). - * @param valueFn e.g. datum2Location or datum2Value - * In axis-chart, value is x, location is y. - */ - ChartLine.prototype.domain = function (valueFn, data) - { - let - /** location may be an interval, so flatten the result. - * Later Array.flat() can be used. - * Value is not an interval (yet), so that .reduce is a no-op). - */ - yFlat = data - .map(valueFn) - .reduce((acc, val) => acc.concat(val), []); - let domain = d3.extent(yFlat); - console.log('ChartLine domain', domain, yFlat); - return domain; - }; - - ChartLine.prototype.line = function (data) - { - let y = this.scales.y, dataConfig = this.dataConfig; - this.scales.x = this.scales.xWidth; - - let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, y); - let line = d3.line() - .x(dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false)) - .y((d) => middle(datum2LocationScaled(d))); - - console.log("line x domain", this.scales.x.domain(), this.scales.x.range()); - - let - g = this.g, - ps = g - .selectAll("g > path." + dataConfig.barClassName) - .data([this]); - ps - .enter() - .append("path") - .attr("class", dataConfig.barClassName + " line") - .attr("stroke", (d) => this.blockColour()) - .datum(data) - .attr("d", line) - .merge(ps) - .datum(data) - .transition().duration(1500) - .attr("d", line); - // data length is constant 1, so .remove() is not needed - ps.exit().remove(); - }; - - - - /** Draw, using .currentData, which is set by calling .filterToZoom(). - * @param barsLine if true, draw .bars, otherwise .line. +ChartLine.prototype.bars = function (data) +{ + let + dataConfig = this.dataConfig, + block = this.block, + g = this.g; + if (dataConfig.barAsHeatmap) + this.scales.x = this.scales.xColour; + let + rs = g + // .select("g." + className + " > g") + .selectAll("rect." + dataConfig.barClassName) + .data(data), + re = rs.enter(), rx = rs.exit(); + let ra = re + .append("rect"); + ra + .attr("class", dataConfig.barClassName) + .attr("fill", (d) => this.blockColour()) + /** parent datum is currently 1, but could be this.block; + * this.parentElement.parentElement.__data__ has the axis id (not the blockId), + */ + .each(function (d) { configureHorizTickHover.apply(this, [d, block, dataConfig.hoverTextFn]); }); + ra + .merge(rs) + .transition().duration(1500) + .attr("x", 0) + .attr("y", (d) => { let li = dataConfig.datum2LocationScaled(d); return li.length ? li[0] : li; }) + // yBand.bandwidth() + .attr("height", dataConfig.rectHeight.bind(dataConfig, /*scaled*/true, /*gIsData*/false)) // equiv : (d, i, g) => dataConfig.rectHeight(true, false, d, i, g); + ; + let barWidth = dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false); + ra + .attr("width", dataConfig.barAsHeatmap ? 20 : barWidth); + if (dataConfig.barAsHeatmap) + ra + .attr('fill', barWidth); + rx.remove(); + console.log(rs.nodes(), re.nodes()); +}; + +/** A single horizontal line for each data point. + * Position is similar to the rectangle which would be drawn by bars(): + * X limits are the same as the rectangle limits (left, right) + * and Y position is at the middle of the equivalent rectangle. + */ +ChartLine.prototype.linebars = function (data) +{ + let + dataConfig = this.dataConfig, + block = this.block, + scales = this.scales, + g = this.g; + let + rs = g + // .select("g." + className + " > g") + .selectAll("path." + dataConfig.barClassName) + .data(data), + re = rs.enter(), rx = rs.exit(); + let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, scales.y); + let line = d3.line(); + + function horizLine(d, i, g) { + let barWidth = dataConfig.rectWidth(/*scaled*/true, /*gIsData*/false, d, i, g); + let y = middle(datum2LocationScaled(d)), + l = line([ + [0, y], + [barWidth, y]]); + return [l]; + } + + let ra = re + .append("path"); + ra + .attr("class", dataConfig.barClassName) + // same comment re parent datum as for bars() + .each(function (d) { configureHorizTickHover.apply(this, [d, block, dataConfig.hoverTextFn]); }); + ra + .merge(rs) + .transition().duration(1500) + .attr('d', horizLine) + .attr("stroke", (d) => this.blockColour()) + ; + + rx.remove(); + console.log(rs.nodes(), re.nodes()); +}; + +/** Calculate the domain of some function of the data, which may be the data value or location. + * This can be used in independent of axis-chart, and can be factored out + * for more general use, along with Chart1:domain(). + * @param valueFn e.g. datum2Location or datum2Value + * In axis-chart, value is x, location is y. + */ +ChartLine.prototype.domain = function (valueFn, data) +{ + let + /** location may be an interval, so flatten the result. + * Later Array.flat() can be used. + * Value is not an interval (yet), so that .reduce is a no-op). */ - ChartLine.prototype.drawContent = function(barsLine) - { - let - data = this.currentData; - /** The Effects probabilities data is keyed by a location which is a SNP, so linebars() is a sensible representaion. - * This uses the data shape to recognise Effects data; this is provisional - * - we can probably lookup the tag 'EffectsPlus' in dataset tags (refn resources/tools/dev/effects2Dataset.pl). - * The effects data takes the form of an array of 5 probabilities, in the 3rd element of feature.value. - */ - let isEffectsData = data.length && data[0].name && data[0].value && (data[0].value.length === 3) && (data[0].value[2].length === 6); - let bars = isEffectsData ? this.linebars : this.bars; - let chartDraw = barsLine ? bars : this.line; - chartDraw.apply(this, [data]); - }; + yFlat = data + .map(valueFn) + .reduce((acc, val) => acc.concat(val), []); + let domain = d3.extent(yFlat); + console.log('ChartLine domain', domain, yFlat); + return domain; +}; + +ChartLine.prototype.line = function (data) +{ + let y = this.scales.y, dataConfig = this.dataConfig; + this.scales.x = this.scales.xWidth; + + let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, y); + let line = d3.line() + .x(dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false)) + .y((d) => middle(datum2LocationScaled(d))); + + console.log("line x domain", this.scales.x.domain(), this.scales.x.range()); + + let + g = this.g, + ps = g + .selectAll("g > path." + dataConfig.barClassName) + .data([this]); + ps + .enter() + .append("path") + .attr("class", dataConfig.barClassName + " line") + .attr("stroke", (d) => this.blockColour()) + .datum(data) + .attr("d", line) + .merge(ps) + .datum(data) + .transition().duration(1500) + .attr("d", line); + // data length is constant 1, so .remove() is not needed + ps.exit().remove(); +}; + + + +/** Draw, using .currentData, which is set by calling .filterToZoom(). + * @param barsLine if true, draw .bars, otherwise .line. + */ +ChartLine.prototype.drawContent = function(barsLine) +{ + let + data = this.currentData; + /** The Effects probabilities data is keyed by a location which is a SNP, so linebars() is a sensible representaion. + * This uses the data shape to recognise Effects data; this is provisional + * - we can probably lookup the tag 'EffectsPlus' in dataset tags (refn resources/tools/dev/effects2Dataset.pl). + * The effects data takes the form of an array of 5 probabilities, in the 3rd element of feature.value. + */ + let isEffectsData = data.length && data[0].name && data[0].value && (data[0].value.length === 3) && (data[0].value[2].length === 6); + let bars = isEffectsData ? this.linebars : this.bars; + let chartDraw = barsLine ? bars : this.line; + chartDraw.apply(this, [data]); +}; /*----------------------------------------------------------------------------*/ - /** Calculate the height of rectangle to be used for this data point - * @param this is DataConfig, not DOM element. - * @param scaled true means apply scale (x) to the result - * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. - * gIsData will be true when called from d3.max(), and false for d3 attr functions. - */ - DataConfig.prototype.rectWidth = function (scaled, gIsData, d, i, g) - { - Ember.assert('rectWidth arguments.length === 5', arguments.length === 5); - /** The scale is linear, so it is OK to scale before dividing by rectHeight. - * Otherwise could use datum2Value() here and apply this.x() before the return. - */ - let d2v = (scaled ? this.datum2ValueScaled : this.datum2Value), - width = d2v(d); - if (this.valueIsArea) { - let h; - // Factor the width consistently by h, don't sometimes use scaled (i.e. pass scaled==false). - width /= (h = this.rectHeight(false, gIsData, d, i, g)); - // dLog('rectWidth', h, width, gIsData); - } - return width; - }; - /** Calculate the height of rectangle to be used for this data point - * @param this is DataConfig, not DOM element. - * @param scaled true means apply scale (y) to the result - * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. - * gIsData will be true when called from d3.max(), and false for d3 attr functions. +/** Calculate the height of rectangle to be used for this data point + * @param this is DataConfig, not DOM element. + * @param scaled true means apply scale (x) to the result + * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. + * gIsData will be true when called from d3.max(), and false for d3 attr functions. + */ +DataConfig.prototype.rectWidth = function (scaled, gIsData, d, i, g) +{ + Ember.assert('rectWidth arguments.length === 5', arguments.length === 5); + /** The scale is linear, so it is OK to scale before dividing by rectHeight. + * Otherwise could use datum2Value() here and apply this.x() before the return. + */ + let d2v = (scaled ? this.datum2ValueScaled : this.datum2Value), + width = d2v(d); + if (this.valueIsArea) { + let h; + // Factor the width consistently by h, don't sometimes use scaled (i.e. pass scaled==false). + width /= (h = this.rectHeight(false, gIsData, d, i, g)); + // dLog('rectWidth', h, width, gIsData); + } + return width; +}; +/** Calculate the height of rectangle to be used for this data point + * @param this is DataConfig, not DOM element. + * @param scaled true means apply scale (y) to the result + * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. + * gIsData will be true when called from d3.max(), and false for d3 attr functions. + */ +DataConfig.prototype.rectHeight = function (scaled, gIsData, d, i, g) +{ + Ember.assert('rectHeight arguments.length === 5', arguments.length === 5); + let height, + d2l = (scaled ? this.datum2LocationScaled : this.datum2Location), + location; + /* if location is an interval, calculate height from it. + * Otherwise, use adjacent points to indicate height. + */ + if ((location = d2l(d)).length) { + height = Math.abs(location[1] - location[0]); + } + else { + /* the boundary between 2 adjacent points is midway between them. + * So sum that half-distance for the previous and next points. + * the y range distance from the previous point to the next point. + * If this point is either end, simply double the other half-distance. */ - DataConfig.prototype.rectHeight = function (scaled, gIsData, d, i, g) - { - Ember.assert('rectHeight arguments.length === 5', arguments.length === 5); - let height, - d2l = (scaled ? this.datum2LocationScaled : this.datum2Location), - location; - /* if location is an interval, calculate height from it. - * Otherwise, use adjacent points to indicate height. + if (! g.length) + /* constant value OK - don't expect to be called if g.length is 0. + * this.scales.y.range() / 10; */ - if ((location = d2l(d)).length) { - height = Math.abs(location[1] - location[0]); - } - else { - /* the boundary between 2 adjacent points is midway between them. - * So sum that half-distance for the previous and next points. - * the y range distance from the previous point to the next point. - * If this point is either end, simply double the other half-distance. - */ - if (! g.length) - /* constant value OK - don't expect to be called if g.length is 0. - * this.scales.y.range() / 10; - */ - height = 10; - else { - let r = []; - function gData(i) { let gi = g[i]; return gIsData ? gi : gi.__data__; }; - if (i > 0) - r.push(gData(i)); - r.push(d); - if (i < g.length-1) - r.push(gData(i+1)); - let y = - r.map(this.datum2LocationScaled); - height = Math.abs(y[y.length-1] - y[0]) * 2 / (y.length-1); - dLog('rectHeight', gIsData, d, i, /*g,*/ r, y, height); - if (! height) - height = 1; - } - } - return height; - }; + height = 10; + else { + let r = []; + function gData(i) { let gi = g[i]; return gIsData ? gi : gi.__data__; }; + if (i > 0) + r.push(gData(i)); + r.push(d); + if (i < g.length-1) + r.push(gData(i+1)); + let y = + r.map(this.datum2LocationScaled); + height = Math.abs(y[y.length-1] - y[0]) * 2 / (y.length-1); + dLog('rectHeight', gIsData, d, i, /*g,*/ r, y, height); + if (! height) + height = 1; + } + } + return height; +}; DataConfig.prototype.addedDefaults = function(valueName) { if (! this.hoverTextFn) From b3aa511fa98f9de35d6c1cdf853f9367cb11a920 Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 7 Feb 2020 06:57:50 +1100 Subject: [PATCH 25/57] trace - fix string label a538a21 4841 Feb 5 09:55 backend/common/utilities/block-features.js --- backend/common/utilities/block-features.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/common/utilities/block-features.js b/backend/common/utilities/block-features.js index a2597951d..e7a2dce95 100644 --- a/backend/common/utilities/block-features.js +++ b/backend/common/utilities/block-features.js @@ -64,7 +64,7 @@ exports.blockFeaturesCounts = function(db, blockId, nBins = 10) { // initial draft based on blockFeaturesCount() let featureCollection = db.collection("Feature"); if (trace_block) - console.log('blockFeaturesCount', blockId, nBins); + console.log('blockFeaturesCounts', blockId, nBins); let ObjectId = ObjectID; let @@ -77,7 +77,7 @@ exports.blockFeaturesCounts = function(db, blockId, nBins = 10) { pipeline = matchBlock; if (trace_block) - console.log('blockFeaturesCount', pipeline); + console.log('blockFeaturesCounts', pipeline); if (trace_block > 1) console.dir(pipeline, { depth: null }); From d4b9b13b88daf116ce9372eb3ffe027b5f550dda Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 7 Feb 2020 09:40:03 +1100 Subject: [PATCH 26/57] add remove. some variable renames for remove: add undraw(), frameRemove(), willDestroyElement(). frontend/app/ : e8a45c5 11292 Feb 7 07:30 components/axis-charts.js 9b4455e 29250 Feb 7 07:30 utils/draw/chart1.js --- frontend/app/components/axis-charts.js | 21 +++++++---- frontend/app/utils/draw/chart1.js | 52 +++++++++++++------------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/frontend/app/components/axis-charts.js b/frontend/app/components/axis-charts.js index c298965df..178cc9b95 100644 --- a/frontend/app/components/axis-charts.js +++ b/frontend/app/components/axis-charts.js @@ -42,17 +42,26 @@ export default InAxis.extend({ /** {dataTypeName : Chart1, ... } */ charts : undefined, - init() { + didReceiveAttrs() { this._super(...arguments); this.set('blocksData', Ember.Object.create()); this.set('charts', Ember.Object.create()); + // axisID isn't planned to change for this component + this.set('axisCharts', new AxisCharts(this.get('axisID'))); }, didRender() { console.log("components/axis-chart didRender()"); this.draw(); }, + willDestroyElement() { + console.log("components/axis-chart willDestroyElement()"); + this.undraw(); + + this._super(...arguments); + }, + axisID : Ember.computed.alias('axis.axisID'), @@ -75,12 +84,6 @@ export default InAxis.extend({ return yAxis; }), - axisCharts : Ember.computed(function () { - /* axisID isn't planned to change for this component; could set this up in - * init(); using a CP has the benefit of lazy-evaluation. */ - return new AxisCharts(this.get('axisID')); - }), - chartTypes : Ember.computed('blocksData.@each', function () { let blocksData = this.get('blocksData'), @@ -179,6 +182,10 @@ export default InAxis.extend({ }, + undraw() { + this.get('axisCharts').frameRemove(); + }, + drawContent() { let charts = this.get('chartsArray'); /* y axis has been updated, so redrawing the content will update y positions. */ diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index c705d7147..1b5182f9b 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -175,13 +175,11 @@ AxisCharts.prototype.getRanges2 = function () AxisCharts.prototype.drawAxes = function (charts) { let dom = this.dom, - /** the axes were originally within the gs,gsa of .group(); hence the var names. - * gs selects the into which the axes will be inserted, and gpa is the - * .enter().append() of that selection. + append = dom.gca; + /** the axes were originally within the gs,gsa of .group(), but are now within the g[clip-path]; + * append here selects the into which the axes will be inserted. */ - gs = dom.gc, - gsa = dom.gca; - gsa.each(Chart1.prototype.drawAxes); + append.each(Chart1.prototype.drawAxes); }; AxisCharts.prototype.commonFrame = function container() @@ -193,7 +191,7 @@ AxisCharts.prototype.commonFrame = function container() /** datum is value in hash : {value : , description: } and with optional attribute description. */ dLog('container', gAxis.node()); - /** parent; contains a clipPath, g > rect, text.resizer. */ + /** parent selection ; contains a clipPath, g clip-path, text.resizer. */ let gps = gAxis .selectAll("g." + className) .data([axisID]), @@ -214,6 +212,11 @@ AxisCharts.prototype.commonFrame = function container() this.dom.gp = gp; }; +AxisCharts.prototype.frameRemove = function container() { + let gp = this.dom && this.dom.gp; + gp && gp.remove(); +}; + AxisCharts.prototype.frame = function container(bbox, charts, allocatedWidth) { let @@ -224,6 +227,7 @@ AxisCharts.prototype.frame = function container(bbox, charts, allocatedWidth) * e.g. this.dom.gp.data() is [axisID] */ function axisClipId(axisID) { return "axis-chart-clip-" + axisID; } + /** gp is the .enter() of g.chart, and gpa is the .append() of that selection. */ let gpa = gp // define the clipPath .append("clipPath") // define a clip path @@ -267,8 +271,8 @@ AxisCharts.prototype.controls = function controls() { let bbox = this.ranges.bbox, - gp = this.dom.gca, - gps = this.dom.gc; + append = this.dom.gca, + select = this.dom.gc; function toggleBarsLineClosure(chart /*, i, g*/) { @@ -276,12 +280,12 @@ AxisCharts.prototype.controls = function controls() } /** currently placed at g.chart, could be inside g.chart>g (clip-path=). */ - let chartTypeToggle = gp + let chartTypeToggle = append .append("circle") .attr("class", "radio toggle chartType") .attr("r", 6) .on("click", toggleBarsLineClosure); - chartTypeToggle.merge(gps.selectAll("g > circle")) + chartTypeToggle.merge(select.selectAll("g > circle")) .attr("cx", bbox.x + bbox.width / 2) /* was o[p], but g.axis-outer translation does x offset of stack. */ .attr("cy", bbox.height - 10) .classed("pushed", (chart1) => { return chart1.barsLine; }); @@ -304,7 +308,7 @@ AxisCharts.prototype.controls = function controls() Chart1.prototype.overlap = function(axisCharts) { let chart1 = this; - // Plan is for Axischarts to own .ranges, but for now there is some overlap. + // Plan is for AxisCharts to own .ranges, but for now there is some overlap. if (! chart1.ranges) { chart1.ranges = axisCharts.ranges; chart1.dom = axisCharts.dom; @@ -312,8 +316,7 @@ Chart1.prototype.overlap = function(axisCharts) { }; Chart1.prototype.setupChart = function(axisID, axisCharts, chartData, blocks, dataConfig, yAxisScale, resizedWidth) { - let chart1 = this; - chart1.scales.yAxis = yAxisScale; + this.scales.yAxis = yAxisScale; //---------------------------------------------------------------------------- @@ -334,33 +337,32 @@ Chart1.prototype.setupChart = function(axisID, axisCharts, chartData, blocks, da blockIds = Object.keys(chartData); blockIds.forEach((blockId) => { let block = blocksById[blockId]; - chart1.createLine(blockId, block); + this.createLine(blockId, block); }); - chart1.group(chart1.dom.gca, 'chart-line'); + this.group(this.dom.gca, 'chart-line'); //---------------------------------------------------------------------------- - chart1.getRanges(chart1.ranges, chartData); + this.getRanges(this.ranges, chartData); - return chart1; + return this; }; Chart1.prototype.drawChart = function(axisCharts, chartData) { - let chart1 = this; /** possibly don't (yet) have chartData for each of blocks[], * i.e. blocksById may be a subset of blocks.mapBy('id'). */ let blockIds = Object.keys(chartData); blockIds.forEach((blockId) => { - chart1.data(blockId, chartData[blockId]); + this.data(blockId, chartData[blockId]); }); - chart1.prepareScales(chartData, axisCharts.ranges.drawSize); + this.prepareScales(chartData, this.ranges.drawSize); blockIds.forEach((blockId) => { - chart1.chartLines[blockId].scaledConfig(); } ); + this.chartLines[blockId].scaledConfig(); } ); - chart1.drawContent(); + this.drawContent(); }; Chart1.prototype.getRanges = function (ranges, chartData) { @@ -815,7 +817,7 @@ ChartLine.prototype.drawContent = function(barsLine) /** Calculate the height of rectangle to be used for this data point * @param this is DataConfig, not DOM element. * @param scaled true means apply scale (x) to the result - * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. + * @param gIsData true means g is __data__, otherwise it is DOM element, and has .__data__ attribute. * gIsData will be true when called from d3.max(), and false for d3 attr functions. */ DataConfig.prototype.rectWidth = function (scaled, gIsData, d, i, g) @@ -837,7 +839,7 @@ DataConfig.prototype.rectWidth = function (scaled, gIsData, d, i, g) /** Calculate the height of rectangle to be used for this data point * @param this is DataConfig, not DOM element. * @param scaled true means apply scale (y) to the result - * @param gIsData true meangs g is __data__, otherwise it is DOM element, and has .__data__ attribute. + * @param gIsData true means g is __data__, otherwise it is DOM element, and has .__data__ attribute. * gIsData will be true when called from d3.max(), and false for d3 attr functions. */ DataConfig.prototype.rectHeight = function (scaled, gIsData, d, i, g) From f636c8048d7a84d4c041d9e09135b20891127a5a Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 7 Feb 2020 12:06:23 +1100 Subject: [PATCH 27/57] options=splitAxes1 is now enabled by default Handle undefined ranges.bbox - can be caused by variation in timing. frontend/app/ : 73fc1d5 240554 Feb 7 11:04 components/draw-map.js 5e9e51e 6345 Feb 7 11:20 utils/data-types.js a46ed27 30394 Feb 7 11:04 utils/draw/chart1.js --- frontend/app/components/draw-map.js | 3 ++ frontend/app/utils/data-types.js | 18 ++++++++++++ frontend/app/utils/draw/chart1.js | 44 +++++++++++++++++++++-------- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/frontend/app/components/draw-map.js b/frontend/app/components/draw-map.js index c338d57d7..e7418dd02 100644 --- a/frontend/app/components/draw-map.js +++ b/frontend/app/components/draw-map.js @@ -1954,6 +1954,9 @@ export default Ember.Component.extend(Ember.Evented, { if (options_param && ! this.get('urlOptions') && (options = parseOptions(options_param))) { + /* splitAxes1 is now enabled by default. */ + if (! options.splitAxes1) + options.splitAxes1 = true; this.set('urlOptions', options); this.get('blockService').injectParsedOptions(options); // alpha enables new features which are not yet robust. diff --git a/frontend/app/utils/data-types.js b/frontend/app/utils/data-types.js index 39d900a2d..7d0fbbaa4 100644 --- a/frontend/app/utils/data-types.js +++ b/frontend/app/utils/data-types.js @@ -1,6 +1,24 @@ import { getAttrOrCP } from './ember-devel'; +/*----------------------------------------------------------------------------*/ +/** Feature values + +The API relies on Features having a .value which is a location or an interval, +represented as [location] or [from, to]. +The plan is to support other values; using JSON in the database and API enables +variant value types to be added in a way which is opaque to the API. +For example, the effects probability data has been added as [location, undefined, [probabilities, ... ]]. +The API relies on .value[0] and optionally .value[1], and ignores other elements of .values[]. +We might choose a different format as we include more data types in the scope, +e.g. we could split non-location values out to a separate field, +so it is desirable to access feature values through an access layer which abstracts away from the storage structure used. +The DataConfig class defined here provides such an abstraction. +It was added to support the axis-charts, but can be progressively used in other modules. +This can be integrated into models/feature.js + + */ + /*----------------------------------------------------------------------------*/ class DataConfig { diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index 1b5182f9b..fee2aa566 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -19,6 +19,21 @@ const useLocalY = false; /*----------------------------------------------------------------------------*/ +/** Introductory notes + +The axis charts code is organised into : +. components/axis-charts.js provides the data feed +. utils/draw/chart1.js renders the data using d3 +. utils/data-types.js accesses values from the data +It is not essential that they be divided this way, e.g. the Ember and JS classes could be combined, but it seems to provide a good module size, and plays to the strengths of each framework, using Ember as the integration / dynamics layer and d3 as the purely mechanistic rendering layer. + +The Ember component connects to other modules, delivering data flows to the rendering functions; e.g. connections include events such as zoom & resize which trigger render updates. +Using JS classes enables 4 small and closely related classes to be in a single file, which was useful when the early prototype code was being split into the number of pieces required by the update structure, and moved between levels as the design took shape. + + */ + +/*----------------------------------------------------------------------------*/ + const dLog = console.debug; @@ -149,7 +164,8 @@ AxisCharts.prototype.getRanges3 = function (resizedWidth) { if (resizedWidth) { let {bbox} = this.ranges; - bbox.width = resizedWidth; + if (bbox) + bbox.width = resizedWidth; dLog('resizedWidth', resizedWidth, bbox); } }; @@ -159,17 +175,20 @@ AxisCharts.prototype.getRanges2 = function () // based on https://bl.ocks.org/mbostock/3885304, axes x & y swapped. let // parentG = this.parentG, - bbox = this.ranges.bbox, - margin = showChartAxes ? - {top: 10, right: 20, bottom: 40, left: 20} : + bbox = this.ranges && this.ranges.bbox; + if (bbox) { + let + margin = showChartAxes ? + {top: 10, right: 20, bottom: 40, left: 20} : {top: 0, right: 0, bottom: 0, left: 0}, - // pp=parentG.node().parentElement, - parentW = bbox.width, // +pp.attr("width") - parentH = bbox.height, // +pp.attr("height") - width = parentW - margin.left - margin.right, - height = parentH - margin.top - margin.bottom; - this.ranges.drawSize = {width, height}; - dLog('getRanges2', parentW, parentH, this.ranges.drawSize); + // pp=parentG.node().parentElement, + parentW = bbox.width, // +pp.attr("width") + parentH = bbox.height, // +pp.attr("height") + width = parentW - margin.left - margin.right, + height = parentH - margin.top - margin.bottom; + this.ranges.drawSize = {width, height}; + dLog('getRanges2', parentW, parentH, this.ranges.drawSize); + } }; AxisCharts.prototype.drawAxes = function (charts) { @@ -385,7 +404,8 @@ Chart1.prototype.getRanges = function (ranges, chartData) { else yDomain = [yAxis.invert(yrange[0]), yAxis.invert(yrange[1])]; - ranges.pxSize = (yDomain[1] - yDomain[0]) / ranges.bbox.height; + if (ranges && ranges.bbox) + ranges.pxSize = (yDomain[1] - yDomain[0]) / ranges.bbox.height; }; From c56959a137bc491a15a260070749cd2e9c8b2d37 Mon Sep 17 00:00:00 2001 From: Don Date: Sun, 9 Feb 2020 22:33:02 +1100 Subject: [PATCH 28/57] define boundaries for featuresCounts bins pass interval to blockFeaturesCounts from FE (later can use block limits cached in BE; need to plan when it is calculated and cache invalidation) : changes in backend/ block.js, auth.js. frontend/app/services/data/block.js : add blocksReferencesLimits() and use to pass interval to getBlockFeaturesCounts(). block-features.js : use $bucket in place of $bucketAuto; add boundaries(), use in blockFeaturesCounts(). axis-charts : didRender() : if chart frame is not yet added, draw later. block-view.js : featuresCounts() : recognise whether result is from $bucket or $bucketAuto, and assign corresponding data type featureCountAutoData / featureCountData. data-types.js : rename featureCount{,Auto}Data and add a new featureCountData which is based on it, for $bucket; add a custom .rectHeight() for that. chart1.js : frame() : use gpa in place of gp. backend/common/ : cea09f7 29216 Feb 7 14:53 models/block.js 695cd83 7354 Feb 9 19:48 utilities/block-features.js frontend/app/ : 5fe2400 11711 Feb 8 18:36 components/axis-charts.js cc080be 2918 Feb 8 17:44 components/draw/block-view.js 048a37e 12311 Feb 7 14:53 services/auth.js 8668d3e 35340 Feb 7 18:09 services/data/block.js 6dd7685 7790 Feb 9 19:48 utils/data-types.js b7a04a1 30517 Feb 8 18:42 utils/draw/chart1.js 2b4d923 6549 Feb 7 19:52 utils/feature-lookup.js --- backend/common/models/block.js | 5 +- backend/common/utilities/block-features.js | 67 +++++++++++++++++++++- frontend/app/components/axis-charts.js | 26 +++++++-- frontend/app/components/draw/block-view.js | 13 +++-- frontend/app/services/auth.js | 6 +- frontend/app/services/data/block.js | 29 +++++++++- frontend/app/utils/data-types.js | 52 ++++++++++++++--- frontend/app/utils/draw/chart1.js | 57 +++++++++--------- frontend/app/utils/feature-lookup.js | 2 +- 9 files changed, 204 insertions(+), 53 deletions(-) diff --git a/backend/common/models/block.js b/backend/common/models/block.js index 2b9fe12e1..2830ccbdc 100644 --- a/backend/common/models/block.js +++ b/backend/common/models/block.js @@ -474,10 +474,10 @@ Block.blockNamespace = async function(blockIds) { * @param blockId block * @param nBins number of bins to partition the block's features into */ - Block.blockFeaturesCounts = function(blockId, nBins, options, res, cb) { + Block.blockFeaturesCounts = function(blockId, interval, nBins, options, res, cb) { let db = this.dataSource.connector; let cursor = - blockFeatures.blockFeaturesCounts(db, blockId, nBins); + blockFeatures.blockFeaturesCounts(db, blockId, interval, nBins); cursor.toArray() .then(function(featureCounts) { cb(null, featureCounts); @@ -633,6 +633,7 @@ Block.blockNamespace = async function(blockIds) { Block.remoteMethod('blockFeaturesCounts', { accepts: [ {arg: 'block', type: 'string', required: true}, + {arg: 'interval', type: 'array', required: false}, {arg: 'nBins', type: 'number', required: false}, {arg: "options", type: "object", http: "optionsFromRequest"}, {arg: 'res', type: 'object', 'http': {source: 'res'}}, diff --git a/backend/common/utilities/block-features.js b/backend/common/utilities/block-features.js index e7a2dce95..84e608397 100644 --- a/backend/common/utilities/block-features.js +++ b/backend/common/utilities/block-features.js @@ -49,29 +49,90 @@ exports.blockFeaturesCount = function(db, blockIds) { /*----------------------------------------------------------------------------*/ +/** Generate an array of even-sized bins to span the given interval. + * Used for mongo aggregation pipeline : $bucket : boundaries. + */ +function boundaries(interval, nBins) { + let b; + if (interval && (interval.length === 2) && (nBins > 0)) { + /* if (interval[1] < interval[0]) + interval = interval.sort(); */ + let intervalLength = interval[1] - interval[0], + binLength = intervalLength / nBins, + direction = Math.sign(intervalLength), + digits = Math.floor(Math.log10(binLength)), + eN1 = Math.exp(digits * Math.log(10)), + mantissa = binLength / eN1, + /** choose 1 2 or 5 as the first digit of the bin size. */ + m1 = mantissa > 5 ? 5 : (mantissa > 2 ? 2 : 1), + lengthRounded = Math.round(m1 * eN1), + start = interval[0], + forward = (direction > 0) ? function (a,b) {return a < b; } : + function (a,b) {return a > b; } + ; + console.log('boundaries', interval, nBins, intervalLength, binLength, direction, digits, eN1, mantissa, m1, lengthRounded); + let location = Math.floor(start / lengthRounded) * lengthRounded; + b = [location]; + do { + location += lengthRounded; + b.push(location); + } + while (forward(location, interval[1])); + console.log('boundaries', b.length, location, b[0], b[b.length-1]); + } + return b; +}; + + /** Count features of the given block in bins. * * @param blockCollection dataSource collection * @param blockId id of data block + * @param interval if given then use $bucket with boundaries in this range, + * otherwise use $bucketAuto. * @param nBins number of bins to group block's features into * * @return cursor : binned feature counts * { "_id" : { "min" : 4000000, "max" : 160000000 }, "count" : 22 } * { "_id" : { "min" : 160000000, "max" : 400000000 }, "count" : 21 } */ -exports.blockFeaturesCounts = function(db, blockId, nBins = 10) { +exports.blockFeaturesCounts = function(db, blockId, interval, nBins = 10) { // initial draft based on blockFeaturesCount() let featureCollection = db.collection("Feature"); + /** The requirement (so far) is for even-size boundaries on even numbers, + * e.g. 1Mb bins, with each bin boundary a multiple of 1e6. + * + * $bucketAuto doesn't require the boundaries to be defined, but there may not + * be a way to get it to use even-sized boundaries which are multiples of 1eN. + * By default it defines bins which have even numbers of features, i.e. the + * bin length will vary. If the parameter 'granularity' is given, the bin + * boundaries are multiples of 1e4 at the start and 1e7 near the end (in a + * dataset [0, 800M]; the bin length increases in an exponential progression. + * + * So $bucket is used instead, and the boundaries are given explicitly. + */ + const useBucketAuto = ! (interval && interval.length === 2); if (trace_block) - console.log('blockFeaturesCounts', blockId, nBins); + console.log('blockFeaturesCounts', blockId, interval, nBins); let ObjectId = ObjectID; + let lengthRounded = 1; let matchBlock = [ {$match : {blockId : ObjectId(blockId)}}, - { $bucketAuto: { groupBy: {$arrayElemAt : ['$value', 0]}, buckets: Number(nBins), granularity : 'R5'} } + useBucketAuto ? + { $bucketAuto : { groupBy: {$arrayElemAt : ['$value', 0]}, buckets: Number(nBins), granularity : 'R5'} } + : { $bucket : + { + groupBy: {$arrayElemAt : ['$value', 0]}, boundaries : boundaries(interval, nBins), + output: { + count: { $sum: 1 }, + idWidth : {$addToSet : lengthRounded } + } + } + } ], pipeline = matchBlock; diff --git a/frontend/app/components/axis-charts.js b/frontend/app/components/axis-charts.js index 178cc9b95..1108581b0 100644 --- a/frontend/app/components/axis-charts.js +++ b/frontend/app/components/axis-charts.js @@ -45,15 +45,29 @@ export default InAxis.extend({ didReceiveAttrs() { this._super(...arguments); - this.set('blocksData', Ember.Object.create()); - this.set('charts', Ember.Object.create()); - // axisID isn't planned to change for this component - this.set('axisCharts', new AxisCharts(this.get('axisID'))); + this.createAttributes(); + }, + + createAttributes() { + /** these objects persist for the life of the object. */ + if (! this.get('blocksData')) { + this.set('blocksData', Ember.Object.create()); + this.set('charts', Ember.Object.create()); + // axisID isn't planned to change for this component + this.set('axisCharts', new AxisCharts(this.get('axisID'))); + } }, didRender() { - console.log("components/axis-chart didRender()"); - this.draw(); + console.log("components/axis-chart didRender()", this.get('axisID')); + + let axisID = this.get('axisID'), + axisCharts = this.get('axisCharts'), + gAxis = axisCharts && axisCharts.selectParentContainer(axisID); + if (! gAxis.empty()) + this.draw(); + else + Ember.run.later(() => this.draw(), 500); }, willDestroyElement() { console.log("components/axis-chart willDestroyElement()"); diff --git a/frontend/app/components/draw/block-view.js b/frontend/app/components/draw/block-view.js index 8b4a204f2..a03f48394 100644 --- a/frontend/app/components/draw/block-view.js +++ b/frontend/app/components/draw/block-view.js @@ -44,7 +44,6 @@ export default Ember.Component.extend({ featuresA = features.map(function (f0) { return f0._internalModel.__data;}); this.ensureBlockFeatures(featuresA); this.setBlockFeaturesData('blockData', featuresA); - // previous : .drawBlockFeatures0() -> drawBlockFeatures(); } } } @@ -52,9 +51,15 @@ export default Ember.Component.extend({ featuresCounts : Ember.computed('block', 'block.featuresCounts.[]', 'axis.axis1d.domainChanged', function () { let featuresCounts = this.get('block.featuresCounts'); - if (featuresCounts && featuresCounts.length) - this.setBlockFeaturesData('featureCountData', featuresCounts); - // previous : .drawBlockFeaturesCounts(featuresCounts); + if (featuresCounts && featuresCounts.length) { + /** recognise the data format : $bucketAuto ._id contains .min and .max, whereas $bucket ._id is a single value. + * @see featureCountAutoDataExample, featureCountDataExample + */ + let id = featuresCounts[0]._id, + /** id.min may be 0 */ + dataTypeName = (id.min !== undefined) ? 'featureCountAutoData' : 'featureCountData'; + this.setBlockFeaturesData(dataTypeName, featuresCounts); + } return featuresCounts; }), diff --git a/frontend/app/services/auth.js b/frontend/app/services/auth.js index 2cc508ae6..f66a97923 100644 --- a/frontend/app/services/auth.js +++ b/frontend/app/services/auth.js @@ -228,10 +228,10 @@ export default Service.extend({ return this._ajax('Blocks/pathsByReference', 'GET', {blockA : blockA, blockB : blockB, reference, max_distance, options : options}, true); }, - getBlockFeaturesCounts(block, nBins, options) { + getBlockFeaturesCounts(block, interval, nBins, options) { if (trace_paths) - dLog('services/auth getBlockFeaturesCounts', block, nBins, options); - return this._ajax('Blocks/blockFeaturesCounts', 'GET', {block, nBins, options}, true); + dLog('services/auth getBlockFeaturesCounts', block, interval, nBins, options); + return this._ajax('Blocks/blockFeaturesCounts', 'GET', {block, interval, nBins, options}, true); }, getBlockFeaturesCount(blocks, options) { diff --git a/frontend/app/services/data/block.js b/frontend/app/services/data/block.js index 8200d07ec..25abc51d3 100644 --- a/frontend/app/services/data/block.js +++ b/frontend/app/services/data/block.js @@ -259,10 +259,35 @@ export default Service.extend(Ember.Evented, { }, /** For trace in Web Inspector console. * Usage e.g. this.get('blocksReferences').map(this.blocksReferencesText); + * @param io {id : blockId, obj : blockObject } */ blocksReferencesText(io) { let b=io.obj; return [io.id, b.view && b.view.longName(), b.mapName]; }, + /** Collate the ranges or feature limits of the references of the given blockIds. + * This is used for constructing boundaries in + * backend/common/utilities/block-features.js : blockFeaturesCounts(). + * @return a hash object mapping from blockId to reference limits [from, to]. + */ + blocksReferencesLimits(blockIds) { + function blocksReferencesLimit(io) { + let b=io.obj; return b.get('range') || b.get('featureLimits'); }; + let + blocksReferences = this.blockIdsReferences(blockIds, true), + result = blocksReferences.reduce(function (result, io) { + if (! result[io.id]) { + result[io.id] = blocksReferencesLimit(io); + } + return result; + }, {}); + + if (trace_block > 1) + dLog('blocksReferencesLimits', blockIds, result, + blocksReferences.map(this.blocksReferencesText) ); + return result; + }, + + /** Set .isViewed for each of the blocksToView[].obj * @param blocksToView form is the result of this.blocksReferences() */ @@ -305,8 +330,10 @@ export default Service.extend(Ember.Evented, { if (! p) { getCounts.apply(this); function getCounts() { + let intervals = this.blocksReferencesLimits([blockId]), + interval = intervals[blockId]; p = summaryTask[taskId] = - this.get('auth').getBlockFeaturesCounts(blockId, nBins, /*options*/{}); + this.get('auth').getBlockFeaturesCounts(blockId, interval, nBins, /*options*/{}); /* this could be structured as a task within models/block.js * A task would have .drop() to avoid concurrent request, but * actually want to bar any subsequent request for the same taskId, diff --git a/frontend/app/utils/data-types.js b/frontend/app/utils/data-types.js index 7d0fbbaa4..c5c7eef35 100644 --- a/frontend/app/utils/data-types.js +++ b/frontend/app/utils/data-types.js @@ -21,6 +21,10 @@ This can be integrated into models/feature.js /*----------------------------------------------------------------------------*/ +const dLog = console.debug; + +/*----------------------------------------------------------------------------*/ + class DataConfig { /* dataTypeName; @@ -79,8 +83,10 @@ function blockDataConfig(chart) { /*----------------------------------------------------------------------------*/ -/** example element of array f : */ -const featureCountDataExample = +/** example element of array f : + * result of $bucketAuto - it defines _id.{min,max} + */ +const featureCountAutoDataExample = { "_id": { "min": 100, @@ -89,8 +95,8 @@ const featureCountDataExample = "count": 109 }; -const featureCountDataProperties = { - dataTypeName : 'featureCountData', +const featureCountAutoDataProperties = { + dataTypeName : 'featureCountAutoData', datum2Location : function datum2Location(d) { return [d._id.min, d._id.max]; }, datum2Value : function(d) { return d.count; }, /** datum2Description() is not used; possibly intended for the same @@ -104,9 +110,41 @@ const featureCountDataProperties = { valueIsArea : true }; +/** example of result of $bucket - it defines _id as a single value (the lower boundary of the bin). +*/ +const featureCountDataExample = + {_id: 77, count: 3}; + +const featureCountDataProperties = Object.assign( + {}, featureCountAutoDataProperties, { + dataTypeName : 'featureCountData', + datum2Location : function datum2Location(d) { return d._id; }, + hoverTextFn : function (d, block) { + let valueText = '' + d._id + ' : ' + d.count, + blockName = block.view && block.view.longName(); + return valueText + '\n' + blockName; + } + } +); + +/** $bucket returns _id equal to the lower bound of the bin / bucket, and does + * not return the upper bound, since the caller provides the list of boundaries + * (see backend/common/utilities/block-features.js : boundaries()). + * The result is sparse - if a bin count is 0 then the bin is omitted from the + * result. So blockFeaturesCounts() adds idWidth : lengthRounded to each bin in + * the result; this value is constant for all bins, because boundaries() + * generates constant-sized bins. + */ +featureCountDataProperties.rectHeight = function (scaled, gIsData, d, i, g) { + if (i < 2) + dLog('rectHeight', d); + return d.idWidth[0]; +}; + + const dataConfigs = - [featureCountDataProperties, blockData, parsedData] - .reduce((result, properties) => { result[properties.dataTypeName] = new DataConfig(properties); return result; }, [] ); + [featureCountAutoDataProperties, featureCountDataProperties, blockData, parsedData] + .reduce((result, properties) => { result[properties.dataTypeName] = new DataConfig(properties); return result; }, {} ); @@ -183,4 +221,4 @@ function hoverTextFn (feature, block) { -export { featureCountDataProperties, dataConfigs, DataConfig, blockDataConfig, blockData, parsedData, hoverTextFn, middle, scaleMaybeInterval, datum2LocationWithBlock }; +export { featureCountAutoDataProperties, featureCountDataProperties, dataConfigs, DataConfig, blockDataConfig, blockData, parsedData, hoverTextFn, middle, scaleMaybeInterval, datum2LocationWithBlock }; diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index fee2aa566..5f3ad996e 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -201,7 +201,7 @@ AxisCharts.prototype.drawAxes = function (charts) { append.each(Chart1.prototype.drawAxes); }; -AxisCharts.prototype.commonFrame = function container() +AxisCharts.prototype.commonFrame = function() { let axisID = this.axisID, gAxis = this.dom.gAxis, @@ -209,51 +209,53 @@ AxisCharts.prototype.commonFrame = function container() /** datum is value in hash : {value : , description: } and with optional attribute description. */ - dLog('container', gAxis.node()); + dLog('commonFrame', gAxis.node()); /** parent selection ; contains a clipPath, g clip-path, text.resizer. */ let gps = gAxis .selectAll("g." + className) .data([axisID]), - gp = gps + gpa = gps .enter() .insert("g", ":first-child") .attr('class', className); if (false) { // not completed. Can base resized() on axis-2d.js - let text = gp + let text = gpa .append("text") .attr('class', 'resizer') .html("⇹") .attr("x", bbox.width-10); - if (gp.size() > 0) + if (gpa.size() > 0) eltWidthResizable("g.axis-use > g." + className + " > text.resizer", resized); } this.dom.gps = gps; - this.dom.gp = gp; + this.dom.gpa = gpa; + this.dom.gp = gpa.merge(gps); }; -AxisCharts.prototype.frameRemove = function container() { +AxisCharts.prototype.frameRemove = function() { let gp = this.dom && this.dom.gp; gp && gp.remove(); }; -AxisCharts.prototype.frame = function container(bbox, charts, allocatedWidth) +AxisCharts.prototype.frame = function(bbox, charts, allocatedWidth) { let gps = this.dom.gps, - gp = this.dom.gp; + gpa = this.dom.gpa; /** datum is axisID, so id and clip-path can be functions. * e.g. this.dom.gp.data() is [axisID] */ function axisClipId(axisID) { return "axis-chart-clip-" + axisID; } - /** gp is the .enter() of g.chart, and gpa is the .append() of that selection. */ - let gpa = - gp // define the clipPath + /** gpa is the .enter().append() (insert) of g.chart, and gPa is the result of + * .append()-ing clipPath to that selection. */ + let gPa = + gpa // define the clipPath .append("clipPath") // define a clip path .attr("id", axisClipId) // give the clipPath an ID .append("rect"), // shape it as a rect gprm = - gpa.merge(gps.selectAll("g > clipPath > rect")) + gPa.merge(gps.selectAll("g > clipPath > rect")) .attr("x", bbox.x) .attr("y", bbox.y) .attr("width", bbox.width) @@ -261,7 +263,7 @@ AxisCharts.prototype.frame = function container(bbox, charts, allocatedWidth) ; let [startOffset, width] = allocatedWidth; let gca = - gp.append("g") + gpa.append("g") .attr("clip-path", (d) => "url(#" + axisClipId(d) + ")") // clip with the rectangle .selectAll("g[clip-path]") .data(Object.values(charts)) @@ -272,8 +274,8 @@ AxisCharts.prototype.frame = function container(bbox, charts, allocatedWidth) ; let g = - gps.merge(gp).selectAll("g." + className+ " > g"); - if (! gp.empty() ) { + this.dom.gp.selectAll("g." + className+ " > g"); + if (! gpa.empty() ) { addParentClass(g); /* .gc is ​​ * .g (assigned later) is g.axis-chart @@ -817,16 +819,19 @@ ChartLine.prototype.line = function (data) ChartLine.prototype.drawContent = function(barsLine) { let + /** could pick up data from this.g.data(). */ data = this.currentData; - /** The Effects probabilities data is keyed by a location which is a SNP, so linebars() is a sensible representaion. - * This uses the data shape to recognise Effects data; this is provisional - * - we can probably lookup the tag 'EffectsPlus' in dataset tags (refn resources/tools/dev/effects2Dataset.pl). - * The effects data takes the form of an array of 5 probabilities, in the 3rd element of feature.value. - */ - let isEffectsData = data.length && data[0].name && data[0].value && (data[0].value.length === 3) && (data[0].value[2].length === 6); - let bars = isEffectsData ? this.linebars : this.bars; - let chartDraw = barsLine ? bars : this.line; - chartDraw.apply(this, [data]); + if (data) { + /** The Effects probabilities data is keyed by a location which is a SNP, so linebars() is a sensible representaion. + * This uses the data shape to recognise Effects data; this is provisional + * - we can probably lookup the tag 'EffectsPlus' in dataset tags (refn resources/tools/dev/effects2Dataset.pl). + * The effects data takes the form of an array of 5 probabilities, in the 3rd element of feature.value. + */ + let isEffectsData = data.length && data[0].name && data[0].value && (data[0].value.length === 3) && (data[0].value[2].length === 6); + let bars = isEffectsData ? this.linebars : this.bars; + let chartDraw = barsLine ? bars : this.line; + chartDraw.apply(this, [data]); + } }; @@ -894,7 +899,7 @@ DataConfig.prototype.rectHeight = function (scaled, gIsData, d, i, g) if (i < g.length-1) r.push(gData(i+1)); let y = - r.map(this.datum2LocationScaled); + r.map(d2l); height = Math.abs(y[y.length-1] - y[0]) * 2 / (y.length-1); dLog('rectHeight', gIsData, d, i, /*g,*/ r, y, height); if (! height) diff --git a/frontend/app/utils/feature-lookup.js b/frontend/app/utils/feature-lookup.js index 1e34c5156..47dd935ae 100644 --- a/frontend/app/utils/feature-lookup.js +++ b/frontend/app/utils/feature-lookup.js @@ -181,7 +181,7 @@ function ensureBlockFeatures(blockId, features) { /* if za has not been populated with features, it will have just .dataset * and .scope, i.e. .length === 2 */ if (Object.keys(za).length == 2) { - dLog('drawBlockFeatures()', blockId, za, features); + dLog('ensureBlockFeatures()', blockId, za, features); // add features to za. features.forEach((f) => za[f.name] = f.value); } From b835b9903ebf7d4b76dd197bbc15c32af306eb6a Mon Sep 17 00:00:00 2001 From: Don Date: Mon, 10 Feb 2020 07:39:32 +1100 Subject: [PATCH 29/57] Return interval length in featuresCounts result when interval is given / using non-auto bucket boundaries. block-features.js : split boundaries() into binEvenLengthRound() and binBoundaries(), so that lengthRounded can be returned in the result as idWidth (because idWidth is constant in this result, it could be factored out using groupBy - this would require the datum2Location function to access some context - the API result). data-types.js : for $bucket result which now includes idWidth, instead of adding a custom rectHeight(), modify datum2Location() to calculate the interval from the given lower bound and interval length : idWidth. 9d19a4d 8023 Feb 9 23:55 backend/common/utilities/block-features.js 5e1addb 7945 Feb 9 23:00 frontend/app/utils/data-types.js --- backend/common/utilities/block-features.js | 56 +++++++++++++++------- frontend/app/utils/data-types.js | 12 ++--- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/backend/common/utilities/block-features.js b/backend/common/utilities/block-features.js index 84e608397..2032dc674 100644 --- a/backend/common/utilities/block-features.js +++ b/backend/common/utilities/block-features.js @@ -49,28 +49,44 @@ exports.blockFeaturesCount = function(db, blockIds) { /*----------------------------------------------------------------------------*/ -/** Generate an array of even-sized bins to span the given interval. - * Used for mongo aggregation pipeline : $bucket : boundaries. +/** Calculate the bin size for even-sized bins to span the given interval. + * The bin size is rounded to be a multiple of a power of 10, only the first 1-2 + * digits are non-zero. + * Used in @see binBoundaries(). + * @return lengthRounded */ -function boundaries(interval, nBins) { - let b; +function binEvenLengthRound(interval, nBins) { + let lengthRounded; if (interval && (interval.length === 2) && (nBins > 0)) { /* if (interval[1] < interval[0]) interval = interval.sort(); */ let intervalLength = interval[1] - interval[0], binLength = intervalLength / nBins, - direction = Math.sign(intervalLength), digits = Math.floor(Math.log10(binLength)), eN1 = Math.exp(digits * Math.log(10)), mantissa = binLength / eN1, /** choose 1 2 or 5 as the first digit of the bin size. */ - m1 = mantissa > 5 ? 5 : (mantissa > 2 ? 2 : 1), - lengthRounded = Math.round(m1 * eN1), - start = interval[0], - forward = (direction > 0) ? function (a,b) {return a < b; } : - function (a,b) {return a > b; } - ; - console.log('boundaries', interval, nBins, intervalLength, binLength, direction, digits, eN1, mantissa, m1, lengthRounded); + m1 = mantissa > 5 ? 5 : (mantissa > 2 ? 2 : 1); + lengthRounded = Math.round(m1 * eN1); + + console.log('binEvenLengthRound', interval, nBins, intervalLength, binLength, digits, eN1, mantissa, m1, lengthRounded); + } + return lengthRounded; +}; +/** Generate an array of even-sized bins to span the given interval. + * Used for mongo aggregation pipeline : $bucket : boundaries. + */ +function binBoundaries(interval, lengthRounded) { + let b; + if (lengthRounded) { + let + start = interval[0], + intervalLength = interval[1] - interval[0], + direction = Math.sign(intervalLength), + forward = (direction > 0) ? + function (a,b) {return a < b; } + : function (a,b) {return a > b; }; + let location = Math.floor(start / lengthRounded) * lengthRounded; b = [location]; do { @@ -78,13 +94,14 @@ function boundaries(interval, nBins) { b.push(location); } while (forward(location, interval[1])); - console.log('boundaries', b.length, location, b[0], b[b.length-1]); + console.log('binBoundaries', direction, b.length, location, b[0], b[b.length-1]); } return b; }; + /** Count features of the given block in bins. * * @param blockCollection dataSource collection @@ -111,22 +128,27 @@ exports.blockFeaturesCounts = function(db, blockId, interval, nBins = 10) { * dataset [0, 800M]; the bin length increases in an exponential progression. * * So $bucket is used instead, and the boundaries are given explicitly. + * This requires interval; if it is not passed, $bucketAuto is used, without granularity. */ const useBucketAuto = ! (interval && interval.length === 2); if (trace_block) console.log('blockFeaturesCounts', blockId, interval, nBins); let ObjectId = ObjectID; - let lengthRounded = 1; - + let lengthRounded, boundaries; + if (! useBucketAuto) { + lengthRounded = binEvenLengthRound(interval, nBins), + boundaries = binBoundaries(interval, lengthRounded); + } + let matchBlock = [ {$match : {blockId : ObjectId(blockId)}}, useBucketAuto ? - { $bucketAuto : { groupBy: {$arrayElemAt : ['$value', 0]}, buckets: Number(nBins), granularity : 'R5'} } + { $bucketAuto : { groupBy: {$arrayElemAt : ['$value', 0]}, buckets: Number(nBins)} } // , granularity : 'R5' : { $bucket : { - groupBy: {$arrayElemAt : ['$value', 0]}, boundaries : boundaries(interval, nBins), + groupBy: {$arrayElemAt : ['$value', 0]}, boundaries, output: { count: { $sum: 1 }, idWidth : {$addToSet : lengthRounded } diff --git a/frontend/app/utils/data-types.js b/frontend/app/utils/data-types.js index c5c7eef35..a0cfc19fa 100644 --- a/frontend/app/utils/data-types.js +++ b/frontend/app/utils/data-types.js @@ -118,7 +118,7 @@ const featureCountDataExample = const featureCountDataProperties = Object.assign( {}, featureCountAutoDataProperties, { dataTypeName : 'featureCountData', - datum2Location : function datum2Location(d) { return d._id; }, + datum2Location : function datum2Location(d) { return [d._id, d._id + d.idWidth[0]]; }, hoverTextFn : function (d, block) { let valueText = '' + d._id + ' : ' + d.count, blockName = block.view && block.view.longName(); @@ -126,7 +126,6 @@ const featureCountDataProperties = Object.assign( } } ); - /** $bucket returns _id equal to the lower bound of the bin / bucket, and does * not return the upper bound, since the caller provides the list of boundaries * (see backend/common/utilities/block-features.js : boundaries()). @@ -134,12 +133,11 @@ const featureCountDataProperties = Object.assign( * result. So blockFeaturesCounts() adds idWidth : lengthRounded to each bin in * the result; this value is constant for all bins, because boundaries() * generates constant-sized bins. + * That is used above in featureCountDataProperties : datum2Location() : it + * generates the upper bound by adding idWidth to the lower bound. + * The form of idWidth is an array with a single value, because it is added + * using an output accumulator in the $bucket : groupBy. */ -featureCountDataProperties.rectHeight = function (scaled, gIsData, d, i, g) { - if (i < 2) - dLog('rectHeight', d); - return d.idWidth[0]; -}; const dataConfigs = From c061e469d94af8e0677f531398b87b95cd173db5 Mon Sep 17 00:00:00 2001 From: Don Date: Mon, 10 Feb 2020 15:44:25 +1100 Subject: [PATCH 30/57] (selected) features table : increase the limit on copy/paste, from 1000 rows (default) -> 10000 24e56bc 3778 Feb 10 15:41 frontend/app/components/table-brushed.js --- frontend/app/components/table-brushed.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/app/components/table-brushed.js b/frontend/app/components/table-brushed.js index 6de6fccab..fe87bdec7 100644 --- a/frontend/app/components/table-brushed.js +++ b/frontend/app/components/table-brushed.js @@ -83,6 +83,10 @@ export default Ember.Component.extend({ manualColumnResize: true, manualRowMove: true, // manualColumnMove: true, + copyPaste: { + /** increase the limit on copy/paste. default is 1000 rows. */ + rowsLimit: 10000 + }, contextMenu: true, sortIndicator: true, columnSorting: { From 263ce801fcf7923b3490bdf8d9f09a143ebede61 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 11 Feb 2020 09:57:08 +1100 Subject: [PATCH 31/57] use nBins from view controls. add interval width to hover text. block.js : add controls(), pathsDensityParams() (copied from paths-progressive.js - will factor to a service). add nBins() and use to replace the constant 100 in getSummary() : getBlockFeaturesCounts request. data-types.js : hoverTextFn() : include idWidth in the text when _id is not an interval - non-auto. chart1.js : add keyFn - improves updates during mousewheel zoom. re-set gca so that axes and controls are not re-appended. frontend/app/ : cd3c308 36841 Feb 11 09:52 services/data/block.js efe00ee 7967 Feb 10 21:57 utils/data-types.js 8be781c 31193 Feb 11 09:40 utils/draw/chart1.js --- frontend/app/services/data/block.js | 46 +++++++++++++++++++++++++++-- frontend/app/utils/data-types.js | 2 +- frontend/app/utils/draw/chart1.js | 22 ++++++++++---- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/frontend/app/services/data/block.js b/frontend/app/services/data/block.js index 25abc51d3..db8f8145d 100644 --- a/frontend/app/services/data/block.js +++ b/frontend/app/services/data/block.js @@ -311,6 +311,48 @@ export default Service.extend(Ember.Evented, { */ this.trigger('receivedBlock', blocksToView); }, + + /** controls() and pathsDensityParams() are copied from paths-progressive.js + * They can be moved into a service control-params, which will be backed by + * query params in the URL. + */ + controls : Ember.computed(function () { + let oa = stacks.oa, + /** This occurs after mapview.js: controls : Ember.Object.create({ view : { } }), + * and draw-map : draw() setup of oa.drawOptions. + * This can be replaced with a controls service. + */ + controls = oa.drawOptions.controls; + dLog('controls', controls); + return controls; + }), + /** This does have a dependency on the parameter values. */ + pathsDensityParams : Ember.computed.alias('controls.view.pathsDensityParams'), + /** + * @param blockId later will use this to lookup axis yRange + */ + nBins(blockId) { + let nBins; + /** based on part of intervalParams(intervals) */ + let vcParams = this.get('pathsDensityParams'); + if (vcParams.nSamples) { + nBins = vcParams.nSamples; + } + if (vcParams.densityFactor) { + /** from paths-aggr.js : blockFeaturesInterval() + */ + let pixelspacing = 5; + let range = 600; // - lookup axis yRange from block; + let nBins = vcParams.densityFactor * range / pixelspacing; + } + if (vcParams.nFeatures) { + if (nBins > vcParams.nFeatures) + nBins = vcParams.nFeatures; + } + dLog('nBins', nBins, vcParams); + return nBins; + }, + getSummary: function (blockIds) { // console.log("block getSummary", id); let blockP = @@ -318,12 +360,12 @@ export default Service.extend(Ember.Evented, { if (this.get('parsedOptions.featuresCounts')) { - /** This will probably become user-configurable */ - const nBins = 100; /** As yet these result promises are not returned, not needed. */ let blockPs = blockIds.map( (blockId) => { + /** densityFactor requires axis yRange, so for that case this will (in future) lookup axis from blockId. */ + const nBins = this.nBins(blockId); let taskId = blockId + '_' + nBins; let summaryTask = this.get('summaryTask'); let p = summaryTask[taskId]; diff --git a/frontend/app/utils/data-types.js b/frontend/app/utils/data-types.js index a0cfc19fa..d61ccfeed 100644 --- a/frontend/app/utils/data-types.js +++ b/frontend/app/utils/data-types.js @@ -120,7 +120,7 @@ const featureCountDataProperties = Object.assign( dataTypeName : 'featureCountData', datum2Location : function datum2Location(d) { return [d._id, d._id + d.idWidth[0]]; }, hoverTextFn : function (d, block) { - let valueText = '' + d._id + ' : ' + d.count, + let valueText = '' + d._id + ' +' + d.idWidth[0] + ' : ' + d.count, blockName = block.view && block.view.longName(); return valueText + '\n' + blockName; } diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index 5f3ad996e..c9448d189 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -277,14 +277,13 @@ AxisCharts.prototype.frame = function(bbox, charts, allocatedWidth) this.dom.gp.selectAll("g." + className+ " > g"); if (! gpa.empty() ) { addParentClass(g); + } /* .gc is ​​ - * .g (assigned later) is g.axis-chart - .chart-line ? + * .g (assigned later) is all g.chart-line, i.e. all chart-lines of all Chart1-s of this axis * .gca contains a g for each chartType / dataTypeName, i.e. per Chart1. */ this.dom.gc = g; this.dom.gca = gca; - } }; @@ -682,7 +681,7 @@ ChartLine.prototype.bars = function (data) rs = g // .select("g." + className + " > g") .selectAll("rect." + dataConfig.barClassName) - .data(data), + .data(data, dataConfig.keyFn.bind(dataConfig)), re = rs.enter(), rx = rs.exit(); let ra = re .append("rect"); @@ -727,7 +726,7 @@ ChartLine.prototype.linebars = function (data) rs = g // .select("g." + className + " > g") .selectAll("path." + dataConfig.barClassName) - .data(data), + .data(data, dataConfig.keyFn.bind(dataConfig)), re = rs.enter(), rx = rs.exit(); let datum2LocationScaled = scaleMaybeInterval(dataConfig.datum2Location, scales.y); let line = d3.line(); @@ -839,6 +838,19 @@ ChartLine.prototype.drawContent = function(barsLine) /*----------------------------------------------------------------------------*/ +DataConfig.prototype.keyFn = function (d, i, g) { + let key = this.datum2Location(d, i, g); + /** looking at d3 bindKey() : it appends the result of the key function to + * "$", which works fine for both a single-value location and an interval; + * e.g. the interval [54.7, 64.8] -> keyValue "$54.7,64.8". Including the + * upper bound of the interval may improvide the identifying power of the key + * in some cases. If there was a problem with this, an interval can be + * reduced to a single value, i.e. + if (key.length) + key = key[0]; */ + return key; +}; + /** Calculate the height of rectangle to be used for this data point * @param this is DataConfig, not DOM element. * @param scaled true means apply scale (x) to the result From cd5c492f6975ada94ec2e1efb9ff5dab377d9643 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 11 Feb 2020 12:09:18 +1100 Subject: [PATCH 32/57] add comment illustrating an example of DOM structure, annotated with selection var names. d610fbe 33642 Feb 11 12:07 frontend/app/utils/draw/chart1.js --- frontend/app/utils/draw/chart1.js | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index c9448d189..9322982dc 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -36,6 +36,44 @@ Using JS classes enables 4 small and closely related classes to be in a single f const dLog = console.debug; +/*----------------------------------------------------------------------------*/ + +/** Example of the structure of the DOM elements generated. The .dom.* selections which address them are indicated in comments. + + + + + + + + + + + + + 0.1 + + + + + + + + + + + + 0.0005 + + + + + + + +*/ + +/*----------------------------------------------------------------------------*/ /** Add a .hasChart class to the which contains this chart. * Currently this is used to hide the so that hover events are From 820a5dc58ee5bca2fe21997933438fb82c1cf440 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 11 Feb 2020 15:37:23 +1100 Subject: [PATCH 33/57] update handling toggleBarsLine(): use a more specific selection for .remove(). bars(): apply width update to existing elements. a6d8c4f 33990 Feb 11 15:21 frontend/app/utils/draw/chart1.js --- frontend/app/utils/draw/chart1.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index 9322982dc..192166528 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -45,7 +45,7 @@ const dLog = console.debug; - + @@ -310,6 +310,11 @@ AxisCharts.prototype.frame = function(bbox, charts, allocatedWidth) .attr('class', (d) => d.dataConfig.dataTypeName) .attr("transform", (d, i) => "translate(" + (startOffset + (i * 30)) + ", 0)") ; + /* to handle removal of chart types, split the above to get a handle for remove : + gcs = gpa ... data(...), + gca = gcs.enter() ...; + gcs.exit().remove(); + */ let g = this.dom.gp.selectAll("g." + className+ " > g"); @@ -501,6 +506,10 @@ Chart1.prototype.group = function (parentG, groupClassName) { /** parentG is g.{{dataTypeName}}, within : g.axis-use > g.chart > g[clip-path] > g.{{dataTypeName}}. * add g.(groupClassName); * within g.axis-use there is also a sibling g.axis-html. */ + /* on subsequent calls (didRender() is called whenever params change), + * parentG is empty, and hence g is empty. + * so this is likely to need a change to handle addition/removal of chartlines after first render. + */ let // data = parentG.data(), gs = parentG .selectAll("g > g." + groupClassName) @@ -516,8 +525,8 @@ Chart1.prototype.group = function (parentG, groupClassName) { , // parentG.selectAll("g > g." + groupClassName); // g = gsa.merge(gs); + gs.exit().remove(); dLog('group', this, parentG.node(), parentG, g.node()); - this.dom.g = g; this.dom.gs = gs; this.dom.gsa = gsa; // set ChartLine .g; used by ChartLine.{lines,bars}. @@ -643,9 +652,9 @@ Chart1.prototype.toggleBarsLine = function () this.barsLine = ! this.barsLine; this.chartTypeToggle .classed("pushed", this.barsLine); - this.dom.g.selectAll("g > *").remove(); Object.keys(this.chartLines).forEach((blockId) => { let chartLine = this.chartLines[blockId]; + chartLine.g.selectAll("g > *").remove(); chartLine.drawContent(this.barsLine); }); }; @@ -730,6 +739,7 @@ ChartLine.prototype.bars = function (data) * this.parentElement.parentElement.__data__ has the axis id (not the blockId), */ .each(function (d) { configureHorizTickHover.apply(this, [d, block, dataConfig.hoverTextFn]); }); + let r = ra .merge(rs) .transition().duration(1500) @@ -739,13 +749,13 @@ ChartLine.prototype.bars = function (data) .attr("height", dataConfig.rectHeight.bind(dataConfig, /*scaled*/true, /*gIsData*/false)) // equiv : (d, i, g) => dataConfig.rectHeight(true, false, d, i, g); ; let barWidth = dataConfig.rectWidth.bind(dataConfig, /*scaled*/true, /*gIsData*/false); - ra + r .attr("width", dataConfig.barAsHeatmap ? 20 : barWidth); if (dataConfig.barAsHeatmap) ra .attr('fill', barWidth); rx.remove(); - console.log(rs.nodes(), re.nodes()); + dLog(rs.nodes(), re.nodes()); }; /** A single horizontal line for each data point. From 180db7edb0a9a04608dc16f55ae522fb53951fe1 Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 12 Feb 2020 10:09:03 +1100 Subject: [PATCH 34/57] Add log functions for development support. Add Chart1 : logSelectionNodes2(), logScales(). ab43e42 34707 Feb 12 10:06 frontend/app/utils/draw/chart1.js --- frontend/app/utils/draw/chart1.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/frontend/app/utils/draw/chart1.js b/frontend/app/utils/draw/chart1.js index 192166528..d8f696596 100644 --- a/frontend/app/utils/draw/chart1.js +++ b/frontend/app/utils/draw/chart1.js @@ -3,6 +3,7 @@ import Ember from 'ember'; import { getAttrOrCP } from '../ember-devel'; import { configureHorizTickHover } from '../hover'; import { eltWidthResizable, noShiftKeyfilter } from '../domElements'; +import { logSelectionNodes } from '../log-selection'; import { noDomain } from '../draw/axis'; import { stacks } from '../stacks'; // just for oa.z and .y, don't commit this. import { inRangeEither } from './zoomPanCalcs'; @@ -368,6 +369,26 @@ AxisCharts.prototype.controls = function controls() /*----------------------------------------------------------------------------*/ +/** For use in web inspector console, e.g. + * d3.selectAll('g.chart > g[clip-path] > g').each((d) => d.logSelectionNodes2()) + * or from Ember inspector /axis-chart : $E.chart.logSelectionNodes2() + */ +Chart1.prototype.logSelectionNodes2 = function(dom) +{ + if (! dom) + dom = this.dom; + Object.keys(dom).forEach((k) => {dLog(k); logSelectionNodes(dom[k]); }); +}; + + +/** For use in web inspector console, e.g. + * d3.selectAll('g.chart > g[clip-path] > g').each((d) => d.logScales()); + */ +Chart1.prototype.logScales = function() { + let scales = this.scales; + Object.keys(scales).forEach((k) => {var s = scales[k]; console.log(k, s.domain(), s.range()); }); +}; + Chart1.prototype.overlap = function(axisCharts) { let chart1 = this; From 5a9f064b8cafc1ccf35ee312b7856cfdabf62143 Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 12 Feb 2020 10:28:33 +1100 Subject: [PATCH 35/57] add notes on d3 devel/debug cc705b2 1276 Feb 12 10:26 doc/notes/d3_devel.md --- doc/notes/d3_devel.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 doc/notes/d3_devel.md diff --git a/doc/notes/d3_devel.md b/doc/notes/d3_devel.md new file mode 100644 index 000000000..6cd0f8137 --- /dev/null +++ b/doc/notes/d3_devel.md @@ -0,0 +1,27 @@ +# debugging techniques for d3 + +## Web Inspector : Console, Elements tab, Ember Inspector + +- nodes(), node(), data(), .__data__ + +- click in Elements tab to select an element, then in console can inspect the element details with + $0, $0.__data__ + +- d3 stores .data(), .datum() on element.__data__, so $0.__data__ shows the d3 datum + +- in console, selection .node() shows the element as it appears in the Elements panel; +right-click on this -> view in Elements panel + +- when single-stepping through d3 attribute changes, note that in a transition the change won't take place until later, so commenting out the .transition().duration() line makes it easy to observe element changes while stepping through code + +- Elements panel highlights attribute changes for about 1 second, so have this displayed and continue (eg. to the next breakpoint) + +- use ember inspector to get component handle, from there get element and datum + +- d3 selection in console : can be used to inspect the app at any time - no need to breakpoint the app + +- Chrome and Firefox Web Inspector are similar, each has at least 1 thing which the other doesn't do or isn't as easy to use + +- more : +https://developers.google.com/web/tools/chrome-devtools/console/utilities +http://anti-code.com/devtools-cheatsheet/ From fda1a46bb93e7a6874a8670b64b26892a7c68d16 Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 12 Feb 2020 16:36:14 +1100 Subject: [PATCH 36/57] add paths-table. disable right panel Block tab unless options=blockTab add paths-table.{hbs,js} draw/block-adj.js : split functions out to form paths-api.js, for use of pathsResultTypes in paths-table. models/block-adj.js : add adjacentTo(). mapview.hbs : check parseOptions.blockTab around button Block. Add Paths tab, use panel/paths-table. ddee626 5835 Feb 12 15:21 frontend/app/components/panel/paths-table.js 7edff76 1276 Feb 12 15:35 frontend/app/templates/components/panel/paths-table.hbs 6cc3d83 8139 Feb 12 13:08 frontend/app/utils/paths-api.js --- frontend/app/components/draw/block-adj.js | 207 +---------------- frontend/app/components/panel/paths-table.js | 157 +++++++++++++ frontend/app/models/block-adj.js | 10 + frontend/app/styles/app.scss | 5 + .../components/panel/paths-table.hbs | 28 +++ frontend/app/templates/mapview.hbs | 20 +- frontend/app/utils/paths-api.js | 217 ++++++++++++++++++ 7 files changed, 434 insertions(+), 210 deletions(-) create mode 100644 frontend/app/components/panel/paths-table.js create mode 100644 frontend/app/templates/components/panel/paths-table.hbs create mode 100644 frontend/app/utils/paths-api.js diff --git a/frontend/app/components/draw/block-adj.js b/frontend/app/components/draw/block-adj.js index 592fb33f0..6d616a018 100644 --- a/frontend/app/components/draw/block-adj.js +++ b/frontend/app/components/draw/block-adj.js @@ -3,12 +3,11 @@ import Ember from 'ember'; const { inject: { service } } = Ember; import { throttle } from '@ember/runloop'; -import PathData from './path-data'; - import AxisEvents from '../../utils/draw/axis-events'; import { stacks, Stacked } from '../../utils/stacks'; import { selectAxis, blockAdjKeyFn, blockAdjEltId, featureEltIdPrefix, featureNameClass, foregroundSelector, selectBlockAdj } from '../../utils/draw/stacksAxes'; import { targetNPaths, pathsFilter } from '../../utils/draw/paths-filter'; +import { pathsResultTypes, pathsApiResultType, flowNames, resultBlockIds, pathsOfFeature, locationPairKeyFn } from '../../utils/paths-api'; /* global d3 */ @@ -512,207 +511,3 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, { }); - -if (false) { - /** Example of param paths passed to draw() above. */ - const examplePaths = -[{"_id":{"name":"myMarkerC"}, - "alignment":[ - {"blockId":"5c75d4f8792ccb326827daa2","repeats":{ - "_id":{"name":"myMarkerC","blockId":"5c75d4f8792ccb326827daa2"}, - "features":[{"_id":"5c75d4f8792ccb326827daa6","name":"myMarkerC","value":[3.1,3.1],"blockId":"5c75d4f8792ccb326827daa2","parentId":null}],"count":1}}, - {"blockId":"5c75d4f8792ccb326827daa1","repeats":{ - "_id":{"name":"myMarkerC","blockId":"5c75d4f8792ccb326827daa1"}, - "features":[{"_id":"5c75d4f8792ccb326827daa5","name":"myMarkerC","value":[0,0],"blockId":"5c75d4f8792ccb326827daa1","parentId":null}],"count":1}}]}]; -} - - -/*----------------------------------------------------------------------------*/ - -function featureEltId(featureBlock) -{ - let id = featurePathKeyFn(featureBlock); - id = featureNameClass(id); - return id; -} - -function featurePathKeyFn (featureBlock) -{ return featureBlock._id.name; } - -/** Given the grouped data for a feature, from the pathsDirect() result, - * generate the cross-product feature.alignment[0].repeats X feature.alignment[1].repeats. - * The result is an array of pairs of features; each pair defines a path and is of type PathData. - * for each pair an element of pairs[] : - * pair.feature0 is in block pair.block0 - * pair.feature1 is in block pair.block1 - * (for the case of pathsResultTypes.direct) : - * pair.block0 === feature.alignment[0].blockId - * pair.block1 === feature.alignment[1].blockId - * i.e. the path goes from the first block in the request params to the 2nd block - * @param pathsResultType e.g. pathsResultTypes.{Direct,Aliases} - * @param feature 1 element of the result array passed to draw() - * @return [PathData, ...] - */ -function pathsOfFeature(store, pathsResultType, owner) { - const PathData = owner.factoryFor('component:draw/path-data'); - return function (feature) { - let blocksFeatures = - [0, 1].map(function (blockIndex) { return pathsResultType.blocksFeatures(feature, blockIndex); }), - blocks = resultBlockIds(pathsResultType, feature), - pairs = - blocksFeatures[0].reduce(function (result, f0) { - let result1 = blocksFeatures[1].reduce(function (result, f1) { - let pair = - pathCreate(store, f0, f1, blocks[0], blocks[1]); - result.push(pair); - return result; - }, result); - return result1; - }, []); - return pairs; - }; -} - -const trace_pc = 1; - -function pathCreate(store, feature0, feature1, block0, block1) { - let - /** not used - same as feature{0,1}.blockId. */ - block0r = store.peekRecord('block', block0), - block1r = store.peekRecord('block', block1); - if (true) { - let properties = { - feature0, - feature1/*, - block0r, - block1r*/ - }, - pair = - PathData.create({ renderer : {} }); - pair.setProperties(properties); - if (trace_pc > 2) - dLog('PathData.create()', PathData, pair); - return pair; - } - else { - let - modelName = 'draw/path-data', - idText = locationPairKeyFn({ feature0, feature1}), - r = store.peekRecord(modelName, idText); - if (r) - dLog('pathCreate', feature0, feature1, block0, block1, r._internalModel.__attributes, r._internalModel.__data); - else if (false) - { - let data = { - type : modelName, - id : idText, - relationships : { - feature0 : { data: { type: "feature", "id": feature0 } }, - feature1 : { data: { type: "feature", "id": feature1 } } /*, - block0 : { data: { type: "block", "id": block0r } }, - block1 : { data: { type: "block", "id": block1r } }*/ - }/*, - attributes : { - 'block-id0' : block0, - 'block-id1' : block1 - }*/ - }; - r = store.push({data}); - if (trace_pc) - dLog('pathCreate', r, r.get('id'), r._internalModel, r._internalModel.__data, store, data); - } - else { - let inputProperties = { - feature0, - feature1/*, - block0r, - block1r*/ - }; - r = store.createRecord(modelName, inputProperties); - } - return r; - } -} - - -function locationPairKeyFn(locationPair) -{ - return locationPair.feature0.id + '_' + locationPair.feature1.id; -} - -/*----------------------------------------------------------------------------*/ - -const pathsApiFields = ['featureAObj', 'featureBObj']; -/** This type is created by paths-progressive.js : requestAliases() : receivedData() */ -const pathsApiResultType = { - // fieldName may be pathsResult or pathsAliasesResult - typeCheck : function(resultElt) { if (! resultElt.featureAObj) { - dLog('pathsApiResultType : typeCheck', resultElt); } }, - pathBlock : function (resultElt, blockIndex) { return resultElt[pathsApiFields[blockIndex]].blockId; }, - /** direct.blocksFeatures() returns an array of features, so match that. See - * similar commment in alias.blocksFeatures. */ - blocksFeatures : function (resultElt, blockIndex) { return [ resultElt[pathsApiFields[blockIndex]] ]; }, - featureEltId : - function (resultElt) - { - let id = pathsApiResultType.featurePathKeyFn(resultElt); - id = featureNameClass(id); - return id; - }, - featurePathKeyFn : function (resultElt) { return resultElt.featureA + '_' + resultElt.featureB; } - -}; - -/** This is provision for using the API result type as data type; not used currently because - * the various forms of result data are converted to path-data. - * These are the result types from : - * Block/paths -> apiLookupAliases() -> task.paths() - * Blocks/pathsViaStream -> pathsAggr.pathsDirect() - * getPathsAliasesViaStream() / getPathsAliasesProgressive() -> Blocks/pathsAliasesProgressive -> dbLookupAliases() -> pathsAggr.pathsAliases() - */ -const pathsResultTypes = { - direct : { - fieldName : 'pathsResult', - typeCheck : function(resultElt) { if (! resultElt._id) { - dLog('direct : typeCheck', resultElt); } }, - pathBlock : function (resultElt, blockIndex) { return resultElt.alignment[blockIndex].blockId; }, - blocksFeatures : function (resultElt, blockIndex) { return resultElt.alignment[blockIndex].repeats.features; }, - featureEltId : featureEltId, - featurePathKeyFn : featurePathKeyFn - }, - - alias : - { - fieldName : 'pathsAliasesResult', - typeCheck : function(resultElt) { if (! resultElt.aliased_features) { - dLog('alias : typeCheck', resultElt); } }, - pathBlock : function (resultElt, blockIndex) { return resultElt.aliased_features[blockIndex].blockId; }, - /** There is currently only 1 element in .aliased_features[blockIndex], but - * pathsOfFeature() handles an array an produces a cross-product, so return - * this 1 element as an array. */ - blocksFeatures : function (resultElt, blockIndex) { return [resultElt.aliased_features[blockIndex]]; }, - featureEltId : - function (resultElt) - { - let id = pathsResultTypes.alias.featurePathKeyFn(resultElt); - id = featureNameClass(id); - return id; - }, - featurePathKeyFn : function (resultElt) { - return resultElt.aliased_features.map(function (f) { return f.name; } ).join('_'); - } - } -}, -/** This matches the index values of services/data/flows-collate.js : flows */ -flowNames = Object.keys(pathsResultTypes); -// add .flowName to each of pathsResultTypes, which could later require non-const declaration. -flowNames.forEach(function (flowName) { pathsResultTypes[flowName].flowName = flowName; } ); - -/** - * @return array[2] of blockId, equivalent to blockAdjId - */ -function resultBlockIds(pathsResultType, featurePath) { - let blockIds = - [0, 1].map(function (blockIndex) { return pathsResultType.pathBlock(featurePath, blockIndex); }); - return blockIds; -} diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js new file mode 100644 index 000000000..cd87958a7 --- /dev/null +++ b/frontend/app/components/panel/paths-table.js @@ -0,0 +1,157 @@ +import Ember from 'ember'; +const { inject: { service } } = Ember; + + +import PathData from '../draw/path-data'; +import { pathsResultTypes } from '../../utils/paths-api'; + + +const dLog = console.debug; + +export default Ember.Component.extend({ + flowsService: service('data/flows-collate'), + + didReceiveAttrs() { + this._super(...arguments); + + dLog('didReceiveAttrs', this.get('block.id')); + }, + + /*--------------------------------------------------------------------------*/ + + actions: { + + selectionChanged: function(selA) { + dLog("selectionChanged in components/panel/paths-table", selA); + for (let i=0; i { r[b.get('id')] = b; return r; }, {}), + namesFlat = blocks.reduce((rowHash, b, i) => { + rowHash['feature' + i] = b.get('datasetId.id'); + rowHash['position' + i] = b.get('scope'); + return rowHash; + }, {}); + result.push(namesFlat); + + let resultElts = blockAdj.get(pathsResultField); + if (resultElts) { + resultElts.forEach((resultElt) => { + /** for each end of the path, if the endpoint is in a brushed block, + * filter out if the endpoint is not in the (brushed) + * selectedFeatures. + */ + + /** for the given block, generate the name format which is used in + * selectedFeatures.Chromosome + */ + function blockDatasetNameAndScope(block) { + return block.get('datasetId.id') + ':' + block.get('scope'); + } + let + pathsResultType = Object.values(pathsResultTypes).find((prt) => prt.fieldName === pathsResultField); + + /** accumulate names for table, not used if out is true. */ + let path = {}; + /** Check one endpoint of the given path resultElt + * @param resultElt one row (path) of the paths result + * @param i 0 or 1 to select either endpoint of the path resultElt + * @param path array to accumulate path endpoint details in text form for table output + * @return true if the endpoint is in selectedFeatures, or its block is not brushed. + */ + function filterOut(resultElt, i, path) { + let + blockId = pathsResultType.pathBlock(resultElt, i), + /** each end may have multiple features - should generate the + * cross-product as is done in block-adj.c : pathsOfFeature() + */ + features = pathsResultType.blocksFeatures(resultElt, i), + feature = features[0], + featureGet = feature.get ? (field) => feature.get(field) : (field) => feature[field], + block1 = featureGet('blockId'), + block = block1.get ? block1 : blocksById[block1], + chrName = blockDatasetNameAndScope(block), + selectedFeaturesOfBlock = selectedFeaturesByBlock[chrName], + featureName = featureGet('name'), + out = selectedFeaturesOfBlock ? + /* endpoint feature is not in selectedFeaturesOfBlock */ + ! selectedFeaturesOfBlock[featureName] + : false; + if (! out) { + let value = featureGet('value'); + path['position' + i] = '' + value; + path['feature' + i] = featureName; + } + return out; + } + let out = filterOut(resultElt, 0, path) || filterOut(resultElt, 1, path); + if (! out) { + result.push(path); + } + }); + } + } + return result; + }, []); + + dLog('filterPaths', tableData, blockAdjs); + return tableData; + }, + + tableDataAliases : Ember.computed( + 'pathsAliasesResult.[]', 'selectedFeatures.[]', + function () { + let tableData = this.filterPaths('pathsAliasesResult'); + dLog('tableData', tableData); + return tableData; + }), + tableData : Ember.computed(function () { + let tableData = this.filterPaths('pathsResult'); + dLog('tableData', tableData); + return tableData; + }), + + /*--------------------------------------------------------------------------*/ + +}); diff --git a/frontend/app/models/block-adj.js b/frontend/app/models/block-adj.js index 9a1411811..de846a8b0 100644 --- a/frontend/app/models/block-adj.js +++ b/frontend/app/models/block-adj.js @@ -48,6 +48,16 @@ export default DS.Model.extend(Ember.Evented, { return block; }, + /*--------------------------------------------------------------------------*/ + + /** @return true if this block is adjacent to the given block-adj + */ + adjacentTo (block) { + let blocks = this.get('blocks'), + found = blocks.indexOf(block); + return found > -1; + }, + /*--------------------------------------------------------------------------*/ /* CFs based on axes could be moved to a component, e.g. draw/ stacks-view or block-adj */ diff --git a/frontend/app/styles/app.scss b/frontend/app/styles/app.scss index af9539361..eff6f9809 100644 --- a/frontend/app/styles/app.scss +++ b/frontend/app/styles/app.scss @@ -195,6 +195,11 @@ div#logo { { overflow-x: scroll; } + +table.contextual-data-table td { + padding-right: 1em; +} + /* -------------------------------------------------------------------------- */ svg { diff --git a/frontend/app/templates/components/panel/paths-table.hbs b/frontend/app/templates/components/panel/paths-table.hbs new file mode 100644 index 000000000..08603d1cf --- /dev/null +++ b/frontend/app/templates/components/panel/paths-table.hbs @@ -0,0 +1,28 @@ +{{#elem/panel-container state="primary"}} + {{#elem/panel-heading icon="filter"}} + Actions + {{/elem/panel-heading}} +
+
+ + {{!-- based on elem/button-submit.hbs, could be factored --}} +
+ +
+
+
+ +
+
+{{/elem/panel-container}} + +{{#data-sorter data=tableData as |ds|}} + {{#data-table data=ds.data selectionMode='multiple' + selectionChanged=(action 'selectionChanged') classNames=tableClassNames as |t|}} + {{t.selectionColumn}} + {{t.sortableColumn propertyName='feature0' name='From Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{t.sortableColumn propertyName='position0' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{t.sortableColumn propertyName='feature1' name='To Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{t.sortableColumn propertyName='position1' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{/data-table}} +{{/data-sorter}} diff --git a/frontend/app/templates/mapview.hbs b/frontend/app/templates/mapview.hbs index 7576c6955..82968f2d3 100644 --- a/frontend/app/templates/mapview.hbs +++ b/frontend/app/templates/mapview.hbs @@ -72,17 +72,25 @@ state=layout.right.tab onClick="setTab"}} {{elem/icon-base name="asterisk"}}  Features {{selectedFeatures.length}} {{/elem/button-tab}} + {{#if model.params.parsedOptions.blockTab}} + {{#elem/button-tab + side="right" + key="block" + state=layout.right.tab onClick="setTab"}} + {{elem/icon-base name="globe"}}  Block + {{/elem/button-tab}} + {{/if}} {{#elem/button-tab side="right" - key="block" + key="dataset" state=layout.right.tab onClick="setTab"}} - {{elem/icon-base name="globe"}}  Block + {{elem/icon-base name="globe"}}  Dataset {{/elem/button-tab}} {{#elem/button-tab side="right" - key="dataset" + key="paths" state=layout.right.tab onClick="setTab"}} - {{elem/icon-base name="globe"}}  Dataset + {{elem/icon-base name="globe"}}  Paths {{/elem/button-tab}} {{#if model.params.parsedOptions.advSettings}} {{#elem/button-tab @@ -112,6 +120,10 @@ {{else if (compare layout.right.tab '===' 'dataset')}} {{panel/manage-dataset dataset=selectedDataset}} + {{else if (compare layout.right.tab '===' 'paths')}} + {{panel/paths-table + selectedFeatures=selectedFeatures + block=selectedBlock}} {{else if (and (compare layout.right.tab '===' 'settings') model.params.parsedOptions.advSettings)}} {{panel/manage-settings selectedFeatures=selectedFeatures diff --git a/frontend/app/utils/paths-api.js b/frontend/app/utils/paths-api.js new file mode 100644 index 000000000..7cd9780bd --- /dev/null +++ b/frontend/app/utils/paths-api.js @@ -0,0 +1,217 @@ + +import PathData from '../components/draw/path-data'; +import { featureNameClass } from './draw/stacksAxes'; + +/*----------------------------------------------------------------------------*/ + +const dLog = console.debug; + +/*----------------------------------------------------------------------------*/ + + +if (false) { + /** Example of param paths passed to draw() above. */ + const examplePaths = +[{"_id":{"name":"myMarkerC"}, + "alignment":[ + {"blockId":"5c75d4f8792ccb326827daa2","repeats":{ + "_id":{"name":"myMarkerC","blockId":"5c75d4f8792ccb326827daa2"}, + "features":[{"_id":"5c75d4f8792ccb326827daa6","name":"myMarkerC","value":[3.1,3.1],"blockId":"5c75d4f8792ccb326827daa2","parentId":null}],"count":1}}, + {"blockId":"5c75d4f8792ccb326827daa1","repeats":{ + "_id":{"name":"myMarkerC","blockId":"5c75d4f8792ccb326827daa1"}, + "features":[{"_id":"5c75d4f8792ccb326827daa5","name":"myMarkerC","value":[0,0],"blockId":"5c75d4f8792ccb326827daa1","parentId":null}],"count":1}}]}]; +} + + +/*----------------------------------------------------------------------------*/ + +function featureEltId(featureBlock) +{ + let id = featurePathKeyFn(featureBlock); + id = featureNameClass(id); + return id; +} + +function featurePathKeyFn (featureBlock) +{ return featureBlock._id.name; } + +/** Given the grouped data for a feature, from the pathsDirect() result, + * generate the cross-product feature.alignment[0].repeats X feature.alignment[1].repeats. + * The result is an array of pairs of features; each pair defines a path and is of type PathData. + * for each pair an element of pairs[] : + * pair.feature0 is in block pair.block0 + * pair.feature1 is in block pair.block1 + * (for the case of pathsResultTypes.direct) : + * pair.block0 === feature.alignment[0].blockId + * pair.block1 === feature.alignment[1].blockId + * i.e. the path goes from the first block in the request params to the 2nd block + * @param pathsResultType e.g. pathsResultTypes.{Direct,Aliases} + * @param feature 1 element of the result array passed to draw() + * @return [PathData, ...] + */ +function pathsOfFeature(store, pathsResultType, owner) { + const PathData = owner.factoryFor('component:draw/path-data'); + return function (feature) { + let blocksFeatures = + [0, 1].map(function (blockIndex) { return pathsResultType.blocksFeatures(feature, blockIndex); }), + blocks = resultBlockIds(pathsResultType, feature), + pairs = + blocksFeatures[0].reduce(function (result, f0) { + let result1 = blocksFeatures[1].reduce(function (result, f1) { + let pair = + pathCreate(store, f0, f1, blocks[0], blocks[1]); + result.push(pair); + return result; + }, result); + return result1; + }, []); + return pairs; + }; +} + +const trace_pc = 1; + +function pathCreate(store, feature0, feature1, block0, block1) { + let + /** not used - same as feature{0,1}.blockId. */ + block0r = store.peekRecord('block', block0), + block1r = store.peekRecord('block', block1); + if (true) { + let properties = { + feature0, + feature1/*, + block0r, + block1r*/ + }, + pair = + PathData.create({ renderer : {} }); + pair.setProperties(properties); + if (trace_pc > 2) + dLog('PathData.create()', PathData, pair); + return pair; + } + else { + let + modelName = 'draw/path-data', + idText = locationPairKeyFn({ feature0, feature1}), + r = store.peekRecord(modelName, idText); + if (r) + dLog('pathCreate', feature0, feature1, block0, block1, r._internalModel.__attributes, r._internalModel.__data); + else if (false) + { + let data = { + type : modelName, + id : idText, + relationships : { + feature0 : { data: { type: "feature", "id": feature0 } }, + feature1 : { data: { type: "feature", "id": feature1 } } /*, + block0 : { data: { type: "block", "id": block0r } }, + block1 : { data: { type: "block", "id": block1r } }*/ + }/*, + attributes : { + 'block-id0' : block0, + 'block-id1' : block1 + }*/ + }; + r = store.push({data}); + if (trace_pc) + dLog('pathCreate', r, r.get('id'), r._internalModel, r._internalModel.__data, store, data); + } + else { + let inputProperties = { + feature0, + feature1/*, + block0r, + block1r*/ + }; + r = store.createRecord(modelName, inputProperties); + } + return r; + } +} + + +function locationPairKeyFn(locationPair) +{ + return locationPair.feature0.id + '_' + locationPair.feature1.id; +} + +/*----------------------------------------------------------------------------*/ + +const pathsApiFields = ['featureAObj', 'featureBObj']; +/** This type is created by paths-progressive.js : requestAliases() : receivedData() */ +const pathsApiResultType = { + // fieldName may be pathsResult or pathsAliasesResult + typeCheck : function(resultElt) { if (! resultElt.featureAObj) { + dLog('pathsApiResultType : typeCheck', resultElt); } }, + pathBlock : function (resultElt, blockIndex) { return resultElt[pathsApiFields[blockIndex]].blockId; }, + /** direct.blocksFeatures() returns an array of features, so match that. See + * similar commment in alias.blocksFeatures. */ + blocksFeatures : function (resultElt, blockIndex) { return [ resultElt[pathsApiFields[blockIndex]] ]; }, + featureEltId : + function (resultElt) + { + let id = pathsApiResultType.featurePathKeyFn(resultElt); + id = featureNameClass(id); + return id; + }, + featurePathKeyFn : function (resultElt) { return resultElt.featureA + '_' + resultElt.featureB; } + +}; + +/** This is provision for using the API result type as data type; not used currently because + * the various forms of result data are converted to path-data. + * These are the result types from : + * Block/paths -> apiLookupAliases() -> task.paths() + * Blocks/pathsViaStream -> pathsAggr.pathsDirect() + * getPathsAliasesViaStream() / getPathsAliasesProgressive() -> Blocks/pathsAliasesProgressive -> dbLookupAliases() -> pathsAggr.pathsAliases() + */ +const pathsResultTypes = { + direct : { + fieldName : 'pathsResult', + typeCheck : function(resultElt) { if (! resultElt._id) { + dLog('direct : typeCheck', resultElt); } }, + pathBlock : function (resultElt, blockIndex) { return resultElt.alignment[blockIndex].blockId; }, + blocksFeatures : function (resultElt, blockIndex) { return resultElt.alignment[blockIndex].repeats.features; }, + featureEltId : featureEltId, + featurePathKeyFn : featurePathKeyFn + }, + + alias : + { + fieldName : 'pathsAliasesResult', + typeCheck : function(resultElt) { if (! resultElt.aliased_features) { + dLog('alias : typeCheck', resultElt); } }, + pathBlock : function (resultElt, blockIndex) { return resultElt.aliased_features[blockIndex].blockId; }, + /** There is currently only 1 element in .aliased_features[blockIndex], but + * pathsOfFeature() handles an array an produces a cross-product, so return + * this 1 element as an array. */ + blocksFeatures : function (resultElt, blockIndex) { return [resultElt.aliased_features[blockIndex]]; }, + featureEltId : + function (resultElt) + { + let id = pathsResultTypes.alias.featurePathKeyFn(resultElt); + id = featureNameClass(id); + return id; + }, + featurePathKeyFn : function (resultElt) { + return resultElt.aliased_features.map(function (f) { return f.name; } ).join('_'); + } + } +}, +/** This matches the index values of services/data/flows-collate.js : flows */ +flowNames = Object.keys(pathsResultTypes); +// add .flowName to each of pathsResultTypes, which could later require non-const declaration. +flowNames.forEach(function (flowName) { pathsResultTypes[flowName].flowName = flowName; } ); + +/** + * @return array[2] of blockId, equivalent to blockAdjId + */ +function resultBlockIds(pathsResultType, featurePath) { + let blockIds = + [0, 1].map(function (blockIndex) { return pathsResultType.pathBlock(featurePath, blockIndex); }); + return blockIds; +} + + +export { pathsResultTypes, pathsApiResultType, flowNames, resultBlockIds, pathsOfFeature, locationPairKeyFn }; From 740384da8749a50a3fbd08c1329c819854a323a1 Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 14 Feb 2020 00:00:45 +1100 Subject: [PATCH 37/57] implement request-all-paths in paths-table paths-table.js : factor selectedFeaturesByBlock() out of filterPaths(). add requestAllPaths(). mapview.js : updateSelectedFeatures() : don't setTab() to selection if paths table is displayed. block-adj.js : adjacentTo() : also check referenceBlocks because clicking on axis selects the reference block add referenceBlocks(); add domainChange() : true if fullDensity. block.js : add datasetNameAndScope(). paths-progressive.js : intervalParams() : if fullDensity, set nSamples etc to 1e6. paths-table.hbs : drop selectionColumn frontend/app/ : 0dac3e6 6798 Feb 13 23:16 components/panel/paths-table.js 11ba494 9794 Feb 13 16:31 controllers/mapview.js c3175bb 14578 Feb 13 23:55 models/block-adj.js 9e4976f 10274 Feb 13 18:27 models/block.js c509ee6 25639 Feb 13 22:25 services/data/paths-progressive.js ba2e92a 1247 Feb 13 22:25 templates/components/panel/paths-table.hbs a7bbe7f 5183 Feb 13 14:58 templates/mapview.hbs --- frontend/app/components/panel/paths-table.js | 62 +++++++++++++++---- frontend/app/controllers/mapview.js | 10 ++- frontend/app/models/block-adj.js | 14 ++++- frontend/app/models/block.js | 16 +++++ .../app/services/data/paths-progressive.js | 26 +++++--- .../components/panel/paths-table.hbs | 9 +-- frontend/app/templates/mapview.hbs | 2 +- 7 files changed, 108 insertions(+), 31 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index cd87958a7..42cc167bb 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -10,11 +10,13 @@ const dLog = console.debug; export default Ember.Component.extend({ flowsService: service('data/flows-collate'), + pathsPro : service('data/paths-progressive'), + didReceiveAttrs() { this._super(...arguments); - dLog('didReceiveAttrs', this.get('block.id')); + dLog('didReceiveAttrs', this.get('block.id'), this); }, /*--------------------------------------------------------------------------*/ @@ -27,16 +29,17 @@ export default Ember.Component.extend({ dLog(selA[i].feature, selA[i].position); } }, + requestAllPaths() { + this.requestAllPaths(); + } }, /*--------------------------------------------------------------------------*/ - /** - * also in utils/paths-filter.js @see pathInDomain() - */ - filterPaths(pathsResultField) { + selectedFeaturesByBlock : Ember.computed( + 'selectedFeatures.[]', + function () { - let selectedBlock = this.get('selectedBlock'); let selectedFeatures = this.get('selectedFeatures'); /** map selectedFeatures to a hash by block (dataset name and scope). * selectedFeatures should be e.g. a WeakSet by feature id, not name. @@ -50,6 +53,19 @@ export default Ember.Component.extend({ } return result; }, {}) : {}; + return selectedFeaturesByBlock; + }), + + + /*--------------------------------------------------------------------------*/ + + /** + * also in utils/paths-filter.js @see pathInDomain() + */ + filterPaths(pathsResultField) { + + let selectedBlock = this.get('selectedBlock'); + let selectedFeaturesByBlock = this.get('selectedFeaturesByBlock'); if (false) { /** form is e.g. : {Chromosome: "myMap:1A.1", Feature: "myMarkerA", Position: "0"} */ @@ -140,18 +156,40 @@ export default Ember.Component.extend({ }, tableDataAliases : Ember.computed( - 'pathsAliasesResult.[]', 'selectedFeatures.[]', + 'pathsAliasesResult.[]', + 'selectedBlock', + 'selectedFeaturesByBlock.@each', function () { let tableData = this.filterPaths('pathsAliasesResult'); dLog('tableData', tableData); return tableData; }), - tableData : Ember.computed(function () { - let tableData = this.filterPaths('pathsResult'); - dLog('tableData', tableData); - return tableData; - }), + tableData : Ember.computed( + 'pathsResult.[]', + 'selectedBlock', + 'selectedFeaturesByBlock.@each', + function () { + let tableData = this.filterPaths('pathsResult'); + dLog('tableData', tableData); + return tableData; + }), + + /*--------------------------------------------------------------------------*/ + + requestAllPaths() { + dLog('requestAllPaths', this); + + let pathsPro = this.get('pathsPro'); + pathsPro.set('fullDensity', true); + let + blockAdjs = this.get('flowsService.blockAdjs'); + blockAdjs.forEach(function (blockAdj) { + let p = blockAdj.call_taskGetPaths(); + }); + + pathsPro.set('fullDensity', false); + }, /*--------------------------------------------------------------------------*/ }); diff --git a/frontend/app/controllers/mapview.js b/frontend/app/controllers/mapview.js index a0ceece95..f30d6ebcd 100644 --- a/frontend/app/controllers/mapview.js +++ b/frontend/app/controllers/mapview.js @@ -37,13 +37,19 @@ export default Ember.Controller.extend(Ember.Evented, ViewedBlocks, { this.set(`layout.${side}.visible`, !visibility); }, setTab: function(side, tab) { - // dLog("setTab", side, tab); + dLog("setTab", side, tab, this.get('layout')); this.set(`layout.${side}.tab`, tab); }, updateSelectedFeatures: function(features) { // dLog("updateselectedFeatures in mapview", features.length); this.set('selectedFeatures', features); - this.send('setTab', 'right', 'selection'); + /** results of a selection impact on the selection (selectedFeatures) tab + * and the paths tab, so if neither of these is currently shown, show the + * selection tab. + */ + let rightTab = this.get('layout.right.tab'); + if ((rightTab !== 'selection') && (rightTab !== 'paths')) + this.send('setTab', 'right', 'selection'); }, /** goto-feature-list is given features by the user and finds them in * blocks; this is that result in a hash, indexed by block id, with value diff --git a/frontend/app/models/block-adj.js b/frontend/app/models/block-adj.js index de846a8b0..d55d3c597 100644 --- a/frontend/app/models/block-adj.js +++ b/frontend/app/models/block-adj.js @@ -54,8 +54,14 @@ export default DS.Model.extend(Ember.Evented, { */ adjacentTo (block) { let blocks = this.get('blocks'), - found = blocks.indexOf(block); - return found > -1; + found = blocks.indexOf(block) > -1; + if (! found) { + /** also check referenceBlocks because clicking on axis selects the reference block */ + let referenceBlocks = this.get('referenceBlocks'); + found = referenceBlocks.indexOf(block) > -1; + dLog('adjacentTo', found, referenceBlocks, block); + } + return found; }, /*--------------------------------------------------------------------------*/ @@ -68,6 +74,7 @@ export default DS.Model.extend(Ember.Evented, { blocks = blockAdjId.map((blockId) => { return this.peekBlock(blockId); } ); return blocks; }), + referenceBlocks : Ember.computed.mapBy('blocks', 'referenceBlock'), /** Stacked Blocks - should be able to retire this. */ sBlocks : Ember.computed('blockAdjId', function () { let @@ -158,6 +165,7 @@ export default DS.Model.extend(Ember.Evented, { /** interim measure to include pathsDensityParams in comparison; planning to * restructure using CP. */ pathsDensityParams = this.get('pathsPro.pathsDensityParams'), + fullDensity = this.get('pathsPro.fullDensity'), /** this can now be simplified to use axesDomains(). */ intervalsAxes = this.get('axisDimensions'), domainsDiffer = function () @@ -188,7 +196,7 @@ export default DS.Model.extend(Ember.Evented, { dLog('domainChange', intervals, intervalsAxes, domainChanges, change); return change; }, - domainChange = ! intervals || domainsDiffer(); + domainChange = fullDensity || ! intervals || domainsDiffer(); return domainChange; }), diff --git a/frontend/app/models/block.js b/frontend/app/models/block.js index a0abd087f..901714503 100644 --- a/frontend/app/models/block.js +++ b/frontend/app/models/block.js @@ -147,6 +147,22 @@ export default DS.Model.extend({ return isChartable; }), + /*--------------------------------------------------------------------------*/ + + /** generate a text name for the block, to be displayed - it should be + * user-readable and uniquely identify the block. + */ + datasetNameAndScope : Ember.computed('datasetId.id', 'scope', function () { + /** This is currently the name format which is used in + * selectedFeatures.Chromosome + * In paths-table.js @see blockDatasetNameAndScope() + */ + let name = (this.get('datasetId.meta.shortName') || this.get('datasetId.id')) + ':' + this.get('scope'); + return name; + }), + + /*--------------------------------------------------------------------------*/ + /** If the dataset of this block has a parent, return the name of that parent (reference dataset). * @return the reference dataset name or undefined if none */ diff --git a/frontend/app/services/data/paths-progressive.js b/frontend/app/services/data/paths-progressive.js index 55a594a86..57470e9a2 100644 --- a/frontend/app/services/data/paths-progressive.js +++ b/frontend/app/services/data/paths-progressive.js @@ -179,16 +179,24 @@ export default Service.extend({ i.domain = undefined; } ); - let vcParams = this.get('pathsDensityParams'); - if (vcParams.nSamples) { - params.nSamples = vcParams.nSamples; + if (this.get('fullDensity')) { + params.nSamples = 1e6; + page.densityFactor = 1e6; + page.thresholdFactor = 1e6; + params.nFeatures = 1e6; } - if (vcParams.densityFactor) { - page.densityFactor = vcParams.densityFactor; - page.thresholdFactor = vcParams.densityFactor; // retire the name .thresholdFactor - } - if (vcParams.nFeatures) { - params.nFeatures = vcParams.nFeatures; + else { + let vcParams = this.get('pathsDensityParams'); + if (vcParams.nSamples) { + params.nSamples = vcParams.nSamples; + } + if (vcParams.densityFactor) { + page.densityFactor = vcParams.densityFactor; + page.thresholdFactor = vcParams.densityFactor; // retire the name .thresholdFactor + } + if (vcParams.nFeatures) { + params.nFeatures = vcParams.nFeatures; + } } return params; diff --git a/frontend/app/templates/components/panel/paths-table.hbs b/frontend/app/templates/components/panel/paths-table.hbs index 08603d1cf..6fdacd943 100644 --- a/frontend/app/templates/components/panel/paths-table.hbs +++ b/frontend/app/templates/components/panel/paths-table.hbs @@ -7,7 +7,7 @@ {{!-- based on elem/button-submit.hbs, could be factored --}}
- +

@@ -16,10 +16,11 @@
{{/elem/panel-container}} +{{selectedBlock.datasetNameAndScope}} + {{#data-sorter data=tableData as |ds|}} - {{#data-table data=ds.data selectionMode='multiple' - selectionChanged=(action 'selectionChanged') classNames=tableClassNames as |t|}} - {{t.selectionColumn}} + {{#data-table data=ds.data + classNames=tableClassNames as |t|}} {{t.sortableColumn propertyName='feature0' name='From Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} {{t.sortableColumn propertyName='position0' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} {{t.sortableColumn propertyName='feature1' name='To Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} diff --git a/frontend/app/templates/mapview.hbs b/frontend/app/templates/mapview.hbs index 82968f2d3..615ec649c 100644 --- a/frontend/app/templates/mapview.hbs +++ b/frontend/app/templates/mapview.hbs @@ -123,7 +123,7 @@ {{else if (compare layout.right.tab '===' 'paths')}} {{panel/paths-table selectedFeatures=selectedFeatures - block=selectedBlock}} + selectedBlock=selectedBlock}} {{else if (and (compare layout.right.tab '===' 'settings') model.params.parsedOptions.advSettings)}} {{panel/manage-settings selectedFeatures=selectedFeatures From 7e1390dbf7bfbed9c998848e43b62434f4c64429 Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 14 Feb 2020 12:35:04 +1100 Subject: [PATCH 38/57] show brushedDomains and counts in paths table paths-table.js : show brushedDomains in table, add setPair(), outCount. axis-brush.js : add brushesByBlock(), brushOfBlock(). paths-table.hbs : add label Selected block. frontend/app/ : c9ac48f 7803 Feb 14 12:27 components/panel/paths-table.js 6ce2028 2029 Feb 14 12:02 services/data/axis-brush.js a8070bf 1301 Feb 14 11:58 templates/components/panel/paths-table.hbs --- frontend/app/components/panel/paths-table.js | 31 ++++++++++++++++--- frontend/app/services/data/axis-brush.js | 19 +++++++++++- .../components/panel/paths-table.hbs | 2 +- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index 42cc167bb..b715c6bac 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -11,6 +11,7 @@ const dLog = console.debug; export default Ember.Component.extend({ flowsService: service('data/flows-collate'), pathsPro : service('data/paths-progressive'), + axisBrush: service('data/axis-brush'), didReceiveAttrs() { @@ -66,6 +67,7 @@ export default Ember.Component.extend({ let selectedBlock = this.get('selectedBlock'); let selectedFeaturesByBlock = this.get('selectedFeaturesByBlock'); + let axisBrush = this.get('axisBrush'); if (false) { /** form is e.g. : {Chromosome: "myMap:1A.1", Feature: "myMarkerA", Position: "0"} */ @@ -81,18 +83,37 @@ export default Ember.Component.extend({ tableData = blockAdjs.reduce(function (result, blockAdj) { // components/panel/manage-settings.js:14: return feature.Block === selectedBlock.id if (! selectedBlock || blockAdj.adjacentTo(selectedBlock)) { + /** Prepare a row with a pair of values to add to the table */ + function setPair(row, i, feature, position) { + row['feature' + i] = feature; + row['position' + i] = position; + return row; + } /** push the names of the 2 adjacent blocks, as an interleaved title row in the table. */ let blocks = blockAdj.get('blocks'), blocksById = blocks.reduce((r, b) => { r[b.get('id')] = b; return r; }, {}), - namesFlat = blocks.reduce((rowHash, b, i) => { - rowHash['feature' + i] = b.get('datasetId.id'); - rowHash['position' + i] = b.get('scope'); + namesFlat = blocks.reduce( + (rowHash, b, i) => + setPair(rowHash, i, b.get('datasetId.id'), b.get('scope')), {}); + // blank row to separate from previous blockAdj + result.push({'feature0': '_'}); + result.push(namesFlat); + /** Add to the table the brushed domain of the 2 blocks. */ + let brushedDomains = blocks.reduce((rowHash, b, i) => { + let ab = axisBrush.brushOfBlock(b), + brushedDomain = ab && ab.get('brushedDomain'); + if (brushedDomain) { + setPair(rowHash, i, brushedDomain[0], brushedDomain[1]); + } return rowHash; }, {}); - result.push(namesFlat); + result.push(brushedDomains); let resultElts = blockAdj.get(pathsResultField); if (resultElts) { + if (resultElts.length) + result.push(setPair({}, 0, 'Paths loaded', resultElts.length)); + let outCount = 0; resultElts.forEach((resultElt) => { /** for each end of the path, if the endpoint is in a brushed block, * filter out if the endpoint is not in the (brushed) @@ -144,8 +165,10 @@ export default Ember.Component.extend({ let out = filterOut(resultElt, 0, path) || filterOut(resultElt, 1, path); if (! out) { result.push(path); + outCount++; } }); + result.push(setPair({}, 0, 'Filtered Count', outCount)); } } return result; diff --git a/frontend/app/services/data/axis-brush.js b/frontend/app/services/data/axis-brush.js index 6e1bd4982..3b39afa49 100644 --- a/frontend/app/services/data/axis-brush.js +++ b/frontend/app/services/data/axis-brush.js @@ -45,6 +45,23 @@ export default Service.extend(Ember.Evented, { dLog(fnName, blocks); return blocks; - }) + }), + brushesByBlock : Ember.computed('brushedAxes.[]', function () { + let brushesByBlock = this.get('brushedAxes').reduce(function (result, ab) { + result[ab.get('block.id')] = ab; + return result; }, {} ); + return brushesByBlock; + }), + /** Lookup brushedAxes for the given block, or its referenceBlock. + */ + brushOfBlock(block) { + let brushesByBlock = this.get('brushesByBlock'), + brush = brushesByBlock[block.get('id')]; + let referenceBlock; + if (! brush && (referenceBlock = block.get('referenceBlock'))) { + brush = brushesByBlock[referenceBlock.get('id')]; + } + return brush; + } }); diff --git a/frontend/app/templates/components/panel/paths-table.hbs b/frontend/app/templates/components/panel/paths-table.hbs index 6fdacd943..2dd6210d1 100644 --- a/frontend/app/templates/components/panel/paths-table.hbs +++ b/frontend/app/templates/components/panel/paths-table.hbs @@ -16,7 +16,7 @@
{{/elem/panel-container}} -{{selectedBlock.datasetNameAndScope}} +
Selected block : {{selectedBlock.datasetNameAndScope}}
{{#data-sorter data=tableData as |ds|}} {{#data-table data=ds.data From 4b8605296c7abbde5c4cadbfbaad89164b95634c Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 14 Feb 2020 16:21:41 +1100 Subject: [PATCH 39/57] Add block column to paths-table add checkboxes (.optionControls) : blockColumn, showDomains, showCounts Add block column, enabled by checkbox. Change setPair() -> setEndpoint() - optional param block. filterOut() : use brushOfBlock to indicate whether axis is brushed (since selectedFeaturesOfBlock may be empty even when brushed). Add : classNames : paths-tables. frontend/app/ : e9425c3 9873 Feb 14 16:16 components/panel/paths-table.js 295f264 30793 Feb 14 16:03 styles/app.scss (partially staged) c1c7192 1921 Feb 14 15:56 templates/components/panel/paths-table.hbs --- frontend/app/components/panel/paths-table.js | 94 ++++++++++++++----- frontend/app/styles/app.scss | 11 ++- .../components/panel/paths-table.hbs | 14 ++- 3 files changed, 94 insertions(+), 25 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index b715c6bac..8782d744d 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -8,11 +8,23 @@ import { pathsResultTypes } from '../../utils/paths-api'; const dLog = console.debug; +/** + * Arguments passed to template : + * @param selectedFeatures catenation of features within all brushed regions + * @param selectedBlock block selected via axis brush, or via click in dataset explorer or view panel, or adding the block. + * + * @desc inputs from template + * @param blockColumn true means show the block name:scope in a column instead of as a row. + */ export default Ember.Component.extend({ flowsService: service('data/flows-collate'), pathsPro : service('data/paths-progressive'), axisBrush: service('data/axis-brush'), + blockColumn : true, + showDomains : false, + + classNames: ['paths-tables'], didReceiveAttrs() { this._super(...arguments); @@ -68,6 +80,15 @@ export default Ember.Component.extend({ let selectedBlock = this.get('selectedBlock'); let selectedFeaturesByBlock = this.get('selectedFeaturesByBlock'); let axisBrush = this.get('axisBrush'); + /** true means show the block name:scope in a column instead of as a row. */ + let blockColumn = this.get('blockColumn'); + /** true means show the brushed domains in the table. This may be omitted, although it + * seems important to know if the axis was brushed and what the region + * was. */ + let showDomains = this.get('showDomains'); + /** true means show counts of paths before and after filtering in the table. */ + let showCounts = this.get('showCounts'); + if (false) { /** form is e.g. : {Chromosome: "myMap:1A.1", Feature: "myMarkerA", Position: "0"} */ @@ -84,35 +105,43 @@ export default Ember.Component.extend({ // components/panel/manage-settings.js:14: return feature.Block === selectedBlock.id if (! selectedBlock || blockAdj.adjacentTo(selectedBlock)) { /** Prepare a row with a pair of values to add to the table */ - function setPair(row, i, feature, position) { + function setEndpoint(row, i, block, feature, position) { + if (block) + row['block' + i] = block; row['feature' + i] = feature; row['position' + i] = position; return row; - } - /** push the names of the 2 adjacent blocks, as an interleaved title row in the table. */ + }; let blocks = blockAdj.get('blocks'), - blocksById = blocks.reduce((r, b) => { r[b.get('id')] = b; return r; }, {}), - namesFlat = blocks.reduce( - (rowHash, b, i) => - setPair(rowHash, i, b.get('datasetId.id'), b.get('scope')), {}); - // blank row to separate from previous blockAdj - result.push({'feature0': '_'}); - result.push(namesFlat); - /** Add to the table the brushed domain of the 2 blocks. */ - let brushedDomains = blocks.reduce((rowHash, b, i) => { - let ab = axisBrush.brushOfBlock(b), - brushedDomain = ab && ab.get('brushedDomain'); - if (brushedDomain) { - setPair(rowHash, i, brushedDomain[0], brushedDomain[1]); - } - return rowHash; - }, {}); - result.push(brushedDomains); + blocksById = blocks.reduce((r, b) => { r[b.get('id')] = b; return r; }, {}); + if (! blockColumn) { + /** push the names of the 2 adjacent blocks, as an interleaved title row in the table. */ + let + namesFlat = blocks.reduce( + (rowHash, b, i) => + setEndpoint(rowHash, i, undefined, b.get('datasetId.id'), b.get('scope')), {}); + // blank row to separate from previous blockAdj + result.push({'feature0': '_'}); + result.push(namesFlat); + } + if (showDomains) { + /** Add to the table the brushed domain of the 2 blocks. */ + let brushedDomains = blocks.reduce((rowHash, b, i) => { + let ab = axisBrush.brushOfBlock(b), + brushedDomain = ab && ab.get('brushedDomain'); + if (brushedDomain) { + setEndpoint(rowHash, i, undefined, brushedDomain[0], brushedDomain[1]); + } + return rowHash; + }, {}); + result.push(brushedDomains); + } let resultElts = blockAdj.get(pathsResultField); if (resultElts) { if (resultElts.length) - result.push(setPair({}, 0, 'Paths loaded', resultElts.length)); + if (showCounts) + result.push(setEndpoint({}, 0, undefined, 'Paths loaded', resultElts.length)); let outCount = 0; resultElts.forEach((resultElt) => { /** for each end of the path, if the endpoint is in a brushed block, @@ -122,6 +151,11 @@ export default Ember.Component.extend({ /** for the given block, generate the name format which is used in * selectedFeatures.Chromosome + * + * block.get('datasetNameAndScope') may be the same value; it can + * use shortName, and its purpose is display, whereas + * selectedFeatures.Chromosome is for identifying the block (and + * could be changed to blockId). */ function blockDatasetNameAndScope(block) { return block.get('datasetId.id') + ':' + block.get('scope'); @@ -151,12 +185,19 @@ export default Ember.Component.extend({ chrName = blockDatasetNameAndScope(block), selectedFeaturesOfBlock = selectedFeaturesByBlock[chrName], featureName = featureGet('name'), + /** if the axis is brushed but there are no features in this block + * within the brush, then selectedFeaturesOfBlock will be + * undefined (empty). If the axis is not brushed then no filter + * is applied on this endpoint. + */ + isBrushed = !!axisBrush.brushOfBlock(block), out = selectedFeaturesOfBlock ? /* endpoint feature is not in selectedFeaturesOfBlock */ ! selectedFeaturesOfBlock[featureName] - : false; + : isBrushed; if (! out) { let value = featureGet('value'); + path['block' + i] = block.get('datasetNameAndScope'); path['position' + i] = '' + value; path['feature' + i] = featureName; } @@ -168,7 +209,8 @@ export default Ember.Component.extend({ outCount++; } }); - result.push(setPair({}, 0, 'Filtered Count', outCount)); + if (showCounts) + result.push(setEndpoint({}, 0, undefined, 'Filtered Count', outCount)); } } return result; @@ -182,6 +224,9 @@ export default Ember.Component.extend({ 'pathsAliasesResult.[]', 'selectedBlock', 'selectedFeaturesByBlock.@each', + 'blockColumn', + 'showDomains', + 'showCounts', function () { let tableData = this.filterPaths('pathsAliasesResult'); dLog('tableData', tableData); @@ -191,6 +236,9 @@ export default Ember.Component.extend({ 'pathsResult.[]', 'selectedBlock', 'selectedFeaturesByBlock.@each', + 'blockColumn', + 'showDomains', + 'showCounts', function () { let tableData = this.filterPaths('pathsResult'); dLog('tableData', tableData); diff --git a/frontend/app/styles/app.scss b/frontend/app/styles/app.scss index eff6f9809..7da6032fe 100644 --- a/frontend/app/styles/app.scss +++ b/frontend/app/styles/app.scss @@ -1353,4 +1353,13 @@ div.metaeditor-panel div.jsoneditor-treepath { div.metaeditor-panel div.jsoneditor { border: 1px solid #d3d3d3 } -/* -------------------------------------------------------------------------- */ \ No newline at end of file +/* -------------------------------------------------------------------------- */ + +.paths-tables .optionControls { + display: flex; + margin-bottom: 1em; +} +.paths-tables .optionControls > div { + flex-grow: 1; +} +/* -------------------------------------------------------------------------- */ diff --git a/frontend/app/templates/components/panel/paths-table.hbs b/frontend/app/templates/components/panel/paths-table.hbs index 2dd6210d1..0b2f370a4 100644 --- a/frontend/app/templates/components/panel/paths-table.hbs +++ b/frontend/app/templates/components/panel/paths-table.hbs @@ -16,13 +16,25 @@
{{/elem/panel-container}} -
Selected block : {{selectedBlock.datasetNameAndScope}}
+
+
Selected block : {{selectedBlock.datasetNameAndScope}}
+ +
{{input type="checkbox" name="blockColumn" checked=blockColumn }} Show block columns
+
{{input type="checkbox" name="showDomains" checked=showDomains }} Show Brushed Regions
+
{{input type="checkbox" name="showCounts" checked=showCounts }} Show Counts
+
{{#data-sorter data=tableData as |ds|}} {{#data-table data=ds.data classNames=tableClassNames as |t|}} + {{#if blockColumn}} + {{t.sortableColumn propertyName='block0' name='Block' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{/if}} {{t.sortableColumn propertyName='feature0' name='From Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} {{t.sortableColumn propertyName='position0' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{#if blockColumn}} + {{t.sortableColumn propertyName='block1' name='Block' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{/if}} {{t.sortableColumn propertyName='feature1' name='To Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} {{t.sortableColumn propertyName='position1' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} {{/data-table}} From 6c920595bab7e8b2f950ef3350382a57a8c16c1a Mon Sep 17 00:00:00 2001 From: Don Date: Sun, 16 Feb 2020 12:08:26 +1100 Subject: [PATCH 40/57] data access for alias paths result. brushed features identification. paths-table.js Use pathsApiResultType for pathsAliasesResult. Split out blockDatasetNameAndScope() as block.js : brushName(), and refine to match brushHelper() more closely (shortName of dataset or name of reference). Combine tableData{,Aliases} into a single flat array result (may later use a structure like result[alias] : paths by [block-adj]). collate-paths.js : aliasDirection : use .get for blockId because it may be a CP. frontend/app/ : 0cabce7 9845 Feb 15 21:40 components/panel/paths-table.js dad1809 11784 Feb 16 11:40 models/block.js 61351c3 2196 Feb 15 21:16 services/data/axis-brush.js (cfbf43d Feb 16 12:02 : trace -> 0) 3df86a8 40290 Feb 15 19:15 utils/draw/collate-paths.js --- frontend/app/components/panel/paths-table.js | 39 ++++++++++---------- frontend/app/models/block.js | 37 +++++++++++++++++++ frontend/app/services/data/axis-brush.js | 4 ++ frontend/app/utils/draw/collate-paths.js | 9 ++++- 4 files changed, 69 insertions(+), 20 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index 8782d744d..36e0d5b4e 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -3,7 +3,7 @@ const { inject: { service } } = Ember; import PathData from '../draw/path-data'; -import { pathsResultTypes } from '../../utils/paths-api'; +import { pathsResultTypes, pathsApiResultType } from '../../utils/paths-api'; const dLog = console.debug; @@ -149,19 +149,11 @@ export default Ember.Component.extend({ * selectedFeatures. */ - /** for the given block, generate the name format which is used in - * selectedFeatures.Chromosome - * - * block.get('datasetNameAndScope') may be the same value; it can - * use shortName, and its purpose is display, whereas - * selectedFeatures.Chromosome is for identifying the block (and - * could be changed to blockId). - */ - function blockDatasetNameAndScope(block) { - return block.get('datasetId.id') + ':' + block.get('scope'); - } let - pathsResultType = Object.values(pathsResultTypes).find((prt) => prt.fieldName === pathsResultField); + pathsResultType = (pathsResultField === 'pathsAliasesResult') ? + pathsApiResultType + : Object.values(pathsResultTypes).find((prt) => prt.fieldName === pathsResultField); + /** accumulate names for table, not used if out is true. */ let path = {}; @@ -181,8 +173,10 @@ export default Ember.Component.extend({ feature = features[0], featureGet = feature.get ? (field) => feature.get(field) : (field) => feature[field], block1 = featureGet('blockId'), - block = block1.get ? block1 : blocksById[block1], - chrName = blockDatasetNameAndScope(block), + block2 = block1.content || block1, + block = block2.get ? block2 : blocksById[block2], + /** brushes are identified by the referenceBlock (axisName). */ + chrName = block.get('brushName'), selectedFeaturesOfBlock = selectedFeaturesByBlock[chrName], featureName = featureGet('name'), /** if the axis is brushed but there are no features in this block @@ -220,7 +214,7 @@ export default Ember.Component.extend({ return tableData; }, - tableDataAliases : Ember.computed( + tableData/*Aliases*/ : Ember.computed( 'pathsAliasesResult.[]', 'selectedBlock', 'selectedFeaturesByBlock.@each', @@ -228,11 +222,18 @@ export default Ember.Component.extend({ 'showDomains', 'showCounts', function () { - let tableData = this.filterPaths('pathsAliasesResult'); + let tableDataAliases = this.filterPaths('pathsAliasesResult'); + dLog('tableDataAliases', tableDataAliases); + let tableData = this.filterPaths('pathsResult'); dLog('tableData', tableData); - return tableData; + let data = + (tableDataAliases.length && tableData.length) ? + tableDataAliases.concat(tableData) + : (tableData.length ? tableData : tableDataAliases) + ; + return data; }), - tableData : Ember.computed( + tableData_bak : Ember.computed( 'pathsResult.[]', 'selectedBlock', 'selectedFeaturesByBlock.@each', diff --git a/frontend/app/models/block.js b/frontend/app/models/block.js index 901714503..d7ab8079c 100644 --- a/frontend/app/models/block.js +++ b/frontend/app/models/block.js @@ -161,6 +161,43 @@ export default DS.Model.extend({ return name; }), + /** for the given block, generate the name format which is used in + * selectedFeatures.Chromosome + * Used by e.g. paths-table to access selectedFeatures - need to match the + * block identification which is used by brushHelper() when it generates + * selectedFeatures. + * + * block.get('datasetNameAndScope') may be the same value; it can + * use shortName, and its purpose is display, whereas + * selectedFeatures.Chromosome is for identifying the block (and + * could be changed to blockId). + */ + brushName : Ember.computed('name', 'datasetId', 'referenceBlock', function() { + /** This calculation replicates the value used by brushHelper(), which draws + * on axisName2MapChr(), makeMapChrName(), copyChrData(). + * That can be replaced by simply this function, which will then be the + * source of the value .Chromosome in selectedFeatures. + */ + let brushName; + /** brushHelper() uses blockR.get('datasetId.meta.shortName') where blockR is the data block, + * and axisName2MapChr(p) where p is the axisName (referenceBlock). + */ + let shortName = this.get('datasetId.meta.shortName'); + /** brushes are identified by the referenceBlock (axisName). */ + let block = this.get('referenceBlock') || this; + if (block) { + let + /** e.g. "IWGSC" */ + blockName = shortName || block.get('datasetId.name'), + /** e.g. "1B" */ + scope = block.get('name'); + brushName = blockName + ':' + scope; + } + + return brushName; + }), + + /*--------------------------------------------------------------------------*/ /** If the dataset of this block has a parent, return the name of that parent (reference dataset). diff --git a/frontend/app/services/data/axis-brush.js b/frontend/app/services/data/axis-brush.js index 3b39afa49..9e5a4a965 100644 --- a/frontend/app/services/data/axis-brush.js +++ b/frontend/app/services/data/axis-brush.js @@ -50,6 +50,8 @@ export default Service.extend(Ember.Evented, { let brushesByBlock = this.get('brushedAxes').reduce(function (result, ab) { result[ab.get('block.id')] = ab; return result; }, {} ); + if (trace_axisBrush) + dLog('brushesByBlock', brushesByBlock); return brushesByBlock; }), /** Lookup brushedAxes for the given block, or its referenceBlock. @@ -61,6 +63,8 @@ export default Service.extend(Ember.Evented, { if (! brush && (referenceBlock = block.get('referenceBlock'))) { brush = brushesByBlock[referenceBlock.get('id')]; } + if (trace_axisBrush > 1) + dLog('brushOfBlock', brush, brushesByBlock, referenceBlock); return brush; } diff --git a/frontend/app/utils/draw/collate-paths.js b/frontend/app/utils/draw/collate-paths.js index d1612b567..cd11e6dbf 100644 --- a/frontend/app/utils/draw/collate-paths.js +++ b/frontend/app/utils/draw/collate-paths.js @@ -750,7 +750,14 @@ function addPathsToCollation(blockA, blockB, paths) * names - that will change probably. */ let /** the order of p.featureA, p.featureB matches the alias order. */ - aliasDirection = p.featureAObj.blockId === blockA, + aliasDirection = p.featureAObj.get('blockId.id') === blockA; + if (! aliasDirection && (p.featureAObj.get('blockId.id') !== blockB)) { + dLog('aliasDirection no match', aliasDirection, + p.featureAObj.get('blockId.id'), p.featureBObj.get('blockId.id'), + blockA, blockB, p); + } + + let aliasFeatures = [p.featureA, p.featureB], /** the order of featureA and featureB matches the order of blockA and blockB, * i.e. featureA is in blockA, and featureB is in blockB From 0449c3605ee5e986ac4230c03850899baf7c72f9 Mon Sep 17 00:00:00 2001 From: Don Date: Mon, 17 Feb 2020 16:39:41 +1100 Subject: [PATCH 41/57] paths-table : match the path endpoint order to the table block order. narrow paths by zoomed domain when not brushed. paths-table.js : use blockIndex to guide which side of the table the endpoint details are placed in. models/block-adj.js : add zoomedDomains(), blockIndex(), filterPathsResult() and use that to add pathsResultFiltered, pathsAliasesResultFiltered, models/block.js : add alias zoomedDomain, not required yet. data/block.js : add blocksById(), based on blocksById in filterPaths() (paths-table.js). collate-paths.js : aliasDirection : handle existing result obj which was not handled by last change. paths-api.js : add param trace to typeCheck(), so it can be used for match in added pathsResultTypeFor(). factor from filterPaths() (paths-table.js) to create pathsResultTypeFor(), featureGetFn(), featureGetBlock(). frontend/app/ : b1ac52e 19291 Feb 17 14:32 components/draw/block-adj.js 863bec7 9852 Feb 17 15:11 components/panel/paths-table.js b5f3a56 18087 Feb 17 15:44 models/block-adj.js 6259044 11852 Feb 16 21:36 models/block.js 2e9bc95 37151 Feb 16 22:12 services/data/block.js d03920f 40355 Feb 17 15:53 utils/draw/collate-paths.js af63589 9400 Feb 17 16:13 utils/paths-api.js --- frontend/app/components/draw/block-adj.js | 2 +- frontend/app/components/panel/paths-table.js | 22 ++--- frontend/app/models/block-adj.js | 85 +++++++++++++++++++- frontend/app/models/block.js | 2 + frontend/app/services/data/block.js | 9 +++ frontend/app/utils/draw/collate-paths.js | 12 ++- frontend/app/utils/paths-api.js | 48 +++++++++-- 7 files changed, 156 insertions(+), 24 deletions(-) diff --git a/frontend/app/components/draw/block-adj.js b/frontend/app/components/draw/block-adj.js index 6d616a018..6d6ebf8cb 100644 --- a/frontend/app/components/draw/block-adj.js +++ b/frontend/app/components/draw/block-adj.js @@ -289,7 +289,7 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, { draw (pathsResultType, featurePaths) { if (featurePaths.length === 0) return; - pathsResultType.typeCheck(featurePaths[0]); + pathsResultType.typeCheck(featurePaths[0], true); let store = this.get('store'); /** blockAdjId is also contained in the result featurePaths diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index 36e0d5b4e..ae68e872e 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -3,7 +3,7 @@ const { inject: { service } } = Ember; import PathData from '../draw/path-data'; -import { pathsResultTypes, pathsApiResultType } from '../../utils/paths-api'; +import { pathsResultTypes, pathsApiResultType, pathsResultTypeFor, featureGetFn, featureGetBlock } from '../../utils/paths-api'; const dLog = console.debug; @@ -114,6 +114,7 @@ export default Ember.Component.extend({ }; let blocks = blockAdj.get('blocks'), blocksById = blocks.reduce((r, b) => { r[b.get('id')] = b; return r; }, {}); + let blockIndex = blockAdj.get('blockIndex'); if (! blockColumn) { /** push the names of the 2 adjacent blocks, as an interleaved title row in the table. */ let @@ -150,9 +151,8 @@ export default Ember.Component.extend({ */ let - pathsResultType = (pathsResultField === 'pathsAliasesResult') ? - pathsApiResultType - : Object.values(pathsResultTypes).find((prt) => prt.fieldName === pathsResultField); + pathsResultTypeName = pathsResultField.replace(/Filtered$/, ''), + pathsResultType = pathsResultTypeFor(pathsResultTypeName, resultElt); /** accumulate names for table, not used if out is true. */ @@ -171,10 +171,8 @@ export default Ember.Component.extend({ */ features = pathsResultType.blocksFeatures(resultElt, i), feature = features[0], - featureGet = feature.get ? (field) => feature.get(field) : (field) => feature[field], - block1 = featureGet('blockId'), - block2 = block1.content || block1, - block = block2.get ? block2 : blocksById[block2], + featureGet = featureGetFn(feature), + block = featureGetBlock(feature, blocksById), /** brushes are identified by the referenceBlock (axisName). */ chrName = block.get('brushName'), selectedFeaturesOfBlock = selectedFeaturesByBlock[chrName], @@ -191,6 +189,7 @@ export default Ember.Component.extend({ : isBrushed; if (! out) { let value = featureGet('value'); + let i = blockIndex.get(block); path['block' + i] = block.get('datasetNameAndScope'); path['position' + i] = '' + value; path['feature' + i] = featureName; @@ -215,16 +214,17 @@ export default Ember.Component.extend({ }, tableData/*Aliases*/ : Ember.computed( - 'pathsAliasesResult.[]', + 'pathsResultFiltered.[]', + 'pathsAliasesResultFiltered.[]', 'selectedBlock', 'selectedFeaturesByBlock.@each', 'blockColumn', 'showDomains', 'showCounts', function () { - let tableDataAliases = this.filterPaths('pathsAliasesResult'); + let tableDataAliases = this.filterPaths('pathsAliasesResultFiltered'); dLog('tableDataAliases', tableDataAliases); - let tableData = this.filterPaths('pathsResult'); + let tableData = this.filterPaths('pathsResultFiltered'); dLog('tableData', tableData); let data = (tableDataAliases.length && tableData.length) ? diff --git a/frontend/app/models/block-adj.js b/frontend/app/models/block-adj.js index d55d3c597..dad841098 100644 --- a/frontend/app/models/block-adj.js +++ b/frontend/app/models/block-adj.js @@ -8,6 +8,9 @@ import { task, timeout } from 'ember-concurrency'; const { inject: { service } } = Ember; import { stacks, Stacked } from '../utils/stacks'; +import { pathsResultTypeFor, featureGetFn, featureGetBlock } from '../utils/paths-api'; +import { inRangeEither } from '../utils/draw/zoomPanCalcs'; + const dLog = console.debug; const trace_blockAdj = 0; @@ -16,6 +19,7 @@ export default DS.Model.extend(Ember.Evented, { pathsPro : service('data/paths-progressive'), flowsService: service('data/flows-collate'), + blocksService : service('data/block'), /** id is blockAdjId[0] + '_' + blockAdjId[1], as per. serializers/block-adj.js : extractId() @@ -67,7 +71,8 @@ export default DS.Model.extend(Ember.Evented, { /*--------------------------------------------------------------------------*/ /* CFs based on axes could be moved to a component, e.g. draw/ stacks-view or block-adj */ - + /** @return an array of block-s, for blockId-s in blockAdjId + */ blocks : Ember.computed('blockAdjId', function () { let blockAdjId = this.get('blockAdjId'), @@ -99,6 +104,19 @@ export default DS.Model.extend(Ember.Evented, { axes1d = axes.mapBy('axis1d'); return axes1d; }), + + /** Return the zoomedDomain for each block of the block-adj, if they are + * zoomed, undefined otherwise. + * + * This is useful because filtering can be skipped if ! zoomed, and the API + * request also can omit the range filter if ! zoomed, so this might be used + * also by intervalParams() (paths-progressive.js). + * + * Similar to following @see axesDomains() + * @desc but that function determines the referenceBlock's domain if the block is not zoomed. + */ + zoomedDomains : Ember.computed.mapBy('axes1d', 'zoomedDomain'), + /** Return the domains (i.e. zoom scope) of the 2 axes of this block-adj. * These are equivalent : * * this.get('axes').mapBy('domain') @@ -256,6 +274,71 @@ export default DS.Model.extend(Ember.Evented, { result.alias = p; return result; }), + /** @return a Map from a block of the block-adj to its position in blockAdjId. */ + blockIndex : Ember.computed('blocks', function () { + let blocks = this.get('blocks'), + blockIndex = blocks.reduce(function (index, block, i) { + index.set(block, i); + return index; + }, new Map()); + return blockIndex; + }), + filterPathsResult(pathsResultTypeName) { + let paths, pathsFiltered; + let blocksById = this.get('blocksService.blocksById'); + if ((paths = this.get(pathsResultTypeName))) { + let pathsResultType = pathsResultTypeFor(pathsResultTypeName, paths && paths[0]); + let + blockIndex = this.get('blockIndex'), + /** blocks[] is parallel to zoomedDomains[]. */ + blocks = this.get('blocks'), + /** just a check, will use block.get('zoomedDomain') in filter. */ + zoomedDomains = this.get('zoomedDomains'); + + function filterOut(resultElt, i) { + /** based on filterPaths: filterOut() in paths-table */ + let + blockId = pathsResultType.pathBlock(resultElt, i), + /** each end may have multiple features. + * Here any features outside of the zoomedDomain of the feature's + * block's axis are filtered out (or will be in a later version), and later the cross-product is + * generated from the remainder. If all features of one end are + * filtered out then the resultElt is filtered out. + */ + features = pathsResultType.blocksFeatures(resultElt, i), + /** currently only handles paths with 1 feature at each end; i.e. it + * will filter out the whole resultElt if feature[0] is out of range. + * To filter out just some features of a resultElt, will need to copy the resultElt and .features[]. + */ + feature = features[0], + featureGet = featureGetFn(feature), + block = featureGetBlock(feature, blocksById), + index = blockIndex.get(block), + zoomedDomain = zoomedDomains[index], + value = featureGet('value'), + out = zoomedDomain && ! inRangeEither(value, zoomedDomain); + return out; + } + + pathsFiltered = paths.filter(function (resultElt, i) { + let include; + let out = filterOut(resultElt, 0) || filterOut(resultElt, 1); + return ! out; + }); + } + return pathsFiltered; + }, + pathsResultFiltered : Ember.computed('blocks', 'pathsResult.[]', function () { + let + pathsFiltered = this.filterPathsResult('pathsResult'); + return pathsFiltered; + }), + pathsAliasesResultFiltered : Ember.computed('pathsAliasesResult.[]', function () { + let + pathsFiltered = this.filterPathsResult('pathsAliasesResult'); + return pathsFiltered; + }), + /** * Depending on zoomCounter is just a stand-in for depending on the domain of each block, * which is part of changing the axes (Stacked) to Ember components, and the dependent keys can be e.g. block0.axis.domain. diff --git a/frontend/app/models/block.js b/frontend/app/models/block.js index d7ab8079c..dc3ff83cd 100644 --- a/frontend/app/models/block.js +++ b/frontend/app/models/block.js @@ -283,6 +283,8 @@ export default DS.Model.extend({ dLog('block axis', this.get('id'), this.get('view'), 'no view.axis for block or referenceBlock', referenceBlock); }), + zoomedDomain : Ember.computed.alias('axis.axis1d.zoomedDomain'), + /*--------------------------------------------------------------------------*/ /** When block is added to an axis, request features, scoped by the axis diff --git a/frontend/app/services/data/block.js b/frontend/app/services/data/block.js index db8f8145d..29e581526 100644 --- a/frontend/app/services/data/block.js +++ b/frontend/app/services/data/block.js @@ -584,6 +584,15 @@ export default Service.extend(Ember.Evented, { console.log('blockValues', records); return records; }), + /** Can be used in place of peekBlock(). + * @return array which maps from blockId to block + */ + blocksById: Ember.computed( + 'blockValues.[]', + function() { + let blocksById = this.get('blockValues').reduce((r, b) => { r[b.get('id')] = b; return r; }, {}); + return blocksById; + }), selected: Ember.computed( 'blockValues.@each.isSelected', function() { diff --git a/frontend/app/utils/draw/collate-paths.js b/frontend/app/utils/draw/collate-paths.js index cd11e6dbf..7fd48b2f2 100644 --- a/frontend/app/utils/draw/collate-paths.js +++ b/frontend/app/utils/draw/collate-paths.js @@ -749,11 +749,15 @@ function addPathsToCollation(blockA, blockB, paths) * featureIndex[], as current data structure is based on feature (feature) * names - that will change probably. */ let - /** the order of p.featureA, p.featureB matches the alias order. */ - aliasDirection = p.featureAObj.get('blockId.id') === blockA; - if (! aliasDirection && (p.featureAObj.get('blockId.id') !== blockB)) { + getBlockId = p.featureAObj.get ? + ((o) => o.get('blockId.id')) + : ((o) => o.blockId), + aBlockId = getBlockId(p.featureAObj), + /** the order of p.featureA, p.featureB matches the alias order. */ + aliasDirection = aBlockId === blockA; + if (! aliasDirection && (aBlockId !== blockB)) { dLog('aliasDirection no match', aliasDirection, - p.featureAObj.get('blockId.id'), p.featureBObj.get('blockId.id'), + aBlockId, getBlockId(p.featureBObj), blockA, blockB, p); } diff --git a/frontend/app/utils/paths-api.js b/frontend/app/utils/paths-api.js index 7cd9780bd..018da3460 100644 --- a/frontend/app/utils/paths-api.js +++ b/frontend/app/utils/paths-api.js @@ -142,8 +142,8 @@ const pathsApiFields = ['featureAObj', 'featureBObj']; /** This type is created by paths-progressive.js : requestAliases() : receivedData() */ const pathsApiResultType = { // fieldName may be pathsResult or pathsAliasesResult - typeCheck : function(resultElt) { if (! resultElt.featureAObj) { - dLog('pathsApiResultType : typeCheck', resultElt); } }, + typeCheck : function(resultElt, trace) { let ok = !! resultElt.featureAObj; if (! ok && trace) { + dLog('pathsApiResultType : typeCheck', resultElt); }; return ok; }, pathBlock : function (resultElt, blockIndex) { return resultElt[pathsApiFields[blockIndex]].blockId; }, /** direct.blocksFeatures() returns an array of features, so match that. See * similar commment in alias.blocksFeatures. */ @@ -169,8 +169,8 @@ const pathsApiResultType = { const pathsResultTypes = { direct : { fieldName : 'pathsResult', - typeCheck : function(resultElt) { if (! resultElt._id) { - dLog('direct : typeCheck', resultElt); } }, + typeCheck : function(resultElt, trace) { let ok = !! resultElt._id; if (! ok && trace) { + dLog('direct : typeCheck', resultElt); }; return ok; }, pathBlock : function (resultElt, blockIndex) { return resultElt.alignment[blockIndex].blockId; }, blocksFeatures : function (resultElt, blockIndex) { return resultElt.alignment[blockIndex].repeats.features; }, featureEltId : featureEltId, @@ -180,8 +180,8 @@ const pathsResultTypes = { alias : { fieldName : 'pathsAliasesResult', - typeCheck : function(resultElt) { if (! resultElt.aliased_features) { - dLog('alias : typeCheck', resultElt); } }, + typeCheck : function(resultElt, trace) { let ok = !! resultElt.aliased_features; if (! ok && trace) { + dLog('alias : typeCheck', resultElt); }; return ok; }, pathBlock : function (resultElt, blockIndex) { return resultElt.aliased_features[blockIndex].blockId; }, /** There is currently only 1 element in .aliased_features[blockIndex], but * pathsOfFeature() handles an array an produces a cross-product, so return @@ -204,6 +204,22 @@ flowNames = Object.keys(pathsResultTypes); // add .flowName to each of pathsResultTypes, which could later require non-const declaration. flowNames.forEach(function (flowName) { pathsResultTypes[flowName].flowName = flowName; } ); +pathsResultTypes.pathsApiResult = pathsApiResultType; + +/** Lookup the pathsResultType, given pathsResultField and optional resultElt. + * + * When matching each candidate pathsResultType (prt) : + * prt.fieldName is checked if it is defined. + * prt.typeCheck() is checked if resultElt is given. + */ +function pathsResultTypeFor(pathsResultField, resultElt) { + let pathsResultType = + Object.values(pathsResultTypes).find( + (prt) => (! prt.fieldName || (prt.fieldName === pathsResultField)) && (!resultElt || prt.typeCheck(resultElt, false))); + return pathsResultType; +} + + /** * @return array[2] of blockId, equivalent to blockAdjId */ @@ -213,5 +229,23 @@ function resultBlockIds(pathsResultType, featurePath) { return blockIds; } +/** Return an accessor function suited to the object type of feature. + */ +function featureGetFn(feature) { + let fn = + feature.get ? (field) => feature.get(field) : (field) => feature[field]; + return fn; +} + +/** Used to get the block of a feature in pathsApiResultType (aliases). + */ +function featureGetBlock(feature, blocksById) { + let + block1 = featureGetFn(feature)('blockId'), + block2 = block1.content || block1, + block = block2.get ? block2 : blocksById[block2]; + return block; +} + -export { pathsResultTypes, pathsApiResultType, flowNames, resultBlockIds, pathsOfFeature, locationPairKeyFn }; +export { pathsResultTypes, pathsApiResultType, flowNames, pathsResultTypeFor, resultBlockIds, pathsOfFeature, locationPairKeyFn, featureGetFn, featureGetBlock }; From 683ac5ba2a7d02602e60c67e7d6812f3ef47d327 Mon Sep 17 00:00:00 2001 From: Don Date: Mon, 17 Feb 2020 23:50:43 +1100 Subject: [PATCH 42/57] paths-table : limit fullDensity requests to blockAdjs listed, i.e. those adjacentTo selectedBlock. Add csv download link. Add filters on block columns. paths-table.js : add columnFields, csvExportData. requestAllPaths() : set loading true until (added) loadingPromises completes. Only request for blockAdj which are .adjacentTo(selectedBlock). app.scss : layout the download link alongside the existing button. paths-table.hbs : add CSV Download button. change block columns to filterableColumn, using data-filterer. package.json : ember-contextual-table : ^1.10.0 -> ^2. add ember-csv for csv download. frontend/app/ : 1255caa 11734 Feb 17 23:43 components/panel/paths-table.js 3b17d41 30943 Feb 17 23:26 styles/app.scss (partially staged) 2291244 2218 Feb 17 21:49 templates/components/panel/paths-table.hbs 20a9476 923604 Feb 17 19:05 frontend/package-lock.json c4d7f16 2581 Feb 17 19:05 frontend/package.json --- frontend/app/components/panel/paths-table.js | 60 +- frontend/app/styles/app.scss | 9 + .../components/panel/paths-table.hbs | 25 +- frontend/package-lock.json | 533 ++++++++++-------- frontend/package.json | 3 +- 5 files changed, 368 insertions(+), 262 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index ae68e872e..8ff194dd5 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -8,6 +8,9 @@ import { pathsResultTypes, pathsApiResultType, pathsResultTypeFor, featureGetFn, const dLog = console.debug; +const columnFields = ['block', 'feature', 'position']; + + /** * Arguments passed to template : * @param selectedFeatures catenation of features within all brushed regions @@ -246,21 +249,72 @@ export default Ember.Component.extend({ return tableData; }), + csvExportData : Ember.computed('tableData', 'blockColumn', function () { + let activeFields = columnFields.slice(this.get('blockColumn') ? 0 : 1); + let headerRow = + [0, 1].reduce(function (result, end) { + activeFields.reduce(function (result, fieldName) { + result.push('"' + fieldName + end + '"'); + return result; + }, result); + return result; + }, []); + dLog('csvExportData', headerRow); + + let data = this.get('tableData').map((d, i) => { + let da = + [0, 1].reduce(function (result, end) { + activeFields.reduce(function (result, fieldName) { + // if undefined, push '' for a blank cell. + let v = d[fieldName + end], + /** wrap text fields with double-quotes, and also position with comma - i.e. intervals 'from,to'. */ + quote = (fieldName === 'block') || (fieldName === 'feature') || (v.indexOf(',') > -1), + outVal = v ? ( quote ? '"' + v + '"' : v) : ''; + result.push(outVal); + return result; + }, result); + return result; + }, []); + if (i === 0) + dLog('csvExportData', d, da); + return da; + }); + data.unshift(headerRow); + return data; + }), + /*--------------------------------------------------------------------------*/ + /** Send API request for the block-adj-s displayed in the table, with + * fullDensity=true, i.e. not limited by GUI path densityFactor / nSamples / + * nFeatures setings. + */ requestAllPaths() { dLog('requestAllPaths', this); - + + let selectedBlock = this.get('selectedBlock'); + let loadingPromises = []; + let pathsPro = this.get('pathsPro'); pathsPro.set('fullDensity', true); let blockAdjs = this.get('flowsService.blockAdjs'); blockAdjs.forEach(function (blockAdj) { - let p = blockAdj.call_taskGetPaths(); + if (! selectedBlock || blockAdj.adjacentTo(selectedBlock)) { + let p = blockAdj.call_taskGetPaths(); + loadingPromises.push(p); + } }); - pathsPro.set('fullDensity', false); + + /* set .loading true while API requests are in progress. */ + if (loadingPromises.length) { + let all = Ember.RSVP.allSettled(loadingPromises); + this.set('loading', true); + all.then(() => this.set('loading', false)); + } + }, /*--------------------------------------------------------------------------*/ diff --git a/frontend/app/styles/app.scss b/frontend/app/styles/app.scss index 7da6032fe..f72c3d64a 100644 --- a/frontend/app/styles/app.scss +++ b/frontend/app/styles/app.scss @@ -1362,4 +1362,13 @@ div.metaeditor-panel div.jsoneditor { .paths-tables .optionControls > div { flex-grow: 1; } + +.paths-tables div.panel-body > div.btn-group > div { + display: flex; +} + +.paths-tables div.panel-body > div.btn-group > div > * { + margin: auto; +} + /* -------------------------------------------------------------------------- */ diff --git a/frontend/app/templates/components/panel/paths-table.hbs b/frontend/app/templates/components/panel/paths-table.hbs index 0b2f370a4..8b0b3f6b5 100644 --- a/frontend/app/templates/components/panel/paths-table.hbs +++ b/frontend/app/templates/components/panel/paths-table.hbs @@ -7,7 +7,8 @@ {{!-- based on elem/button-submit.hbs, could be factored --}}
- + + {{#component "ember-csv@file-anchor" data=csvExportData}} CSV Download{{/component}}

@@ -24,18 +25,20 @@
{{input type="checkbox" name="showCounts" checked=showCounts }} Show Counts
-{{#data-sorter data=tableData as |ds|}} - {{#data-table data=ds.data - classNames=tableClassNames as |t|}} - {{#if blockColumn}} - {{t.sortableColumn propertyName='block0' name='Block' sortinformationupdated=(action ds.onsortfieldupdated)}} - {{/if}} - {{t.sortableColumn propertyName='feature0' name='From Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} - {{t.sortableColumn propertyName='position0' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} - {{#if blockColumn}} - {{t.sortableColumn propertyName='block1' name='Block' sortinformationupdated=(action ds.onsortfieldupdated)}} +{{#data-filterer data=tableData as |df|}} + {{#data-sorter data=df.data as |ds|}} + {{#data-table data=ds.data + classNames=tableClassNames as |t|}} + {{#if blockColumn}} + {{t.filterableColumn propertyName='block0' name='Block' sortinformationupdated=(action ds.onsortfieldupdated) filterinformationupdated=(action df.onfilterfieldupdated) }} + {{/if}} + {{t.sortableColumn propertyName='feature0' name='From Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{t.sortableColumn propertyName='position0' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{#if blockColumn}} + {{t.filterableColumn propertyName='block1' name='Block' sortinformationupdated=(action ds.onsortfieldupdated) filterinformationupdated=(action df.onfilterfieldupdated) }} {{/if}} {{t.sortableColumn propertyName='feature1' name='To Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} {{t.sortableColumn propertyName='position1' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} {{/data-table}} {{/data-sorter}} +{{/data-filterer}} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 64993e345..f75fd8603 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2963,7 +2963,6 @@ "version": "0.1.11", "resolved": "https://registry.npmjs.org/babel-plugin-debug-macros/-/babel-plugin-debug-macros-0.1.11.tgz", "integrity": "sha512-hZw5qNNGAR02Y+yBUrtsnJHh8OXavkayPRqKGAXnIm4t5rWVpj3ArwsC7TWdpZsBguQvHAeyTxZ7s23yY60HHg==", - "dev": true, "requires": { "semver": "^5.3.0" } @@ -3855,7 +3854,6 @@ "version": "6.1.4", "resolved": "https://registry.npmjs.org/broccoli-babel-transpiler/-/broccoli-babel-transpiler-6.1.4.tgz", "integrity": "sha512-h63g7iOBWdxj0GuZw8kNsyaD1T9weKsY3I+gp3rOefozbHwUesJ43vzLy0jj3t/rbiP2czcJAlyHS48EcRil8Q==", - "dev": true, "requires": { "babel-core": "^6.14.0", "broccoli-funnel": "^1.0.0", @@ -6557,15 +6555,6 @@ "ember-cli-babel": "^6.6.0" } }, - "ember-array-contains-helper": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ember-array-contains-helper/-/ember-array-contains-helper-1.3.2.tgz", - "integrity": "sha1-2n+Q1gM/3tujuUIitomNY02DNqM=", - "requires": { - "ember-cli-babel": "^6.1.0", - "ember-runtime-enumerable-includes-polyfill": "^2.0.0" - } - }, "ember-array-helper": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ember-array-helper/-/ember-array-helper-3.0.0.tgz", @@ -8937,13 +8926,54 @@ } }, "ember-contextual-table": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/ember-contextual-table/-/ember-contextual-table-1.10.0.tgz", - "integrity": "sha1-/1SBOygWmVGL2Q4+Z2LsUtvcRmw=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ember-contextual-table/-/ember-contextual-table-2.0.2.tgz", + "integrity": "sha1-eUUq6+NAMgIrog/ZbagRiTqzD30=", "requires": { - "ember-array-contains-helper": "1.3.2", - "ember-cli-babel": "^6.3.0", - "ember-cli-htmlbars": "^2.0.1" + "ember-cli-babel": "6.6.0", + "ember-cli-htmlbars": "2.0.3" + }, + "dependencies": { + "amd-name-resolver": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/amd-name-resolver/-/amd-name-resolver-0.0.6.tgz", + "integrity": "sha1-0+S6Lfyqsdggwb6d6UfGeCjP5ZU=", + "requires": { + "ensure-posix-path": "^1.0.1" + } + }, + "babel-plugin-ember-modules-api-polyfill": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-ember-modules-api-polyfill/-/babel-plugin-ember-modules-api-polyfill-1.6.0.tgz", + "integrity": "sha512-HIOU4QBiselFqEvx6QaKrS/zxnfRQygQyA8wGdVUd42zO26G0jUqbEr1IE/NkTAbP4zsF0sY/ZLtVpjYiVB3VQ==", + "requires": { + "ember-rfc176-data": "^0.2.0" + } + }, + "ember-cli-babel": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/ember-cli-babel/-/ember-cli-babel-6.6.0.tgz", + "integrity": "sha1-qDYrxEhBv9+Js4nzGX8QTXulJto=", + "requires": { + "amd-name-resolver": "0.0.6", + "babel-plugin-debug-macros": "^0.1.11", + "babel-plugin-ember-modules-api-polyfill": "^1.4.1", + "babel-plugin-transform-es2015-modules-amd": "^6.24.0", + "babel-polyfill": "^6.16.0", + "babel-preset-env": "^1.5.1", + "broccoli-babel-transpiler": "^6.0.0", + "broccoli-debug": "^0.6.2", + "broccoli-funnel": "^1.0.0", + "broccoli-source": "^1.1.0", + "clone": "^2.0.0", + "ember-cli-version-checker": "^2.0.0" + } + }, + "ember-rfc176-data": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/ember-rfc176-data/-/ember-rfc176-data-0.2.7.tgz", + "integrity": "sha512-pJE2w+sI22UDsYmudI4nCp3WcImpUzXwe9qHfpOcEu3yM/HD1nGpDRt6kZD0KUnDmqkLeik/nYyzEwN/NU6xxA==" + } } }, "ember-cookies": { @@ -8956,6 +8986,14 @@ "ember-getowner-polyfill": "^1.1.0 || ^2.0.0" } }, + "ember-csv": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ember-csv/-/ember-csv-1.0.2.tgz", + "integrity": "sha512-BMf1pmkoclLIR2+iPrvCwCgV7uaGrpA+yJCVlDc996FFIrGjFDlwDeg/Gh5hRoIRN9XHpbCBN7wVziOIIecHIg==", + "requires": { + "ember-cli-babel": "^6.6.0" + } + }, "ember-data": { "version": "2.18.2", "resolved": "https://registry.npmjs.org/ember-data/-/ember-data-2.18.2.tgz", @@ -10524,6 +10562,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/ember-runtime-enumerable-includes-polyfill/-/ember-runtime-enumerable-includes-polyfill-2.1.0.tgz", "integrity": "sha512-au18iI8VbEDYn3jLFZzETnKN5ciPgCUxMRucEP3jkq7qZ6sE0FVKpWMPY/h9tTND3VOBJt6fgPpEBJoJVCUudg==", + "dev": true, "requires": { "ember-cli-babel": "^6.9.0", "ember-cli-version-checker": "^2.1.0" @@ -17538,37 +17577,37 @@ "dependencies": { "abbrev": { "version": "1.0.7", - "resolved": false, + "resolved": "", "integrity": "sha1-W2A1su6dT7XPhZ8Iqb6BsghJGEM=", "dev": true }, "ansi-regex": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-xQYbbg74qBd15Q9dZhUb9r83EQc=", "dev": true }, "ansicolors": { "version": "0.3.2", - "resolved": false, + "resolved": "", "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", "dev": true }, "ansistyles": { "version": "0.1.3", - "resolved": false, + "resolved": "", "integrity": "sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk=", "dev": true }, "aproba": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-xKwsxb7PuLCZ3n758CeQ59Mtme8=", "dev": true }, "archy": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, @@ -17584,13 +17623,13 @@ }, "chownr": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", "dev": true }, "cmd-shim": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-RRKjc9I5FnmuxRrR1HM1Wem4XUo=", "dev": true, "requires": { @@ -17600,7 +17639,7 @@ "dependencies": { "graceful-fs": { "version": "3.0.8", - "resolved": false, + "resolved": "", "integrity": "sha1-zoE+cl+oL35hR9UcmlymgnBVHCI=", "dev": true } @@ -17608,7 +17647,7 @@ }, "columnify": { "version": "1.5.4", - "resolved": false, + "resolved": "", "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", "dev": true, "requires": { @@ -17618,7 +17657,7 @@ "dependencies": { "wcwidth": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-AtBZ/3qPx0Hg9rXaHmmytA2uym8=", "dev": true, "requires": { @@ -17627,7 +17666,7 @@ "dependencies": { "defaults": { "version": "1.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", "dev": true, "requires": { @@ -17636,7 +17675,7 @@ "dependencies": { "clone": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=", "dev": true } @@ -17648,7 +17687,7 @@ }, "config-chain": { "version": "1.1.9", - "resolved": false, + "resolved": "", "integrity": "sha1-Oax9TcqE+q2SYSTFTP8lpTqov24=", "dev": true, "requires": { @@ -17658,7 +17697,7 @@ "dependencies": { "proto-list": { "version": "1.2.4", - "resolved": false, + "resolved": "", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "dev": true } @@ -17672,19 +17711,19 @@ }, "debuglog": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=", "dev": true }, "editor": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-YMf4e9YrzGqJT6jM1q+3gjok90I=", "dev": true }, "fs-vacuum": { "version": "1.2.7", - "resolved": false, + "resolved": "", "integrity": "sha1-deUB+dKIm6L+n+Evk2ul2tUMo1o=", "dev": true, "requires": { @@ -17695,7 +17734,7 @@ }, "fstream": { "version": "1.0.8", - "resolved": false, + "resolved": "", "integrity": "sha1-fo16c6uzZH7zbkuKFcqAHboD0Dg=", "dev": true, "requires": { @@ -17720,7 +17759,7 @@ }, "glob": { "version": "6.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-XwLNiVh85YsVSuCFXeAqLmOYb8o=", "dev": true, "requires": { @@ -17733,7 +17772,7 @@ "dependencies": { "minimatch": { "version": "3.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-UjYVelHk8ATBd/s8Un/33Xjw74M=", "dev": true, "requires": { @@ -17742,7 +17781,7 @@ "dependencies": { "brace-expansion": { "version": "1.1.2", - "resolved": false, + "resolved": "", "integrity": "sha1-8hRF0EiLZY4nce/YcO/1HfKfBO8=", "dev": true, "requires": { @@ -17752,13 +17791,13 @@ "dependencies": { "balanced-match": { "version": "0.3.0", - "resolved": false, + "resolved": "", "integrity": "sha1-qRzdHr7xqGZZ5w/03vAWJfwtZ1Y=", "dev": true }, "concat-map": { "version": "0.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true } @@ -17768,7 +17807,7 @@ }, "path-is-absolute": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-Jj2tpmqz8vsQv3+dJN2PPlcO+RI=", "dev": true } @@ -17776,13 +17815,13 @@ }, "graceful-fs": { "version": "4.1.2", - "resolved": false, + "resolved": "", "integrity": "sha1-/iI5t1dJcuZ+QfgIgj+b+kqZHjc=", "dev": true }, "has-unicode": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-o82Wwwe6QdVZxaLuQIwSoRxMLsM=", "dev": true }, @@ -17794,19 +17833,19 @@ }, "iferr": { "version": "0.1.5", - "resolved": false, + "resolved": "", "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", "dev": true }, "imurmurhash": { "version": "0.1.4", - "resolved": false, + "resolved": "", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, "inflight": { "version": "1.0.4", - "resolved": false, + "resolved": "", "integrity": "sha1-bLtFIevVHODsCpNr/XZX736bFyo=", "dev": true, "requires": { @@ -17816,19 +17855,19 @@ }, "inherits": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", "dev": true }, "ini": { "version": "1.3.4", - "resolved": false, + "resolved": "", "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", "dev": true }, "init-package-json": { "version": "1.9.1", - "resolved": false, + "resolved": "", "integrity": "sha1-oo4FtbrrM2PNRz32jTDTqAUjoxw=", "dev": true, "requires": { @@ -17857,7 +17896,7 @@ }, "promzard": { "version": "0.3.0", - "resolved": false, + "resolved": "", "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", "dev": true, "requires": { @@ -17874,19 +17913,19 @@ }, "lockfile": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-nTU+z+P1TRULtX+J1RdGk1o5xPU=", "dev": true }, "lodash._baseindexof": { "version": "3.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=", "dev": true }, "lodash._baseuniq": { "version": "3.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-ISP6DbLWnCjVvrHB821hUip0AjQ=", "dev": true, "requires": { @@ -17897,19 +17936,19 @@ }, "lodash._bindcallback": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", "dev": true }, "lodash._cacheindexof": { "version": "3.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=", "dev": true }, "lodash._createcache": { "version": "3.1.2", - "resolved": false, + "resolved": "", "integrity": "sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=", "dev": true, "requires": { @@ -17918,13 +17957,13 @@ }, "lodash._getnative": { "version": "3.9.1", - "resolved": false, + "resolved": "", "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", "dev": true }, "lodash.clonedeep": { "version": "3.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-oKHkDYKl6on/WxR7hETtY9koJ9s=", "dev": true, "requires": { @@ -17934,7 +17973,7 @@ "dependencies": { "lodash._baseclone": { "version": "3.3.0", - "resolved": false, + "resolved": "", "integrity": "sha1-MDUZv2OT/n5C802LYw73eU41Qrc=", "dev": true, "requires": { @@ -17948,19 +17987,19 @@ "dependencies": { "lodash._arraycopy": { "version": "3.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-due3wfH7klRzdIeKVi7Qaj5Q9uE=", "dev": true }, "lodash._arrayeach": { "version": "3.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-urFWsqkNPxu9XGU0AzSeXlkz754=", "dev": true }, "lodash._baseassign": { "version": "3.2.0", - "resolved": false, + "resolved": "", "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", "dev": true, "requires": { @@ -17970,7 +18009,7 @@ "dependencies": { "lodash._basecopy": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", "dev": true } @@ -17978,7 +18017,7 @@ }, "lodash._basefor": { "version": "3.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-Okzs5bcDHq54pEHFQWuQh47u5aE=", "dev": true } @@ -17988,19 +18027,19 @@ }, "lodash.isarguments": { "version": "3.0.4", - "resolved": false, + "resolved": "", "integrity": "sha1-67uITEjSc2akTqb+5X7XtaMqgeA=", "dev": true }, "lodash.isarray": { "version": "3.0.4", - "resolved": false, + "resolved": "", "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", "dev": true }, "lodash.keys": { "version": "3.1.2", - "resolved": false, + "resolved": "", "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", "dev": true, "requires": { @@ -18011,13 +18050,13 @@ }, "lodash.restparam": { "version": "3.6.1", - "resolved": false, + "resolved": "", "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", "dev": true }, "lodash.union": { "version": "3.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-pKMGb8Fdan+BUczpvf5j3Of1vP8=", "dev": true, "requires": { @@ -18028,7 +18067,7 @@ "dependencies": { "lodash._baseflatten": { "version": "3.1.4", - "resolved": false, + "resolved": "", "integrity": "sha1-B3D/gBMa9uNPO1EXlqe6UhTmX/c=", "dev": true, "requires": { @@ -18040,7 +18079,7 @@ }, "lodash.uniq": { "version": "3.2.2", - "resolved": false, + "resolved": "", "integrity": "sha1-FGw28l510ZUBukAuiLoUk39jzYs=", "dev": true, "requires": { @@ -18053,7 +18092,7 @@ "dependencies": { "lodash._basecallback": { "version": "3.3.1", - "resolved": false, + "resolved": "", "integrity": "sha1-t7K7Q9whYEJKIczybFfkQ3cqjic=", "dev": true, "requires": { @@ -18065,7 +18104,7 @@ "dependencies": { "lodash._baseisequal": { "version": "3.0.7", - "resolved": false, + "resolved": "", "integrity": "sha1-2AJfdjOdKTQnZ9zIh85cuVpbUfE=", "dev": true, "requires": { @@ -18076,7 +18115,7 @@ "dependencies": { "lodash.istypedarray": { "version": "3.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-k5exE8FfQk8yCvBsqlnMSV4gk84=", "dev": true } @@ -18084,7 +18123,7 @@ }, "lodash.pairs": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-u+CNV4bu6qCaFckevw3LfSvjJqk=", "dev": true, "requires": { @@ -18095,7 +18134,7 @@ }, "lodash._isiterateecall": { "version": "3.0.9", - "resolved": false, + "resolved": "", "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", "dev": true } @@ -18103,7 +18142,7 @@ }, "lodash.without": { "version": "3.2.1", - "resolved": false, + "resolved": "", "integrity": "sha1-1pYUs1EuUilLarq3gufKllOM6BY=", "dev": true, "requires": { @@ -18113,7 +18152,7 @@ "dependencies": { "lodash._basedifference": { "version": "3.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-8sIEKWwqeOArOJCBtu3KyTPPYpw=", "dev": true, "requires": { @@ -18126,7 +18165,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": false, + "resolved": "", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -18135,7 +18174,7 @@ "dependencies": { "minimist": { "version": "0.0.8", - "resolved": false, + "resolved": "", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true } @@ -18143,7 +18182,7 @@ }, "node-gyp": { "version": "3.2.1", - "resolved": false, + "resolved": "", "integrity": "sha1-9d1WmXClCEZMw8Fdfp6NLehjjdU=", "dev": true, "requires": { @@ -18165,7 +18204,7 @@ "dependencies": { "glob": { "version": "4.5.3", - "resolved": false, + "resolved": "", "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=", "dev": true, "requires": { @@ -18177,7 +18216,7 @@ "dependencies": { "minimatch": { "version": "2.0.10", - "resolved": false, + "resolved": "", "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", "dev": true, "requires": { @@ -18186,7 +18225,7 @@ "dependencies": { "brace-expansion": { "version": "1.1.2", - "resolved": false, + "resolved": "", "integrity": "sha1-8hRF0EiLZY4nce/YcO/1HfKfBO8=", "dev": true, "requires": { @@ -18196,13 +18235,13 @@ "dependencies": { "balanced-match": { "version": "0.3.0", - "resolved": false, + "resolved": "", "integrity": "sha1-qRzdHr7xqGZZ5w/03vAWJfwtZ1Y=", "dev": true }, "concat-map": { "version": "0.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true } @@ -18220,7 +18259,7 @@ }, "minimatch": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-4N0hILSeG3JM6NcUxSCCKpQ4V20=", "dev": true, "requires": { @@ -18230,13 +18269,13 @@ "dependencies": { "lru-cache": { "version": "2.7.3", - "resolved": false, + "resolved": "", "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", "dev": true }, "sigmund": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", "dev": true } @@ -18244,7 +18283,7 @@ }, "npmlog": { "version": "1.2.1", - "resolved": false, + "resolved": "", "integrity": "sha1-KOe+YZYJtT960d0wChDWTXFiaLY=", "dev": true, "requires": { @@ -18255,13 +18294,13 @@ "dependencies": { "ansi": { "version": "0.3.0", - "resolved": false, + "resolved": "", "integrity": "sha1-dLLx8YfIVTx/lQFby3YAn7Q9OOA=", "dev": true }, "are-we-there-yet": { "version": "1.0.4", - "resolved": false, + "resolved": "", "integrity": "sha1-Un/jife8upCAYQa5kkTqoH6Ib4U=", "dev": true, "requires": { @@ -18271,13 +18310,13 @@ "dependencies": { "delegates": { "version": "0.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-tLV74RoWU1F6BLJ/CUm9wyff45A=", "dev": true }, "readable-stream": { "version": "1.1.13", - "resolved": false, + "resolved": "", "integrity": "sha1-9u73ZPUUyJ4rniMUanW6EGdW0j4=", "dev": true, "requires": { @@ -18289,19 +18328,19 @@ "dependencies": { "core-util-is": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, "isarray": { "version": "0.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", "dev": true }, "string_decoder": { "version": "0.10.31", - "resolved": false, + "resolved": "", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true } @@ -18311,7 +18350,7 @@ }, "gauge": { "version": "1.2.2", - "resolved": false, + "resolved": "", "integrity": "sha1-BbZzChmo/K08NAoULwlFIio/gVs=", "dev": true, "requires": { @@ -18324,7 +18363,7 @@ "dependencies": { "lodash.pad": { "version": "3.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-LgeOvDOzMdK6NL+HMq8Sn9XARiQ=", "dev": true, "requires": { @@ -18334,13 +18373,13 @@ "dependencies": { "lodash._basetostring": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", "dev": true }, "lodash._createpadding": { "version": "3.6.1", - "resolved": false, + "resolved": "", "integrity": "sha1-SQe0OFla3FTuiTVSemxCTALIGoc=", "dev": true, "requires": { @@ -18349,7 +18388,7 @@ "dependencies": { "lodash.repeat": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-9LmNx+9nJWzmHnh04YZe2yCODt8=", "dev": true, "requires": { @@ -18362,7 +18401,7 @@ }, "lodash.padleft": { "version": "3.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-FQFR8eAkXtuhXVCvLXHx1c/0ZTA=", "dev": true, "requires": { @@ -18372,13 +18411,13 @@ "dependencies": { "lodash._basetostring": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", "dev": true }, "lodash._createpadding": { "version": "3.6.1", - "resolved": false, + "resolved": "", "integrity": "sha1-SQe0OFla3FTuiTVSemxCTALIGoc=", "dev": true, "requires": { @@ -18387,7 +18426,7 @@ "dependencies": { "lodash.repeat": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-9LmNx+9nJWzmHnh04YZe2yCODt8=", "dev": true, "requires": { @@ -18400,7 +18439,7 @@ }, "lodash.padright": { "version": "3.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-efd3C6qjlzjAQK61Rl6NiPKqzsA=", "dev": true, "requires": { @@ -18410,13 +18449,13 @@ "dependencies": { "lodash._basetostring": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", "dev": true }, "lodash._createpadding": { "version": "3.6.1", - "resolved": false, + "resolved": "", "integrity": "sha1-SQe0OFla3FTuiTVSemxCTALIGoc=", "dev": true, "requires": { @@ -18425,7 +18464,7 @@ "dependencies": { "lodash.repeat": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-9LmNx+9nJWzmHnh04YZe2yCODt8=", "dev": true, "requires": { @@ -18442,7 +18481,7 @@ }, "path-array": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-bBQTDDMITwFQVTxlezg5erZ6qk4=", "dev": true, "requires": { @@ -18451,7 +18490,7 @@ "dependencies": { "array-index": { "version": "0.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-TV6vBsw9klhHzXPRU1whe6MG0+E=", "dev": true, "requires": { @@ -18460,7 +18499,7 @@ "dependencies": { "debug": { "version": "2.2.0", - "resolved": false, + "resolved": "", "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", "dev": true, "requires": { @@ -18469,7 +18508,7 @@ "dependencies": { "ms": { "version": "0.7.1", - "resolved": false, + "resolved": "", "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", "dev": true } @@ -18483,7 +18522,7 @@ }, "nopt": { "version": "3.0.6", - "resolved": false, + "resolved": "", "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "dev": true, "requires": { @@ -18492,13 +18531,13 @@ }, "normalize-git-url": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-1A1BnQWhWHAnHlBTTbt7jM2bClw=", "dev": true }, "normalize-package-data": { "version": "2.3.5", - "resolved": false, + "resolved": "", "integrity": "sha1-jZJPFClg4Xd+f/4XBUNjHMfLAt8=", "dev": true, "requires": { @@ -18510,7 +18549,7 @@ "dependencies": { "is-builtin-module": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { @@ -18519,7 +18558,7 @@ "dependencies": { "builtin-modules": { "version": "1.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-EFOVX9mUpXRuUl5Kxxe4HK8HSRw=", "dev": true } @@ -18529,13 +18568,13 @@ }, "npm-cache-filename": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-3tMGxbC/yHCp6fr4I7xfKD4FrhE=", "dev": true }, "npm-install-checks": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-qTVAtT8E+p2RbScz1lQfbbfYjkY=", "dev": true, "requires": { @@ -18551,7 +18590,7 @@ }, "npmlog": { "version": "1.2.1", - "resolved": false, + "resolved": "", "integrity": "sha1-KOe+YZYJtT960d0wChDWTXFiaLY=", "dev": true, "requires": { @@ -18562,13 +18601,13 @@ "dependencies": { "ansi": { "version": "0.3.0", - "resolved": false, + "resolved": "", "integrity": "sha1-dLLx8YfIVTx/lQFby3YAn7Q9OOA=", "dev": true }, "are-we-there-yet": { "version": "1.0.4", - "resolved": false, + "resolved": "", "integrity": "sha1-Un/jife8upCAYQa5kkTqoH6Ib4U=", "dev": true, "requires": { @@ -18578,13 +18617,13 @@ "dependencies": { "delegates": { "version": "0.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-tLV74RoWU1F6BLJ/CUm9wyff45A=", "dev": true }, "readable-stream": { "version": "1.1.13", - "resolved": false, + "resolved": "", "integrity": "sha1-9u73ZPUUyJ4rniMUanW6EGdW0j4=", "dev": true, "requires": { @@ -18596,19 +18635,19 @@ "dependencies": { "core-util-is": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-awcIWu+aPMrG7lO/nT3wwVIaVTg=", "dev": true }, "isarray": { "version": "0.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", "dev": true }, "string_decoder": { "version": "0.10.31", - "resolved": false, + "resolved": "", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true } @@ -18618,7 +18657,7 @@ }, "gauge": { "version": "1.2.2", - "resolved": false, + "resolved": "", "integrity": "sha1-BbZzChmo/K08NAoULwlFIio/gVs=", "dev": true, "requires": { @@ -18631,7 +18670,7 @@ "dependencies": { "lodash.pad": { "version": "3.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-LgeOvDOzMdK6NL+HMq8Sn9XARiQ=", "dev": true, "requires": { @@ -18641,13 +18680,13 @@ "dependencies": { "lodash._basetostring": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", "dev": true }, "lodash._createpadding": { "version": "3.6.1", - "resolved": false, + "resolved": "", "integrity": "sha1-SQe0OFla3FTuiTVSemxCTALIGoc=", "dev": true, "requires": { @@ -18656,7 +18695,7 @@ "dependencies": { "lodash.repeat": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-9LmNx+9nJWzmHnh04YZe2yCODt8=", "dev": true, "requires": { @@ -18669,7 +18708,7 @@ }, "lodash.padleft": { "version": "3.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-FQFR8eAkXtuhXVCvLXHx1c/0ZTA=", "dev": true, "requires": { @@ -18679,13 +18718,13 @@ "dependencies": { "lodash._basetostring": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", "dev": true }, "lodash._createpadding": { "version": "3.6.1", - "resolved": false, + "resolved": "", "integrity": "sha1-SQe0OFla3FTuiTVSemxCTALIGoc=", "dev": true, "requires": { @@ -18694,7 +18733,7 @@ "dependencies": { "lodash.repeat": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-9LmNx+9nJWzmHnh04YZe2yCODt8=", "dev": true, "requires": { @@ -18707,7 +18746,7 @@ }, "lodash.padright": { "version": "3.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-efd3C6qjlzjAQK61Rl6NiPKqzsA=", "dev": true, "requires": { @@ -18717,13 +18756,13 @@ "dependencies": { "lodash._basetostring": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", "dev": true }, "lodash._createpadding": { "version": "3.6.1", - "resolved": false, + "resolved": "", "integrity": "sha1-SQe0OFla3FTuiTVSemxCTALIGoc=", "dev": true, "requires": { @@ -18732,7 +18771,7 @@ "dependencies": { "lodash.repeat": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-9LmNx+9nJWzmHnh04YZe2yCODt8=", "dev": true, "requires": { @@ -18761,7 +18800,7 @@ }, "npm-user-validate": { "version": "0.1.2", - "resolved": false, + "resolved": "", "integrity": "sha1-1YXaC0fJ9BqebKaEtv2EukHr6H0=", "dev": true }, @@ -18778,7 +18817,7 @@ }, "once": { "version": "1.3.3", - "resolved": false, + "resolved": "", "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", "dev": true, "requires": { @@ -18787,13 +18826,13 @@ }, "opener": { "version": "1.4.1", - "resolved": false, + "resolved": "", "integrity": "sha1-iXWQrNGu0zEbcDtYvMtNQ/VvKJU=", "dev": true }, "osenv": { "version": "0.1.3", - "resolved": false, + "resolved": "", "integrity": "sha1-g88FxtZFj8TVrGNi6jJdkvJ1Qhc=", "dev": true, "requires": { @@ -18803,13 +18842,13 @@ "dependencies": { "os-homedir": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-DWK99EuRb9O73PLKsZGUj7CU8Ac=", "dev": true }, "os-tmpdir": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-6bQjoe2vR5iCVi6S7XHXdDoHG24=", "dev": true } @@ -18817,13 +18856,13 @@ }, "path-is-inside": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-mNjx0DC/BL167uShulSF1AMY/Yk=", "dev": true }, "read": { "version": "1.0.7", - "resolved": false, + "resolved": "", "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", "dev": true, "requires": { @@ -18832,7 +18871,7 @@ "dependencies": { "mute-stream": { "version": "0.0.5", - "resolved": false, + "resolved": "", "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", "dev": true } @@ -18840,7 +18879,7 @@ }, "read-cmd-shim": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-LV0Vd4ajfAVdIgd8MsU/gynpHHs=", "dev": true, "requires": { @@ -18849,7 +18888,7 @@ }, "read-installed": { "version": "4.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc=", "dev": true, "requires": { @@ -18864,7 +18903,7 @@ "dependencies": { "util-extend": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-u3A7eUgCk93Nz7PGqf6iD0g0Fbw=", "dev": true } @@ -18872,7 +18911,7 @@ }, "read-package-tree": { "version": "5.1.2", - "resolved": false, + "resolved": "", "integrity": "sha1-46SIeS9Az0cIGfAaYQ5xnWTwkJQ=", "dev": true, "requires": { @@ -18885,7 +18924,7 @@ }, "readable-stream": { "version": "2.0.5", - "resolved": false, + "resolved": "", "integrity": "sha1-okJvjc1FUcd6M/lu3yiGojyClmk=", "dev": true, "requires": { @@ -18899,13 +18938,13 @@ "dependencies": { "process-nextick-args": { "version": "1.0.6", - "resolved": false, + "resolved": "", "integrity": "sha1-D5awAc6pCxJZLOVm7bl+wR5pvQU=", "dev": true }, "util-deprecate": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true } @@ -18913,7 +18952,7 @@ }, "readdir-scoped-modules": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-n6+jfShr5dksuuve4DDcm19AZ0c=", "dev": true, "requires": { @@ -18925,7 +18964,7 @@ }, "request": { "version": "2.67.0", - "resolved": false, + "resolved": "", "integrity": "sha1-ivdHgOK/EeoK6aqWXBHxGv0nJ0I=", "dev": true, "requires": { @@ -18953,13 +18992,13 @@ "dependencies": { "aws-sign2": { "version": "0.6.0", - "resolved": false, + "resolved": "", "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", "dev": true }, "bl": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-ramoqJptesYIYvfex9sgeHPgw/U=", "dev": true, "requires": { @@ -18968,13 +19007,13 @@ }, "caseless": { "version": "0.11.0", - "resolved": false, + "resolved": "", "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", "dev": true }, "combined-stream": { "version": "1.0.5", - "resolved": false, + "resolved": "", "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", "dev": true, "requires": { @@ -18983,7 +19022,7 @@ "dependencies": { "delayed-stream": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true } @@ -18991,19 +19030,19 @@ }, "extend": { "version": "3.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-WkdDU7nzNT3dgXbf03uRyDpG8dQ=", "dev": true }, "forever-agent": { "version": "0.6.1", - "resolved": false, + "resolved": "", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", "dev": true }, "form-data": { "version": "1.0.0-rc3", - "resolved": false, + "resolved": "", "integrity": "sha1-01vGLn+8KTeuePlIqqDTjZBgdXc=", "dev": true, "requires": { @@ -19014,7 +19053,7 @@ "dependencies": { "async": { "version": "1.5.0", - "resolved": false, + "resolved": "", "integrity": "sha1-J5ZkJyNXOFlWVjP8YnRES+4vjOM=", "dev": true } @@ -19022,7 +19061,7 @@ }, "har-validator": { "version": "2.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-Wp4SVkpXHPC4Hvk8IVe9FhcWiIM=", "dev": true, "requires": { @@ -19034,7 +19073,7 @@ "dependencies": { "chalk": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-UJr7ZwZudJn36zU1x3RFdyri0Bk=", "dev": true, "requires": { @@ -19047,19 +19086,19 @@ "dependencies": { "ansi-styles": { "version": "2.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-mQ90cUaSe1Wakyv5KVkWPWDA0OI=", "dev": true }, "escape-string-regexp": { "version": "1.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-ni2LJbwlVcMzZyN1DgPwmcJzW7U=", "dev": true }, "has-ansi": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "dev": true, "requires": { @@ -19068,7 +19107,7 @@ }, "supports-color": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", "dev": true } @@ -19076,7 +19115,7 @@ }, "commander": { "version": "2.9.0", - "resolved": false, + "resolved": "", "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", "dev": true, "requires": { @@ -19085,7 +19124,7 @@ "dependencies": { "graceful-readlink": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", "dev": true } @@ -19093,7 +19132,7 @@ }, "is-my-json-valid": { "version": "2.12.3", - "resolved": false, + "resolved": "", "integrity": "sha1-WjnR12stu4MUC70Vex1e5L3IWtY=", "dev": true, "requires": { @@ -19105,13 +19144,13 @@ "dependencies": { "generate-function": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", "dev": true }, "generate-object-property": { "version": "1.2.0", - "resolved": false, + "resolved": "", "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", "dev": true, "requires": { @@ -19120,7 +19159,7 @@ "dependencies": { "is-property": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", "dev": true } @@ -19128,13 +19167,13 @@ }, "jsonpointer": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-OvHdIP6FRjkQ1GmjheMwF9KgMNk=", "dev": true }, "xtend": { "version": "4.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", "dev": true } @@ -19142,7 +19181,7 @@ }, "pinkie-promise": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-TINTjeH25mDCngoTRGhE96foglk=", "dev": true, "requires": { @@ -19151,7 +19190,7 @@ "dependencies": { "pinkie": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-QjbIb8KfJhwgRbvoH3jLsqXoMGw=", "dev": true } @@ -19161,7 +19200,7 @@ }, "hawk": { "version": "3.1.2", - "resolved": false, + "resolved": "", "integrity": "sha1-kMkBGIhuIZddGtSumz4oTtGaLeg=", "dev": true, "requires": { @@ -19173,7 +19212,7 @@ "dependencies": { "boom": { "version": "2.10.1", - "resolved": false, + "resolved": "", "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", "dev": true, "requires": { @@ -19182,7 +19221,7 @@ }, "cryptiles": { "version": "2.0.5", - "resolved": false, + "resolved": "", "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", "dev": true, "requires": { @@ -19191,13 +19230,13 @@ }, "hoek": { "version": "2.16.3", - "resolved": false, + "resolved": "", "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", "dev": true }, "sntp": { "version": "1.0.9", - "resolved": false, + "resolved": "", "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", "dev": true, "requires": { @@ -19208,7 +19247,7 @@ }, "http-signature": { "version": "1.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-XS1+m270mYCtWxKNjk7wmjHJDZU=", "dev": true, "requires": { @@ -19219,13 +19258,13 @@ "dependencies": { "assert-plus": { "version": "0.1.5", - "resolved": false, + "resolved": "", "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA=", "dev": true }, "jsprim": { "version": "1.2.2", - "resolved": false, + "resolved": "", "integrity": "sha1-8gyQaskqvVjjt5rIvHCkiDJRLaE=", "dev": true, "requires": { @@ -19236,19 +19275,19 @@ "dependencies": { "extsprintf": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", "dev": true }, "json-schema": { "version": "0.2.2", - "resolved": false, + "resolved": "", "integrity": "sha1-UDVPGfYDkXxpX3C4Wvp3w7DyNQY=", "dev": true }, "verror": { "version": "1.3.6", - "resolved": false, + "resolved": "", "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", "dev": true, "requires": { @@ -19259,7 +19298,7 @@ }, "sshpk": { "version": "1.7.1", - "resolved": false, + "resolved": "", "integrity": "sha1-Vl44bEKnfmBi+9FMBHL/Ic1TOYw=", "dev": true, "requires": { @@ -19274,19 +19313,19 @@ "dependencies": { "asn1": { "version": "0.2.3", - "resolved": false, + "resolved": "", "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", "dev": true }, "assert-plus": { "version": "0.2.0", - "resolved": false, + "resolved": "", "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", "dev": true }, "dashdash": { "version": "1.10.1", - "resolved": false, + "resolved": "", "integrity": "sha1-Cr8a+JqPUSmoHxjCs1sh3yJiL2A=", "dev": true, "requires": { @@ -19303,7 +19342,7 @@ }, "ecc-jsbn": { "version": "0.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", "dev": true, "optional": true, @@ -19313,7 +19352,7 @@ }, "jodid25519": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", "dev": true, "optional": true, @@ -19323,14 +19362,14 @@ }, "jsbn": { "version": "0.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-ZQmH2g3XT06/WhE3eiqi0nPpff0=", "dev": true, "optional": true }, "tweetnacl": { "version": "0.13.2", - "resolved": false, + "resolved": "", "integrity": "sha1-RTFhdwRp1FzSZsNkBOK8maj6mUQ=", "dev": true, "optional": true @@ -19341,25 +19380,25 @@ }, "is-typedarray": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, "isstream": { "version": "0.1.2", - "resolved": false, + "resolved": "", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, "json-stringify-safe": { "version": "5.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, "mime-types": { "version": "2.1.8", - "resolved": false, + "resolved": "", "integrity": "sha1-+vV4I94EvHy/9O6CxrY5RugSrnI=", "dev": true, "requires": { @@ -19368,7 +19407,7 @@ "dependencies": { "mime-db": { "version": "1.20.0", - "resolved": false, + "resolved": "", "integrity": "sha1-SW+Q/QH+DgMciCPsOqlFD/2hjtg=", "dev": true } @@ -19376,37 +19415,37 @@ }, "node-uuid": { "version": "1.4.7", - "resolved": false, + "resolved": "", "integrity": "sha1-baWhdmjEs91ZYjvaEc9/pMH2Cm8=", "dev": true }, "oauth-sign": { "version": "0.8.0", - "resolved": false, + "resolved": "", "integrity": "sha1-k4/ch1dlulJxN9iuydF44k3rxVM=", "dev": true }, "qs": { "version": "5.2.0", - "resolved": false, + "resolved": "", "integrity": "sha1-qfMRQq9GjLcrJbMBNrokVoNJFr4=", "dev": true }, "stringstream": { "version": "0.0.5", - "resolved": false, + "resolved": "", "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", "dev": true }, "tough-cookie": { "version": "2.2.1", - "resolved": false, + "resolved": "", "integrity": "sha1-OwUWt5nnDoFkQ2oURuflh3/aEY4=", "dev": true }, "tunnel-agent": { "version": "0.4.2", - "resolved": false, + "resolved": "", "integrity": "sha1-EQTj82rIcSXChycAZ9WC0YEzv+4=", "dev": true } @@ -19414,13 +19453,13 @@ }, "retry": { "version": "0.8.0", - "resolved": false, + "resolved": "", "integrity": "sha1-I2dijcDtskex6rZJ3FOshiisLV8=", "dev": true }, "rimraf": { "version": "2.5.0", - "resolved": false, + "resolved": "", "integrity": "sha1-MMCWzfdy4mvz4dLP+EwhllQam7Y=", "dev": true, "requires": { @@ -19429,7 +19468,7 @@ "dependencies": { "glob": { "version": "6.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-XwLNiVh85YsVSuCFXeAqLmOYb8o=", "dev": true, "requires": { @@ -19442,7 +19481,7 @@ "dependencies": { "minimatch": { "version": "3.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-UjYVelHk8ATBd/s8Un/33Xjw74M=", "dev": true, "requires": { @@ -19451,7 +19490,7 @@ "dependencies": { "brace-expansion": { "version": "1.1.2", - "resolved": false, + "resolved": "", "integrity": "sha1-8hRF0EiLZY4nce/YcO/1HfKfBO8=", "dev": true, "requires": { @@ -19461,13 +19500,13 @@ "dependencies": { "balanced-match": { "version": "0.3.0", - "resolved": false, + "resolved": "", "integrity": "sha1-qRzdHr7xqGZZ5w/03vAWJfwtZ1Y=", "dev": true }, "concat-map": { "version": "0.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true } @@ -19477,7 +19516,7 @@ }, "path-is-absolute": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-Jj2tpmqz8vsQv3+dJN2PPlcO+RI=", "dev": true } @@ -19487,13 +19526,13 @@ }, "semver": { "version": "5.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-hfLPhVBGXE3wAM99hvawVBBqueU=", "dev": true }, "sha": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-YDCCL70smCOUn49y7WQR7lzyWq4=", "dev": true, "requires": { @@ -19503,13 +19542,13 @@ }, "slide": { "version": "1.1.6", - "resolved": false, + "resolved": "", "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", "dev": true }, "sorted-object": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-XR9PnB+yzUiWWWcwTiEutEz7bQU=", "dev": true }, @@ -19521,7 +19560,7 @@ }, "strip-ansi": { "version": "3.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-dRC2ZVZ8qRTMtdfgcnY6yWi+NyQ=", "dev": true, "requires": { @@ -19530,7 +19569,7 @@ }, "tar": { "version": "2.2.1", - "resolved": false, + "resolved": "", "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", "dev": true, "requires": { @@ -19541,7 +19580,7 @@ "dependencies": { "block-stream": { "version": "0.0.8", - "resolved": false, + "resolved": "", "integrity": "sha1-Boj0baK7+c/wxPaCJaDLlcvopGs=", "dev": true, "requires": { @@ -19552,25 +19591,25 @@ }, "text-table": { "version": "0.2.0", - "resolved": false, + "resolved": "", "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, "uid-number": { "version": "0.0.6", - "resolved": false, + "resolved": "", "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", "dev": true }, "umask": { "version": "1.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=", "dev": true }, "unique-filename": { "version": "1.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-0F8v5AMlYIcfMOk8vnNe6iAVFPM=", "dev": true, "requires": { @@ -19579,13 +19618,13 @@ }, "unpipe": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", "dev": true }, "validate-npm-package-license": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", "dev": true, "requires": { @@ -19595,7 +19634,7 @@ "dependencies": { "spdx-correct": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-rAdfXy9qBsC/3RyEfrPd492CIeo=", "dev": true, "requires": { @@ -19604,7 +19643,7 @@ }, "spdx-expression-parse": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-T7t+c4yemPoLCRTf2WGsZin7ze8=", "dev": true, "requires": { @@ -19614,7 +19653,7 @@ "dependencies": { "spdx-exceptions": { "version": "1.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-Oexe0s693wjRgFVdfpnDr/m0dko=", "dev": true } @@ -19622,7 +19661,7 @@ }, "spdx-license-ids": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-BnTpyaIw+YABa1sHOhCqFlcBZ3w=", "dev": true } @@ -19630,7 +19669,7 @@ }, "validate-npm-package-name": { "version": "2.2.2", - "resolved": false, + "resolved": "", "integrity": "sha1-9laVsi9zJEQgGaPH+jmm5/0pkIU=", "dev": true, "requires": { @@ -19639,7 +19678,7 @@ "dependencies": { "builtins": { "version": "0.0.7", - "resolved": false, + "resolved": "", "integrity": "sha1-NVIZzWzxjb58Acx/0tznZc/cVJo=", "dev": true } @@ -19647,7 +19686,7 @@ }, "which": { "version": "1.2.1", - "resolved": false, + "resolved": "", "integrity": "sha1-oBDEOq3hp5ij5sGx5FPUXLSXorw=", "dev": true, "requires": { @@ -19656,7 +19695,7 @@ "dependencies": { "is-absolute": { "version": "0.1.7", - "resolved": false, + "resolved": "", "integrity": "sha1-hHSREZ/MtftDYhfMc39/qtUPYD8=", "dev": true, "requires": { @@ -19665,7 +19704,7 @@ "dependencies": { "is-relative": { "version": "0.1.3", - "resolved": false, + "resolved": "", "integrity": "sha1-kF/uiuhvRbPsYUvDwVyGnfCHboI=", "dev": true } @@ -19675,7 +19714,7 @@ }, "wrappy": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-HmWWmWXMvC20VIxrhKbyxa7dRzk=", "dev": true }, diff --git a/frontend/package.json b/frontend/package.json index dcdd068cd..82105929a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -65,7 +65,8 @@ "ember-cli-shims": "^1.1.0", "ember-composable-helpers": "^2.1.0", "ember-concurrency": "^0.8.17", - "ember-contextual-table": "^1.10.0", + "ember-contextual-table": "^2", + "ember-csv": "^1.0.2", "ember-lodash": "^4.19.4", "ember-promise-helpers": "^1.0.6", "ember-radio-button": "^1.2.4", From b3150c70859f93323ecbb6a93e5e7488aa2ab080 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 18 Feb 2020 18:23:33 +1100 Subject: [PATCH 43/57] paths-table : switch to HandsOnTable, to get rectangle select for copy/paste. paths-table.js : classNames : drop the plural - paths-table{s,}, seems like the requirement for now is all block-adjs in one table. add columnFieldsCap, capitalize(). add hoTableId, useHandsOnTable, didRender(), tableDataArray(), showData(), createHoTable(), onSelectionChange(), highlightFeature(). csvExportData() : use : instead of , as separator within an interval pair. factor activeFields to a CP. 3db84fd 18813 Feb 18 18:16 frontend/app/components/panel/paths-table.js a04ef70 31026 Feb 18 17:42 frontend/app/styles/app.scss cc6d41a 2366 Feb 18 17:34 frontend/app/templates/components/panel/paths-table.hbs --- frontend/app/components/panel/paths-table.js | 283 ++++++++++++++++-- frontend/app/styles/app.scss | 12 +- .../components/panel/paths-table.hbs | 42 +-- 3 files changed, 284 insertions(+), 53 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index 8ff194dd5..65eb76518 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -1,14 +1,41 @@ import Ember from 'ember'; const { inject: { service } } = Ember; +/* global d3 Handsontable */ + import PathData from '../draw/path-data'; import { pathsResultTypes, pathsApiResultType, pathsResultTypeFor, featureGetFn, featureGetBlock } from '../../utils/paths-api'; +import { eltClassName } from '../../utils/domElements'; const dLog = console.debug; +const trace = 0; +/** for trace */ +const fileName = 'panel/paths-table.js'; const columnFields = ['block', 'feature', 'position']; +const columnFieldsCap = columnFields.map((s) => s.capitalize()); + +var capitalize = (string) => { + return string[0].toUpperCase() + string.slice(1); +}; + +/** .moveToFront() is used in highlightFeature() + * table-brushed.js:highlightFeature() will also install (and override) this. + */ +if (! d3.selection.prototype.moveToFront) { + d3.selection.prototype.moveToFront = function() { + return this.each(function(){ + this.parentNode.appendChild(this); + }); + }; +} + +/** Switch to select either HandsOnTable or ember-contextual-table. */ +const useHandsOnTable = true; +/** id of element which will hold HandsOnTable. */ +const hoTableId = 'paths-table-ho'; /** @@ -18,23 +45,39 @@ const columnFields = ['block', 'feature', 'position']; * * @desc inputs from template * @param blockColumn true means show the block name:scope in a column instead of as a row. + * + * @desc attributes + * @param loading true indicates the requestAllPaths() API request is in progress. */ export default Ember.Component.extend({ flowsService: service('data/flows-collate'), pathsPro : service('data/paths-progressive'), axisBrush: service('data/axis-brush'), + useHandsOnTable : useHandsOnTable, + /** true enables display of the 'block' column for each end of the path. */ blockColumn : true, + /** true enables display of the brushedDomains in the table. */ showDomains : false, - classNames: ['paths-tables'], + classNames: ['paths-table'], didReceiveAttrs() { this._super(...arguments); - dLog('didReceiveAttrs', this.get('block.id'), this); + if (trace) + dLog('didReceiveAttrs', this.get('block.id'), this); + }, + + didRender() { + if (trace) + dLog(fileName + " : didRender()"); + if (useHandsOnTable && ! this.get('table')) { + this.set('table', this.createHoTable(this.get('tableData'))); + } }, + /*--------------------------------------------------------------------------*/ actions: { @@ -47,11 +90,20 @@ export default Ember.Component.extend({ }, requestAllPaths() { this.requestAllPaths(); + }, + showData(d) + { + this.showData(d); } }, /*--------------------------------------------------------------------------*/ + /** Reduce the selectedFeatures value to a structure grouped by block. + * + * The form of the input selectedFeatures is an array of : + * e.g. : {Chromosome: "myMap:1A.1", Feature: "myMarkerA", Position: "12.3"} + */ selectedFeaturesByBlock : Ember.computed( 'selectedFeatures.[]', function () { @@ -93,15 +145,6 @@ export default Ember.Component.extend({ let showCounts = this.get('showCounts'); - if (false) { - /** form is e.g. : {Chromosome: "myMap:1A.1", Feature: "myMarkerA", Position: "0"} */ - let chrName = "", // e.g. "myMap:1A.1", - position = "", // e.g. "12.3", - result = - {Chromosome: chrName, Feature: featureName, Position: position}; - } - - let blockAdjs = this.get('flowsService.blockAdjs'), tableData = blockAdjs.reduce(function (result, blockAdj) { @@ -212,11 +255,15 @@ export default Ember.Component.extend({ return result; }, []); - dLog('filterPaths', tableData, blockAdjs); + dLog('filterPaths', tableData.length, blockAdjs); return tableData; }, - tableData/*Aliases*/ : Ember.computed( + /** Map the paths (direct and by-alias) for each block-adj bounding + * selectedBlock into an array formatted for display in the table (either + * ember-contextual-table or HandsOnTable). + */ + tableData : Ember.computed( 'pathsResultFiltered.[]', 'pathsAliasesResultFiltered.[]', 'selectedBlock', @@ -225,10 +272,12 @@ export default Ember.Component.extend({ 'showDomains', 'showCounts', function () { - let tableDataAliases = this.filterPaths('pathsAliasesResultFiltered'); - dLog('tableDataAliases', tableDataAliases); let tableData = this.filterPaths('pathsResultFiltered'); - dLog('tableData', tableData); + if (tableData.length < 20) + dLog('tableData', tableData); + let tableDataAliases = this.filterPaths('pathsAliasesResultFiltered'); + if (tableDataAliases.length < 20) + dLog('tableDataAliases', tableDataAliases); let data = (tableDataAliases.length && tableData.length) ? tableDataAliases.concat(tableData) @@ -236,25 +285,23 @@ export default Ember.Component.extend({ ; return data; }), - tableData_bak : Ember.computed( - 'pathsResult.[]', - 'selectedBlock', - 'selectedFeaturesByBlock.@each', - 'blockColumn', - 'showDomains', - 'showCounts', - function () { - let tableData = this.filterPaths('pathsResult'); - dLog('tableData', tableData); - return tableData; - }), - csvExportData : Ember.computed('tableData', 'blockColumn', function () { + /** From columnFields, select those columns which are enabled. + * The blockColumn flag enables the 'block' column. + */ + activeFields : Ember.computed('blockColumn', function () { let activeFields = columnFields.slice(this.get('blockColumn') ? 0 : 1); + return activeFields; + }), + /** Map from tableData to the data form used by the csv export / download. + */ + csvExportData : Ember.computed('tableData', 'blockColumn', function () { + let activeFields = this.get('activeFields'); let headerRow = [0, 1].reduce(function (result, end) { activeFields.reduce(function (result, fieldName) { - result.push('"' + fieldName + end + '"'); + let columnHeading = capitalize(fieldName); + result.push('"' + columnHeading + end + '"'); return result; }, result); return result; @@ -269,7 +316,13 @@ export default Ember.Component.extend({ let v = d[fieldName + end], /** wrap text fields with double-quotes, and also position with comma - i.e. intervals 'from,to'. */ quote = (fieldName === 'block') || (fieldName === 'feature') || (v.indexOf(',') > -1), - outVal = v ? ( quote ? '"' + v + '"' : v) : ''; + /** If position value is an interval, it will appear at this point as 'number,number'. + * What to use for interval separator ? There is a convention of using + * '-' for intervals in genome data, but that may cause problems in a + * csv - Excel may evaluate it as minus. In a tsv ',' would be ok. + * For now use ':'. */ + v1 = v.replace(/,/, ':'), + outVal = v1 ? ( quote ? '"' + v1 + '"' : v1) : ''; result.push(outVal); return result; }, result); @@ -283,6 +336,35 @@ export default Ember.Component.extend({ return data; }), + /** Convert each element of the array tableData from name:value hash/object to + * an array, with 1 element per column. + * Added for hoTable, but not required as HO also accepts a name:value hash, + * as an alternative to an array per row. + */ + tableDataArray : Ember.computed('tableData.[]', function () { + let activeFields = this.get('activeFields'); + /** this could be used in csvExportData() - it also uses an array for each row. */ + let data = this.get('tableData').map(function (d, i) { + let da = + [0, 1].reduce(function (result, end) { + activeFields.reduce(function (result, fieldName) { + // if undefined, push '' for a blank cell. + let v = d[fieldName + end], + outVal = v ? v : ''; + result.push(outVal); + return result; + }, result); + return result; + }, []); + + if (trace && (i === 0)) + dLog('tableDataArray', d, da); + return da; + }); + return data; + }), + + /*--------------------------------------------------------------------------*/ /** Send API request for the block-adj-s displayed in the table, with @@ -318,4 +400,143 @@ export default Ember.Component.extend({ }, /*--------------------------------------------------------------------------*/ + /** + * @param data array of [block, feature, position, block, feature, position]. + * see columnFields, activeFields. + */ + showData : function(data) + { + if (trace && data.length < 20) + dLog("showData", data); + let table = this.get('table'); + if (table) + { + table.loadData(data); + } + }, + + createHoTable: function(data) { + /** copied from table-brushed.js */ + var that = this; + dLog("createHoTable", this); + + let tableDiv = Ember.$('#' + hoTableId)[0]; + dLog("tableDiv", tableDiv); + var table = new Handsontable(tableDiv, { + data: data || [['', '', '']], + minRows: 1, + rowHeaders: true, + columns: [ + { + data: 'block0', + type: 'text' + }, + { + data: 'feature0', + type: 'text' + }, + { + data: 'position0', + type: 'numeric', + numericFormat: { + pattern: '0,0.*' + } + }, + { + data: 'block1', + type: 'text' + }, + { + data: 'feature1', + type: 'text' + }, + { + data: 'position1', + type: 'numeric', + numericFormat: { + pattern: '0,0.*' + } + } + ], + colHeaders: [ + 'Block', + 'Feature', + 'Position', + 'Block', + 'Feature', + 'Position' + ], + headerTooltips: true, + colWidths: [100, 135, 60, 100, 135, 60], + height: 600, + manualRowResize: true, + manualColumnResize: true, + manualRowMove: true, + // manualColumnMove: true, + copyPaste: { + /** increase the limit on copy/paste. default is 1000 rows. */ + rowsLimit: 10000 + }, + contextMenu: true, + sortIndicator: true, + columnSorting: { + column: 2, + sortOrder: true + } + }); + + let $ = Ember.$; + $('#' + hoTableId).on('mouseleave', function(e) { + that.highlightFeature(); + }).on("mouseover", function(e) { + if (e.target.tagName == "TD") { + var tr = e.target.parentNode; + /** determine which half of the table the hovered cell is in, the left or right, + * and identify the feature cell in this row, in that half of the table. */ + let row = Array.from(e.target.parentElement.childNodes), + colIndex = row.indexOf(e.target), + featureIndex = (colIndex > (1 + 2)) * (1 + 2) + (1 + 1), + featureNode = tr.childNodes[featureIndex]; + if (featureNode) { + var feature_name = $(featureNode).text(); + if (feature_name && feature_name.length && (feature_name.indexOf(" ") == -1)) { + that.highlightFeature(feature_name); + return; + } + } + } + // that.highlightFeature(); + }); + return table; + }, + + + onSelectionChange: function () { + let data = this.get('tableData'), + me = this, + table = this.get('table'); + if (table) + { + if (trace) + dLog(fileName, "onSelectionChange", table, data.length); + // me.send('showData', data); + Ember.run.throttle(() => table.updateSettings({data:data}), 500); + } + }.observes('tableData'), + + highlightFeature: function(feature) { + d3.selectAll("g.axis-outer > circle") + .attr("r", 2) + .style("fill", "red") + .style("stroke", "red"); + if (feature) { + /** see also handleFeatureCircleMouseOver(). */ + d3.selectAll("g.axis-outer > circle." + eltClassName(eltClassName(feature))) + .attr("r", 5) + .style("fill", "yellow") + .style("stroke", "black") + .moveToFront(); + } + } + }); diff --git a/frontend/app/styles/app.scss b/frontend/app/styles/app.scss index f72c3d64a..02bf638a5 100644 --- a/frontend/app/styles/app.scss +++ b/frontend/app/styles/app.scss @@ -1355,20 +1355,24 @@ div.metaeditor-panel div.jsoneditor { } /* -------------------------------------------------------------------------- */ -.paths-tables .optionControls { +.paths-table .optionControls { display: flex; margin-bottom: 1em; } -.paths-tables .optionControls > div { +.paths-table .optionControls > div { flex-grow: 1; } -.paths-tables div.panel-body > div.btn-group > div { +.paths-table div.panel-body > div.btn-group > div { display: flex; } -.paths-tables div.panel-body > div.btn-group > div > * { +.paths-table div.panel-body > div.btn-group > div > * { margin: auto; } +.paths-table div.panel-body > div.btn-group > div > button > a { + color : inherit; +} + /* -------------------------------------------------------------------------- */ diff --git a/frontend/app/templates/components/panel/paths-table.hbs b/frontend/app/templates/components/panel/paths-table.hbs index 8b0b3f6b5..f680b4891 100644 --- a/frontend/app/templates/components/panel/paths-table.hbs +++ b/frontend/app/templates/components/panel/paths-table.hbs @@ -8,7 +8,7 @@ {{!-- based on elem/button-submit.hbs, could be factored --}}
- {{#component "ember-csv@file-anchor" data=csvExportData}} CSV Download{{/component}} +

@@ -25,20 +25,26 @@
{{input type="checkbox" name="showCounts" checked=showCounts }} Show Counts
-{{#data-filterer data=tableData as |df|}} - {{#data-sorter data=df.data as |ds|}} - {{#data-table data=ds.data - classNames=tableClassNames as |t|}} - {{#if blockColumn}} - {{t.filterableColumn propertyName='block0' name='Block' sortinformationupdated=(action ds.onsortfieldupdated) filterinformationupdated=(action df.onfilterfieldupdated) }} - {{/if}} - {{t.sortableColumn propertyName='feature0' name='From Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} - {{t.sortableColumn propertyName='position0' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} - {{#if blockColumn}} - {{t.filterableColumn propertyName='block1' name='Block' sortinformationupdated=(action ds.onsortfieldupdated) filterinformationupdated=(action df.onfilterfieldupdated) }} - {{/if}} - {{t.sortableColumn propertyName='feature1' name='To Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} - {{t.sortableColumn propertyName='position1' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} - {{/data-table}} -{{/data-sorter}} -{{/data-filterer}} +{{#if useHandsOnTable}} +
+ +{{else}} + + {{#data-filterer data=tableData as |df|}} + {{#data-sorter data=df.data as |ds|}} + {{#data-table data=ds.data + classNames=tableClassNames as |t|}} + {{#if blockColumn}} + {{t.filterableColumn propertyName='block0' name='Block' sortinformationupdated=(action ds.onsortfieldupdated) filterinformationupdated=(action df.onfilterfieldupdated) }} + {{/if}} + {{t.sortableColumn propertyName='feature0' name='From Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{t.sortableColumn propertyName='position0' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{#if blockColumn}} + {{t.filterableColumn propertyName='block1' name='Block' sortinformationupdated=(action ds.onsortfieldupdated) filterinformationupdated=(action df.onfilterfieldupdated) }} + {{/if}} + {{t.sortableColumn propertyName='feature1' name='To Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{t.sortableColumn propertyName='position1' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{/data-table}} + {{/data-sorter}} + {{/data-filterer}} +{{/if}} From 4012997142a47b6bff71dd3dc05d668a36162e46 Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 19 Feb 2020 09:20:04 +1100 Subject: [PATCH 44/57] handle empty cell in table. paths-table.js: csvExportData() : factor into format(), and don't apply it when cell has no text. Apply .colResizable to .contextual-data-table. ember-cli-build.js and frontend/package.json : add import colresizable. 472f18e 19208 Feb 19 09:18 frontend/app/components/panel/paths-table.js 76f9f3b 2272 Feb 19 08:49 frontend/ember-cli-build.js 9758b42 924033 Feb 18 23:03 frontend/package-lock.json b8d6319 2611 Feb 18 23:03 frontend/package.json --- frontend/app/components/panel/paths-table.js | 39 ++++++++++++++------ frontend/ember-cli-build.js | 1 + frontend/package-lock.json | 13 +++++++ frontend/package.json | 1 + 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index 65eb76518..d159136aa 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -1,7 +1,8 @@ import Ember from 'ember'; const { inject: { service } } = Ember; -/* global d3 Handsontable */ +/* global d3 */ +/* global Handsontable */ import PathData from '../draw/path-data'; @@ -33,7 +34,7 @@ if (! d3.selection.prototype.moveToFront) { } /** Switch to select either HandsOnTable or ember-contextual-table. */ -const useHandsOnTable = true; +const useHandsOnTable = false; /** id of element which will hold HandsOnTable. */ const hoTableId = 'paths-table-ho'; @@ -62,6 +63,17 @@ export default Ember.Component.extend({ classNames: ['paths-table'], + didInsertElement() { + this._super(...arguments); + + if (! useHandsOnTable) { + this.$(".contextual-data-table").colResizable({ + liveDrag:true, + draggingClass:"dragging" + }); + } + }, + didReceiveAttrs() { this._super(...arguments); @@ -314,15 +326,20 @@ export default Ember.Component.extend({ activeFields.reduce(function (result, fieldName) { // if undefined, push '' for a blank cell. let v = d[fieldName + end], - /** wrap text fields with double-quotes, and also position with comma - i.e. intervals 'from,to'. */ - quote = (fieldName === 'block') || (fieldName === 'feature') || (v.indexOf(',') > -1), - /** If position value is an interval, it will appear at this point as 'number,number'. - * What to use for interval separator ? There is a convention of using - * '-' for intervals in genome data, but that may cause problems in a - * csv - Excel may evaluate it as minus. In a tsv ',' would be ok. - * For now use ':'. */ - v1 = v.replace(/,/, ':'), - outVal = v1 ? ( quote ? '"' + v1 + '"' : v1) : ''; + outVal = v && format(fieldName, v); + function format (fieldName, v) { + let + /** wrap text fields with double-quotes, and also position with comma - i.e. intervals 'from,to'. */ + quote = (fieldName === 'block') || (fieldName === 'feature') || (v.indexOf(',') > -1), + /** If position value is an interval, it will appear at this point as 'number,number'. + * What to use for interval separator ? There is a convention of using + * '-' for intervals in genome data, but that may cause problems in a + * csv - Excel may evaluate it as minus. In a tsv ',' would be ok. + * For now use ':'. */ + v1 = v.replace(/,/, ':'), + outVal = v1 ? ( quote ? '"' + v1 + '"' : v1) : ''; + return outVal; + } result.push(outVal); return result; }, result); diff --git a/frontend/ember-cli-build.js b/frontend/ember-cli-build.js index 2a6c0942b..365824429 100644 --- a/frontend/ember-cli-build.js +++ b/frontend/ember-cli-build.js @@ -58,6 +58,7 @@ module.exports = function(defaults) { app.import('vendor/js/divgrid/divgrid.js'); app.import('node_modules/popper.js/dist/umd/popper.js'); app.import('node_modules/tooltip.js/dist/umd/tooltip.js'); + app.import('node_modules/colresizable/colResizable-1.6.min.js'); app.import('bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff', { destDir: 'fonts' diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f75fd8603..8c478d4f7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5564,6 +5564,14 @@ "text-hex": "1.0.x" } }, + "colresizable": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/colresizable/-/colresizable-1.6.0.tgz", + "integrity": "sha1-NfmpOTuT58d3xw1xH6Q7hZUhnx8=", + "requires": { + "jquery": "1 - 2" + } + }, "combine-source-map": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", @@ -15657,6 +15665,11 @@ "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", "dev": true }, + "jquery": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz", + "integrity": "sha1-LInWiJterFIqfuoywUUhVZxsvwI=" + }, "js-base64": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 82105929a..c880e51d1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -59,6 +59,7 @@ }, "private": true, "dependencies": { + "colresizable": "^1.6.0", "ember-array-helper": ">=0", "ember-cli-htmlbars": "^2.0.3", "ember-cli-htmlbars-inline-precompile": "^1.0.2", From ffd9c2543cda4a71e61a27a4ae7d318b3256e848 Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 19 Feb 2020 14:12:37 +1100 Subject: [PATCH 45/57] apply formatting to interval string in output. revert useHandsOnTable from false to true. csvExportData() : apply format() to text representation of interval in position column. Change useHandsOnTable back from false to true. This was intended to be in the previous commit - the file committed was : b4e1431 19209 Feb 19 09:11 (not as indicated in the commit message). c60a15a 19687 Feb 19 14:07 frontend/app/components/panel/paths-table.js --- frontend/app/components/panel/paths-table.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index d159136aa..cb2d435c7 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -34,7 +34,7 @@ if (! d3.selection.prototype.moveToFront) { } /** Switch to select either HandsOnTable or ember-contextual-table. */ -const useHandsOnTable = false; +const useHandsOnTable = true; /** id of element which will hold HandsOnTable. */ const hoTableId = 'paths-table-ho'; @@ -326,7 +326,15 @@ export default Ember.Component.extend({ activeFields.reduce(function (result, fieldName) { // if undefined, push '' for a blank cell. let v = d[fieldName + end], - outVal = v && format(fieldName, v); + outVal = (v === undefined) ? '' : ((typeof v !== "string") ? v: format(fieldName, v)); + /** Format some table cell values : wrap text values with double-quotes; + * If the position field is an interval it will present here as a + * string containing the start&end locations of the interval, + * separated by a comma - change the comma to a semi-colon. + * + * @param fieldName one of columnFields[] + * @param v is type "string" and is not undefined. + */ function format (fieldName, v) { let /** wrap text fields with double-quotes, and also position with comma - i.e. intervals 'from,to'. */ @@ -337,7 +345,7 @@ export default Ember.Component.extend({ * csv - Excel may evaluate it as minus. In a tsv ',' would be ok. * For now use ':'. */ v1 = v.replace(/,/, ':'), - outVal = v1 ? ( quote ? '"' + v1 + '"' : v1) : ''; + outVal = quote ? '"' + v1 + '"' : v1; return outVal; } result.push(outVal); From 1a7208215981eb58e1fd8d772615c06beedcc1bc Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 20 Feb 2020 12:37:41 +1100 Subject: [PATCH 46/57] Implement numeric sorting of position columns in paths-table paths-table.js : Add positionEnd, showInterval. Factor out as CPs : rowValues, headerRow, columns, colTypeNames, colHeaders, colWidths; add colTypes, onColumnsChange(). app.scss : align .numeric. paths-table.hbs : add positionEnd, class=numeric. frontend/app/ : 35f7816 22419 Feb 20 12:29 components/panel/paths-table.js 6440a17 5607 Jan 8 20:51 components/panel/view-controls.js 3cdeb86 31655 Feb 20 12:11 styles/app.scss (partially staged) 11e4dc9 2893 Feb 20 12:25 templates/components/panel/paths-table.hbs --- frontend/app/components/panel/paths-table.js | 172 ++++++++++++------ frontend/app/styles/app.scss | 12 ++ .../components/panel/paths-table.hbs | 11 +- 3 files changed, 138 insertions(+), 57 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index cb2d435c7..bba2ea9dd 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -15,7 +15,7 @@ const trace = 0; /** for trace */ const fileName = 'panel/paths-table.js'; -const columnFields = ['block', 'feature', 'position']; +const columnFields = ['block', 'feature', 'position', 'positionEnd']; const columnFieldsCap = columnFields.map((s) => s.capitalize()); var capitalize = (string) => { @@ -149,6 +149,8 @@ export default Ember.Component.extend({ let axisBrush = this.get('axisBrush'); /** true means show the block name:scope in a column instead of as a row. */ let blockColumn = this.get('blockColumn'); + /** true means if the position is an interval, also show the end of the interval. */ + let showInterval = this.get('showInterval'); /** true means show the brushed domains in the table. This may be omitted, although it * seems important to know if the axis was brushed and what the region * was. */ @@ -167,7 +169,12 @@ export default Ember.Component.extend({ if (block) row['block' + i] = block; row['feature' + i] = feature; - row['position' + i] = position; + if (showInterval && position.length) { + row['position' + i] = position[0]; + row['positionEnd' + i] = position[1]; + } + else + row['position' + i] = position; return row; }; let blocks = blockAdj.get('blocks'), @@ -189,7 +196,7 @@ export default Ember.Component.extend({ let ab = axisBrush.brushOfBlock(b), brushedDomain = ab && ab.get('brushedDomain'); if (brushedDomain) { - setEndpoint(rowHash, i, undefined, brushedDomain[0], brushedDomain[1]); + setEndpoint(rowHash, i, undefined, undefined, brushedDomain); } return rowHash; }, {}); @@ -249,7 +256,12 @@ export default Ember.Component.extend({ let value = featureGet('value'); let i = blockIndex.get(block); path['block' + i] = block.get('datasetNameAndScope'); - path['position' + i] = '' + value; + if (value.length) { + path['position' + i] = value[0]; + path['positionEnd' + i] = value[1]; + } + else + path['position' + i] = '' + value; path['feature' + i] = featureName; } return out; @@ -281,6 +293,7 @@ export default Ember.Component.extend({ 'selectedBlock', 'selectedFeaturesByBlock.@each', 'blockColumn', + 'showInterval', 'showDomains', 'showCounts', function () { @@ -301,24 +314,40 @@ export default Ember.Component.extend({ /** From columnFields, select those columns which are enabled. * The blockColumn flag enables the 'block' column. */ - activeFields : Ember.computed('blockColumn', function () { + activeFields : Ember.computed('blockColumn', 'showInterval', function () { let activeFields = columnFields.slice(this.get('blockColumn') ? 0 : 1); + if (! this.get('showInterval')) + activeFields.pop(); // pop off : positionEnd return activeFields; }), - /** Map from tableData to the data form used by the csv export / download. - */ - csvExportData : Ember.computed('tableData', 'blockColumn', function () { + /** Generate the names of values in a row. */ + rowValues : Ember.computed('activeFields', function () { let activeFields = this.get('activeFields'); - let headerRow = + let rowValues = [0, 1].reduce(function (result, end) { activeFields.reduce(function (result, fieldName) { - let columnHeading = capitalize(fieldName); - result.push('"' + columnHeading + end + '"'); + result.push(fieldName + end); return result; }, result); return result; }, []); - dLog('csvExportData', headerRow); + return rowValues; + }), + /** Generate the column headings row. + */ + headerRow : Ember.computed('rowValues', function () { + let headerRow = this.get('rowValues').map((fieldName) => capitalize(fieldName)); + dLog('headerRow', headerRow); + return headerRow; + }), + /** Map from tableData to the data form used by the csv export / download. + */ + csvExportData : Ember.computed('tableData', 'blockColumn', function () { + let activeFields = this.get('activeFields'); + /** column header text does not contain punctuation, but wrapping with + * quotes may help clarify to Xl that it is text. + */ + let headerRow = this.get('headerRow').map((name) => '"' + name + '"'); let data = this.get('tableData').map((d, i) => { let da = @@ -332,6 +361,10 @@ export default Ember.Component.extend({ * string containing the start&end locations of the interval, * separated by a comma - change the comma to a semi-colon. * + * With the addition of showInterval, intervals won't be expressed as + * strings containing comma, so most of this will be not needed, can + * be reduced to just the quoting. + * * @param fieldName one of columnFields[] * @param v is type "string" and is not undefined. */ @@ -440,59 +473,76 @@ export default Ember.Component.extend({ } }, + /** for HO table configuration. */ + columns : Ember.computed('rowValues', function () { + let columns = this.get('rowValues').map((name) => { + let options = { data : name}; + if (name.match(/^position/)) { + options.type = 'numeric'; + options.numericFormat = { + pattern: '0,0.*' + }; + } + else + options.type = 'text'; + return options; + }); + return columns; + }), + + /** for HO table configuration. */ + colTypes : { + block : { width : 100, header : 'Block'}, + feature : { width : 135, header : 'Feature'}, + position : { width : 60, header : 'Position'}, + positionEnd : { width : 60, header : 'Position End'} + }, + /** for HO table configuration. + * + * colTypeNames and colHeaders could also be used for generating + * filterableColumn / sortableColumn in the template, although for the current + * number of columns it probably would not reduce the code size. + */ + colTypeNames : Ember.computed('rowValues', function () { + let colTypeNames = this.get('rowValues').map((name) => { + return name.replace(/[01]$/, ''); + }); + return colTypeNames; + }), + /** for HO table configuration. */ + colHeaders : Ember.computed('colTypeNames', function () { + let colTypes = this.get('colTypes'), + colHeaders = this.get('colTypeNames').map((name) => colTypes[name].header); + return colHeaders; + }), + /** for HO table configuration. + * HO column settings (columns, colHeaders, colWidths) could be combined into + * a single CP, but the widths will change depending on window resize, + * independent of columns changing. + */ + colWidths : Ember.computed('colTypeNames', function () { + let colTypes = this.get('colTypes'), + colWidths = this.get('colTypeNames').map((name) => colTypes[name].width); + return colWidths; + }), + createHoTable: function(data) { /** copied from table-brushed.js */ var that = this; dLog("createHoTable", this); let tableDiv = Ember.$('#' + hoTableId)[0]; + + dLog("tableDiv", tableDiv); var table = new Handsontable(tableDiv, { data: data || [['', '', '']], minRows: 1, rowHeaders: true, - columns: [ - { - data: 'block0', - type: 'text' - }, - { - data: 'feature0', - type: 'text' - }, - { - data: 'position0', - type: 'numeric', - numericFormat: { - pattern: '0,0.*' - } - }, - { - data: 'block1', - type: 'text' - }, - { - data: 'feature1', - type: 'text' - }, - { - data: 'position1', - type: 'numeric', - numericFormat: { - pattern: '0,0.*' - } - } - ], - colHeaders: [ - 'Block', - 'Feature', - 'Position', - 'Block', - 'Feature', - 'Position' - ], + columns: this.get('columns'), + colHeaders: this.get('colHeaders'), headerTooltips: true, - colWidths: [100, 135, 60, 100, 135, 60], + colWidths: this.get('colWidths'), height: 600, manualRowResize: true, manualColumnResize: true, @@ -536,19 +586,31 @@ export default Ember.Component.extend({ }, - onSelectionChange: function () { + onDataChange: function () { let data = this.get('tableData'), me = this, table = this.get('table'); if (table) { if (trace) - dLog(fileName, "onSelectionChange", table, data.length); + dLog(fileName, "onDataChange", table, data.length); // me.send('showData', data); Ember.run.throttle(() => table.updateSettings({data:data}), 500); } }.observes('tableData'), + onColumnsChange : function () { + let table = this.get('table'); + if (table) { + let colSettings = { + columns: this.get('columns'), + colHeaders: this.get('colHeaders'), + colWidths: this.get('colWidths') + }; + table.updateSettings(colSettings); + } + }.observes('columns', 'colHeaders', 'colWidths'), + highlightFeature: function(feature) { d3.selectAll("g.axis-outer > circle") .attr("r", 2) diff --git a/frontend/app/styles/app.scss b/frontend/app/styles/app.scss index 02bf638a5..055848e35 100644 --- a/frontend/app/styles/app.scss +++ b/frontend/app/styles/app.scss @@ -1375,4 +1375,16 @@ div.metaeditor-panel div.jsoneditor { color : inherit; } +.paths-table td.contextual-cell.numeric { + /* Align a numeric value. + * CSS does not currently offer a way to align numeric content by decimal point + * where the fractional part may vary in length, or there may be no decimal point, + * as proposed here : https://www.w3.org/Style/CSS/Tracker/issues/38 + * td align="char" char="." is not supported. (https://www.w3schools.com/tags/att_td_char.asp) + * An alternative is to split the integer and fractional part into 2 columns, + * e.g. https://stackoverflow.com/a/5937346, https://stackoverflow.com/a/29924653 */ + text-align: right; + padding-right: 1em !important; +} + /* -------------------------------------------------------------------------- */ diff --git a/frontend/app/templates/components/panel/paths-table.hbs b/frontend/app/templates/components/panel/paths-table.hbs index f680b4891..dfe6a7cd1 100644 --- a/frontend/app/templates/components/panel/paths-table.hbs +++ b/frontend/app/templates/components/panel/paths-table.hbs @@ -21,6 +21,7 @@
Selected block : {{selectedBlock.datasetNameAndScope}}
{{input type="checkbox" name="blockColumn" checked=blockColumn }} Show block columns
+
{{input type="checkbox" name="showInterval" checked=showInterval }} Show Interval
{{input type="checkbox" name="showDomains" checked=showDomains }} Show Brushed Regions
{{input type="checkbox" name="showCounts" checked=showCounts }} Show Counts
@@ -38,12 +39,18 @@ {{t.filterableColumn propertyName='block0' name='Block' sortinformationupdated=(action ds.onsortfieldupdated) filterinformationupdated=(action df.onfilterfieldupdated) }} {{/if}} {{t.sortableColumn propertyName='feature0' name='From Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} - {{t.sortableColumn propertyName='position0' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{t.sortableColumn propertyName='position0' name='Position' sortinformationupdated=(action ds.onsortfieldupdated) class="numeric" }} + {{#if showInterval}} + {{t.sortableColumn propertyName='positionEnd0' name='Position End' sortinformationupdated=(action ds.onsortfieldupdated) class="numeric" }} + {{/if}} {{#if blockColumn}} {{t.filterableColumn propertyName='block1' name='Block' sortinformationupdated=(action ds.onsortfieldupdated) filterinformationupdated=(action df.onfilterfieldupdated) }} {{/if}} {{t.sortableColumn propertyName='feature1' name='To Feature' sortinformationupdated=(action ds.onsortfieldupdated)}} - {{t.sortableColumn propertyName='position1' name='Position' sortinformationupdated=(action ds.onsortfieldupdated)}} + {{t.sortableColumn propertyName='position1' name='Position' sortinformationupdated=(action ds.onsortfieldupdated) class="numeric" }} + {{#if showInterval}} + {{t.sortableColumn propertyName='positionEnd1' name='Position End' sortinformationupdated=(action ds.onsortfieldupdated) class="numeric" }} + {{/if}} {{/data-table}} {{/data-sorter}} {{/data-filterer}} From a6e884cf0576ca17006570c2b173b59c3d95326c Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 20 Feb 2020 19:30:32 +1100 Subject: [PATCH 47/57] upgrade handsontable to enable sorting by other columns in paths-table. In right panel move Features tab to be adjacent Paths. paths-table.js : add multiColumnSorting and licenseKey: non-commercial-and-evaluation. mapview.hbs : move Features tab from before Dataset to adjacent Paths. frontend/app/ : a6485f9 22887 Feb 20 19:06 components/panel/paths-table.js 46ae7bf 2898 Feb 20 17:28 templates/components/panel/paths-table.hbs befb675 5183 Feb 20 16:05 templates/mapview.hbs 6beaeff 826 Feb 20 18:57 frontend/bower.json --- frontend/app/components/panel/paths-table.js | 13 +++++++++---- .../app/templates/components/panel/paths-table.hbs | 4 +++- frontend/app/templates/mapview.hbs | 12 ++++++------ frontend/bower.json | 2 +- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index bba2ea9dd..ead4d9330 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -554,10 +554,15 @@ export default Ember.Component.extend({ }, contextMenu: true, sortIndicator: true, - columnSorting: { - column: 2, - sortOrder: true - } + multiColumnSorting: true, + /* in the latest non-commercial version 6.2.2, multiColumnSorting is + * present but didn't work with 'multiColumnSorting: true'; + * it is fine for all other features used. + * The following line enables the app to work with versions after 6.2.2, + * refn : https://handsontable.com/blog/articles/2019/3/handsontable-drops-open-source-for-a-non-commercial-license + * This app is used for academic research. + */ + licenseKey: 'non-commercial-and-evaluation' }); let $ = Ember.$; diff --git a/frontend/app/templates/components/panel/paths-table.hbs b/frontend/app/templates/components/panel/paths-table.hbs index dfe6a7cd1..0686d4b79 100644 --- a/frontend/app/templates/components/panel/paths-table.hbs +++ b/frontend/app/templates/components/panel/paths-table.hbs @@ -7,7 +7,7 @@ {{!-- based on elem/button-submit.hbs, could be factored --}}
- +

@@ -22,8 +22,10 @@
{{input type="checkbox" name="blockColumn" checked=blockColumn }} Show block columns
{{input type="checkbox" name="showInterval" checked=showInterval }} Show Interval
+{{#if false}}
{{input type="checkbox" name="showDomains" checked=showDomains }} Show Brushed Regions
{{input type="checkbox" name="showCounts" checked=showCounts }} Show Counts
+{{/if}}
{{#if useHandsOnTable}} diff --git a/frontend/app/templates/mapview.hbs b/frontend/app/templates/mapview.hbs index 615ec649c..d0256cf22 100644 --- a/frontend/app/templates/mapview.hbs +++ b/frontend/app/templates/mapview.hbs @@ -66,12 +66,6 @@ {{#if layout.right.visible}}
{{#if useHandsOnTable}} From 60fe00814073e6e45fafbb954f0ae03011d361cc Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 21 Feb 2020 14:43:46 +1100 Subject: [PATCH 49/57] show paths-table count in tab paths-table.js: Add sendUpdatePathsCount(), action updatePathsCount(). mapview.js : add pathsTableSummary, action updatePathsCount(). frontend/app/ : df6ed5f 25135 Feb 21 14:36 components/panel/paths-table.js 6a102f7 10497 Feb 21 11:30 controllers/mapview.js 45f7e7d 5295 Feb 21 13:55 templates/mapview.hbs --- frontend/app/components/panel/paths-table.js | 44 ++++++++++++++++---- frontend/app/controllers/mapview.js | 8 ++++ frontend/app/templates/mapview.hbs | 5 ++- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index f69fa3e45..0fd0bb7b4 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -22,6 +22,19 @@ var capitalize = (string) => { return string[0].toUpperCase() + string.slice(1); }; +/** Concat the given arrays. + * If either array is empty then return the other array (possibly Array.concat() + * does this optimisation already). + * Used in tableData() - it is expected in this use case that one or both of + * tableData{,Aliases} will commonly be empty. + */ +function concatOpt(a, b) { + let c = (a.length && b.length) ? a.concat(b) + : (b.length ? b : a); + return c; +} + + /** .moveToFront() is used in highlightFeature() * table-brushed.js:highlightFeature() will also install (and override) this. */ @@ -80,6 +93,12 @@ export default Ember.Component.extend({ } }, + willDestroyElement() { + this.sendUpdatePathsCount(''); + + this._super(...arguments); + }, + didReceiveAttrs() { this._super(...arguments); @@ -112,9 +131,17 @@ export default Ember.Component.extend({ showData(d) { this.showData(d); - } + }, + updatePathsCount(pathsCount) { + this.sendAction('updatePathsCount', pathsCount); + }, }, + sendUpdatePathsCount(pathsCount) { + Ember.run.later(() => this.send('updatePathsCount', pathsCount)); + }, + + /*--------------------------------------------------------------------------*/ /** Reduce the selectedFeatures value to a structure grouped by block. @@ -157,6 +184,7 @@ export default Ember.Component.extend({ let blockColumn = this.get('blockColumn'); /** true means if the position is an interval, also show the end of the interval. */ let showInterval = this.get('showInterval'); + let devControls = this.get('devControls'); /** true means show the brushed domains in the table. This may be omitted, although it * seems important to know if the axis was brushed and what the region * was. */ @@ -187,8 +215,11 @@ export default Ember.Component.extend({ let blocks = blockAdj.get('blocks'), blocksById = blocks.reduce((r, b) => { r[b.get('id')] = b; return r; }, {}); let blockIndex = blockAdj.get('blockIndex'); - if (! blockColumn) { - /** push the names of the 2 adjacent blocks, as an interleaved title row in the table. */ + if (devControls && ! blockColumn) { + /** push the names of the 2 adjacent blocks, as an interleaved title row in the table. + * This may be a useful feature for some users, but for now is only + * enabled as a development check. + */ let namesFlat = blocks.reduce( (rowHash, b, i) => @@ -328,11 +359,8 @@ export default Ember.Component.extend({ let tableDataAliases = this.filterPaths('pathsAliasesResultFiltered'); if (tableDataAliases.length < 20) dLog('tableDataAliases', tableDataAliases); - let data = - (tableDataAliases.length && tableData.length) ? - tableDataAliases.concat(tableData) - : (tableData.length ? tableData : tableDataAliases) - ; + let data = concatOpt(tableData, tableDataAliases); + this.sendUpdatePathsCount(data.length); return data; }), diff --git a/frontend/app/controllers/mapview.js b/frontend/app/controllers/mapview.js index 3bf76bead..c8766da2c 100644 --- a/frontend/app/controllers/mapview.js +++ b/frontend/app/controllers/mapview.js @@ -66,6 +66,12 @@ export default Ember.Controller.extend(Ember.Evented, ViewedBlocks, { // dLog("updateFeaturesInBlocks in mapview", featuresInBlocks); this.set('featuresInBlocks', featuresInBlocks); }, + /** from paths-table */ + updatePathsCount: function(pathsCount) { + dLog("updatePathsCount in mapview", pathsCount); + this.set('pathsTableSummary.count', pathsCount); + }, + /** Change the state of the named block to viewed. * If this block has a parent block, also add the parent. @@ -209,6 +215,8 @@ export default Ember.Controller.extend(Ember.Evented, ViewedBlocks, { mapsToView: [], selectedFeatures: [], + /** counts of selected paths, from paths-table; shown in tab. */ + pathsTableSummary : {}, scaffolds: undefined, diff --git a/frontend/app/templates/mapview.hbs b/frontend/app/templates/mapview.hbs index d0256cf22..0872fff8f 100644 --- a/frontend/app/templates/mapview.hbs +++ b/frontend/app/templates/mapview.hbs @@ -90,7 +90,7 @@ side="right" key="paths" state=layout.right.tab onClick="setTab"}} - {{elem/icon-base name="globe"}}  Paths + {{elem/icon-base name="globe"}}  Paths {{pathsTableSummary.count}} {{/elem/button-tab}} {{#if model.params.parsedOptions.advSettings}} {{#elem/button-tab @@ -123,7 +123,8 @@ {{else if (compare layout.right.tab '===' 'paths')}} {{panel/paths-table selectedFeatures=selectedFeatures - selectedBlock=selectedBlock}} + selectedBlock=selectedBlock + updatePathsCount=(action 'updatePathsCount')}} {{else if (and (compare layout.right.tab '===' 'settings') model.params.parsedOptions.advSettings)}} {{panel/manage-settings selectedFeatures=selectedFeatures From 3d180eee2951af83356da552ce3ef697ae0d92b6 Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 21 Feb 2020 16:29:12 +1100 Subject: [PATCH 50/57] use domain filter before density filter for drawing paths - use same CP as paths-table. block-adj.js : Use the Filtered form of the API result. 0140b76 19357 Feb 21 15:37 frontend/app/components/draw/block-adj.js --- frontend/app/components/draw/block-adj.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/app/components/draw/block-adj.js b/frontend/app/components/draw/block-adj.js index 6d6ebf8cb..a3bdea7b9 100644 --- a/frontend/app/components/draw/block-adj.js +++ b/frontend/app/components/draw/block-adj.js @@ -98,8 +98,10 @@ export default Ember.Component.extend(Ember.Evented, AxisEvents, { */ drawCurrent : function(prType) { let - /** e.g. 'pathsResult' or 'pathsAliasesResult' */ - pathsResult = this.get('blockAdj.' + prType.fieldName), + /** e.g. 'pathsResult' or 'pathsAliasesResult' + * Use the filtered form of the API result. + */ + pathsResult = this.get('blockAdj.' + prType.fieldName + 'Filtered'), fnName = prType.fieldName + 'Length', length = pathsResult && pathsResult.length; dLog(fnName, this, length); From f17884da91aa5493f1595cfa2a1df0cf7208534c Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 21 Feb 2020 18:03:19 +1100 Subject: [PATCH 51/57] handle marker names starting with a digit re. hover/highlight in feature & paths tables table-brushed.js and paths-table.js : apply eltClassName() just once. domElements.js : eltClassName() : prefix a leading digit with _ frontend/app/ : 9fed20e 25121 Feb 21 17:23 components/panel/paths-table.js c6cbffc 3764 Feb 21 17:23 components/table-brushed.js c457be8 11685 Feb 21 18:01 utils/domElements.js --- frontend/app/components/panel/paths-table.js | 2 +- frontend/app/components/table-brushed.js | 2 +- frontend/app/utils/domElements.js | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index 0fd0bb7b4..9f3549d6b 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -676,7 +676,7 @@ export default Ember.Component.extend({ .style("stroke", "red"); if (feature) { /** see also handleFeatureCircleMouseOver(). */ - d3.selectAll("g.axis-outer > circle." + eltClassName(eltClassName(feature))) + d3.selectAll("g.axis-outer > circle." + eltClassName(feature)) .attr("r", 5) .style("fill", "yellow") .style("stroke", "black") diff --git a/frontend/app/components/table-brushed.js b/frontend/app/components/table-brushed.js index fe87bdec7..3f190f671 100644 --- a/frontend/app/components/table-brushed.js +++ b/frontend/app/components/table-brushed.js @@ -138,7 +138,7 @@ export default Ember.Component.extend({ .style("stroke", "red"); if (feature) { /** see also handleFeatureCircleMouseOver(). */ - d3.selectAll("g.axis-outer > circle." + eltClassName(eltClassName(feature))) + d3.selectAll("g.axis-outer > circle." + eltClassName(feature)) .attr("r", 5) .style("fill", "yellow") .style("stroke", "black") diff --git a/frontend/app/utils/domElements.js b/frontend/app/utils/domElements.js index 85409349f..6392059cc 100644 --- a/frontend/app/utils/domElements.js +++ b/frontend/app/utils/domElements.js @@ -273,7 +273,12 @@ function eltClassName(f) * elsewhere, but handle it here by converting f to a string. */ let fString = (typeof(f) == 'string') ? f : '' + f, - fPrefixed = CSS.escape(f); // cssHexEncode(fString.replace(/^([\d])/, "_$1")); + /** d3.selectAll() is not matching the result of CSS.escape() on marker names + * starting with a digit. Prefixing with _ first works. Then CSS.escape() can + * handle any following punctuation. + */ + fPrefixNumber = fString.replace(/^([\d])/, "_$1"), + fPrefixed = CSS.escape(fPrefixNumber); // cssHexEncode(); return fPrefixed; } From dcda2d13a1e235d34f701d20f2188da40c9c1ef9 Mon Sep 17 00:00:00 2001 From: Don Date: Sun, 23 Feb 2020 18:51:26 +1100 Subject: [PATCH 52/57] Allow multiple getBlockFeatureLimits() for different blocks in parallel. controllers/mapview.js : Move blockFromId() out of actions. models/blocks.js : Add (copied from data/blocks.js) : valueOrLength(), ensureFeatureLimits(), taskGetLimits(), getLimits(); although the API is the same, the models case is for a loaded block, and the services case is for all blocks or a blockId (which may not be loaded). This can be rationalised when re-organising the model construction. routes/mapview.js : guard against concurrent blocksLimitsTask. data/block.js : factor part of ensureFeatureLimits() to models/block.ensureFeatureLimits(). frontend/app/ : 843b106 10588 Feb 23 18:41 controllers/mapview.js d5b73ee 14277 Feb 23 18:41 models/block.js a8e0a7e 5077 Feb 22 16:52 routes/mapview.js 86fa1a7 36674 Feb 22 18:33 services/data/block.js --- frontend/app/controllers/mapview.js | 18 +++++--- frontend/app/models/block.js | 69 ++++++++++++++++++++++++++++- frontend/app/routes/mapview.js | 7 ++- frontend/app/services/data/block.js | 13 +----- 4 files changed, 88 insertions(+), 19 deletions(-) diff --git a/frontend/app/controllers/mapview.js b/frontend/app/controllers/mapview.js index c8766da2c..0b9584010 100644 --- a/frontend/app/controllers/mapview.js +++ b/frontend/app/controllers/mapview.js @@ -150,11 +150,6 @@ export default Ember.Controller.extend(Ember.Evented, ViewedBlocks, { t.apply(this, [id]); } }, - blockFromId : function(blockId) { - let store = this.get('store'), - block = store.peekRecord('block', blockId); - return block; - }, selectBlock: function(block) { dLog('SELECT BLOCK mapview', block.get('name'), block.get('mapName'), block.id, block); @@ -248,6 +243,13 @@ export default Ember.Controller.extend(Ember.Evented, ViewedBlocks, { blockIds : readOnly('model.viewedBlocks.blockIds'), + blockFromId : function(blockId) { + let store = this.get('store'), + block = store.peekRecord('block', blockId); + return block; + }, + + /** Used by the template to indicate when & whether any data is loaded for the graph. */ hasData: Ember.computed( @@ -280,7 +282,11 @@ export default Ember.Controller.extend(Ember.Evented, ViewedBlocks, { * Also adding a similar request to updateModal (refreshDatasets) so by this * time that result should have been received. */ - this.get('block').ensureFeatureLimits(id); + let block = this.blockFromId(id); + if (block) + block.ensureFeatureLimits(); + else + this.get('block').ensureFeatureLimits(id); /** Before progressive loading this would load the data (features) of the block. */ const progressiveLoading = true; diff --git a/frontend/app/models/block.js b/frontend/app/models/block.js index dc3ff83cd..8be6e96b3 100644 --- a/frontend/app/models/block.js +++ b/frontend/app/models/block.js @@ -6,19 +6,28 @@ const { inject: { service } } = Ember; import { A } from '@ember/array'; import { and } from '@ember/object/computed'; +import { task } from 'ember-concurrency'; import { intervalMerge } from '../utils/interval-calcs'; +/*----------------------------------------------------------------------------*/ + const trace_block = 0; const dLog = console.debug; - const moduleName = 'models/block'; +/*----------------------------------------------------------------------------*/ + +/** trace the (array) value or just the length depending on trace level. */ +function valueOrLength(value) { return (trace_block > 1) ? value : value.length; } + +/*----------------------------------------------------------------------------*/ export default DS.Model.extend({ pathsP : service('data/paths-progressive'), // for getBlockFeaturesInterval() blockService : service('data/block'), + auth: service('auth'), datasetId: DS.belongsTo('dataset'), annotations: DS.hasMany('annotation', { async: false }), @@ -147,6 +156,64 @@ export default DS.Model.extend({ return isChartable; }), + /*--------------------------------------------------------------------------*/ + + /*--------------------------------------------------------------------------*/ + + /** these 3 functions ensureFeatureLimits(), taskGetLimits(), getLimits() (and + * also valueOrLength()) are copied from services/data/block.js; + * although the API is the same, this use case is for a loaded block, and the + * services/data/ case is for all blocks or a blockId (which may not be + * loaded). + * This can be rationalised when re-organising the model construction. + */ + + /** get featureLimits if not already received. After upload the block won't have + * .featureLimits until requested + */ + ensureFeatureLimits() { + let limits = this.get('featureLimits'); + /** Reference blocks don't have .featureLimits so don't request it. + * block.get('isData') depends on featureCount, which won't be present for + * newly uploaded blocks. Only references have .range (atm). + */ + let isData = ! this.get('range'); + if (! limits && isData) { + let blocksLimitsTasks = this.get('taskGetLimits').perform(); + } + }, + + /** Call getLimits() in a task - yield the block limits result. + */ + taskGetLimits: task(function * () { + let blockLimits = yield this.getLimits(); + if (trace_block) + dLog('taskGetLimits', this, valueOrLength(blockLimits)); + blockLimits.forEach((bfc) => { + if (bfc._id !== this.get('id')) + dLog('taskGetLimits', bfc._id); + else { + dLog('taskGetLimits', bfc, this); + this.set('featureLimits', [bfc.min, bfc.max]); + if (! this.get('featureCount')) + this.set('featureCount', bfc.featureCount); + } + }); + + return blockLimits; + }).drop(), + + getLimits: function () { + let blockId = this.get('id'); + dLog("block getLimits", blockId); + + let blockP = + this.get('auth').getBlockFeatureLimits(blockId, /*options*/{}); + + return blockP; + }, + + /*--------------------------------------------------------------------------*/ /** generate a text name for the block, to be displayed - it should be diff --git a/frontend/app/routes/mapview.js b/frontend/app/routes/mapview.js index 20d320c96..1c8b09158 100644 --- a/frontend/app/routes/mapview.js +++ b/frontend/app/routes/mapview.js @@ -75,7 +75,12 @@ let config = { this.controllerFor(this.fullRouteName).setViewedOnly(params.mapsToView, true); let blockService = this.get('block'); - let blocksLimitsTask = blockService.getBlocksLimits(undefined); + let blocksLimitsTask = this.get('blocksLimitsTask'); + dLog('blocksLimitsTask', blocksLimitsTask); + if (! blocksLimitsTask || ! blocksLimitsTask.get('isRunning')) { + blocksLimitsTask = blockService.getBlocksLimits(undefined); + this.set('blocksLimitsTask', blocksLimitsTask); + } let allInitially = params.parsedOptions && params.parsedOptions.allInitially; let getBlocks = blockService.get('getBlocks' + (allInitially ? '' : 'Summary')); let viewedBlocksTasks = (params.mapsToView && params.mapsToView.length) ? diff --git a/frontend/app/services/data/block.js b/frontend/app/services/data/block.js index 29e581526..5bae0f16b 100644 --- a/frontend/app/services/data/block.js +++ b/frontend/app/services/data/block.js @@ -521,19 +521,10 @@ export default Service.extend(Ember.Evented, { */ let blocks = blockId ? - store.peekRecord('block', blockId) + [ store.peekRecord('block', blockId) ] : store.peekAll('block'); blocks.forEach((block) => { - let limits = block.get('featureLimits'); - /** Reference blocks don't have .featureLimits so don't request it. - * block.get('isData') depends on featureCount, which won't be present for - * newly uploaded blocks. Only references have .range (atm). - */ - let isData = ! block.get('range'); - if (! limits && isData) { - /** getBlocksLimits() takes a single blockId as param. */ - let blocksLimitsTasks = this.getBlocksLimits(block.get('id')); - } + block.ensureFeatureLimits(); }); } }, From 31bce04ce9d25bae525dc81df43babb6864c6c53 Mon Sep 17 00:00:00 2001 From: Don Date: Mon, 24 Feb 2020 14:52:45 +1100 Subject: [PATCH 53/57] features table : lookup block from datasetId and scope extracted from selectedFeatures.Chromosome manage-features.js : add blockIdFromName() to lookup block by datasetId and scope extracted from selectedFeatures.Chromosome, based on blocksByReferenceAndScope; use this for the block and intersect filters. This replaces the use (added in 65b9c8e) of feature.Block which is undefined - selectedFeatures contains .Chromosome not .Block, and it is name + scope rather than block Id. selectedFeatures will change to a Set of features and this can then be replaced with a direct lookup of feature.block. 952172c 2485 Feb 24 14:36 frontend/app/components/panel/manage-features.js --- .../app/components/panel/manage-features.js | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/frontend/app/components/panel/manage-features.js b/frontend/app/components/panel/manage-features.js index 80de9df05..abb251a2d 100644 --- a/frontend/app/components/panel/manage-features.js +++ b/frontend/app/components/panel/manage-features.js @@ -1,12 +1,33 @@ +import Ember from 'ember'; +const { inject: { service } } = Ember; + import ManageBase from './manage-base' + export default ManageBase.extend({ + block: service('data/block'), + filterOptions: { 'all': {'formal': 'All', 'icon': 'plus'}, 'block': {'formal': 'Block', 'icon': 'globe'}, 'intersect': {'formal': 'Intersect', 'icon': 'random'} }, filter: 'all', + + /** From the .Chromosome field of selectedFeatures, lookup the block. + */ + blockIdFromName(datasetIdAndScope) { + let blocksByReferenceAndScope = this.get('block.blocksByReferenceAndScope'), + [datasetId, scope]=datasetIdAndScope.split(':'), + /** In a GM there can be multiple blocks with the same scope - at this point + * there is no way to distinguish them. This will be resolved when + * selectedFeatures is be converted to a Set of features, from which .block + * can be accessed. + */ + block = blocksByReferenceAndScope.get(datasetId).get(scope)[0]; + return block && block.get('id'); + }, + data: Ember.computed('selectedBlock', 'selectedFeatures', 'filter', function() { let selectedBlock = this.get('selectedBlock') let selectedFeatures = this.get('selectedFeatures') @@ -14,18 +35,23 @@ export default ManageBase.extend({ // perform filtering according to selectedChr let filtered = selectedFeatures //all if (filter == 'block' && selectedBlock) { - filtered = selectedFeatures.filter(function(feature) { - return feature.Block === selectedBlock.id - }) + filtered = selectedFeatures.filter((feature) => { + /** was : feature.Block, which is not defined, so lookup from .Chromosome */ + let featureBlock = this.blockIdFromName(feature.Chromosome); + return featureBlock === selectedBlock.id; + }); } else if (filter == 'intersect') { //split by block let blocks = {} - selectedFeatures.forEach(function(feature) { - if (!blocks[feature.Block]) { - blocks[feature.Block] = {} + selectedFeatures.forEach((feature) => { + /** was : feature.Block */ + let featureBlock = this.blockIdFromName(feature.Chromosome); + if (!blocks[featureBlock]) { + blocks[featureBlock] = {}; } - let block = blocks[feature.Block] + let block = blocks[featureBlock]; + // block[] could be a WeakSet. block[feature.Feature] = true }) filtered = selectedFeatures.filter(function(feature) { From 35ecc36e2a5d41f6f794eba6589b1e854f00af4d Mon Sep 17 00:00:00 2001 From: Don Date: Mon, 24 Feb 2020 14:53:21 +1100 Subject: [PATCH 54/57] Add non-commercial-and-evaluation to use of HandsOnTable in features table table-brushed.js : add licenseKey : non-commercial-and-evaluation. 1067347 3871 Feb 24 14:30 frontend/app/components/table-brushed.js --- frontend/app/components/table-brushed.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/app/components/table-brushed.js b/frontend/app/components/table-brushed.js index 3f190f671..141732d54 100644 --- a/frontend/app/components/table-brushed.js +++ b/frontend/app/components/table-brushed.js @@ -3,7 +3,8 @@ import { eltClassName } from '../utils/domElements'; // import Handsontable from 'handsontable'; -/* global d3 Handsontable */ +/* global d3 */ +/* global Handsontable */ const trace = 0; const dLog = console.debug; @@ -92,7 +93,9 @@ export default Ember.Component.extend({ columnSorting: { column: 2, sortOrder: true - } + }, + // see comment in paths-table.js + licenseKey: 'non-commercial-and-evaluation' }); that.set('table', table); $("#table-brushed").on('mouseleave', function(e) { From 2465dca56e83f530e74484d78a1f069eee5e009d Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 25 Feb 2020 14:46:14 +1100 Subject: [PATCH 55/57] Show paths count in tab while paths table is not displayed. paths-table.js : add right-panel-paths to classNames. add manageHoTable() (part factored from didRender), destroyHoTable(). table-brushed.js : add willDestroyElement() to table.destroy(). mapview.js : define layout as an Ember.Object because the dependencies on its fields seem to be intermittent. add rightPanelClass. app.scss : display .right-panel-paths within .right-panel-content only while paths tab is selected. mapview.hbs : add id right-panel-content, class rightPanelClass; replace conditional around paths-table with passing to it : visible=(layout.right.tab===paths) paths-table.hbs : wrap the whole content with if visible. frontend/app/ : 73c8edb 26897 Feb 25 14:30 components/panel/paths-table.js 51e75d1 4206 Feb 25 13:55 components/table-brushed.js 5594a95 11173 Feb 25 13:55 controllers/mapview.js 14d23e0 31971 Feb 24 21:03 styles/app.scss (partially staged) f6a3b83 3073 Feb 25 14:11 templates/components/panel/paths-table.hbs ab531fc 5338 Feb 25 14:16 templates/mapview.hbs --- frontend/app/components/panel/paths-table.js | 49 +++++++++++++++++-- frontend/app/components/table-brushed.js | 14 ++++++ frontend/app/controllers/mapview.js | 18 +++++-- frontend/app/styles/app.scss | 11 +++++ .../components/panel/paths-table.hbs | 4 ++ frontend/app/templates/mapview.hbs | 14 +++--- 6 files changed, 98 insertions(+), 12 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index 9f3549d6b..a24f91684 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -56,6 +56,12 @@ const hoTableId = 'paths-table-ho'; * Arguments passed to template : * @param selectedFeatures catenation of features within all brushed regions * @param selectedBlock block selected via axis brush, or via click in dataset explorer or view panel, or adding the block. + * @param visible true when the tab containing the paths-table is selected. + * value is : mapview .get('layout.right.tab') === 'paths', + * This enables paths-table to provide continuous display of pathsCount in the tab, + * even while the table is not displayed. + * The paths-table component could be split into a calculation component and a display component. + * The calculation is parameterised by user selections and actions so a service doesn't seem suited. * * @desc inputs from template * @param blockColumn true means show the block name:scope in a column instead of as a row. @@ -80,7 +86,7 @@ export default Ember.Component.extend({ /** true filters out paths which do not have >=1 end in a brush. */ onlyBrushedAxes : true, - classNames: ['paths-table'], + classNames: ['paths-table', 'right-panel-paths'], didInsertElement() { this._super(...arguments); @@ -95,6 +101,7 @@ export default Ember.Component.extend({ willDestroyElement() { this.sendUpdatePathsCount(''); + this.destroyHoTable(); this._super(...arguments); }, @@ -109,9 +116,45 @@ export default Ember.Component.extend({ didRender() { if (trace) dLog(fileName + " : didRender()"); - if (useHandsOnTable && ! this.get('table')) { - this.set('table', this.createHoTable(this.get('tableData'))); + this.manageHoTable(); + }, + + /** Create & destroy the HandsOnTable as indicated by .visible (layout.right.tab === 'paths') + * The sibling panel manage-features also has a HandsOnTable, and there seems + * to be a clash if they coexist. + + * The paths-table component exists permanently so that it can provide + * tableData.length for display in the tab via updatePathsCount(). + */ + manageHoTable : function() { + if (useHandsOnTable) { + let + visible = this.get('visible'), + table = this.get('table'); + + dLog('manageHoTable', visible, table, this); + /* If the paths-table component was split into a calculation component and + * a display component, then these 2 parts would go into the + * didInsertElement() and willDestroyElement() of the display component. + */ + if (visible && ! table) { + /* Using run.later() here prevents an overlap in time between the + * HandsOnTable-s of Features and Paths tables, when switching from + * Features to Paths. + */ + Ember.run.later(() => this.set('table', this.createHoTable(this.get('tableData')))); + } + if (! visible && table) { + this.destroyHoTable(); + } } + }.observes('visible'), + + destroyHoTable() { + let table = this.get('table'); + dLog('destroyHoTable', table); + table.destroy(); + this.set('table', null); }, diff --git a/frontend/app/components/table-brushed.js b/frontend/app/components/table-brushed.js index 141732d54..3b4c57d97 100644 --- a/frontend/app/components/table-brushed.js +++ b/frontend/app/components/table-brushed.js @@ -37,6 +37,20 @@ export default Ember.Component.extend({ dLog("components/table-brushed.js: didInsertElement"); }, + /** Destroy the HandsOnTable so that it does not clash with the HandsOnTable + * created by paths-table. + */ + willDestroyElement() { + let table = this.get('table'); + if (table) { + dLog('willDestroyElement', table); + table.destroy(); + this.set('table', undefined); + } + + this._super(...arguments); + }, + didRender() { dLog("components/table-brushed.js: didRender"); diff --git a/frontend/app/controllers/mapview.js b/frontend/app/controllers/mapview.js index 0b9584010..a4334104a 100644 --- a/frontend/app/controllers/mapview.js +++ b/frontend/app/controllers/mapview.js @@ -193,7 +193,7 @@ export default Ember.Controller.extend(Ember.Evented, ViewedBlocks, { } }, - layout: { + layout: Ember.Object.create({ 'left': { 'visible': true, 'tab': 'view' @@ -202,7 +202,7 @@ export default Ember.Controller.extend(Ember.Evented, ViewedBlocks, { 'visible': true, 'tab': 'selection' } - }, + }), controls : Ember.Object.create({ view : { } }), @@ -296,7 +296,19 @@ export default Ember.Controller.extend(Ember.Evented, ViewedBlocks, { dLog("block", id, block); // block.set('isViewed', true); } - } + }, + /** Provide a class for the div which wraps the right panel. + * + * The class indicates which of the tabs in the right panel is currently + * selected/displayed. paths-table css uses this to display:none the + * components div; the remainder of the template is disabled via {{#if + * (compare layout.right.tab '===' 'paths')}} which wraps the whole component. + */ + rightPanelClass : Ember.computed('layout.right.tab', function () { + let tab = this.get('layout.right.tab'); + dLog('rightPanelClass', tab); + return 'right-panel-' + tab; + }), }); diff --git a/frontend/app/styles/app.scss b/frontend/app/styles/app.scss index 055848e35..dc013e15f 100644 --- a/frontend/app/styles/app.scss +++ b/frontend/app/styles/app.scss @@ -1321,6 +1321,17 @@ ul.feature-found { body > div.ember-popover { padding: 3px 5px; } +/* -------------------------------------------------------------------------- */ + +/* -------------------------------------------------------------------------- */ + +#right-panel-content > .right-panel-paths { + display : none; +} +#right-panel-content.right-panel-paths > .right-panel-paths { + display : block; +} + /* -------------------------------------------------------------------------- */ /* JSON editor, in RHS panel */ diff --git a/frontend/app/templates/components/panel/paths-table.hbs b/frontend/app/templates/components/panel/paths-table.hbs index 0ccae385e..9ee549cb7 100644 --- a/frontend/app/templates/components/panel/paths-table.hbs +++ b/frontend/app/templates/components/panel/paths-table.hbs @@ -1,3 +1,5 @@ +{{#if visible}} + {{#elem/panel-container state="primary"}} {{#elem/panel-heading icon="filter"}} Actions @@ -58,3 +60,5 @@ {{/data-sorter}} {{/data-filterer}} {{/if}} + +{{/if}} {{!-- visible --}} diff --git a/frontend/app/templates/mapview.hbs b/frontend/app/templates/mapview.hbs index 0872fff8f..f504acaee 100644 --- a/frontend/app/templates/mapview.hbs +++ b/frontend/app/templates/mapview.hbs @@ -107,7 +107,7 @@ {{elem/icon-base name="remove"}} {{/elem/button-tab}} -
+
{{#if (compare layout.right.tab '===' 'selection')}} {{panel/manage-features selectedFeatures=selectedFeatures @@ -120,11 +120,6 @@ {{else if (compare layout.right.tab '===' 'dataset')}} {{panel/manage-dataset dataset=selectedDataset}} - {{else if (compare layout.right.tab '===' 'paths')}} - {{panel/paths-table - selectedFeatures=selectedFeatures - selectedBlock=selectedBlock - updatePathsCount=(action 'updatePathsCount')}} {{else if (and (compare layout.right.tab '===' 'settings') model.params.parsedOptions.advSettings)}} {{panel/manage-settings selectedFeatures=selectedFeatures @@ -137,6 +132,13 @@ showScaffoldMarkers=showScaffoldMarkers}} {{/if}} + + {{panel/paths-table + visible=(compare layout.right.tab '===' 'paths') + selectedFeatures=selectedFeatures + selectedBlock=selectedBlock + updatePathsCount=(action 'updatePathsCount')}} +
{{else}} From e0962241a46304cdea3353a70cb01c3bfc76f4f1 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 25 Feb 2020 16:10:30 +1100 Subject: [PATCH 56/57] trigger display of initial paths count in tab paths-table.js : evaluate tableData.length, to send action updatePathsCount. b32f9ce 27215 Feb 25 16:08 frontend/app/components/panel/paths-table.js --- frontend/app/components/panel/paths-table.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index a24f91684..bf7dff989 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -97,6 +97,13 @@ export default Ember.Component.extend({ draggingClass:"dragging" }); } + + /** trigger the initial display of tableData.length (pathsCount) in tab. + * Another approach would be to yield this from the template, and in + * mapview.hbs ember-wormhole the count into span.badge (in button-tab paths + * Paths). + */ + Ember.run.later(() => this.get('tableData.length'), 2000); }, willDestroyElement() { From 1d6437aaddad37fd1c76cf6bf585076444326244 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 25 Feb 2020 17:50:27 +1100 Subject: [PATCH 57/57] update version 1.2 -> 1.3. disable editing in paths-table HandsOnTable. paths-table.js : change contextMenu: true -> false, and add readOnly: true, comments: false. {,frontend/}package.json : update version 1.{2,3}.0 d5a8a48 27439 Feb 25 17:39 frontend/app/components/panel/paths-table.js 486c0a7 2611 Feb 25 17:25 frontend/package.json ae85461 1920 Feb 25 17:25 package.json --- frontend/app/components/panel/paths-table.js | 6 +++++- frontend/package.json | 2 +- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/app/components/panel/paths-table.js b/frontend/app/components/panel/paths-table.js index bf7dff989..167206fb0 100644 --- a/frontend/app/components/panel/paths-table.js +++ b/frontend/app/components/panel/paths-table.js @@ -655,7 +655,11 @@ export default Ember.Component.extend({ /** increase the limit on copy/paste. default is 1000 rows. */ rowsLimit: 10000 }, - contextMenu: true, + // disable editing, refn: https://stackoverflow.com/a/40801002 + readOnly: true, // make table cells read-only + contextMenu: false, // disable context menu to change things + comments: false, // prevent editing of comments + sortIndicator: true, multiColumnSorting: true, /* in the latest non-commercial version 6.2.2, multiColumnSorting is diff --git a/frontend/package.json b/frontend/package.json index c880e51d1..206c01f95 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "pretzel-frontend", - "version": "1.2.0", + "version": "1.3.0", "description": "Frontend code for Pretzel", "license": "MIT", "author": "", diff --git a/package.json b/package.json index 666d5d0a9..a072bba51 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pretzel", "private": true, - "version": "1.2.0", + "version": "1.3.0", "dependencies": { }, "repository" :