diff --git a/src/lib/index.js b/src/lib/index.js index ed572a0cdcc..13cf3c86eb2 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -75,6 +75,7 @@ var statsModule = require('./stats'); lib.aggNums = statsModule.aggNums; lib.len = statsModule.len; lib.mean = statsModule.mean; +lib.median = statsModule.median; lib.midRange = statsModule.midRange; lib.variance = statsModule.variance; lib.stdev = statsModule.stdev; diff --git a/src/lib/stats.js b/src/lib/stats.js index 552b5bfd9b0..a646aa0c1d5 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -74,6 +74,15 @@ exports.stdev = function(data, len, mean) { return Math.sqrt(exports.variance(data, len, mean)); }; +/** + * median of a finite set of numbers + * reference page: https://en.wikipedia.org/wiki/Median#Finite_set_of_numbers +**/ +exports.median = function(data) { + var b = data.slice().sort(); + return exports.interp(b, 0.5); +}; + /** * interp() computes a percentile (quantile) for a given distribution. * We interpolate the distribution (to compute quantiles, we follow method #10 here: diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index 1b1551e0143..9bfcd6f604d 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -503,6 +503,8 @@ function getTraceAttributes(type) { var out = { meta: _module.meta || {}, + categories: _module.categories || {}, + type: type, attributes: formatAttributes(attributes), }; diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 8c920ecbac0..04a44783209 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -817,8 +817,13 @@ module.exports = { categoryorder: { valType: 'enumerated', values: [ - 'trace', 'category ascending', 'category descending', 'array' - /* , 'value ascending', 'value descending'*/ // value ascending / descending to be implemented later + 'trace', 'category ascending', 'category descending', 'array', + 'total ascending', 'total descending', + 'min ascending', 'min descending', + 'max ascending', 'max descending', + 'sum ascending', 'sum descending', + 'mean ascending', 'mean descending', + 'median ascending', 'median descending' ], dflt: 'trace', role: 'info', @@ -828,11 +833,12 @@ module.exports = { 'By default, plotly uses *trace*, which specifies the order that is present in the data supplied.', 'Set `categoryorder` to *category ascending* or *category descending* if order should be determined by', 'the alphanumerical order of the category names.', - /* 'Set `categoryorder` to *value ascending* or *value descending* if order should be determined by the', - 'numerical order of the values.',*/ // // value ascending / descending to be implemented later 'Set `categoryorder` to *array* to derive the ordering from the attribute `categoryarray`. If a category', 'is not found in the `categoryarray` array, the sorting behavior for that attribute will be identical to', - 'the *trace* mode. The unspecified categories will follow the categories in `categoryarray`.' + 'the *trace* mode. The unspecified categories will follow the categories in `categoryarray`.', + 'Set `categoryorder` to *total ascending* or *total descending* if order should be determined by the', + 'numerical order of the values.', + 'Similarly, the order can be determined by the min, max, sum, mean or median of all the values.' ].join(' ') }, categoryarray: { diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index a407b912e78..5781b4490ad 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -612,6 +612,37 @@ module.exports = function setConvert(ax, fullLayout) { } }; + // sort the axis (and all the matching ones) by _initialCategories + // returns the indices of the traces affected by the reordering + ax.sortByInitialCategories = function() { + var affectedTraces = []; + var emptyCategories = function() { + ax._categories = []; + ax._categoriesMap = {}; + }; + + emptyCategories(); + + if(ax._initialCategories) { + for(var j = 0; j < ax._initialCategories.length; j++) { + setCategoryIndex(ax._initialCategories[j]); + } + } + + affectedTraces = affectedTraces.concat(ax._traceIndices); + + // Propagate to matching axes + var group = ax._matchGroup; + for(var axId2 in group) { + if(axId === axId2) continue; + var ax2 = fullLayout[axisIds.id2name(axId2)]; + ax2._categories = ax._categories; + ax2._categoriesMap = ax._categoriesMap; + affectedTraces = affectedTraces.concat(ax2._traceIndices); + } + return affectedTraces; + }; + // Propagate localization into the axis so that // methods in Axes can use it w/o having to pass fullLayout // Default (non-d3) number formatting uses separators directly diff --git a/src/plots/cartesian/type_defaults.js b/src/plots/cartesian/type_defaults.js index 16237397bf8..5edf2a37365 100644 --- a/src/plots/cartesian/type_defaults.js +++ b/src/plots/cartesian/type_defaults.js @@ -80,14 +80,8 @@ function setAutoType(ax, data) { ax.type = autoType(boxPositions, calendar, opts); } else if(d0.type === 'splom') { var dimensions = d0.dimensions; - var diag = d0._diag; - for(i = 0; i < dimensions.length; i++) { - var dim = dimensions[i]; - if(dim.visible && (diag[i][0] === id || diag[i][1] === id)) { - ax.type = autoType(dim.values, calendar, opts); - break; - } - } + var dim = dimensions[d0._axesDim[id]]; + if(dim.visible) ax.type = autoType(dim.values, calendar, opts); } else { ax.type = autoType(d0[axLetter] || [d0[axLetter + '0']], calendar, opts); } diff --git a/src/plots/plots.js b/src/plots/plots.js index 76db1aae239..67b397c8fd0 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -2843,10 +2843,196 @@ plots.doCalcdata = function(gd, traces) { doCrossTraceCalc(gd); + // Sort axis categories per value if specified + var sorted = sortAxisCategoriesByValue(axList, gd); + if(sorted.length) { + // If a sort operation was performed, run calc() again + for(i = 0; i < sorted.length; i++) calci(sorted[i], true); + for(i = 0; i < sorted.length; i++) calci(sorted[i], false); + doCrossTraceCalc(gd); + } + Registry.getComponentMethod('fx', 'calc')(gd); Registry.getComponentMethod('errorbars', 'calc')(gd); }; +var sortAxisCategoriesByValueRegex = /(total|sum|min|max|mean|median) (ascending|descending)/; + +function sortAxisCategoriesByValue(axList, gd) { + var affectedTraces = []; + var i, j, k, l, o; + + function zMapCategory(type, ax, value) { + var axLetter = ax._id.charAt(0); + if(type === 'histogram2dcontour') { + var counterAxLetter = ax._counterAxes[0]; + var counterAx = axisIDs.getFromId(gd, counterAxLetter); + + var xCategorical = axLetter === 'x' || (counterAxLetter === 'x' && counterAx.type === 'category'); + var yCategorical = axLetter === 'y' || (counterAxLetter === 'y' && counterAx.type === 'category'); + + return function(o, l) { + if(o === 0 || l === 0) return -1; // Skip first row and column + if(xCategorical && o === value[l].length - 1) return -1; + if(yCategorical && l === value.length - 1) return -1; + + return (axLetter === 'y' ? l : o) - 1; + }; + } else { + return function(o, l) { + return axLetter === 'y' ? l : o; + }; + } + } + + var aggFn = { + 'min': function(values) {return Lib.aggNums(Math.min, null, values);}, + 'max': function(values) {return Lib.aggNums(Math.max, null, values);}, + 'sum': function(values) {return Lib.aggNums(function(a, b) { return a + b;}, null, values);}, + 'total': function(values) {return Lib.aggNums(function(a, b) { return a + b;}, null, values);}, + 'mean': function(values) {return Lib.mean(values);}, + 'median': function(values) {return Lib.median(values);} + }; + + for(i = 0; i < axList.length; i++) { + var ax = axList[i]; + if(ax.type !== 'category') continue; + + // Order by value + var match = ax.categoryorder.match(sortAxisCategoriesByValueRegex); + if(match) { + var aggregator = match[1]; + var order = match[2]; + + // Store values associated with each category + var categoriesValue = []; + for(j = 0; j < ax._categories.length; j++) { + categoriesValue.push([ax._categories[j], []]); + } + + // Collect values across traces + for(j = 0; j < ax._traceIndices.length; j++) { + var traceIndex = ax._traceIndices[j]; + var fullTrace = gd._fullData[traceIndex]; + var axLetter = ax._id.charAt(0); + + // Skip over invisible traces + if(fullTrace.visible !== true) continue; + + var type = fullTrace.type; + if(Registry.traceIs(fullTrace, 'histogram')) delete fullTrace._autoBinFinished; + + var cd = gd.calcdata[traceIndex]; + for(k = 0; k < cd.length; k++) { + var cdi = cd[k]; + var cat, catIndex, value; + + if(type === 'splom') { + // If `splom`, collect values across dimensions + // Find which dimension the current axis is representing + var currentDimensionIndex = fullTrace._axesDim[ax._id]; + + // Apply logic to associated x axis if it's defined + if(axLetter === 'y') { + var associatedXAxisID = fullTrace._diag[currentDimensionIndex][0]; + if(associatedXAxisID) ax = gd._fullLayout[axisIDs.id2name(associatedXAxisID)]; + } + + var categories = cdi.trace.dimensions[currentDimensionIndex].values; + for(l = 0; l < categories.length; l++) { + cat = categories[l]; + catIndex = ax._categoriesMap[cat]; + + // Collect associated values at index `l` over all other dimensions + for(o = 0; o < cdi.trace.dimensions.length; o++) { + if(o === currentDimensionIndex) continue; + var dimension = cdi.trace.dimensions[o]; + categoriesValue[catIndex][1].push(dimension.values[l]); + } + } + } else if(type === 'scattergl') { + // If `scattergl`, collect all values stashed under cdi.t + for(l = 0; l < cdi.t.x.length; l++) { + if(axLetter === 'x') { + cat = cdi.t.x[l]; + catIndex = cat; + value = cdi.t.y[l]; + } + + if(axLetter === 'y') { + cat = cdi.t.y[l]; + catIndex = cat; + value = cdi.t.x[l]; + } + categoriesValue[catIndex][1].push(value); + } + // must clear scene 'batches', so that 2nd + // _module.calc call starts from scratch + if(cdi.t && cdi.t._scene) { + delete cdi.t._scene.dirty; + } + } else if(cdi.hasOwnProperty('z')) { + // If 2dMap, collect values in `z` + value = cdi.z; + var mapping = zMapCategory(fullTrace.type, ax, value); + + for(l = 0; l < value.length; l++) { + for(o = 0; o < value[l].length; o++) { + catIndex = mapping(o, l); + if(catIndex + 1) categoriesValue[catIndex][1].push(value[l][o]); + } + } + } else { + // For all other 2d cartesian traces + if(axLetter === 'x') { + cat = cdi.p + 1 ? cdi.p : cdi.x; + value = cdi.s || cdi.v || cdi.y; + } else if(axLetter === 'y') { + cat = cdi.p + 1 ? cdi.p : cdi.y; + value = cdi.s || cdi.v || cdi.x; + } + if(!Array.isArray(value)) value = [value]; + for(l = 0; l < value.length; l++) { + categoriesValue[cat][1].push(value[l]); + } + } + } + } + + ax._categoriesValue = categoriesValue; + + var categoriesAggregatedValue = []; + for(j = 0; j < categoriesValue.length; j++) { + categoriesAggregatedValue.push([ + categoriesValue[j][0], + aggFn[aggregator](categoriesValue[j][1]) + ]); + } + + // Sort by aggregated value + categoriesAggregatedValue.sort(function(a, b) { + return a[1] - b[1]; + }); + + ax._categoriesAggregatedValue = categoriesAggregatedValue; + + // Set new category order + ax._initialCategories = categoriesAggregatedValue.map(function(c) { + return c[0]; + }); + + // Reverse if descending + if(order === 'descending') { + ax._initialCategories.reverse(); + } + + // Sort all matching axes + affectedTraces = affectedTraces.concat(ax.sortByInitialCategories()); + } + } + return affectedTraces; +} + function setupAxisCategories(axList, fullData) { for(var i = 0; i < axList.length; i++) { var ax = axList[i]; diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index 0e424c5223d..092d5795fdd 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -80,6 +80,10 @@ module.exports = function calc(gd, trace) { cdi.pos = posDistinct[i]; cdi.pts = pts; + // Sort categories by values + cdi[posLetter] = cdi.pos; + cdi[valLetter] = cdi.pts.map(function(pt) { return pt.v; }); + cdi.min = boxVals[0]; cdi.max = boxVals[bvLen - 1]; cdi.mean = Lib.mean(boxVals, bvLen); diff --git a/src/traces/ohlc/calc.js b/src/traces/ohlc/calc.js index f6ad3bfa74e..0121eb382b2 100644 --- a/src/traces/ohlc/calc.js +++ b/src/traces/ohlc/calc.js @@ -86,6 +86,10 @@ function calcCommon(gd, trace, x, ya, ptFunc) { pt.i = i; pt.dir = increasing ? 'increasing' : 'decreasing'; + // For categoryorder, store low and high + pt.x = pt.pos; + pt.y = [li, hi]; + if(hasTextArray) pt.tx = trace.text[i]; if(hasHovertextArray) pt.htx = trace.hovertext[i]; diff --git a/src/traces/splom/defaults.js b/src/traces/splom/defaults.js index 9e7846d99e7..aba52b88b75 100644 --- a/src/traces/splom/defaults.js +++ b/src/traces/splom/defaults.js @@ -127,6 +127,7 @@ function handleAxisDefaults(traceIn, traceOut, layout, coerce) { var mustShiftX = !showDiag && !showLower; var mustShiftY = !showDiag && !showUpper; + traceOut._axesDim = {}; for(i = 0; i < dimLength; i++) { var dim = dimensions[i]; var i0 = i === 0; @@ -143,6 +144,8 @@ function handleAxisDefaults(traceIn, traceOut, layout, coerce) { fillAxisStashes(xaId, yaId, dim, xList); fillAxisStashes(yaId, xaId, dim, yList); diag[i] = [xaId, yaId]; + traceOut._axesDim[xaId] = i; + traceOut._axesDim[yaId] = i; } // fill in splom subplot keys diff --git a/test/image/baselines/hist_category_total_ascending.png b/test/image/baselines/hist_category_total_ascending.png new file mode 100644 index 00000000000..18fef9510d9 Binary files /dev/null and b/test/image/baselines/hist_category_total_ascending.png differ diff --git a/test/image/baselines/scatter_category_total_descending.png b/test/image/baselines/scatter_category_total_descending.png new file mode 100644 index 00000000000..ac0bf18d8e5 Binary files /dev/null and b/test/image/baselines/scatter_category_total_descending.png differ diff --git a/test/image/baselines/sort_by_total_matching_axes.png b/test/image/baselines/sort_by_total_matching_axes.png new file mode 100644 index 00000000000..bd3ce318f85 Binary files /dev/null and b/test/image/baselines/sort_by_total_matching_axes.png differ diff --git a/test/image/mocks/hist_category_total_ascending.json b/test/image/mocks/hist_category_total_ascending.json new file mode 100644 index 00000000000..a17c963047f --- /dev/null +++ b/test/image/mocks/hist_category_total_ascending.json @@ -0,0 +1,39 @@ +{ + "data": [{ + "x": ["a", "b", "c", "a", "b", "d", "b", "c", "b", "b"], + "type": "histogram" + }, + { + "x": ["d", "c", "a", "e", "a"], + "type": "histogram" + }, + { + "y": ["a", "b", "c", "a", "b", "d", "b", "c"], + "type": "histogram", + "xaxis": "x2", + "yaxis": "y2" + }, + { + "y": ["d", "c", "b", "a", "e", "a", "b"], + "type": "histogram", + "xaxis": "x2", + "yaxis": "y2" + }], + "layout": { + "title": "categoryorder: \"total ascending\"", + "height": 400, + "width": 600, + "barmode": "stack", + "xaxis": { + "domain": [0, 0.45], + "categoryorder": "total ascending" + }, + "xaxis2": { + "domain": [0.55, 1] + }, + "yaxis2": { + "anchor": "x2", + "categoryorder": "total ascending" + } + } +} diff --git a/test/image/mocks/scatter_category_total_descending.json b/test/image/mocks/scatter_category_total_descending.json new file mode 100644 index 00000000000..a95a4f0a2b6 --- /dev/null +++ b/test/image/mocks/scatter_category_total_descending.json @@ -0,0 +1,26 @@ +{ + "data": [ + { + "x": ["a", "b", "c", "d"], + "y": [4, 2, 3, 1], + "type": "scatter", + "mode": "markers" + }, + { + "x": ["a", "b", "c", "d", "c", "c"], + "type": "histogram" + } + ], + "layout": { + "title": "xaxis.categoryorder: \"total descending\"", + "width": 400, + "height": 400, + "xaxis": { + "domain": [ + 0, + 1 + ], + "categoryorder": "total descending" + } + } +} diff --git a/test/image/mocks/sort_by_total_matching_axes.json b/test/image/mocks/sort_by_total_matching_axes.json new file mode 100644 index 00000000000..c97303a0cef --- /dev/null +++ b/test/image/mocks/sort_by_total_matching_axes.json @@ -0,0 +1,29 @@ +{ + "data": [{ + "type": "scatter", + "orientation": "v", + "x": ["a", "b", "c"], + "y": [7, 2, 3] + }, { + "type": "bar", + "x": ["a", "b", "c"], + "y": [10, 20, 30], + "yaxis": "y2", + "xaxis": "x2" + }], + "layout": { + "xaxis": { + "type": "category", + "categoryorder": "total ascending" + }, + "xaxis2": { + "matches": "x" + }, + "yaxis": { + "domain": [0, 0.45] + }, + "yaxis2": { + "domain": [0.55, 1] + } + } +} diff --git a/test/jasmine/tests/calcdata_test.js b/test/jasmine/tests/calcdata_test.js index 430bfc5e7ca..f26b1481ce7 100644 --- a/test/jasmine/tests/calcdata_test.js +++ b/test/jasmine/tests/calcdata_test.js @@ -3,6 +3,8 @@ var Plotly = require('@lib/index'); var BADNUM = require('@src/constants/numerical').BADNUM; var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); +var Lib = require('@src/lib'); describe('calculated data and points', function() { var gd; @@ -869,6 +871,253 @@ describe('calculated data and points', function() { 'b': 1 }); }); + + describe('by value', function() { + var schema = Plotly.PlotSchema.get(); + var traces = Object.keys(schema.traces); + var tracesSchema = []; + var i, j, k; + for(i = 0; i < traces.length; i++) { + tracesSchema.push(schema.traces[traces[i]]); + } + var cartesianTraces = tracesSchema.filter(function(t) { + return t.categories.length && t.categories.indexOf('cartesian') !== -1; + }); + + // excludedTraces are traces that do not support sorting by value + var excludedTraces = [ 'carpet', 'contourcarpet']; + + var supportedCartesianTraces = cartesianTraces.filter(function(t) { + if(excludedTraces.indexOf(t.type) === -1) return true; + }); + + var cat = ['a', 'b', 'c']; + + // oneOrientationTraces are traces for which swapping x/y is not supported + var oneOrientationTraces = ['ohlc', 'candlestick']; + + function makeData(type, axName, a, b) { + var input = [a, b]; + var cat = input[axName === 'yaxis' ? 1 : 0]; + var data = input[axName === 'yaxis' ? 0 : 1]; + + var measure = []; + for(j = 0; j < data.length; j++) { + measure.push('absolute'); + } + + var z = Lib.init2dArray(cat.length, data.length); + for(j = 0; j < z.length; j++) { + for(k = 0; k < z[j].length; k++) { + z[j][k] = 0; + } + } + if(axName === 'xaxis') { + for(j = 0; j < b.length; j++) { + z[0][j] = b[j]; + } + } + if(axName === 'yaxis') { + for(j = 0; j < b.length; j++) { + z[j][0] = b[j]; + } + } + + return Lib.extendDeep({}, { + orientation: axName === 'yaxis' ? 'h' : 'v', + type: type, + x: cat, + a: cat, + + b: data, + y: data, + z: z, + + // For OHLC + open: data, + close: data, + high: data, + low: data, + + // For histogram + nbinsx: cat.length, + nbinsy: data.length, + + // For waterfall + measure: measure, + + // For splom + dimensions: [ + { + label: 'DimensionA', + values: a + }, + { + label: 'DimensionB', + values: b + } + ] + }); + } + + supportedCartesianTraces.forEach(function(trace) { + ['xaxis', 'yaxis'].forEach(function(axName) { + if(axName === 'yaxis' && oneOrientationTraces.indexOf(trace.type) !== -1) return; + + function checkAggregatedValue(baseMock, expectedAgg, finalOrder, done) { + var mock = Lib.extendDeep({}, baseMock); + + if(mock.data[0].type.match(/histogram/)) { + for(i = 0; i < mock.data.length; i++) { + mock.data[i][axName === 'yaxis' ? 'y' : 'x'].push('a'); + mock.data[i][axName === 'yaxis' ? 'x' : 'y'].push(7); + } + } + + Plotly.newPlot(gd, mock) + .then(function(gd) { + var agg = gd._fullLayout[trace.type === 'splom' ? 'xaxis' : axName]._categoriesAggregatedValue.sort(function(a, b) { + return a[0] > b[0] ? 1 : -1; + }); + expect(agg).toEqual(expectedAgg, 'wrong aggregation for ' + axName); + + if(finalOrder) { + expect(gd._fullLayout[trace.type === 'splom' ? 'xaxis' : axName]._categories).toEqual(finalOrder, 'wrong order'); + } + }) + .catch(failTest) + .then(done); + } + + ['total ascending', 'total descending'].forEach(function(categoryorder) { + it('sorts ' + axName + ' by ' + categoryorder + ' for trace type ' + trace.type, function(done) { + var data = [7, 2, 3]; + var baseMock = {data: [makeData(trace.type, axName, cat, data)], layout: {}}; + baseMock.layout[axName] = { type: 'category', categoryorder: categoryorder}; + + // Set expectations + var finalOrder = ['b', 'c', 'a']; + if(categoryorder === 'total descending') finalOrder.reverse(); + var expectedAgg = [['a', 7], ['b', 2], ['c', 3]]; + + if(trace.type === 'ohlc' || trace.type === 'candlestick') expectedAgg = [['a', 14], ['b', 4], ['c', 6]]; + if(trace.type.match(/histogram/)) expectedAgg = [['a', 2], ['b', 1], ['c', 1]]; + + checkAggregatedValue(baseMock, expectedAgg, finalOrder, done); + }); + }); + + it('sums values across traces of type ' + trace.type, function(done) { + var type = trace.type; + var data = [7, 2, 3]; + var data2 = [5, 4, 2]; + var baseMock = { data: [makeData(type, axName, cat, data), makeData(type, axName, cat, data2)], layout: {}}; + baseMock.layout[axName] = { type: 'category', categoryorder: 'total ascending'}; + + var expectedAgg = [['a', data[0] + data2[0]], ['b', data[1] + data2[1]], ['c', data[2] + data2[2]]]; + if(type === 'ohlc' || type === 'candlestick') expectedAgg = [['a', 2 * expectedAgg[0][1]], ['b', 2 * expectedAgg[1][1]], ['c', 2 * expectedAgg[2][1]]]; + if(type.match(/histogram/)) expectedAgg = [['a', 4], ['b', 2], ['c', 2]]; + + checkAggregatedValue(baseMock, expectedAgg, false, done); + }); + + it('ignores values from traces that are not visible ' + trace.type, function(done) { + var type = trace.type; + var data = [7, 2, 3]; + var data2 = [5, 4, 2]; + var baseMock = { data: [makeData(type, axName, cat, data), makeData(type, axName, cat, data2)], layout: {}}; + baseMock.layout[axName] = { type: 'category', categoryorder: 'total ascending'}; + + // Hide second trace + baseMock.data[1].visible = 'legendonly'; + var expectedAgg = [['a', data[0]], ['b', data[1]], ['c', data[2]]]; + if(type === 'ohlc' || type === 'candlestick') expectedAgg = [['a', 2 * expectedAgg[0][1]], ['b', 2 * expectedAgg[1][1]], ['c', 2 * expectedAgg[2][1]]]; + if(type.match(/histogram/)) expectedAgg = [['a', 2], ['b', 1], ['c', 1]]; + + checkAggregatedValue(baseMock, expectedAgg, false, done); + }); + + it('finds the minimum value per category across traces of type ' + trace.type, function(done) { + var type = trace.type; + var data = [7, 2, 3]; + var data2 = [5, 4, 2]; + var baseMock = { data: [makeData(type, axName, cat, data), makeData(type, axName, cat, data2)], layout: {}}; + baseMock.layout[axName] = { type: 'category', categoryorder: 'min ascending'}; + + var expectedAgg = [['a', Math.min(data[0], data2[0])], ['b', Math.min(data[1], data2[1])], ['c', Math.min(data[2], data2[2])]]; + if(trace.categories.indexOf('2dMap') !== -1) expectedAgg = [['a', 0], ['b', 0], ['c', 0]]; + if(type === 'histogram') expectedAgg = [['a', 2], ['b', 1], ['c', 1]]; + + checkAggregatedValue(baseMock, expectedAgg, false, done); + }); + + it('finds the maximum value per category across traces of type ' + trace.type, function(done) { + var type = trace.type; + var data = [7, 2, 3]; + var data2 = [5, 4, 2]; + var baseMock = { data: [makeData(type, axName, cat, data), makeData(type, axName, cat, data2)], layout: {}}; + baseMock.layout[axName] = { type: 'category', categoryorder: 'max ascending'}; + + var expectedAgg = [['a', Math.max(data[0], data2[0])], ['b', Math.max(data[1], data2[1])], ['c', Math.max(data[2], data2[2])]]; + if(type.match(/histogram/)) expectedAgg = [['a', 2], ['b', 1], ['c', 1]]; + + checkAggregatedValue(baseMock, expectedAgg, false, done); + }); + + it('takes the mean of all values per category across traces of type ' + trace.type, function(done) { + var type = trace.type; + var data = [7, 2, 3]; + var data2 = [5, 4, 2]; + var baseMock = { data: [makeData(type, axName, cat, data), makeData(type, axName, cat, data2)], layout: {}}; + baseMock.layout[axName] = { type: 'category', categoryorder: 'mean ascending'}; + + var expectedAgg = [['a', (data[0] + data2[0]) / 2 ], ['b', (data[1] + data2[1]) / 2], ['c', (data[2] + data2[2]) / 2]]; + if(type === 'histogram') expectedAgg = [['a', 2], ['b', 1], ['c', 1]]; + if(type === 'histogram2d') expectedAgg = [['a', 2 / 3], ['b', 1 / 3], ['c', 1 / 3]]; + if(type === 'contour' || type === 'heatmap') expectedAgg = [['a', expectedAgg[0][1] / 3], ['b', expectedAgg[1][1] / 3], ['c', expectedAgg[2][1] / 3]]; + if(type === 'histogram2dcontour') expectedAgg = [['a', 2 / 4], ['b', 1 / 4], ['c', 1 / 4]]; // TODO: this result is inintuitive + + checkAggregatedValue(baseMock, expectedAgg, false, done); + }); + + it('takes the median of all values per category across traces of type ' + trace.type, function(done) { + var type = trace.type; + var data = [7, 2, 3]; + var data2 = [5, 4, 2]; + var data3 = [6, 5, 7]; + var baseMock = { data: [makeData(type, axName, cat, data), makeData(type, axName, cat, data2), makeData(type, axName, cat, data3)], layout: {}}; + baseMock.layout[axName] = { type: 'category', categoryorder: 'median ascending'}; + + var expectedAgg = [['a', 6], ['b', 4], ['c', 3]]; + if(type === 'histogram') expectedAgg = [['a', 2], ['b', 1], ['c', 1]]; + if(type === 'histogram2d') expectedAgg = [['a', 1], ['b', 0], ['c', 0]]; + if(type === 'histogram2dcontour' || type === 'contour' || type === 'heatmap') expectedAgg = [['a', 0], ['b', 0], ['c', 0]]; + checkAggregatedValue(baseMock, expectedAgg, false, done); + }); + }); + }); + + it('works on asymmetric splom', function(done) { + var mock = require('@mocks/splom_multi-axis-type'); + var mockCopy = Lib.extendDeep(mock, {}); + + var order = ['donald', 'georgeW', 'bill', 'ronald', 'richard', 'jimmy', 'george', 'barack', 'gerald', 'lyndon']; + + Plotly.newPlot(gd, mockCopy) + .then(function() { + return Plotly.relayout(gd, 'yaxis5.categoryorder', 'total descending'); + }) + .then(function() { + expect(gd._fullLayout.yaxis5._categories).toEqual(order, 'wrong order'); + return Plotly.relayout(gd, 'yaxis5.categoryorder', 'total ascending'); + }) + .then(function() { + expect(gd._fullLayout.yaxis5._categories).toEqual(order.reverse(), 'wrong order'); + }) + .catch(failTest) + .then(done); + }); + }); }); describe('customdata', function() { diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index cdfe16bf1e1..38dc37927a0 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -176,6 +176,20 @@ describe('Test lib.js:', function() { }); }); + describe('median() should', function() { + it('return the middle value exactly for odd number of observations:', function() { + var input = [1, 8, 9, 2, 7, 6, 3]; + var res = Lib.median(input); + expect(res).toEqual(6); + }); + + it('return the mean of the two middle values for even number of observations', function() { + var input = [4, 3, 2, 1, 5, 6, 8, 9]; + var res = Lib.median(input); + expect(res).toEqual(4.5); + }); + }); + describe('stdev() should', function() { it('return 0 on input [2, 2, 2, 2, 2]:', function() { var input = [2, 2, 2, 2];