diff --git a/src/lib/sort_traces.js b/src/lib/sort_traces.js new file mode 100644 index 00000000000..d3086781deb --- /dev/null +++ b/src/lib/sort_traces.js @@ -0,0 +1,144 @@ +'use strict'; + +function zipArrays(arrays) { + var zipped = []; + arrays[0].forEach(function(e, i) { + var row = []; + arrays.forEach(function(arr) { + row.push(arr[i]); + }); + zipped.push(row); + }); + return zipped; +} + +function sortObjecstByKey(a, b, key) { + if(a[key] === b[key]) return 0; + if(a[key] < b[key]) return -1; + return 1; +} + +function matrixToObjectList(matrix, cols) { + var zipped = zipArrays(matrix); + + var objList = []; + + zipped.forEach(function(row) { + var objRow = {}; + cols.forEach(function(col, idx) { + objRow[col] = row[idx]; + }); + objRow.y = row[row.length - 1]; + objList.push(objRow); + }); + return objList; +} + +exports.matrixToObjectList = matrixToObjectList; + +function sortObjectList(cols, objList) { + var sortedObjectList = objList.map(function(e) { + return e; + }); + cols.slice().reverse().forEach(function(key) { + sortedObjectList = sortedObjectList.sort(function(a, b) { + return sortObjecstByKey(a, b, key); + }); + }); + return sortedObjectList; +} + +exports.sortObjectList = sortObjectList; + +function objectListToList(objectList) { + var list = []; + objectList.forEach(function(item) { + list.push(Object.values(item)); + }); + return list; +} + +exports.objectListToList = objectListToList; + +function sortedMatrix(list, removeNull) { + var xs = []; + var y = []; + + list.slice().forEach(function(item) { + var val = item.pop(); + + if(removeNull & item.includes(null)) { + return; + } + + y.push(val); + xs.push(item); + }); + + return [xs, y]; +} + +exports.sortedMatrix = sortedMatrix; + +function squareMatrix(matrix) { + var width = matrix[0].length; + var height = matrix.length; + + if(width === height) { + return matrix; + } + + var newMatrix = []; + + if(width > height) { + for(var rw = 0; rw < height; rw++) { + newMatrix.push(matrix[rw].slice()); + } + for(var i = height; i < width; i++) { + newMatrix.push(Array(width)); + } + } else { + for(var row = 0; row < height; row++) { + var rowExpansion = Array(height - width); + var rowSlice = matrix[row].slice(); + Array.prototype.push.apply(rowSlice, rowExpansion); + newMatrix.push(rowSlice); + } + } + return newMatrix; +} + +exports.squareMatrix = squareMatrix; + +function transpose(matrix) { + var height = matrix.length; + var width = matrix[0].length; + + var squaredMatrix = squareMatrix(matrix); + + var newMatrix = []; + + // prevent inplace change and mantain the main diagonal + for(var rw = 0; rw < squaredMatrix.length; rw++) { + newMatrix.push(squaredMatrix[rw].slice()); + } + + for(var i = 0; i < newMatrix.length; i++) { + for(var j = 0; j < i; j++) { + newMatrix = newMatrix.slice(); + var temp = newMatrix[i][j]; + newMatrix[i][j] = newMatrix[j][i]; + newMatrix[j][i] = temp; + } + } + if(width > height) { + for(var row = 0; row < newMatrix.length; row++) { + newMatrix[row] = newMatrix[row].slice(0, height); + } + } else { + newMatrix = newMatrix.slice(0, width); + } + return newMatrix; +} + +exports.transpose = transpose; \ No newline at end of file diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 1386cb8418c..facd8c217d6 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1880,19 +1880,36 @@ function formatCategory(ax, out) { } function formatMultiCategory(ax, out, hover) { - var v = Math.round(out.x); - var cats = ax._categories[v] || []; - var tt = cats[1] === undefined ? '' : String(cats[1]); - var tt2 = cats[0] === undefined ? '' : String(cats[0]); + var v = Math.round(out.x); + var cats = + ax._categories[v].map(function (cat) { + return cat; + }) || []; + var texts = cats + .slice() + .reverse() + .map(function (cat) { + return cat === undefined ? "" : String(cat); + }); - if(hover) { - // TODO is this what we want? - out.text = tt2 + ' - ' + tt; - } else { - // setup for secondary labels - out.text = tt; - out.text2 = tt2; - } + if (hover) { + // TODO is this what we want? + var hoverText = ""; + cats.forEach(function (text, index) { + text = String(text); + if (index < texts.length - 1) { + hoverText = hoverText + " " + text + " - "; + } else { + hoverText = hoverText + " " + text; + } + }); + + out.text = hoverText; + } else { + // setup for secondary labels + out.text = texts[0]; + out.texts = texts; + } } function formatLinear(ax, out, hover, extraPrecision, hideexp) { @@ -2616,40 +2633,72 @@ axes.drawOne = function(gd, ax, opts) { }); }); - if(ax.type === 'multicategory') { - var pad = {x: 2, y: 10}[axLetter]; - - seq.push(function() { - var bboxKey = {x: 'height', y: 'width'}[axLetter]; - var standoff = getLabelLevelBbox()[bboxKey] + pad + - (ax._tickAngles[axId + 'tick'] ? ax.tickfont.size * LINE_SPACING : 0); - - return axes.drawLabels(gd, ax, { - vals: getSecondaryLabelVals(ax, vals), - layer: mainAxLayer, - cls: axId + 'tick2', - repositionOnUpdate: true, - secondary: true, - transFn: transTickFn, - labelFns: axes.makeLabelFns(ax, mainLinePositionShift + standoff * majorTickSigns[4]) - }); + var tickNames = ["tick"]; + + if (ax.type === "multicategory") { + ax.levels + .slice() + .reverse() + .slice(0, ax.levelNr - 1) + .forEach(function (_lvl) { + var pad = { x: 0 * _lvl, y: 10 }[axLetter]; + + var tickName = "tick" + String(_lvl); + tickNames.push(tickName); + + seq.push(function () { + var bboxKey = { x: "height", y: "width" }[axLetter]; + var standoff = + _lvl * getLabelLevelBbox()[bboxKey] + + pad + + (ax._tickAngles[axId + "tick"] + ? ax.tickfont.size * LINE_SPACING + : 0); + + return axes.drawLabels(gd, ax, { + vals: getSecondaryLabelVals(ax, vals, _lvl), + layer: mainAxLayer, + cls: axId + tickName, + repositionOnUpdate: true, + secondary: true, + transFn: transTickFn, + labelFns: axes.makeLabelFns( + ax, + mainLinePosition + standoff * majorTickSigns[4] + ), + }); }); + }); - seq.push(function() { - ax._depth = majorTickSigns[4] * (getLabelLevelBbox('tick2')[ax.side] - mainLinePositionShift); + tickNames = tickNames.sort(); - return drawDividers(gd, ax, { - vals: dividerVals, - layer: mainAxLayer, - path: axes.makeTickPath(ax, mainLinePositionShift, majorTickSigns[4], { len: ax._depth }), - transFn: transTickFn - }); + ax.levels.forEach(function (_lvl, idx) { + seq.push(function () { + ax._depth = + majorTickSigns[4] * + (getLabelLevelBbox(tickNames.slice()[_lvl])[ax.side] - + mainLinePosition); + + var levelDividers = dividerVals.slice().filter(function (divider) { + return divider.level === idx; }); - } else if(ax.title.hasOwnProperty('standoff')) { - seq.push(function() { - ax._depth = majorTickSigns[4] * (getLabelLevelBbox()[ax.side] - mainLinePositionShift); + + return drawDividers(gd, ax, { + vals: levelDividers, + layer: mainAxLayer, + path: axes.makeTickPath(ax, mainLinePosition, majorTickSigns[4], { + len: ax._depth, + }), + transFn: transTickFn, + level: _lvl, }); - } + }); + }); +} else if(ax.title.hasOwnProperty('standoff')) { + seq.push(function() { + ax._depth = majorTickSigns[4] * (getLabelLevelBbox()[ax.side] - mainLinePositionShift); + }); +} var hasRangeSlider = Registry.getComponentMethod('rangeslider', 'isVisible')(ax); @@ -2824,55 +2873,76 @@ function getBoundaryVals(ax, vals) { return out; } -function getSecondaryLabelVals(ax, vals) { - var out = []; - var lookup = {}; - - for(var i = 0; i < vals.length; i++) { - var d = vals[i]; - if(lookup[d.text2]) { - lookup[d.text2].push(d.x); - } else { - lookup[d.text2] = [d.x]; - } +function getSecondaryLabelVals(ax, vals, level) { + var out = []; + var lookup = {}; + var appearences = {}; + var current; + var currentParent = null; + var parent = null; + + for (var i = 0; i < vals.length; i++) { + var d = vals[i]; + var text = d.texts[level]; + parent = d.texts[level + 1]; + if (lookup[text]) { + if ((d.texts[level] === current) & (parent === currentParent)) { + lookup[text][appearences[text]].push(d.x); + } else { + appearences[text] = appearences[text] + 1; + lookup[text].push([d.x]); + } + } else { + appearences[text] = 0; + lookup[text] = [[d.x]]; } + current = d.texts[level]; + currentParent = d.texts[level + 1]; + } - for(var k in lookup) { - out.push(tickTextObj(ax, Lib.interp(lookup[k], 0.5), k)); - } + Object.keys(lookup).forEach(function (key) { + lookup[key].forEach(function (pos) { + out.push(tickTextObj(ax, Lib.interp(pos, 0.5), key)); + }); + }); - return out; + return out; } function getDividerVals(ax, vals) { var out = []; var i, current; - - var reversed = (vals.length && vals[vals.length - 1].x < vals[0].x); - + + var reversed = vals.length && vals[vals.length - 1].x < vals[0].x; + // never used for labels; // no need to worry about the other tickTextObj keys - var _push = function(d, bndIndex) { - var xb = d.xbnd[bndIndex]; - if(xb !== null) { - out.push(Lib.extendFlat({}, d, {x: xb})); - } + var _push = function (d, bndIndex, level) { + var xb = d.xbnd[bndIndex]; + if (xb !== null) { + var _out = Lib.extendFlat({}, d, { x: xb }); + _out.level = level; + out.push(_out); + } }; - - if(ax.showdividers && vals.length) { - for(i = 0; i < vals.length; i++) { - var d = vals[i]; - if(d.text2 !== current) { - _push(d, reversed ? 1 : 0); - } - current = d.text2; + + if (ax.showdividers && vals.length) { + ax.levels.forEach(function (_lvl) { + current = undefined; + for (i = 0; i < vals.length; i++) { + var d = vals[i]; + if (d.texts[_lvl] !== current) { + _push(d, reversed ? 1 : 0, _lvl); + } + current = d.texts[_lvl]; + // text2 } _push(vals[i - 1], reversed ? 0 : 1); + }); } - return out; -} - + } + function calcLabelLevelBbox(ax, cls, mainLinePositionShift) { var top, bottom; var left, right; @@ -3987,24 +4057,30 @@ axes.drawLabels = function(gd, ax, opts) { * - {fn} transFn */ function drawDividers(gd, ax, opts) { - var cls = ax._id + 'divider'; + var cls = ax._id + "divider"; var vals = opts.vals; - - var dividers = opts.layer.selectAll('path.' + cls) - .data(vals, tickDataFn); - - dividers.exit().remove(); - - dividers.enter().insert('path', ':first-child') - .classed(cls, 1) - .classed('crisp', 1) - .call(Color.stroke, ax.dividercolor) - .style('stroke-width', Drawing.crispRound(gd, ax.dividerwidth, 1) + 'px'); - + + var dividers = opts.layer.selectAll("path." + cls).data(vals, tickDataFn); + + if (ax.type === "multicategory") { + if (opts.level === 0) { + dividers.exit().remove(); + } + } else { + dividers.exit().remove(); + } + dividers - .attr('transform', opts.transFn) - .attr('d', opts.path); -} + .enter() + .insert("path", ":first-child") + .classed(cls, 1) + .classed("crisp", 1) + .call(Color.stroke, ax.dividercolor) + .style("stroke-width", Drawing.crispRound(gd, ax.dividerwidth, 1) + "px"); + + dividers.attr("transform", opts.transFn).attr("d", opts.path); + } + /** * Get axis position in px, that is the distance for the graph's diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 14f17f9c2aa..3bf0425f493 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -156,9 +156,11 @@ module.exports = function setConvert(ax, fullLayout) { var arrayOut = new Array(len); for(var i = 0; i < len; i++) { - var v0 = (arrayIn[0] || [])[i]; - var v1 = (arrayIn[1] || [])[i]; - arrayOut[i] = getCategoryIndex([v0, v1]); + var vs = []; + for(var j = 0; j < ax.levelNr; j++) { + vs.push((arrayIn[j] || [])[i]); + } + arrayOut[i] = getCategoryIndex(vs); } return arrayOut; @@ -333,6 +335,7 @@ module.exports = function setConvert(ax, fullLayout) { // N.B. multicategory axes don't define d2c and d2l, // as 'data-to-calcdata' conversion needs to take into // account all data array items as in ax.makeCalcdata. + var sortLib = require('../../lib/sort_traces'); ax.r2d = ax.c2d = ax.l2d = getCategoryName; ax.d2r = ax.d2l_noadd = getCategoryPosition; @@ -357,7 +360,13 @@ module.exports = function setConvert(ax, fullLayout) { return ensureNumber(v); }; - ax.setupMultiCategory = function(fullData) { + ax.setupMultiCategory = function(gd) { + var fullData = gd._fullData; + // axes_test should set up category maps correctly for multicategory axes + if(!fullData) { + fullData = gd; + } + var traceIndices = ax._traceIndices; var i, j; @@ -371,49 +380,98 @@ module.exports = function setConvert(ax, fullLayout) { } } - // [ [cnt, {$cat: index}], for 1,2 ] - var seen = [[0, {}], [0, {}]]; - // [ [arrayIn[0][i], arrayIn[1][i]], for i .. N ] - var list = []; + var axLabels = []; + var fullObjectList = []; + var cols = []; for(i = 0; i < traceIndices.length; i++) { var trace = fullData[traceIndices[i]]; + cols = []; + + for(var k = 0; k < fullData[traceIndices[0]][axLetter].length; k++) { + cols.push('col' + k.toString()); + } + if(cols.length < 2) { + return; + } if(axLetter in trace) { var arrayIn = trace[axLetter]; - var len = trace._length || Lib.minRowLength(arrayIn); - - if(isArrayOrTypedArray(arrayIn[0]) && isArrayOrTypedArray(arrayIn[1])) { - for(j = 0; j < len; j++) { - var v0 = arrayIn[0][j]; - var v1 = arrayIn[1][j]; - - if(isValidCategory(v0) && isValidCategory(v1)) { - list.push([v0, v1]); - - if(!(v0 in seen[0][1])) { - seen[0][1][v0] = seen[0][0]++; - } - if(!(v1 in seen[1][1])) { - seen[1][1][v1] = seen[1][0]++; - } + if(isArrayOrTypedArray(arrayIn[0])) { + var arrays = arrayIn.map(function(x) { + return x; + }); + var valLetter; + if(trace.type === 'ohlc' | trace.type === 'candlestick') { + var t = trace; + var valsTransform = sortLib.transpose([t.open, t.high, t.low, t.close]); + arrays.push(valsTransform); + } else if(trace.z) { + if(axLetter === 'x') { + arrays.push(sortLib.transpose(trace.z)); + } else { + arrays.push(trace.z); } + valLetter = 'z'; + } else if(axLetter === 'y' && trace.x) { + arrays.push(trace.x); + valLetter = 'x'; + } else if(trace.y) { + arrays.push(trace.y); + valLetter = 'y'; + } else { + var nullArray = arrayIn[0].map(function() {return null;}); + arrays.push(nullArray); + } + var objList = sortLib.matrixToObjectList(arrays, cols); + + Array.prototype.push.apply(fullObjectList, objList); + + // convert the trace data from list to object and sort (backwards, stable sort) + var sortedObjectList = sortLib.sortObjectList(cols, objList); + var matrix = sortLib.objectListToList(sortedObjectList); + var sortedMatrix = sortLib.sortedMatrix(matrix); + + axLabels = sortedMatrix[0].slice(); + var axVals = sortedMatrix[1]; + + if(valLetter === 'z' & axLetter === 'x') { + axVals = sortLib.transpose(axVals); + } + + if(trace.type === 'ohlc' | trace.type === 'candlestick') { + var sortedValsTransform = sortLib.transpose(axVals); + gd._fullData[i].open = sortedValsTransform[0]; + gd._fullData[i].high = sortedValsTransform[1]; + gd._fullData[i].low = sortedValsTransform[2]; + gd._fullData[i].close = sortedValsTransform[3]; + } + // Could/should set sorted y axis values for each trace as the sorted values are already available. + // Need write access to gd._fullData, bad? Should probably be done right at newPlot, or on setting gd._fullData + + var transposedAxLabels = sortLib.transpose(axLabels); + if(gd._fullData) { + gd._fullData[i][axLetter] = transposedAxLabels; + } + if(valLetter) { + gd._fullData[i][valLetter] = axVals; } } } } - list.sort(function(a, b) { - var ind0 = seen[0][1]; - var d = ind0[a[0]] - ind0[b[0]]; - if(d) return d; + if(axLabels.length) { + ax.levelNr = axLabels[0].length; + ax.levels = axLabels[0].map(function(_, idx) {return idx;}); + var fullSortedObjectList = sortLib.sortObjectList(cols, fullObjectList.slice()); + var fullList = sortLib.objectListToList(fullSortedObjectList); + var fullSortedMatrix = sortLib.sortedMatrix(fullList, true); - var ind1 = seen[1][1]; - return ind1[a[1]] - ind1[b[1]]; - }); + var fullXs = fullSortedMatrix[0].slice(); - for(i = 0; i < list.length; i++) { - setCategoryIndex(list[i]); + for(i = 0; i < fullXs.length; i++) { + setCategoryIndex(fullXs[i]); + } } }; } diff --git a/src/plots/plots.js b/src/plots/plots.js index 3dcddc841f4..c4f027948d7 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -3158,7 +3158,7 @@ plots.doCalcdata = function(gd, traces) { calcdata[i] = cd; } - setupAxisCategories(axList, fullData, fullLayout); + setupAxisCategories(axList, gd, fullLayout); // 'transform' loop - must calc container traces first // so that if their dependent traces can get transform properly @@ -3166,7 +3166,7 @@ plots.doCalcdata = function(gd, traces) { for(i = 0; i < fullData.length; i++) transformCalci(i); // clear stuff that should recomputed in 'regular' loop - if(hasCalcTransform) setupAxisCategories(axList, fullData, fullLayout); + if(hasCalcTransform) setupAxisCategories(axList, gd, fullLayout); // 'regular' loop - make sure container traces (eg carpet) calc before // contained traces (eg contourcarpet) @@ -3374,13 +3374,13 @@ function sortAxisCategoriesByValue(axList, gd) { return affectedTraces; } -function setupAxisCategories(axList, fullData, fullLayout) { +function setupAxisCategories(axList, gd, fullLayout) { var axLookup = {}; function setupOne(ax) { ax.clearCalc(); if(ax.type === 'multicategory') { - ax.setupMultiCategory(fullData); + ax.setupMultiCategory(gd); } axLookup[ax._id] = 1;