From 1a8a0f0aeb6e81b415a3d0e18c7f28494695d1b3 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Mon, 27 Nov 2017 16:21:11 -0500 Subject: [PATCH 01/25] Initial parcats trace implementation --- lib/index.js | 2 +- lib/parcats.js | 11 + src/components/fx/hover.js | 76 + src/components/fx/index.js | 1 + src/traces/parcats/attributes.js | 183 ++ src/traces/parcats/base_plot.js | 34 + src/traces/parcats/calc.js | 575 +++++ src/traces/parcats/colorbar.js | 52 + src/traces/parcats/constants.js | 60 + src/traces/parcats/defaults.js | 120 ++ src/traces/parcats/index.js | 29 + src/traces/parcats/parcats.js | 1917 +++++++++++++++++ src/traces/parcats/plot.js | 43 + test/image/mocks/parcats_basic.json | 16 + test/image/mocks/parcats_bundled.json | 21 + .../image/mocks/parcats_bundled_reversed.json | 22 + test/image/mocks/parcats_reordered.json | 17 + test/image/mocks/parcats_unbundled.json | 22 + test/jasmine/tests/parcats_test.js | 788 +++++++ 19 files changed, 3988 insertions(+), 1 deletion(-) create mode 100644 lib/parcats.js create mode 100644 src/traces/parcats/attributes.js create mode 100644 src/traces/parcats/base_plot.js create mode 100644 src/traces/parcats/calc.js create mode 100644 src/traces/parcats/colorbar.js create mode 100644 src/traces/parcats/constants.js create mode 100644 src/traces/parcats/defaults.js create mode 100644 src/traces/parcats/index.js create mode 100644 src/traces/parcats/parcats.js create mode 100644 src/traces/parcats/plot.js create mode 100644 test/image/mocks/parcats_basic.json create mode 100644 test/image/mocks/parcats_bundled.json create mode 100644 test/image/mocks/parcats_bundled_reversed.json create mode 100644 test/image/mocks/parcats_reordered.json create mode 100644 test/image/mocks/parcats_unbundled.json create mode 100644 test/jasmine/tests/parcats_test.js diff --git a/lib/index.js b/lib/index.js index 2587276c897..9a9d189f805 100644 --- a/lib/index.js +++ b/lib/index.js @@ -38,7 +38,7 @@ Plotly.register([ require('./pointcloud'), require('./heatmapgl'), require('./parcoords'), - + require('./parcats'), require('./scattermapbox'), require('./sankey'), diff --git a/lib/parcats.js b/lib/parcats.js new file mode 100644 index 00000000000..a45e80ef2e1 --- /dev/null +++ b/lib/parcats.js @@ -0,0 +1,11 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = require('../src/traces/parcats'); diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index ff8175c69c5..a1cf4ada20b 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -153,6 +153,82 @@ exports.loneHover = function loneHover(hoverItem, opts) { return hoverLabel.node(); }; +// TODO: replace loneHover? +exports.customHovers = function customHovers(hoverItems, opts) { + + if (!Array.isArray(hoverItems)) { + hoverItems = [hoverItems]; + } + + var pointsData = hoverItems.map(function(hoverItem) { + return { + color: hoverItem.color || Color.defaultLine, + x0: hoverItem.x0 || hoverItem.x || 0, + x1: hoverItem.x1 || hoverItem.x || 0, + y0: hoverItem.y0 || hoverItem.y || 0, + y1: hoverItem.y1 || hoverItem.y || 0, + xLabel: hoverItem.xLabel, + yLabel: hoverItem.yLabel, + zLabel: hoverItem.zLabel, + text: hoverItem.text, + name: hoverItem.name, + idealAlign: hoverItem.idealAlign, + + // optional extra bits of styling + borderColor: hoverItem.borderColor, + fontFamily: hoverItem.fontFamily, + fontSize: hoverItem.fontSize, + fontColor: hoverItem.fontColor, + + // filler to make createHoverText happy + trace: { + index: 0, + hoverinfo: '' + }, + xa: {_offset: 0}, + ya: {_offset: 0}, + index: 0 + }; + }); + + + var container3 = d3.select(opts.container), + outerContainer3 = opts.outerContainer ? + d3.select(opts.outerContainer) : container3; + + var fullOpts = { + hovermode: 'closest', + rotateLabels: false, + bgColor: opts.bgColor || Color.background, + container: container3, + outerContainer: outerContainer3 + }; + + var hoverLabel = createHoverText(pointsData, fullOpts, opts.gd); + + // Fix vertical overlap + var tooltipSpacing = 5; + var lastBottomY = 0; + hoverLabel + .sort(function(a, b) {return a.y0 - b.y0}) + .each(function(d) { + var topY = d.y0 - d.by / 2; + + if ((topY - tooltipSpacing) < lastBottomY ) { + d.offset = (lastBottomY - topY) + tooltipSpacing; + } else { + d.offset = 0; + } + + lastBottomY = topY + d.by + d.offset; + }); + + + alignHoverText(hoverLabel, fullOpts.rotateLabels); + + return hoverLabel.node(); +}; + // The actual implementation is here: function _hover(gd, evt, subplot, noHoverEvent) { if(!subplot) subplot = 'xy'; diff --git a/src/components/fx/index.js b/src/components/fx/index.js index 77c618b8354..c495537e379 100644 --- a/src/components/fx/index.js +++ b/src/components/fx/index.js @@ -45,6 +45,7 @@ module.exports = { unhover: dragElement.unhover, loneHover: require('./hover').loneHover, + customHovers: require('./hover').customHovers, loneUnhover: loneUnhover, click: require('./click') diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js new file mode 100644 index 00000000000..94648ceccf4 --- /dev/null +++ b/src/traces/parcats/attributes.js @@ -0,0 +1,183 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var fontAttrs = require('../../plots/font_attributes'); +var extendFlat = require('../../lib/extend').extendFlat; +var colorAttributes = require('../../components/colorscale/color_attributes'); + +var scatterAttrs = require('../scatter/attributes'); +var scatterMarkerAttrs = scatterAttrs.marker; +var colorbarAttrs = require('../../components/colorbar/attributes'); + +var marker = extendFlat({ + editType: 'calc' + }, colorAttributes('marker', 'calc'), + { + showscale: scatterMarkerAttrs.showscale, + colorbar: colorbarAttrs, + shape: { + valType: 'enumerated', + values: ['straight', 'curved'], + dflt: 'curved', + role: 'info', + editType: 'plot', + description: 'Sets the shape of the paths'}, + }); + +module.exports = { + domain: { + x: { + valType: 'info_array', + role: 'info', + items: [ + {valType: 'number', min: 0, max: 1, editType: 'calc'}, + {valType: 'number', min: 0, max: 1, editType: 'calc'} + ], + dflt: [0, 1], + editType: 'calc', + description: [ + 'Sets the horizontal domain of this `parcats` trace', + '(in plot fraction).' + ].join(' ') + }, + y: { + valType: 'info_array', + role: 'info', + items: [ + {valType: 'number', min: 0, max: 1, editType: 'calc'}, + {valType: 'number', min: 0, max: 1, editType: 'calc'} + ], + dflt: [0, 1], + editType: 'calc', + description: [ + 'Sets the vertical domain of this `parcats` trace', + '(in plot fraction).' + ].join(' ') + }, + editType: 'calc' + }, + + tooltip: { + valType: 'boolean', + dflt: true, + role: 'info', + editType: 'plot', + description: 'Shows a tooltip when hover mode is `category` or `color`.' + }, + + hovermode: { + valType: 'enumerated', + values: ['none', 'category', 'color'], + dflt: 'category', + role: 'info', + editType: 'plot', + description: 'Sets the hover mode of the parcats diagram' + }, + bundlecolors: { + valType: 'boolean', + dflt: true, + role: 'info', + editType: 'plot', + description: 'Sort paths so that like colors are bundled together' + }, + sortpaths: { + valType: 'enumerated', + values: ['forward', 'backward'], + dflt: 'forward', + role: 'info', + editType: 'plot', + description: [ + 'If `forward` then sort paths based on dimensions from left to right.', + 'If `backward` sort based on dimensions from right to left.' + ].join(' ') + }, + // labelfont: fontAttrs({ + // editType: 'calc', + // description: 'Sets the font for the `dimension` labels.' + // }), + // + // catfont: fontAttrs({ + // editType: 'calc', + // description: 'Sets the font for the `category` labels.' + // }), + + dimensions: { + _isLinkedToArray: 'dimension', + label: { + valType: 'string', + role: 'info', + editType: 'calc', + description: 'The shown name of the dimension.' + }, + catDisplayInds: { + valType: 'data_array', + role: 'info', + editType: 'calc', + dflt: [], + description: [ + '' + ].join(' ') + }, + catValues: { + valType: 'data_array', + role: 'info', + editType: 'calc', + dflt: [], + description: [ + '' + ].join(' ') + }, + catLabels: { + valType: 'data_array', + role: 'info', + editType: 'calc', + dflt: [], + description: [ + '' + ].join(' ') + }, + values: { + valType: 'data_array', + role: 'info', + dflt: [], + editType: 'calc', + description: [ + 'Dimension values. `values[n]` represents the category value of the `n`th point in the dataset,', + 'therefore the `values` vector for all dimensions must be the same (longer vectors', + 'will be truncated). Each value must an element of `catValues`.' + ].join(' ') + }, + displayInd: { + valType: 'integer', + role: 'info', + editType: 'calc', + description: [ + 'The display index of dimension, from left to right, zero indexed, defaults to dimension' + + 'index.' + ].join(' ') + }, + editType: 'calc', + description: 'The dimensions (variables) of the parallel categories diagram.' + }, + + marker: marker, + counts: { + valType: 'number', + min: 0, + dflt: 1, + arrayOk: true, + role: 'info', + editType: 'calc', + description: [ + 'The number of observations represented by each state. Defaults to 1 so that each state represents ' + + 'one observation' + ] + } +}; diff --git a/src/traces/parcats/base_plot.js b/src/traces/parcats/base_plot.js new file mode 100644 index 00000000000..8b8ed601398 --- /dev/null +++ b/src/traces/parcats/base_plot.js @@ -0,0 +1,34 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var getModuleCalcData = require('../../plots/get_data').getModuleCalcData; +var parcatsPlot = require('./plot'); + +var PARCATS = 'parcats'; +exports.name = PARCATS; + +exports.plot = function(gd, traces, transitionOpts, makeOnCompleteCallback) { + + var cdModuleAndOthers = getModuleCalcData(gd.calcdata, PARCATS); + + if(cdModuleAndOthers.length) { + var calcData = cdModuleAndOthers[0]; + parcatsPlot(gd, calcData, transitionOpts, makeOnCompleteCallback); + } +}; + +exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { + var hadTable = (oldFullLayout._has && oldFullLayout._has('parcats')); + var hasTable = (newFullLayout._has && newFullLayout._has('parcats')); + + if(hadTable && !hasTable) { + oldFullLayout._paperdiv.selectAll('.parcats').remove(); + } +}; diff --git a/src/traces/parcats/calc.js b/src/traces/parcats/calc.js new file mode 100644 index 00000000000..740d64061ac --- /dev/null +++ b/src/traces/parcats/calc.js @@ -0,0 +1,575 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +// Requirements +// ============ +var wrap = require('../../lib/gup').wrap; +var hasColorscale = require('../../components/colorscale/has_colorscale'); +var colorscaleCalc = require('../../components/colorscale/calc'); +var parcatConstants = require('./constants'); + +var Drawing = require('../../components/drawing'); +var Lib = require('../../lib'); + +// Exports +// ======= +/** + * Create a wrapped ParcatsModel object from trace + * + * Note: trace defaults have already been applied + * @param {Object} gd + * @param {Object} trace + * @return {Array.} + */ +module.exports = function calc(gd, trace) { + + // Process inputs + // -------------- + if (trace.dimensions.length === 0) { + // No dimensions specified. Nothing to compute + return [] + } + + console.log(['calc', trace, gd.data, gd._fullData]); + + // Compute unique information + // -------------------------- + // UniqueInfo per dimension + var uniqueInfoDims = trace.dimensions.map(function(dim) { + return getUniqueInfo(dim.values, dim.catValues) + }); + + // Number of values and counts + // --------------------------- + var numValues = trace.dimensions[0].values.length; + + // Process counts + // -------------- + var counts, + count, + totalCount; + if (Lib.isArrayOrTypedArray(trace.counts)) { + counts = trace.counts; + } else { + counts = [trace.counts]; + } + + // Validate dimension display order + // -------------------------------- + validateDimensionDisplayInds(trace); + + // Validate category display order + // ------------------------------- + trace.dimensions.forEach(function(dim, dimInd) { + validateCategoryProperties(dim, uniqueInfoDims[dimInd]); + }); + + // Handle path colors + // ------------------ + var marker = trace.marker; + var markerColorscale; + + // Process colorscale + if (marker) { + if(hasColorscale(trace, 'marker')) { + colorscaleCalc(trace, trace.marker.color, 'marker', 'c'); + } + markerColorscale = Drawing.tryColorscale(marker); + } else { + markerColorscale = Lib.identity; + } + + // Build color generation function + function getMarkerColorInfo(index) { + var value; + if (!marker) { + value = parcatConstants.defaultColor; + } else if (Array.isArray(marker.color)) { + value = marker.color[index % marker.color.length]; + } else { + value = marker.color; + } + + return {color: markerColorscale(value), rawColor: value}; + } + + console.log(markerColorscale); + + // Build/Validate category labels/order + // ------------------------------------ + // properties: catValues, catorder, catlabels + // + // 1) if catValues and catorder are specified + // a) cat order must be the same length with no collisions or holes, otherwise it is discarded + // b) Additional categories in data that are not specified are appended to catValues, and next indexes are + // appended to catorder + // c) catValues updated in data/_fullData + // 2) if catorder but not catValues is specified + // a) catorder must be same length as inferred catValues with no collisions or holes + // otherwise it is discarded and set to 0 to catValues.length + // 3) if catValues but not catorder is specified + // a) Append unspecified values to catValues + // b) set carorder to 0 to catValues.length + // 4) if neither are specified + // a) Set catValues to unique catValues + // b) Set carorder to 0 to catValues.length + // + //uniqueInfoDims[0].uniqueValues + + // Category order logic + // 1) + + // Build path info + // --------------- + // Mapping from category inds to PathModel objects + var pathModels = {}; + + // Category inds array for each dimension + var categoryIndsDims = uniqueInfoDims.map(function(di) {return di.inds}); + + // Initialize total count + totalCount = 0; + + for (var valueInd=0; valueInd < numValues; valueInd++) { + + // Category inds for this input value across dimensions + var categoryIndsPath = []; + for (var d=0; d < categoryIndsDims.length; d++) { + categoryIndsPath.push(categoryIndsDims[d][valueInd]); + } + + // Count + count = counts[valueInd % counts.length]; + + // Update total count + totalCount+= count; + + // Path color + var pathColorInfo = getMarkerColorInfo(valueInd); + + // path key + var pathKey = categoryIndsPath + '-' + pathColorInfo.rawColor; + + // Create / Update PathModel + if (pathModels[pathKey] === undefined) { + pathModels[pathKey] = createPathModel(categoryIndsPath, + pathColorInfo.color, + pathColorInfo.rawColor); + } + updatePathModel(pathModels[pathKey], valueInd, count); + } + + // Build categories info + // --------------------- + + // Array of DimensionModel objects + var dimensionModels = trace.dimensions.map(function(di, i) { + return createDimensionModel(i, di.displayInd, di.label, totalCount); + }); + + + for (valueInd=0; valueInd < numValues; valueInd++) { + + count = counts[valueInd % counts.length]; + + for (d=0; d < dimensionModels.length; d++) { + var catInd = uniqueInfoDims[d].inds[valueInd]; + var cats = dimensionModels[d].categories; + + + if (cats[catInd] === undefined) { + var catLabel = trace.dimensions[d].catLabels[catInd]; + var displayInd = trace.dimensions[d].catDisplayInds[catInd]; + + cats[catInd] = createCategoryModel(d, catInd, displayInd, catLabel); + } + + updateCategoryModel(cats[catInd], valueInd, count); + } + } + + // Compute unique + return wrap(createParcatsModel(dimensionModels, pathModels, totalCount)); +}; + + +// Utilities +// ========= +function getValue(arrayOrScalar, index) { + var value; + if(!Array.isArray(arrayOrScalar)) value = arrayOrScalar; + else if(index < arrayOrScalar.length) value = arrayOrScalar[index]; + return value; +} + + +// Models +// ====== + +// Parcats Model +// ------------- +/** + * @typedef {Object} ParcatsModel + * Object containing calculated information about a parcats trace + * + * @property {Array.} dimensions + * Array of dimension models + * @property {Object.} paths + * Dictionary from category inds string (e.g. "1,2,1,1") to path model + * @property {Number} maxCats + * The maximum number of categories of any dimension in the diagram + * @property {Number} count + * Total number of input values + * @property {Object} trace + */ + +/** + * Create and new ParcatsModel object + * @param {Array.} dimensions + * @param {Object.} paths + * @param {Number} count + * @return {ParcatsModel} + */ +function createParcatsModel(dimensions, paths, count) { + var maxCats = dimensions + .map(function(d) {return d.categories.length}) + .reduce(function(v1, v2) {return Math.max(v1, v2)}); + return {dimensions: dimensions, paths: paths, trace: undefined, maxCats: maxCats, count: count} +} + +// Dimension Model +// --------------- +/** + * @typedef {Object} DimensionModel + * Object containing calculated information about a single dimension + * + * @property {Number} dimensionInd + * The index of this dimension + * @property {Number} displayInd + * The display index of this dimension (where 0 is the left most dimension) + * @property {String} dimensionLabel + * The label of this dimension + * @property {Number} count + * Total number of input values + * @property {Array.} categories + * @property {Number|null} dragX + * The x position of dimension that is currently being dragged. null if not being dragged + */ + +/** + * Create and new DimensionModel object with an empty categories array + * @param {Number} dimensionInd + * @param {Number} displayInd + * The display index of this dimension (where 0 is the left most dimension) + * @param {String} dimensionLabel + * @param {Number} count + * Total number of input values + * @return {DimensionModel} + */ +function createDimensionModel(dimensionInd, displayInd, dimensionLabel, count) { + return { + dimensionInd: dimensionInd, + displayInd: displayInd, + dimensionLabel: dimensionLabel, + count: count, + categories: [], + dragX: null + } +} + +// Category Model +// -------------- +/** + * @typedef {Object} CategoryModel + * Object containing calculated information about a single category. + * + * @property {Number} dimensionInd + * The index of this categories dimension + * @property {Number} categoryInd + * The index of this category + * @property {Number} displayInd + * The display index of this category (where 0 is the topmost category) + * @property {String} categoryLabel + * The name of this category + * @property {Array} valueInds + * Array of indices (into the original value array) of all samples in this category + * @property {Number} count + * The number of elements from the original array in this path + * @property {Number|null} dragY + * The y position of category that is currently being dragged. null if not being dragged + */ + +/** + * Create and return a new CategoryModel object + * @param {Number} dimensionInd + * @param {Number} categoryInd + * @param {Number} displayInd + * The display index of this category (where 0 is the topmost category) + * @param {String} categoryLabel + * @return {CategoryModel} + */ +function createCategoryModel(dimensionInd, categoryInd, displayInd, categoryLabel) { + return { + dimensionInd: dimensionInd, + categoryInd: categoryInd, + displayInd: displayInd, + categoryLabel: categoryLabel, + valueInds: [], + count: 0, + dragY: null + } +} + +/** + * Update a CategoryModel object with a new value index + * Note: The calling parameter is modified in place. + * + * @param {CategoryModel} categoryModel + * @param {Number} valueInd + * @param {Number} count + */ +function updateCategoryModel(categoryModel, valueInd, count) { + categoryModel.valueInds.push(valueInd); + categoryModel.count+= count; +} + + +// Path Model +// ---------- +/** + * @typedef {Object} PathModel + * Object containing calculated information about the samples in a path. + * + * @property {Array} categoryInds + * Array of category indices for each dimension (length `numDimensions`) + * @param {String} pathColor + * Color of this path. (Note: Any colorscaling has already taken place) + * @property {Array} valueInds + * Array of indices (into the original value array) of all samples in this path + * @property {Number} count + * The number of elements from the original array in this path + * @property {String} color + * The path's color (ass CSS color string) + * @property rawColor + * The raw color value specified by the user. May be a CSS color string or a Number + */ + +/** + * Create and return a new PathModel object + * @param {Array} categoryInds + * @param color + * @param rawColor + * @return {PathModel} + */ +function createPathModel(categoryInds, color, rawColor) { + return { + categoryInds: categoryInds, + color: color, + rawColor: rawColor, + valueInds: [], + count: 0 + } +} + +/** + * Update a PathModel object with a new value index + * Note: The calling parameter is modified in place. + * + * @param {PathModel} pathModel + * @param {Number} valueInd + * @param {Number} count + */ +function updatePathModel(pathModel, valueInd, count) { + pathModel.valueInds.push(valueInd); + pathModel.count+= count; +} + +// Unique calculations +// =================== +/** + * @typedef {Object} UniqueInfo + * Object containing information about the unique values of an input array + * + * @property {Array} uniqueValues + * The unique values in the input array + * @property {Array} uniqueCounts + * The number of times each entry in uniqueValues occurs in input array. + * This has the same length as `uniqueValues` + * @property {Array} inds + * Indices into uniqueValues that would reproduce original input array + */ + +/** + * Compute unique value information for an array + * + * IMPORTANT: Note that values are considered unique + * if their string representations are unique. + * + * @param {Array} values + * @param {Array|undefined} uniqueValues + * Array of expected unique values. The uniqueValues property of the resulting UniqueInfo object will begin with + * these entries. Entries are included even if there are zero occurrences in the values array. Entries found in + * the values array that are not present in uniqueValues will be included at the end of the array in the + * UniqueInfo object. + * @return {UniqueInfo} + */ +function getUniqueInfo(values, uniqueValues) { + + // Initialize uniqueValues if not specified + if (uniqueValues === undefined || uniqueValues === null) { + uniqueValues = []; + } else { + // Shallow copy so append below doesn't alter input array + uniqueValues = uniqueValues.map(function(e){return e}); + } + + // Initialize Variables + var uniqueValueCounts = {}, + uniqueValueInds = {}, + inds = []; + + // Initialize uniqueValueCounts and + uniqueValues.forEach(function(uniqueVal, valInd) { + uniqueValueCounts[uniqueVal] = 0; + uniqueValueInds[uniqueVal] = valInd; + }); + + // Compute the necessary unique info in a single pass + for(var i = 0; i < values.length; i++) { + var item = values[i]; + var itemInd; + + if(uniqueValueCounts[item] === undefined) { + // This item has a previously unseen value + uniqueValueCounts[item] = 1; + itemInd = uniqueValues.push(item) - 1; + uniqueValueInds[item] = itemInd; + } else { + // Increment count for this item + uniqueValueCounts[item]++; + itemInd = uniqueValueInds[item]; + } + inds.push(itemInd) + } + + // Build UniqueInfo + var uniqueCounts = uniqueValues.map(function (v) { return uniqueValueCounts[v] }); + + return { + uniqueValues: uniqueValues, + uniqueCounts: uniqueCounts, + inds: inds + } +} + + +/** + * Validate the requested display order for the dimensions. + * If the display order is a permutation of 0 through dimensions.length - 1 then leave it alone. Otherwise, repalce + * the display order with the dimension order + * @param {Object} trace + */ +function validateDimensionDisplayInds(trace) { + var displayInds = trace.dimensions.map(function(dim) {return dim.displayInd}); + if (!isRangePermutation(displayInds)) { + trace.dimensions.forEach(function (dim, i){ + dim.displayInd = i; + }) + } +} + + +/** + * Validate the requested display order for the dimensions. + * If the display order is a permutation of 0 through dimensions.length - 1 then leave it alone. Otherwise, repalce + * the display order with the dimension order + * @param {Object} dim + * @param {UniqueInfo} uniqueInfoDim + */ +function validateCategoryProperties(dim, uniqueInfoDim) { + var uniqueDimVals = uniqueInfoDim.uniqueValues; + + // Update catValues + dim.catValues = uniqueDimVals; + + // Handle catDisplayInds + if (dim.catDisplayInds.length !== uniqueDimVals.length || !isRangePermutation(dim.catDisplayInds)) { + dim.catDisplayInds = uniqueDimVals.map(function(v, i) {return i}); + } + + // Handle catLabels + if (dim.catLabels === null || dim.catLabels === undefined) { + dim.catLabels = []; + } else { + // Shallow copy to avoid modifying input array + dim.catLabels = dim.catLabels.map(function(v) {return v}); + } + + // Extend catLabels with elements from uniqueInfoDim.uniqueValues + for (var i=dim.catLabels.length; i < uniqueInfoDim.uniqueValues.length; i++) { + dim.catLabels.push(uniqueInfoDim.uniqueValues[i]) + } +} + +/** + * Determine whether an array contains a permutation of the integers from 0 to the array's length - 1 + * @param {Array} inds + * @return {boolean} + */ +function isRangePermutation(inds) { + var indsSpecified = new Array(inds.length); + + for (var i=0; i < inds.length; i++) { + // Check for out of bounds + if (inds[i] < 0 || inds[i] >= inds.length) { + return false; + } + + // Check for collisions with already specified index + if (indsSpecified[inds[i]] !== undefined) { + return false; + } + + indsSpecified[inds[i]] = true; + } + + // Nothing out of bounds and no collisions. We have a permutation + return true +} + +/** + * Determine whether two arrays are permutations of each other + * This is accomplished by sorting both arrays lexicographically and checking element equality + * @param {Array} a1 + * @param {Array} a2 + */ +function arePermutations(a1, a2) { + + // Check for equal length + if (a1 === null || a2 === null || a1.length !== a2.length) { + return false + } else { + var a1_sorted = a1.map(function(v){return v}); + a1_sorted.sort(); + + var a2_sorted = a2.map(function(v){return v}); + a2_sorted.sort(); + + for(var i=0; i < a1_sorted.length; i++) { + if (a1_sorted[i] !== a2_sorted[i]) { + // Elements not equal + return false; + } + } + + // Same number of elemenets and all elements equal + return true; + } +} diff --git a/src/traces/parcats/colorbar.js b/src/traces/parcats/colorbar.js new file mode 100644 index 00000000000..34311390879 --- /dev/null +++ b/src/traces/parcats/colorbar.js @@ -0,0 +1,52 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var isNumeric = require('fast-isnumeric'); + +var Lib = require('../../lib'); +var Plots = require('../../plots/plots'); +var Colorscale = require('../../components/colorscale'); +var drawColorbar = require('../../components/colorbar/draw'); + + +module.exports = function colorbar(gd, cd) { + var trace = cd[0].trace, + marker = trace.marker, + cbId = 'cb' + trace.uid; + + gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); + + if((marker === undefined) || !marker.showscale) { + Plots.autoMargin(gd, cbId); + return; + } + + var vals = marker.color, + cmin = marker.cmin, + cmax = marker.cmax; + + if(!isNumeric(cmin)) cmin = Lib.aggNums(Math.min, null, vals); + if(!isNumeric(cmax)) cmax = Lib.aggNums(Math.max, null, vals); + + var cb = cd[0].t.cb = drawColorbar(gd, cbId); + var sclFunc = Colorscale.makeColorScaleFunc( + Colorscale.extractScale( + marker.colorscale, + cmin, + cmax + ), + { noNumericCheck: true } + ); + + cb.fillcolor(sclFunc) + .filllevels({start: cmin, end: cmax, size: (cmax - cmin) / 254}) + .options(marker.colorbar)(); +}; diff --git a/src/traces/parcats/constants.js b/src/traces/parcats/constants.js new file mode 100644 index 00000000000..4036485d788 --- /dev/null +++ b/src/traces/parcats/constants.js @@ -0,0 +1,60 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + + +module.exports = { + maxDimensionCount: 12, + defaultColor: 'lightgray', + + // overdrag: 45, + // verticalPadding: 2, // otherwise, horizontal lines on top or bottom are of lower width + // tickDistance: 50, + // canvasPixelRatio: 1, + // blockLineCount: 5000, + // scatter: false, + // layers: ['contextLineLayer', 'focusLineLayer', 'pickLineLayer'], + // axisTitleOffset: 28, + // axisExtentOffset: 10, + // bar: { + // width: 4, // Visible width of the filter bar + // capturewidth: 10, // Mouse-sensitive width for interaction (Fitts law) + // fillcolor: 'magenta', // Color of the filter bar fill + // fillopacity: 1, // Filter bar fill opacity + // strokecolor: 'white', // Color of the filter bar side lines + // strokeopacity: 1, // Filter bar side stroke opacity + // strokewidth: 1, // Filter bar side stroke width in pixels + // handleheight: 16, // Height of the filter bar vertical resize areas on top and bottom + // handleopacity: 1, // Opacity of the filter bar vertical resize areas on top and bottom + // handleoverlap: 0 // A larger than 0 value causes overlaps with the filter bar, represented as pixels.' + // }, + cn: { + className: 'parcats' + // axisExtentText: 'axis-extent-text', + // parcoordsLineLayers: 'parcoords-line-layers', + // parcoordsLineLayer: 'parcoords-lines', + // parcoords: 'parcoords', + // parcoordsControlView: 'parcoords-control-view', + // yAxis: 'y-axis', + // axisOverlays: 'axis-overlays', + // axis: 'axis', + // axisHeading: 'axis-heading', + // axisTitle: 'axis-title', + // axisExtent: 'axis-extent', + // axisExtentTop: 'axis-extent-top', + // axisExtentTopText: 'axis-extent-top-text', + // axisExtentBottom: 'axis-extent-bottom', + // axisExtentBottomText: 'axis-extent-bottom-text', + // axisBrush: 'axis-brush' + }, + // id: { + // filterBarPattern: 'filter-bar-pattern' + // + // } +}; diff --git a/src/traces/parcats/defaults.js b/src/traces/parcats/defaults.js new file mode 100644 index 00000000000..2fee8302855 --- /dev/null +++ b/src/traces/parcats/defaults.js @@ -0,0 +1,120 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Lib = require('../../lib'); +var attributes = require('./attributes'); +var parcatConstants = require('./constants'); +var hasColorscale = require('../../components/colorscale/has_colorscale'); +var colorscaleDefaults = require('../../components/colorscale/defaults'); +var colorbarDefaults = require('../../components/colorbar/defaults'); + +function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce) { + + coerce('marker.color', defaultColor); + + if (traceIn.marker) { + coerce('marker.cmin'); + coerce('marker.cmax'); + coerce('marker.cauto'); + coerce('marker.colorscale'); + coerce('marker.showscale'); + coerce('marker.shape'); + colorbarDefaults(traceIn.marker, traceOut.marker, layout); + } +} + +function dimensionsDefaults(traceIn, traceOut) { + var dimensionsIn = traceIn.dimensions || [], + dimensionsOut = traceOut.dimensions = []; + + var dimensionIn, dimensionOut, i; + var commonLength = Infinity; + + if(dimensionsIn.length > parcatConstants.maxDimensionCount) { + Lib.log('parcats traces support up to ' + parcatConstants.maxDimensionCount + ' dimensions at the moment'); + dimensionsIn.splice(parcatConstants.maxDimensionCount); + } + + function coerce(attr, dflt) { + return Lib.coerce(dimensionIn, dimensionOut, attributes.dimensions, attr, dflt); + } + + for(i = 0; i < dimensionsIn.length; i++) { + dimensionIn = dimensionsIn[i]; + dimensionOut = {}; + + if(!Lib.isPlainObject(dimensionIn)) { + continue; + } + + // Dimension level + var values = coerce('values'); + coerce('label'); + coerce('displayInd', i); + + // Category level + coerce('catDisplayInds'); + coerce('catValues'); + coerce('catLabels'); + + // Pass through catValues, catorder, and catlabels (validated in calc since this is where unique info is available) + + // pass through marker (color, line) + // Pass through font + + commonLength = Math.min(commonLength, dimensionOut.values.length); + + // dimensionOut._index = i; + dimensionsOut.push(dimensionOut); + } + + if(isFinite(commonLength)) { + for(i = 0; i < dimensionsOut.length; i++) { + dimensionOut = dimensionsOut[i]; + if(dimensionOut.values.length > commonLength) { + dimensionOut.values = dimensionOut.values.slice(0, commonLength); + } + } + } + + // handle dimension order + // If specified for all dimensions and no collisions or holes keep, otherwise discard + + // Pass through value colors + // Pass through opacity + + // Pass through dimension font + // Pass through category font + + return dimensionsOut; +} + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + console.log(traceIn); + + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var dimensions = dimensionsDefaults(traceIn, traceOut); + + coerce('domain.x'); + coerce('domain.y'); + + markerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + + coerce('hovermode'); + coerce('tooltip'); + coerce('bundlecolors'); + coerce('sortpaths'); + coerce('counts'); + + console.log(['dimensionsDefaults', traceIn, traceOut]); +}; diff --git a/src/traces/parcats/index.js b/src/traces/parcats/index.js new file mode 100644 index 00000000000..868245feea7 --- /dev/null +++ b/src/traces/parcats/index.js @@ -0,0 +1,29 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Parcats = {}; + +Parcats.attributes = require('./attributes'); +Parcats.supplyDefaults = require('./defaults'); +Parcats.calc = require('./calc'); +Parcats.plot = require('./plot'); +Parcats.colorbar = require('./colorbar'); + +Parcats.moduleType = 'trace'; +Parcats.name = 'parcats'; +Parcats.basePlotModule = require('./base_plot'); +Parcats.categories = ['noOpacity', 'markerColorscale']; +Parcats.meta = { + description: [ + 'Parallel categories diagram for multidimensional categorical data.' + ].join(' ') +}; + +module.exports = Parcats; diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js new file mode 100644 index 00000000000..4d98819a81d --- /dev/null +++ b/src/traces/parcats/parcats.js @@ -0,0 +1,1917 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +// var c = require('./constants'); +var d3 = require('d3'); +var Plotly = require('../../plot_api/plot_api'); +var Fx = require('../../components/fx'); +var Lib = require('../../lib'); +var tinycolor = require('tinycolor2'); + +function performPlot(parcatsModels, graphDiv, layout, svg) { + + var viewModels = parcatsModels.map(createParcatsViewModel.bind(0, graphDiv, layout)); + + console.log(['viewModels', viewModels]); + + // Get (potentially empty) parcatslayer selection with bound data to single element array + var layerSelection = svg.selectAll('g.parcatslayer').data([null]); + + // Initialize single parcatslayer group if it doesn't exist + layerSelection.enter() + .append('g') + .attr('class', 'parcatslayer') + .style('pointer-events', 'all'); + + // Bind data to children of layerSelection and get reference to traceSelection + var traceSelection = layerSelection + .selectAll('g.trace.parcats') + .data(viewModels, key); + + // Initialize group for each trace/dimensions + var traceEnter = traceSelection.enter() + .append('g') + .attr('class', 'trace parcats'); + + // Update properties for each trace + traceSelection + .attr('transform', function (d) { + return 'translate(' + d.x + ', ' + d.y + ')'; + }); + + // Initialize paths group + traceEnter + .append('g') + .attr('class', 'paths'); + + // Update paths transform + var pathsSelection = traceSelection + .select('g.paths'); + + // Get paths selection + var pathSelection = pathsSelection + .selectAll('path.path') + .data(function (d) { + return d.paths + }, key); + + // Update existing path colors + pathSelection + .attr('fill', function(d) { + return d.model.color + }); + + // Create paths + var pathSelectionEnter = pathSelection + .enter() + .append('path') + .attr('class', 'path') + .attr('stroke-opacity', 0) + .attr('fill', function(d) { + return d.model.color + }) + .attr('fill-opacity', 0); + + stylePathsNoHover(pathSelectionEnter); + + // Set path geometry + pathSelection + .attr('d', function (d) { + return d.svgD + }); + + // sort paths + if (!pathSelectionEnter.empty()) { + // Only sort paths if there has been a change. + // Otherwise paths are already sorted or a hover operation may be in progress + pathSelection.sort(compareRawColor); + } + + // Remove any old paths + pathSelection.exit().remove(); + + // Path hover + pathSelection + .on('mouseover', mouseoverPath) + .on('mouseout', mouseoutPath) + .on('click', clickPath); + + // Initialize dimensions group + traceEnter.append('g').attr('class', 'dimensions'); + + // Update dimensions transform + var dimensionsSelection = traceSelection + .select('g.dimensions'); + + // Get dimension selection + var dimensionSelection = dimensionsSelection + .selectAll('g.dimension') + .data(function (d) { + return d.dimensions + }, key); + + // Create dimension groups + dimensionSelection.enter() + .append('g') + .attr('class', 'dimension'); + + // Update dimension group transforms + dimensionSelection.attr('transform', function (d) { + return 'translate(' + d.x + ', 0)'; + }); + + // Remove any old dimensions + dimensionSelection.exit().remove(); + + // Get category selection + var categorySelection = dimensionSelection + .selectAll('g.category') + .data(function (d) { + return d.categories; + }, key); + + // Initialize category groups + var categoryGroupEnterSelection = categorySelection + .enter() + .append('g') + .attr('class', 'category'); + + // Update category transforms + categorySelection + .attr('transform', function (d) { + return 'translate(0, ' + d.y + ')' + }); + + + // Initialize rectangle + categoryGroupEnterSelection + .append('rect') + .attr('class', 'catrect') + .attr('pointer-events', 'none'); + + + // Update rectangle + categorySelection.select('rect.catrect') + .attr('fill', 'none') + .attr('width', function (d) { + return d.width; + }) + .attr('height', function (d) { + return d.height; + }); + + styleCategoriesNoHover(categoryGroupEnterSelection); + + // Initialize color band rects + var bandSelection = categorySelection + .selectAll('rect.bandrect') + .data( + /** @param {CategoryViewModel} catViewModel*/ + function (catViewModel) { + return catViewModel.bands; + }, key); + + // Raise all update bands to the top so that fading enter/exit bands will be behind + bandSelection.each(function() {Lib.raiseToTop(this)}); + + // Update band color + bandSelection + .attr('fill', function(d) { + return d.color + }); + + var bandsSelectionEnter = bandSelection.enter() + .append('rect') + .attr('class', 'bandrect') + .attr('cursor', 'move') + .attr('stroke-opacity', 0) + .attr('fill', function(d) { + return d.color + }) + .attr('fill-opacity', 0); + + bandSelection + .attr('fill', function (d) { + return d.color; + }) + .attr('width', function (d) { + return d.width; + }) + .attr('height', function (d) { + return d.height; + }) + .attr('y', function (d) { + return d.y; + }); + + styleBandsNoHover(bandsSelectionEnter); + + bandSelection.exit().remove(); + + // Initialize category label + categoryGroupEnterSelection + .append('text') + .attr('class', 'catlabel') + .attr('pointer-events', 'none'); + + // Update category label + categorySelection.select('text.catlabel') + .attr('text-anchor', + function (d) { + if (catInRightDim(d)) { + // Place label to the right of category + return 'start'; + } else { + // Place label to the left of category + return 'end'; + } + }) + .attr('alignment-baseline', 'middle') + + .style('text-shadow', + 'rgb(255, 255, 255) -1px 1px 2px, ' + + 'rgb(255, 255, 255) 1px 1px 2px, ' + + 'rgb(255, 255, 255) 1px -1px 2px, ' + + 'rgb(255, 255, 255) -1px -1px 2px') + .attr('font-size', 10) + .style('fill', 'rgb(0, 0, 0)') + .attr('x', + function (d) { + if (catInRightDim(d)) { + // Place label to the right of category + return d.width + 5; + } else { + // Place label to the left of category + return -5; + } + }) + .attr('y', function (d) { + return d.height / 2; + }) + .text(function (d) { + return d.model.categoryLabel; + }); + + // Initialize dimension label + categoryGroupEnterSelection + .append('text') + .attr('class', 'dimlabel'); + + // Update dimension label + categorySelection.select('text.dimlabel') + .attr('text-anchor', 'middle') + .attr('alignment-baseline', 'baseline') + .attr('cursor', 'ew-resize') + .attr('font-size', 14) + .attr('x', function (d) { + return d.width / 2; + }) + .attr('y', -5) + .text(function (d, i) { + if (i === 0) { + // Add dimension label above topmost category + return d.parcatsViewModel.model.dimensions[d.model.dimensionInd].dimensionLabel; + } else { + return null + } + }); + + // Category hover + // categorySelection.select('rect.catrect') + categorySelection.selectAll('rect.bandrect') + .on('mouseover', mouseoverCategoryBand) + .on('mouseout', function (d) { + mouseoutCategory(d.parcatsViewModel) + }); + + // Remove unused categories + categorySelection.exit().remove(); + + // Setup drag + dimensionSelection.call(d3.behavior.drag() + .origin(function (d) { + return {x: d.x, y: 0} + }) + .on("dragstart", dragDimensionStart) + .on("drag", dragDimension) + .on("dragend", dragDimensionEnd)); + + + // Save off selections to view models + traceSelection.each(function (d) { + d.traceSelection = d3.select(this); + d.pathSelection = d3.select(this).selectAll('g.paths').selectAll('path.path'); + d.dimensionSelection = d3.select(this).selectAll('g.dimensions').selectAll('g.dimension') + }); + + // Remove any orphan traces + traceSelection.exit().remove(); +} + +/** + * Create / update parcat traces + * + * @param {Object} graphDiv + * @param {Object} svg + * @param {Array.} parcatsModels + * @param {Layout} layout + */ +module.exports = function(graphDiv, svg, parcatsModels, layout) { + console.log(['parcats', parcatsModels, layout]); + performPlot(parcatsModels, graphDiv, layout, svg); +}; + +/** + * Function the returns the key property of an object for use with as D3 join function + * @param d + */ +function key(d) { + return d.key; +} + + /** True if a category view model is in the right-most display dimension + * @param {CategoryViewModel} d */ + function catInRightDim(d) { + var numDims = d.parcatsViewModel.dimensions.length, + leftDimInd = d.parcatsViewModel.dimensions[numDims - 1].model.dimensionInd; + return d.model.dimensionInd === leftDimInd; + } + +/** + * @param {PathViewModel} a + * @param {PathViewModel} b + */ +function compareRawColor(a, b) { + if (a.model.rawColor > b.model.rawColor) { + return 1 + } else if (a.model.rawColor < b.model.rawColor){ + return -1 + } else { + return 0 + } +} + +/** + * Handle path mouseover + * @param {PathViewModel} d + */ +function mouseoverPath(d) { + + if (!d.parcatsViewModel.dragDimension) { + // We're not currently dragging + + if (d.parcatsViewModel.hovermode !== 'none') { + + // Raise path to top + Lib.raiseToTop(this); + + stylePathsHover(d3.select(this)); + + // Emit hover event + var points = buildPointsArrayForPath(d); + d.parcatsViewModel.graphDiv.emit('plotly_hover', {points: points, event: d3.event}); + + // Handle tooltip + if (d.parcatsViewModel.tooltip) { + + // Mouse + var hoverX = d3.mouse(this)[0]; + + // Tooltip + var gd = d.parcatsViewModel.graphDiv; + var fullLayout = gd._fullLayout; + var rootBBox = fullLayout._paperdiv.node().getBoundingClientRect(); + var graphDivBBox = d.parcatsViewModel.graphDiv.getBoundingClientRect(); + + // Find path center in path coordinates + var pathCenterX, + pathCenterY, + dimInd; + + for (dimInd = 0; dimInd < (d.leftXs.length - 1); dimInd++) { + if (d.leftXs[dimInd] + d.dimWidths[dimInd] - 2 <= hoverX && hoverX <= d.leftXs[dimInd + 1] + 2) { + var leftDim = d.parcatsViewModel.dimensions[dimInd]; + var rightDim = d.parcatsViewModel.dimensions[dimInd + 1]; + pathCenterX = (leftDim.x + leftDim.width + rightDim.x) / 2; + pathCenterY = (d.topYs[dimInd] + d.topYs[dimInd + 1] + d.height) / 2; + break + } + } + + // Find path center in root coordinates + var hoverCenterX = d.parcatsViewModel.x + pathCenterX; + var hoverCenterY = d.parcatsViewModel.y + pathCenterY; + + var textColor = tinycolor.mostReadable(d.model.color, ['black', 'white']); + + var tooltip = Fx.customHovers({ + x: hoverCenterX - rootBBox.left + graphDivBBox.left, + y: hoverCenterY - rootBBox.top + graphDivBBox.top, + text: [ + ['Count:', d.model.count].join(' '), + ['P:', (d.model.count / d.parcatsViewModel.model.count).toFixed(3)].join(' ') + ].join('
'), + + color: d.model.color, + borderColor: 'black', + fontFamily: 'Monaco, "Courier New", monospace', + fontSize: 10, + fontColor: textColor, + idealAlign: d3.event.x < hoverCenterX ? 'right' : 'left' + }, { + container: fullLayout._hoverlayer.node(), + outerContainer: fullLayout._paper.node(), + gd: gd + }); + } + } + } +} + +/** + * Handle path mouseout + * @param {PathViewModel} d + */ +function mouseoutPath(d) { + if (!d.parcatsViewModel.dragDimension) { + // We're not currently dragging + stylePathsNoHover(d3.select(this)); + + // Remove tooltip + Fx.loneUnhover(d.parcatsViewModel.graphDiv._fullLayout._hoverlayer.node()); + + // Restore path order + d.parcatsViewModel.pathSelection.sort(compareRawColor); + } +} + +/** + * Build array of point objects for a path + * + * For use in click/hover events + * @param {PathViewModel} d + */ +function buildPointsArrayForPath(d) { + var points = []; + var curveNumber = getTraceIndex(d.parcatsViewModel); + + for (var i = 0; i < d.model.valueInds.length; i++) { + var pointNumber = d.model.valueInds[i]; + points.push({ + curveNumber: curveNumber, + pointNumber: pointNumber + }) + } + return points; +} + +/** + * Handle path click + * @param {PathViewModel} d + */ +function clickPath(d) { + if (d.parcatsViewModel.hovermode !== 'none') { + var points = buildPointsArrayForPath(d); + d.parcatsViewModel.graphDiv.emit('plotly_click', {points: points, event: d3.event}); + } +} + +function stylePathsNoHover(pathSelection) { + pathSelection + .attr('fill', function(d) { + return d.model.color + }) + .attr('fill-opacity', 0.6) + .attr('stroke', 'lightgray') + .attr('stroke-width', 0.2) + .attr('stroke-opacity', 1.0); +} + +function stylePathsHover(pathSelection) { + pathSelection + .attr('fill-opacity', 0.8) + .attr('stroke', function(d) { + return tinycolor.mostReadable(d.model.color, ['black', 'white']); + }) + .attr('stroke-width', 0.3); +} + +function styleCategoryHover(categorySelection) { + categorySelection + .select('rect.catrect') + .attr('stroke', 'black') + .attr('stroke-width', 2.5); +} + +function styleCategoriesNoHover(categorySelection) { + categorySelection + .select('rect.catrect') + .attr('stroke', 'black') + .attr('stroke-width', 1) + .attr('stroke-opacity', 1); +} + +function styleBandsHover(bandsSelection) { + bandsSelection + .attr('stroke', 'black') + .attr('stroke-width', 1.5) +} + +function styleBandsNoHover(bandsSelection) { + bandsSelection + .attr('stroke', 'black') + .attr('stroke-width', 0.2) + .attr('stroke-opacity', 1.0) + .attr('fill-opacity', 1.0); +} + +/** + * Return selection of all paths that pass through the specified category + * @param {CategoryBandViewModel} catBandViewModel + */ +function selectPathsThroughCategoryBandColor(catBandViewModel) { + + var allPaths = catBandViewModel.parcatsViewModel.pathSelection; + var dimInd = catBandViewModel.categoryViewModel.model.dimensionInd; + var catInd = catBandViewModel.categoryViewModel.model.categoryInd; + + return allPaths + .filter( + /** @param {PathViewModel} pathViewModel */ + function(pathViewModel) { + return pathViewModel.model.categoryInds[dimInd] === catInd && + pathViewModel.model.color === catBandViewModel.color; + }) +} + + +/** + * Perform hover styling for all paths that pass though the specified band element's category + * + * @param {HTMLElement} bandElement + * HTML element for band + * + */ +function styleForCategoryHovermode(bandElement) { + + // Get all bands in the current category + var bandSel = d3.select(bandElement.parentNode).selectAll('rect.bandrect'); + + // Raise and style paths + bandSel.each(function(bvm) { + var paths = selectPathsThroughCategoryBandColor(bvm); + stylePathsHover(paths); + paths.each(function(d) { + // Raise path to top + Lib.raiseToTop(this); + }); + }); + + // Style category + styleCategoryHover(d3.select(bandElement.parentNode)); +} + +/** + * Perform hover styling for all paths that pass though the category of the specified band element and share the + * same color + * + * @param {HTMLElement} bandElement + * HTML element for band + * + */ +function styleForColorHovermode(bandElement) { + var bandViewModel = d3.select(bandElement).datum(); + var catPaths = selectPathsThroughCategoryBandColor(bandViewModel); + stylePathsHover(catPaths); + catPaths.each(function(d) { + // Raise path to top + Lib.raiseToTop(this); + }); + + // Style category for drag + d3.select(bandElement.parentNode) + .selectAll('rect.bandrect') + .filter(function(b) {return b.color === bandViewModel.color}) + .each(function(b) { + Lib.raiseToTop(this); + styleBandsHover(d3.select(this)); + }) +} + + +/** + * @param {HTMLElement} bandElement + * HTML element for band + * @param eventName + * Event name (plotly_hover or plotly_click) + * @param event + * Mouse Event + */ +function emitPointsEventCategoryHovermode(bandElement, eventName, event) { + // Get all bands in the current category + var bandViewModel = d3.select(bandElement).datum(); + var gd = bandViewModel.parcatsViewModel.graphDiv; + var bandSel = d3.select(bandElement.parentNode).selectAll('rect.bandrect'); + + var points = []; + bandSel.each(function(bvm) { + var paths = selectPathsThroughCategoryBandColor(bvm); + paths.each(function(pathViewModel) { + // Extend points array + Array.prototype.push.apply(points, buildPointsArrayForPath(pathViewModel)); + }) + }); + + gd.emit(eventName, {points: points, event: event}); +} + +/** + * @param {HTMLElement} bandElement + * HTML element for band + * @param eventName + * Event name (plotly_hover or plotly_click) + * @param event + * Mouse Event + */ +function emitPointsEventColorHovermode(bandElement, eventName, event) { + var bandViewModel = d3.select(bandElement).datum(); + var gd = bandViewModel.parcatsViewModel.graphDiv; + var paths = selectPathsThroughCategoryBandColor(bandViewModel); + + var points = []; + paths.each(function(pathViewModel) { + // Extend points array + Array.prototype.push.apply(points, buildPointsArrayForPath(pathViewModel)); + }); + + gd.emit(eventName, {points: points, event: event}); +} + +/** + * Create tooltip for a band element's category (for use when hovermode === 'category') + * + * @param {ClientRect} rootBBox + * Client bounding box for root of figure + * @param {HTMLElement} bandElement + * HTML element for band + * + */ +function createTooltipForCategoryHovermode(rootBBox, bandElement) { + // Build tooltips + var rectSelection = d3.select(bandElement.parentNode).select('rect.catrect'); + var rectBoundingBox = rectSelection.node().getBoundingClientRect(); + + // Models + /** @type {CategoryViewModel} */ + var catViewModel = rectSelection.datum(); + var parcatsViewModel = catViewModel.parcatsViewModel; + var dimensionModel = parcatsViewModel.model.dimensions[catViewModel.model.dimensionInd]; + + // Positions + var hoverCenterY = rectBoundingBox.top + rectBoundingBox.height / 2; + var hoverCenterX, + tooltipIdealAlign; + + if (parcatsViewModel.dimensions.length > 1 && + dimensionModel.displayInd === parcatsViewModel.dimensions.length - 1) { + + // right most dimension + hoverCenterX = rectBoundingBox.left; + tooltipIdealAlign = 'left'; + } else { + hoverCenterX = rectBoundingBox.left + rectBoundingBox.width; + tooltipIdealAlign = 'right'; + } + + var countStr = ['Count:', catViewModel.model.count].join(' '); + var pStr = ['P(' + catViewModel.model.categoryLabel + '):', + (catViewModel.model.count / catViewModel.parcatsViewModel.model.count).toFixed(3)].join(' '); + + return { + x: hoverCenterX - rootBBox.left, + y: hoverCenterY - rootBBox.top, + // name: 'NAME', + text: [ + countStr, + pStr + ].join('
'), + + color: 'lightgray', + borderColor: 'black', + fontFamily: 'Monaco, "Courier New", monospace', + fontSize: 12, + fontColor: 'black', + idealAlign: tooltipIdealAlign + }; +} + + +/** + * Create tooltip for a band element's category (for use when hovermode === 'category') + * + * @param {ClientRect} rootBBox + * Client bounding box for root of figure + * @param {HTMLElement} bandElement + * HTML element for band + * + */ +function createTooltipForColorHovermode(rootBBox, bandElement) { + + var bandBoundingBox = bandElement.getBoundingClientRect(); + + // Models + /** @type {CategoryBandViewModel} */ + var bandViewModel = d3.select(bandElement).datum(); + var catViewModel = bandViewModel.categoryViewModel; + var parcatsViewModel = catViewModel.parcatsViewModel; + var dimensionModel = parcatsViewModel.model.dimensions[catViewModel.model.dimensionInd]; + + // positions + var hoverCenterY = bandBoundingBox.y + bandBoundingBox.height / 2; + + var hoverCenterX, + tooltipIdealAlign; + if (parcatsViewModel.dimensions.length > 1 && + dimensionModel.displayInd === parcatsViewModel.dimensions.length - 1) { + // right most dimension + hoverCenterX = bandBoundingBox.left; + tooltipIdealAlign = 'left'; + } else { + hoverCenterX = bandBoundingBox.left + bandBoundingBox.width; + tooltipIdealAlign = 'right'; + } + + // Labels + var catLabel = catViewModel.model.categoryLabel; + + // Counts + var totalCount = bandViewModel.parcatsViewModel.model.count; + + var bandColorCount = 0; + bandViewModel.categoryViewModel.bands.forEach(function(b) { + if (b.color === bandViewModel.color) { + bandColorCount += b.count; + } + }); + + var catCount = catViewModel.model.count; + + var colorCount = 0; + parcatsViewModel.pathSelection.each( + /** @param {PathViewModel} pathViewModel */ + function(pathViewModel) { + if (pathViewModel.model.color === bandViewModel.color) { + colorCount += pathViewModel.model.count; + } + }); + + // Create talbe to align probability terms + var countStr = ['Count:', bandColorCount].join(' '); + var pColorAndCatLable = 'P(color ∩ ' + catLabel + '): '; + var pColorAndCatValue = (bandColorCount / totalCount).toFixed(3); + var pColorAndCatRow = pColorAndCatLable + pColorAndCatValue; + + var pCatGivenColorLabel = 'P(' + catLabel + ' | color): '; + var pCatGivenColorValue = (bandColorCount / colorCount).toFixed(3); + var pCatGivenColorRow = pCatGivenColorLabel + pCatGivenColorValue; + + var pColorGivenCatLabel = 'P(color | ' + catLabel + '): '; + var pColorGivenCatValue = (bandColorCount / catCount).toFixed(3); + var pColorGivenCatRow = pColorGivenCatLabel + pColorGivenCatValue; + + // Compute text color + var textColor = tinycolor.mostReadable(bandViewModel.color, ['black', 'white']); + + return { + x: hoverCenterX - rootBBox.left, + y: hoverCenterY - rootBBox.top, + // name: 'NAME', + text: [ + countStr, + pColorAndCatRow, pCatGivenColorRow, pColorGivenCatRow + ].join('
'), + + color: bandViewModel.color, + borderColor: 'black', + fontFamily: 'Monaco, "Courier New", monospace', + fontColor: textColor, + fontSize: 10, + idealAlign: tooltipIdealAlign + } +} + +/** + * Handle dimension mouseover + * @param {CategoryBandViewModel} bandViewModel + */ +function mouseoverCategoryBand(bandViewModel) { + if (!bandViewModel.parcatsViewModel.dragDimension) { + // We're not currently dragging + + // Mouse + var mouseY = d3.mouse(this)[1]; + if (mouseY < -1) { + // Hover is above above the category rectangle (probably the dimension title text) + return; + } + + var gd = bandViewModel.parcatsViewModel.graphDiv; + var fullLayout = gd._fullLayout; + var rootBBox = fullLayout._paperdiv.node().getBoundingClientRect(); + var hovermode = bandViewModel.parcatsViewModel.hovermode; + var showTooltip = bandViewModel.parcatsViewModel.tooltip; + + /** @type {HTMLElement} */ + var bandElement = this; + + // Handle style and events + if (hovermode === 'category') { + styleForCategoryHovermode(bandElement); + emitPointsEventCategoryHovermode(bandElement, 'plotly_hover', d3.event); + } else if (hovermode === 'color') { + styleForColorHovermode(bandElement); + emitPointsEventColorHovermode(bandElement, 'plotly_hover', d3.event); + } + + // Handle tooltip + var hoverTooltip; + if (showTooltip) { + if (hovermode === 'category') { + hoverTooltip = createTooltipForCategoryHovermode(rootBBox, bandElement); + } else if (hovermode === 'color') { + hoverTooltip = createTooltipForColorHovermode(rootBBox, bandElement); + } + } + + if (hoverTooltip) { + Fx.loneHover(hoverTooltip, { + container: fullLayout._hoverlayer.node(), + outerContainer: fullLayout._paper.node(), + gd: gd + }); + } + } +} + +/** + * Handle dimension mouseout + * @param {ParcatsViewModel} parcatsViewModel + */ +function mouseoutCategory(parcatsViewModel) { + + if (!parcatsViewModel.dragDimension) { + // We're not dragging anything + + // Reset unhovered styles + stylePathsNoHover(parcatsViewModel.pathSelection); + styleCategoriesNoHover(parcatsViewModel.dimensionSelection.selectAll('g.category')); + styleBandsNoHover(parcatsViewModel.dimensionSelection.selectAll('g.category').selectAll('rect.bandrect')); + + // Remove tooltip + Fx.loneUnhover(parcatsViewModel.graphDiv._fullLayout._hoverlayer.node()); + + // Restore path order + parcatsViewModel.pathSelection.sort(compareRawColor); + } +} + + +/** + * Handle dimension drag start + * @param {DimensionViewModel} d + */ +function dragDimensionStart(d) { + + // Save off initial drag indexes for dimension + d.dragDimensionDisplayInd = d.model.displayInd; + d.initialDragDimensionDisplayInds = d.parcatsViewModel.model.dimensions.map(function(d) {return d.displayInd}); + d.dragHasMoved = false; + + // Check for category hit + d.dragCategoryDisplayInd = null; + d3.select(this) + .selectAll('g.category') + .select('rect.catrect') + .each( + /** @param {CategoryViewModel} catViewModel */ + function (catViewModel) { + var catMouseX = d3.mouse(this)[0]; + var catMouseY = d3.mouse(this)[1]; + + + if (-2 <= catMouseX && catMouseX <= catViewModel.width + 2 && + -2 <= catMouseY && catMouseY <= catViewModel.height + 2) { + + // Save off initial drag indexes for categories + d.dragCategoryDisplayInd = catViewModel.model.displayInd; + d.initialDragCategoryDisplayInds = d.model.categories.map(function (c) { + return c.displayInd + }); + + // Initialize categories dragY to be the current y position + catViewModel.model.dragY = catViewModel.y; + + // Raise category + Lib.raiseToTop(this.parentNode); + + // Get band element + d3.select(this.parentNode) + .selectAll('rect.bandrect') + /** @param {CategoryBandViewModel} bandViewModel */ + .each(function(bandViewModel) { + if (bandViewModel.y < catMouseY && catMouseY <= bandViewModel.y + bandViewModel.height) { + d.potentialClickBand = this; + } + }) + } + }); + + // Update toplevel drag dimension + d.parcatsViewModel.dragDimension = d; + + // Remove any tooltip if any + Fx.loneUnhover(d.parcatsViewModel.graphDiv._fullLayout._hoverlayer.node()); +} + +/** + * Handle dimension drag + * @param {DimensionViewModel} d + */ +function dragDimension(d) { + d.dragHasMoved = true; + + if (d.dragDimensionDisplayInd === null) { + return + } + + var dragDimInd = d.dragDimensionDisplayInd, + prevDimInd = dragDimInd - 1, + nextDimInd = dragDimInd + 1; + + var dragDimension = d.parcatsViewModel + .dimensions[dragDimInd]; + + // Update category + if (d.dragCategoryDisplayInd !== null) { + + var dragCategory = dragDimension.categories[d.dragCategoryDisplayInd]; + + // Update dragY by dy + dragCategory.model.dragY += d3.event.dy; + var categoryY = dragCategory.model.dragY; + + // Check for category drag swaps + var catDisplayInd = dragCategory.model.displayInd; + var dimCategoryViews = dragDimension.categories; + + var catAbove = dimCategoryViews[catDisplayInd - 1]; + var catBelow = dimCategoryViews[catDisplayInd + 1]; + + // Check for overlap above + if (catAbove !== undefined) { + + if (categoryY < (catAbove.y + catAbove.height/2.0)) { + + // Swap display inds + dragCategory.model.displayInd = catAbove.model.displayInd; + catAbove.model.displayInd = catDisplayInd; + } + } + + if (catBelow !== undefined) { + + if ((categoryY + dragCategory.height) > (catBelow.y + catBelow.height/2.0)) { + + // Swap display inds + dragCategory.model.displayInd = catBelow.model.displayInd; + catBelow.model.displayInd = catDisplayInd; + } + } + + // Update category drag display index + d.dragCategoryDisplayInd = dragCategory.model.displayInd; + } + + // Update dimension position + dragDimension.model.dragX = d3.event.x; + + // Check for dimension swaps + var prevDimension = d.parcatsViewModel.dimensions[prevDimInd]; + var nextDimension = d.parcatsViewModel.dimensions[nextDimInd]; + + if (prevDimension !== undefined) { + if (dragDimension.model.dragX < (prevDimension.x + prevDimension.width)) { + + // Swap display inds + dragDimension.model.displayInd = prevDimension.model.displayInd; + prevDimension.model.displayInd = dragDimInd; + } + } + + if (nextDimension !== undefined) { + if ((dragDimension.model.dragX + dragDimension.width) > nextDimension.x) { + + // Swap display inds + dragDimension.model.displayInd = nextDimension.model.displayInd; + nextDimension.model.displayInd = d.dragDimensionDisplayInd; + } + } + + // Update drag display index + d.dragDimensionDisplayInd = dragDimension.model.displayInd; + + // Update view models + updateDimensionViewModels(d.parcatsViewModel); + updatePathViewModels(d.parcatsViewModel); + + // Update svg geometry + updateSvgCategories(d.parcatsViewModel); + updateSvgPaths(d.parcatsViewModel); +} + + +/** + * Handle dimension drag end + * @param {DimensionViewModel} d + */ +function dragDimensionEnd(d) { + + if (d.dragDimensionDisplayInd === null) { + return + } + + d3.select(this).selectAll('text').attr('font-weight', 'normal'); + + // Compute restyle command + // ----------------------- + var restyleData = {}; + var traceInd = getTraceIndex(d.parcatsViewModel); + + // ### Handle dimension reordering ### + var finalDragDimensionDisplayInds = d.parcatsViewModel.model.dimensions.map(function(d) {return d.displayInd}); + var anyDimsReordered = d.initialDragDimensionDisplayInds.some(function(initDimDisplay, dimInd) { + return initDimDisplay !== finalDragDimensionDisplayInds[dimInd] + }); + + if (anyDimsReordered) { + finalDragDimensionDisplayInds.forEach(function(finalDimDisplay, dimInd) { + restyleData['dimensions[' + dimInd + '].displayInd'] = finalDimDisplay; + }) + } + + // ### Handle category reordering ### + var anyCatsReordered = false; + if (d.dragCategoryDisplayInd !== null) { + var finalDragCategoryDisplayInds = d.model.categories.map(function (c) { + return c.displayInd + }); + + anyCatsReordered = d.initialDragCategoryDisplayInds.some(function (initCatDisplay, catInd) { + return initCatDisplay !== finalDragCategoryDisplayInds[catInd]; + }); + + if (anyCatsReordered) { + restyleData['dimensions[' + d.model.dimensionInd + '].catDisplayInds'] = [finalDragCategoryDisplayInds]; + } + } + + // Handle potential click event + // ---------------------------- + if (!d.dragHasMoved && d.potentialClickBand) { + if (d.parcatsViewModel.hovermode === 'color') { + emitPointsEventColorHovermode(d.potentialClickBand, 'plotly_click', d3.event.sourceEvent); + } else if (d.parcatsViewModel.hovermode === 'category') { + emitPointsEventCategoryHovermode(d.potentialClickBand, 'plotly_click', d3.event.sourceEvent); + } + } + + // Nullify drag states + // ------------------- + d.model.dragX = null; + if (d.dragCategoryDisplayInd !== null) { + var dragCategory = d.parcatsViewModel + .dimensions[d.dragDimensionDisplayInd] + .categories[d.dragCategoryDisplayInd]; + + dragCategory.model.dragY = null; + d.dragCategoryDisplayInd = null; + } + + d.dragDimensionDisplayInd = null; + d.parcatsViewModel.dragDimension = null; + d.dragHasMoved = null; + d.potentialClickBand = null; + + // Update view models + // ------------------ + updateDimensionViewModels(d.parcatsViewModel); + updatePathViewModels(d.parcatsViewModel); + + // Perform transition + // ------------------ + var transition = d3.transition() + .duration(300) + .ease('cubic-in-out'); + + transition + .each(function() { + updateSvgCategories(d.parcatsViewModel, true); + updateSvgPaths(d.parcatsViewModel, true); + }) + .each("end", function() { + if (anyDimsReordered || anyCatsReordered) { + // Perform restyle if the order of categories or dimensions changed + console.log('Do Plotly.restyle!'); + console.log([d.parcatsViewModel.graphDiv, restyleData, [traceInd]]); + Plotly.restyle(d.parcatsViewModel.graphDiv, restyleData, [traceInd]); + } + }); +} + +/** + * + * @param {ParcatsViewModel} parcatsViewModel + */ +function getTraceIndex(parcatsViewModel) { + var traceInd; + var allTraces = parcatsViewModel.graphDiv._fullData; + for (var i=0; i < allTraces.length; i++) { + if (parcatsViewModel.key === allTraces[i].uid) { + traceInd = i; + break; + } + } + return traceInd; +} + +/** Update the svg paths for view model + * @param {ParcatsViewModel} parcatsViewModel + * @param {boolean} hasTransition Whether to update element with transition + */ +function updateSvgPaths(parcatsViewModel, hasTransition) { + + if (hasTransition === undefined) { + hasTransition = false; + } + + function transition(selection) { + return hasTransition? selection.transition(): selection + } + + // Update binding + parcatsViewModel.pathSelection.data(function(d) { + return d.paths + }, key); + + // Update paths + transition(parcatsViewModel.pathSelection).attr('d', function(d) { + return d.svgD + }); +} + +/** Update the svg paths for view model + * @param {ParcatsViewModel} parcatsViewModel + * @param {boolean} hasTransition Whether to update element with transition + */ +function updateSvgCategories(parcatsViewModel, hasTransition) { + + if (hasTransition === undefined) { + hasTransition = false; + } + + function transition(selection) { + return hasTransition? selection.transition(): selection + } + + // Update binding + parcatsViewModel.dimensionSelection + .data(function(d) { + return d.dimensions;}, key); + + var categorySelection = parcatsViewModel.dimensionSelection + .selectAll('g.category') + .data(function(d){return d.categories;}, key); + + // Update dimension position + transition(parcatsViewModel.dimensionSelection) + .attr('transform', function(d) { + return 'translate(' + d.x + ', 0)'; + }); + + // Update category position + transition(categorySelection) + .attr('transform', function(d) { + return 'translate(0, ' + d.y + ')' + }); + + var dimLabelSelection = categorySelection.select('.dimlabel'); + + // ### Update dimension label + // Only the top-most display category should have the dimension label + dimLabelSelection + .text(function(d, i) { + if (i === 0) { + // Add dimension label above topmost category + return d.parcatsViewModel.model.dimensions[d.model.dimensionInd].dimensionLabel; + } else { + return null + } + }); + + // Update category label + // Categories in the right-most display dimension have their labels on + // the right, all others on the left + var catLabelSelection = categorySelection.select('.catlabel'); + catLabelSelection + .attr('text-anchor', + function (d) { + if (catInRightDim(d)) { + // Place label to the right of category + return 'start'; + } else { + // Place label to the left of category + return 'end'; + } + }) + .attr('x', + function (d) { + if (catInRightDim(d)) { + // Place label to the right of category + return d.width + 5; + } else { + // Place label to the left of category + return -5; + } + }); + + // Update bands + // Initialize color band rects + var bandSelection = categorySelection + .selectAll('rect.bandrect') + .data( + /** @param {CategoryViewModel} catViewModel*/ + function (catViewModel) { + return catViewModel.bands; + }, key); + + var bandsSelectionEnter = bandSelection.enter() + .append('rect') + .attr('class', 'bandrect') + .attr('cursor', 'move') + .attr('stroke-opacity', 0) + .attr('fill', function(d) { + return d.color + }) + .attr('fill-opacity', 0); + + bandSelection + .attr('fill', function (d) { + return d.color; + }) + .attr('width', function (d) { + return d.width; + }) + .attr('height', function (d) { + return d.height; + }) + .attr('y', function (d) { + return d.y; + }); + + styleBandsNoHover(bandsSelectionEnter); + + // Raise bands to the top + bandSelection.each(function() {Lib.raiseToTop(this)}); + + // Remove unused bands + bandSelection.exit().remove() +} + +/** + * Create a ParcatsViewModel traces + * @param {Object} graphDiv + * Top-level graph div element + * @param {Layout} layout + * SVG layout object + * @param {Array.} wrappedParcatsModel + * Wrapped ParcatsModel for this trace + * @return {ParcatsViewModel} + */ +function createParcatsViewModel(graphDiv, layout, wrappedParcatsModel) { + // Unwrap model + var parcatsModel = wrappedParcatsModel[0]; + + // Compute margin + var margin = layout.margin || {l: 80, r: 80, t: 100, b: 80}; + + // Compute pixel position/extents + var trace = parcatsModel.trace, + domain = trace.domain, + figureWidth = layout.width, + figureHeight = layout.height, + traceWidth = Math.floor(figureWidth * (domain.x[1] - domain.x[0])), + traceHeight = Math.floor(figureHeight * (domain.y[1] - domain.y[0])), + traceX = domain.x[0] * figureWidth + margin['l'], + traceY = layout.height - domain.y[1] * layout.height + margin['t']; + + // Handle path shape + // ----------------- + var pathShape; + if (trace.marker && trace.marker.shape){ + pathShape = trace.marker.shape; + } else { + pathShape = 'curved'; + } + + // Initialize parcatsViewModel + // paths and dimensions are missing at this point + var parcatsViewModel = { + key: trace.uid, + model: parcatsModel, + x: traceX, + y: traceY, + width: traceWidth, + height: traceHeight, + hovermode: trace.hovermode, + tooltip: trace.tooltip, + bundlecolors: trace.bundlecolors, + sortpaths: trace.sortpaths, + pathShape: pathShape, + dragDimension: null, + margin: margin, + paths: [], + dimensions: [], + graphDiv: graphDiv, + traceSelection: null, + pathSelection: null, + dimensionSelection: null + }; + + // Update dimension view models if we have at least 1 dimension + if (parcatsModel.dimensions) { + updateDimensionViewModels(parcatsViewModel); + + // Update path view models if we have at least 2 dimensions + updatePathViewModels(parcatsViewModel); + } + // Inside a categories view model + return parcatsViewModel +} + +/** + * Build the SVG string to represents a parallel categories path + * @param {Array.} leftXPositions + * Array of the x positions of the left edge of each dimension (in display order) + * @param {Array.} pathYs + * Array of the y positions of the top of the path at each dimension (in display order) + * @param {Array.} dimWidths + * Array of the widths of each dimension in display order + * @param {Number} pathHeight + * The height of the path in pixels + * @param {Number} curvature + * The curvature factor for the path. 0 results in a straight line and values greater than zero result in curved paths + * @return {string} + */ +function buildSvgPath(leftXPositions, pathYs, dimWidths, pathHeight, curvature) { + + // Compute the x midpoint of each path segment + var xRefPoints1 = [], + xRefPoints2 = [], + refInterpolator, + d; + + for (d = 0; d < dimWidths.length - 1; d++) { + refInterpolator = d3.interpolateNumber(dimWidths[d] + leftXPositions[d], leftXPositions[d + 1]); + xRefPoints1.push(refInterpolator(curvature)); + xRefPoints2.push(refInterpolator(1 - curvature)); + } + + // Move to top of path on left edge of left-most category + var svgD = 'M ' + leftXPositions[0] + ',' + pathYs[0]; + + // Horizontal line to right edge + svgD += 'l' + dimWidths[0] + ',0 '; + + // Horizontal line to right edge + for (d = 1; d < dimWidths.length; d++) { + + // Curve to left edge of category + svgD += 'C' + xRefPoints1[d-1] + ',' + pathYs[d-1] + + ' ' + xRefPoints2[d-1] + ',' + pathYs[d] + + ' ' + leftXPositions[d] + ',' + pathYs[d]; + + // svgD += 'L' + leftXPositions[d] + ',' + pathYs[d]; + + // Horizontal line to right edge + svgD += 'l' + dimWidths[d] + ',0 '; + } + + // Line down + svgD += 'l' + '0,' + pathHeight + ' '; + + // Line to left edge of right-most category + svgD += 'l -' + dimWidths[dimWidths.length - 1] + ',0 '; + + for (d = dimWidths.length - 2; d >= 0; d--) { + + // Curve to right edge of category + svgD += 'C' + xRefPoints2[d] + ',' + (pathYs[d+1] + pathHeight) + + ' ' + xRefPoints1[d] + ',' + (pathYs[d] + pathHeight) + + ' ' + (leftXPositions[d] + dimWidths[d]) + ',' + (pathYs[d] + pathHeight); + + // svgD += 'L' + (leftXPositions[d] + dimWidths[d]) + ',' + (pathYs[d] + pathHeight); + + // Horizontal line to right edge + svgD += 'l-' + dimWidths[d] + ',0 '; + } + + // Close path + svgD += 'Z'; + return svgD; +} + +/** + * Update the path view models based on the dimension view models in a ParcatsViewModel + * + * @param {ParcatsViewModel} parcatsViewModel + * View model for trace + */ +function updatePathViewModels(parcatsViewModel) { + + // Initialize an array of the y position of the top of the next path to be added to each category. + // + // nextYPositions[d][c] is the y position of the next path through category with index c of dimension with index d + var dimensionViewModels = parcatsViewModel.dimensions; + var parcatsModel = parcatsViewModel.model; + var nextYPositions = dimensionViewModels.map( + function (d) { + return d.categories.map( + function (c) { + return c.y + }) + }); + + // Array from category index to category display index for each true dimension index + var catToDisplayIndPerDim = parcatsViewModel.model.dimensions.map( + function (d) { + return d.categories.map(function(c) {return c.displayInd}) + }); + + // Array from true dimension index to dimension display index + var dimToDisplayInd = parcatsViewModel.model.dimensions.map(function(d) {return d.displayInd}); + var displayToDimInd = parcatsViewModel.dimensions.map(function(d) {return d.model.dimensionInd}); + + // Array of the x position of the left edge of the rectangles for each dimension + var leftXPositions = dimensionViewModels.map( + function (d) { + return d.x + }); + + // Compute dimension widths + var dimWidths = dimensionViewModels.map(function(d) {return d.width}); + + // Build sorted Array of PathModel objects + var pathModels = []; + for (var p in parcatsModel.paths) { + if (parcatsModel.paths.hasOwnProperty(p)) { + pathModels.push(parcatsModel.paths[p]) + } + } + + // Compute category display inds to use for sorting paths + function pathDisplayCategoryInds(pathModel) { + var dimensionInds = pathModel.categoryInds.map(function(catInd, dimInd){return catToDisplayIndPerDim[dimInd][catInd]}); + var displayInds = displayToDimInd.map(function(dimInd) { + return dimensionInds[dimInd] + }); + return displayInds; + } + + // Sort in ascending order by display index array + pathModels.sort(function(v1, v2) { + + // Build display inds for each path + var sortArray1 = pathDisplayCategoryInds(v1); + var sortArray2 = pathDisplayCategoryInds(v2); + + // Handle path sort order + if (parcatsViewModel.sortpaths === 'backward') { + sortArray1.reverse(); + sortArray2.reverse(); + } + + // Append the first value index of the path to break ties + sortArray1.push(v1.valueInds[0]); + sortArray2.push(v2.valueInds[0]); + + // Handle color bundling + if (parcatsViewModel.bundlecolors) { + // Prepend sort array with the raw color value + sortArray1.unshift(v1.rawColor); + sortArray2.unshift(v2.rawColor); + } + + // colors equal, sort by display categories + if (sortArray1 < sortArray2) { + return -1; + } + if (sortArray1 > sortArray2) { + return 1; + } + + return 0; + }); + + // Create path models + var pathViewModels = new Array(pathModels.length), + totalCount = dimensionViewModels[0].model.count, + totalHeight = dimensionViewModels[0].categories + .map(function(c) { + return c.height}).reduce( + function(v1, v2) {return v1+v2}); + + + for(var pathNumber=0; pathNumber < pathModels.length; pathNumber++) { + var pathModel = pathModels[pathNumber]; + + var pathHeight; + if (totalCount > 0) { + pathHeight = totalHeight * (pathModel.count / totalCount); + } else { + pathHeight = 0; + } + + // Build path y coords + var pathYs = new Array(nextYPositions.length); + for (var d=0; d < pathModel.categoryInds.length; d++) { + var catInd = pathModel.categoryInds[d]; + var catDisplayInd = catToDisplayIndPerDim[d][catInd]; + var dimDisplayInd = dimToDisplayInd[d]; + + // Update next y position + pathYs[dimDisplayInd] = nextYPositions[dimDisplayInd][catDisplayInd]; + nextYPositions[dimDisplayInd][catDisplayInd] += pathHeight; + + // Update category color information + var catViewModle = parcatsViewModel.dimensions[dimDisplayInd].categories[catDisplayInd]; + var numBands = catViewModle.bands.length; + var lastCatBand = catViewModle.bands[numBands-1]; + + if (lastCatBand === undefined || pathModel.rawColor !== lastCatBand.rawColor) { + // Create a new band + var bandY = lastCatBand === undefined? 0: lastCatBand.y + lastCatBand.height; + catViewModle.bands.push({ + key: bandY, + color: pathModel.color, + rawColor: pathModel.rawColor, + height: pathHeight, + width: catViewModle.width, + count: pathModel.count, + y: bandY, + categoryViewModel: catViewModle, + parcatsViewModel: parcatsViewModel + }); + } else { + // Extend current band + var currentBand = catViewModle.bands[numBands-1]; + currentBand.height += pathHeight; + currentBand.count += pathModel.count; + } + } + + // build svg path + var svgD; + if (parcatsViewModel.pathShape === 'curved') { + svgD = buildSvgPath(leftXPositions, pathYs, dimWidths, pathHeight, 0.5); + } else { + svgD = buildSvgPath(leftXPositions, pathYs, dimWidths, pathHeight, 0); + } + + pathViewModels[pathNumber] = { + key: pathModel.categoryInds + '-' + pathModel.valueInds[0], + model: pathModel, + height: pathHeight, + leftXs: leftXPositions, + topYs: pathYs, + dimWidths: dimWidths, + svgD: svgD, + parcatsViewModel: parcatsViewModel + } + } + + parcatsViewModel['paths'] = pathViewModels; + + // * @property key + // * Unique key for this model + // * @property {PathModel} model + // * Source path model + // * @property {Number} height + // * Height of this path (pixels) + // * @property {String} svgD + // * SVG path "d" attribute string +} + +/** + * Update the dimension view models based on the dimension models in a ParcatsViewModel + * + * @param {ParcatsViewModel} parcatsViewModel + * View model for trace + */ +function updateDimensionViewModels(parcatsViewModel) { + + // Compute dimension ordering + var dimensionsIndInfo = parcatsViewModel.model.dimensions.map(function(d, i) { + return {displayInd: d.displayInd, dimensionInd: d.dimensionInd} + }); + + dimensionsIndInfo.sort(function(a, b) { + return a.displayInd - b.displayInd + }); + + var dimensions = []; + for (var displayInd in dimensionsIndInfo) { + var dimensionInd = dimensionsIndInfo[displayInd].dimensionInd; + var dimModel = parcatsViewModel.model.dimensions[dimensionInd]; + dimensions.push(createDimensionViewModel(parcatsViewModel, dimModel)); + } + + parcatsViewModel['dimensions'] = dimensions; +} + +/** + * Create a parcats DimensionViewModel + * + * @param {ParcatsViewModel} parcatsViewModel + * View model for trace + * @param {DimensionModel} dimensionModel + * @return {DimensionViewModel} + */ +function createDimensionViewModel(parcatsViewModel, dimensionModel) { + + // Compute dimension x position + var categoryLabelPad = 40, + dimWidth = 16, + numDimensions = parcatsViewModel.model.dimensions.length, + displayInd = dimensionModel.displayInd; + + // Compute x coordinate values + var dimDx, + dimX0, + dimX; + + if (numDimensions > 1) { + dimDx = (parcatsViewModel.width - 2*categoryLabelPad - dimWidth) / (numDimensions - 1); + } else { + dimDx = 0 + } + dimX0 = categoryLabelPad; + dimX = dimX0 + dimDx*displayInd; + + // Compute categories + var categories = [], + maxCats = parcatsViewModel.model.maxCats, + numCats = dimensionModel.categories.length, + catSpacing = 8, + totalCount = dimensionModel.count, + totalHeight = parcatsViewModel.height - catSpacing * (maxCats - 1), + nextCatHeight, + nextCatModel, + nextCat, + catInd, + catDisplayInd; + + // Compute starting Y offset + var nextCatY = (maxCats - numCats) * catSpacing / 2.0; + + // Compute category ordering + var categoryIndInfo = dimensionModel.categories.map(function(c, i) { + return {displayInd: c.displayInd, categoryInd: c.categoryInd} + }); + + categoryIndInfo.sort(function(a, b) { + return a.displayInd - b.displayInd + }); + + for (catDisplayInd = 0; catDisplayInd < numCats; catDisplayInd++) { + catInd = categoryIndInfo[catDisplayInd].categoryInd; + nextCatModel = dimensionModel.categories[catInd]; + + if (totalCount > 0) { + nextCatHeight = (nextCatModel.count / totalCount) * totalHeight; + } else { + nextCatHeight = 0; + } + + nextCat = { + key: catInd, + model: nextCatModel, + width: dimWidth, + height: nextCatHeight, + y: nextCatModel.dragY !== null? nextCatModel.dragY: nextCatY, + bands: [], + parcatsViewModel: parcatsViewModel + }; + + nextCatY = nextCatY + nextCatHeight + catSpacing; + categories.push(nextCat) + } + + return { + key: dimensionModel.dimensionInd, + x: dimensionModel.dragX !== null? dimensionModel.dragX: dimX, + y: 0, + width: dimWidth, + model: dimensionModel, + categories: categories, + parcatsViewModel: parcatsViewModel, + dragCategoryDisplayInd: null, + dragDimensionDisplayInd: null, + initialDragDimensionDisplayInds: null, + initialDragCategoryDisplayInds: null, + dragHasMoved: null, + potentialClickBand: null + } +} + +// JSDoc typedefs +// ============== +/** + * @typedef {Object} Layout + * Object containing svg layout information + * + * @property {Number} width (pixels) + * Usable width for Figure (after margins are removed) + * @property {Number} height (pixels) + * Usable height for Figure (after margins are removed) + * @property {Margin} margin + * Margin around the Figure (pixels) + */ + +/** + * @typedef {Object} Margin + * Object containing padding information in pixels + * + * @property {Number} t + * Top margin + * @property {Number} r + * Right margin + * @property {Number} b + * Bottom margin + * @property {Number} l + * Left margin + */ + +/** + * @typedef {Object} ParcatsViewModel + * Object containing calculated parcats view information + * + * These are quantities that require Layout information to calculate + * @property key + * Unique key for this model + * @property {ParcatsModel} model + * Source parcats model + * @property {Array.} dimensions + * Array of dimension view models + * @property {Number} width + * Width for this trace (pixels) + * @property {Number} height + * Height for this trace (pixels) + * @property {Number} x + * X position of this trace with respect to the Figure (pixels) + * @property {Number} y + * Y position of this trace with respect to the Figure (pixels) + * @property {String} hovermode + * Hover mode. One of: 'none', 'category', or 'color' + * @property {Boolean} tooltip + * Whether to display a tooltip for the 'category' or 'color' hovermodes (has no effect if 'hovermode' is 'none') + * @property {Boolean} bundlecolors + * Whether paths should be sorted so that like colors are bundled together as they pass through categories + * @property {String} sortpaths + * If 'forward' then sort paths based on dimensions from left to right. If 'backward' sort based on dimensions + * from right to left + * @property {String} pathShape + * The shape of the paths. Either 'straight' or 'curved'. + * @property {DimensionViewModel|null} dragDimension + * Dimension currently being dragged. Null if no drag in progress + * @property {Margin} margin + * Margin around the Figure + * @property {Object} graphDiv + * Top-level graph div element + * @property {Object} traceSelection + * D3 selection of this view models trace group element + * @property {Object} pathSelection + * D3 selection of this view models path elements + * @property {Object} dimensionSelection + * D3 selection of this view models dimension group element + */ + +/** + * @typedef {Object} DimensionViewModel + * Object containing calculated parcats dimension view information + * + * These are quantities that require Layout information to calculate + * @property key + * Unique key for this model + * @property {DimensionModel} model + * Source dimension model + * @property {Number} x + * X position of the center of this dimension with respect to the Figure (pixels) + * @property {Number} y + * Y position of the top of this dimension with respect to the Figure (pixels) + * @property {Number} width + * Width of categories in this dimension (pixels) + * @property {ParcatsViewModel} parcatsViewModel + * The parent trace's view model + * @property {Array.} categories + * Dimensions category view models + * @property {Number|null} dragCategoryDisplayInd + * Display index of category currently being dragged. null if no category is being dragged + * @property {Number|null} dragDimensionDisplayInd + * Display index of the dimension being dragged. null if no dimension is being dragged + * @property {Array.|null} initialDragDimensionDisplayInds + * Dimensions display indexes at the beginning of the current drag. null if no dimension is being dragged + * @property {Array.|null} initialDragCategoryDisplayInds + * Category display indexes for the at the beginning of the current drag. null if no category is being dragged + * @property {HTMLElement} potentialClickBand + * Band under mouse when current drag began. If no drag movement takes place then a click will be emitted for this + * band. Null if not drag in progress. + * @property {Boolean} dragHasMoved + * True if there is an active drag and the drag has moved. If drag doesn't move before being ended then + * this may be interpreted as a click. Null if no drag in progress + */ + +/** + * @typedef {Object} CategoryViewModel + * Object containing calculated parcats category view information + * + * These are quantities that require Layout information to calculate + * @property key + * Unique key for this model + * @property {CategoryModel} model + * Source category model + * @property {Number} width + * Width for this category (pixels) + * @property {Number} height + * Height for this category (pixels) + * @property {Number} y + * Y position of this cateogry with respect to the Figure (pixels) + * @property {Array.} bands + * Array of color bands inside the category + * @property {ParcatsViewModel} parcatsViewModel + * The parent trace's view model + */ + +/** + * @typedef {Object} CategoryBandViewModel + * Object containing calculated category band information. A category band is a region inside a category covering + * paths of a single color + * + * @property key + * Unique key for this model + * @property color + * Band color + * @property rawColor + * Raw color value for band + * @property {Number} width + * Band width + * @property {Number} height + * Band height + * @property {Number} y + * Y position of top of the band with respect to the category + * @property {Number} count + * The number of samples represented by the band + * @property {CategoryViewModel} categoryViewModel + * The parent categorie's view model + * @property {ParcatsViewModel} parcatsViewModel + * The parent trace's view model + */ + +/** + * @typedef {Object} PathViewModel + * Object containing calculated parcats path view information + * + * These are quantities that require Layout information to calculate + * @property key + * Unique key for this model + * @property {PathModel} model + * Source path model + * @property {Number} height + * Height of this path (pixels) + * @property {Array.} leftXs + * The x position of the left edge of each display dimension + * @property {Array.} topYs + * The y position of the top of the path for each display dimension + * @property {Array.} dimWidths + * The width of each display dimension + * @property {String} svgD + * SVG path "d" attribute string + * @property {ParcatsViewModel} parcatsViewModel + * The parent trace's view model + */ diff --git a/src/traces/parcats/plot.js b/src/traces/parcats/plot.js new file mode 100644 index 00000000000..35a37311bf8 --- /dev/null +++ b/src/traces/parcats/plot.js @@ -0,0 +1,43 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + + +var parcats = require('./parcats'); + +/** + * Create / update parcat traces + * + * @param {Object} graphDiv + * @param {Array.} parcatsModels + */ +module.exports = function plot(graphDiv, parcatsModels, transitionOpts, makeOnCompleteCallback) { + console.log(['plot', parcatsModels, transitionOpts, makeOnCompleteCallback]); + var fullLayout = graphDiv._fullLayout, + svg = fullLayout._paper, + size = fullLayout._size; + + parcats( + graphDiv, + svg, + parcatsModels, + { + width: size.w, + height: size.h, + margin: { + t: size.t, + r: size.r, + b: size.b, + l: size.l + } + }, + transitionOpts, + makeOnCompleteCallback + ); +}; diff --git a/test/image/mocks/parcats_basic.json b/test/image/mocks/parcats_basic.json new file mode 100644 index 00000000000..b52442186ed --- /dev/null +++ b/test/image/mocks/parcats_basic.json @@ -0,0 +1,16 @@ +{ + "data": [ + {"type": "parcats", + "domain": {"x": [0.125, 0.625],"y": [0.25, 0.75]}, + "dimensions":[ + {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1]}, + {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"]}, + {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}]} + ], + "layout": { + "height": 602, + "width": 592, + "margin": { + "l": 40, "r": 40, "t": 50, "b": 40 + }} +} diff --git a/test/image/mocks/parcats_bundled.json b/test/image/mocks/parcats_bundled.json new file mode 100644 index 00000000000..8f923c82b80 --- /dev/null +++ b/test/image/mocks/parcats_bundled.json @@ -0,0 +1,21 @@ +{ + "data": [ + {"type": "parcats", + "domain": {"x": [0.125, 0.625],"y": [0.25, 0.75]}, + "dimensions":[ + {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1]}, + {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"]}, + {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}], + "bundlecolors": true, + "marker": { + "color": [0, 0, 1, 1, 0, 1, 0, 0, 0] + } + } + ], + "layout": { + "height": 602, + "width": 592, + "margin": { + "l": 40, "r": 40, "t": 50, "b": 40 + }} +} diff --git a/test/image/mocks/parcats_bundled_reversed.json b/test/image/mocks/parcats_bundled_reversed.json new file mode 100644 index 00000000000..ee6b6c34d1a --- /dev/null +++ b/test/image/mocks/parcats_bundled_reversed.json @@ -0,0 +1,22 @@ +{ + "data": [ + {"type": "parcats", + "domain": {"x": [0.125, 0.625],"y": [0.25, 0.75]}, + "dimensions":[ + {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1]}, + {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"]}, + {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}], + "bundlecolors": true, + "sortpaths": "backward", + "marker": { + "color": [0, 0, 1, 1, 0, 1, 0, 0, 0] + } + } + ], + "layout": { + "height": 602, + "width": 592, + "margin": { + "l": 40, "r": 40, "t": 50, "b": 40 + }} +} diff --git a/test/image/mocks/parcats_reordered.json b/test/image/mocks/parcats_reordered.json new file mode 100644 index 00000000000..7d61464e431 --- /dev/null +++ b/test/image/mocks/parcats_reordered.json @@ -0,0 +1,17 @@ +{ + "data": [ + {"type": "parcats", + "domain": {"x": [0.125, 0.625],"y": [0.25, 0.75]}, + "dimensions": [ + {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1], "displayInd": 0}, + {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"], "displayInd": 2, + "catDisplayInds": [1, 2, 0], "CatValues": ["A", "B", "C"]}, + {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11], "displayInd": 1}]} + ], + "layout": { + "height": 602, + "width": 592, + "margin": { + "l": 40, "r": 40, "t": 50, "b": 40 + }} +} diff --git a/test/image/mocks/parcats_unbundled.json b/test/image/mocks/parcats_unbundled.json new file mode 100644 index 00000000000..d857574df4a --- /dev/null +++ b/test/image/mocks/parcats_unbundled.json @@ -0,0 +1,22 @@ +{ + "data": [ + {"type": "parcats", + "domain": {"x": [0.125, 0.625],"y": [0.25, 0.75]}, + "dimensions":[ + {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1]}, + {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"]}, + {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}], + "bundlecolors": false, + "marker": { + "color": [0, 0, 1, 1, 0, 1, 0, 0, 0], + "shape": "straight" + } + } + ], + "layout": { + "height": 602, + "width": 592, + "margin": { + "l": 40, "r": 40, "t": 50, "b": 40 + }} +} diff --git a/test/jasmine/tests/parcats_test.js b/test/jasmine/tests/parcats_test.js new file mode 100644 index 00000000000..7c0d9263f85 --- /dev/null +++ b/test/jasmine/tests/parcats_test.js @@ -0,0 +1,788 @@ +var Plotly = require('@lib/index'); +var Lib = require('@src/lib'); + +var d3 = require('d3'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); +var click = require('../assets/click'); +var getClientPosition = require('../assets/get_client_position'); +var mouseEvent = require('../assets/mouse_event'); +var delay = require('../assets/delay'); + +var customAssertions = require('../assets/custom_assertions'); +var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle; +var assertHoverLabelContent = customAssertions.assertHoverLabelContent; + +var CALLBACK_DELAY = 500; + +// Validation helpers +// ================== +function checkDimensionCalc(gd, dimInd, dimProps) { + /** @type {ParcatsModel} */ + var calcdata = gd.calcdata[0][0]; + var dimension = calcdata.dimensions[dimInd]; + for(var dimProp in dimProps) { + if (dimProps.hasOwnProperty(dimProp)) { + expect(dimension[dimProp]).toEqual(dimProps[dimProp]); + } + } +} + +function checkCategoryCalc(gd, dimInd, catInd, catProps) { + /** @type {ParcatsModel} */ + var calcdata = gd.calcdata[0][0]; + var dimension = calcdata.dimensions[dimInd]; + var category = dimension.categories[catInd]; + for(var catProp in catProps) { + if (catProps.hasOwnProperty(catProp)) { + expect(category[catProp]).toEqual(catProps[catProp]); + } + } +} + +function checkParcatsModelView(gd) { + var fullLayout = gd._fullLayout; + var size = fullLayout._size; + + // Make sure we have a 512x512 area for traces + expect(size.h).toEqual(512); + expect(size.w).toEqual(512); + + /** @type {ParcatsViewModel} */ + var parcatsViewModel = d3.select('g.trace.parcats').datum(); + + // Check location/size of this trace inside overall traces area + expect(parcatsViewModel.x).toEqual(64 + margin.r); + expect(parcatsViewModel.y).toEqual(128 + margin.t); + expect(parcatsViewModel.width).toEqual(256); + expect(parcatsViewModel.height).toEqual(256); + + // Check location of dimensions + expect(parcatsViewModel.dimensions[0].x).toEqual(categoryLabelPad); + expect(parcatsViewModel.dimensions[0].y).toEqual(0); + + expect(parcatsViewModel.dimensions[1].x).toEqual(categoryLabelPad + dimDx); + expect(parcatsViewModel.dimensions[1].y).toEqual(0); + + expect(parcatsViewModel.dimensions[2].x).toEqual(categoryLabelPad + 2*dimDx); + expect(parcatsViewModel.dimensions[2].y).toEqual(0); + + // Check location of categories + /** @param {Array.} categories */ + function checkCategoryPositions(categories) { + var nextY = (3 - categories.length) * catSpacing / 2; + for (var c=0; c Date: Fri, 27 Jul 2018 12:57:32 -0400 Subject: [PATCH 02/25] color attribute fixes for rebase on 1.39.3 --- src/traces/parcats/attributes.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index 94648ceccf4..82a187957f8 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -10,7 +10,7 @@ var fontAttrs = require('../../plots/font_attributes'); var extendFlat = require('../../lib/extend').extendFlat; -var colorAttributes = require('../../components/colorscale/color_attributes'); +var colorAttributes = require('../../components/colorscale/attributes'); var scatterAttrs = require('../scatter/attributes'); var scatterMarkerAttrs = scatterAttrs.marker; @@ -18,7 +18,7 @@ var colorbarAttrs = require('../../components/colorbar/attributes'); var marker = extendFlat({ editType: 'calc' - }, colorAttributes('marker', 'calc'), + }, colorAttributes('marker', {editType: 'calc'}), { showscale: scatterMarkerAttrs.showscale, colorbar: colorbarAttrs, From ee7fd1780e947438d05245b2bf930cfb59e05113 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 27 Jul 2018 13:19:25 -0400 Subject: [PATCH 03/25] lint fixes --- src/components/fx/hover.js | 6 +- src/traces/parcats/attributes.js | 23 +- src/traces/parcats/calc.js | 133 +++------ src/traces/parcats/constants.js | 44 +-- src/traces/parcats/defaults.js | 11 +- src/traces/parcats/parcats.js | 438 ++++++++++++++--------------- src/traces/parcats/plot.js | 1 - test/jasmine/tests/parcats_test.js | 97 +++---- 8 files changed, 320 insertions(+), 433 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index a1cf4ada20b..c2e616b253b 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -156,7 +156,7 @@ exports.loneHover = function loneHover(hoverItem, opts) { // TODO: replace loneHover? exports.customHovers = function customHovers(hoverItems, opts) { - if (!Array.isArray(hoverItems)) { + if(!Array.isArray(hoverItems)) { hoverItems = [hoverItems]; } @@ -210,11 +210,11 @@ exports.customHovers = function customHovers(hoverItems, opts) { var tooltipSpacing = 5; var lastBottomY = 0; hoverLabel - .sort(function(a, b) {return a.y0 - b.y0}) + .sort(function(a, b) {return a.y0 - b.y0;}) .each(function(d) { var topY = d.y0 - d.by / 2; - if ((topY - tooltipSpacing) < lastBottomY ) { + if((topY - tooltipSpacing) < lastBottomY) { d.offset = (lastBottomY - topY) + tooltipSpacing; } else { d.offset = 0; diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index 82a187957f8..3962dbf21b9 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -8,7 +8,6 @@ 'use strict'; -var fontAttrs = require('../../plots/font_attributes'); var extendFlat = require('../../lib/extend').extendFlat; var colorAttributes = require('../../components/colorscale/attributes'); @@ -17,8 +16,8 @@ var scatterMarkerAttrs = scatterAttrs.marker; var colorbarAttrs = require('../../components/colorbar/attributes'); var marker = extendFlat({ - editType: 'calc' - }, colorAttributes('marker', {editType: 'calc'}), + editType: 'calc' +}, colorAttributes('marker', {editType: 'calc'}), { showscale: scatterMarkerAttrs.showscale, colorbar: colorbarAttrs, @@ -169,15 +168,15 @@ module.exports = { marker: marker, counts: { - valType: 'number', - min: 0, - dflt: 1, - arrayOk: true, - role: 'info', - editType: 'calc', - description: [ - 'The number of observations represented by each state. Defaults to 1 so that each state represents ' + + valType: 'number', + min: 0, + dflt: 1, + arrayOk: true, + role: 'info', + editType: 'calc', + description: [ + 'The number of observations represented by each state. Defaults to 1 so that each state represents ' + 'one observation' - ] + ] } }; diff --git a/src/traces/parcats/calc.js b/src/traces/parcats/calc.js index 740d64061ac..4384e213c24 100644 --- a/src/traces/parcats/calc.js +++ b/src/traces/parcats/calc.js @@ -32,18 +32,16 @@ module.exports = function calc(gd, trace) { // Process inputs // -------------- - if (trace.dimensions.length === 0) { + if(trace.dimensions.length === 0) { // No dimensions specified. Nothing to compute - return [] + return []; } - console.log(['calc', trace, gd.data, gd._fullData]); - // Compute unique information // -------------------------- // UniqueInfo per dimension var uniqueInfoDims = trace.dimensions.map(function(dim) { - return getUniqueInfo(dim.values, dim.catValues) + return getUniqueInfo(dim.values, dim.catValues); }); // Number of values and counts @@ -55,7 +53,7 @@ module.exports = function calc(gd, trace) { var counts, count, totalCount; - if (Lib.isArrayOrTypedArray(trace.counts)) { + if(Lib.isArrayOrTypedArray(trace.counts)) { counts = trace.counts; } else { counts = [trace.counts]; @@ -77,7 +75,7 @@ module.exports = function calc(gd, trace) { var markerColorscale; // Process colorscale - if (marker) { + if(marker) { if(hasColorscale(trace, 'marker')) { colorscaleCalc(trace, trace.marker.color, 'marker', 'c'); } @@ -89,9 +87,9 @@ module.exports = function calc(gd, trace) { // Build color generation function function getMarkerColorInfo(index) { var value; - if (!marker) { + if(!marker) { value = parcatConstants.defaultColor; - } else if (Array.isArray(marker.color)) { + } else if(Array.isArray(marker.color)) { value = marker.color[index % marker.color.length]; } else { value = marker.color; @@ -100,8 +98,6 @@ module.exports = function calc(gd, trace) { return {color: markerColorscale(value), rawColor: value}; } - console.log(markerColorscale); - // Build/Validate category labels/order // ------------------------------------ // properties: catValues, catorder, catlabels @@ -121,7 +117,7 @@ module.exports = function calc(gd, trace) { // a) Set catValues to unique catValues // b) Set carorder to 0 to catValues.length // - //uniqueInfoDims[0].uniqueValues + // uniqueInfoDims[0].uniqueValues // Category order logic // 1) @@ -132,16 +128,18 @@ module.exports = function calc(gd, trace) { var pathModels = {}; // Category inds array for each dimension - var categoryIndsDims = uniqueInfoDims.map(function(di) {return di.inds}); + var categoryIndsDims = uniqueInfoDims.map(function(di) {return di.inds;}); // Initialize total count totalCount = 0; + var valueInd; + var d; - for (var valueInd=0; valueInd < numValues; valueInd++) { + for(valueInd = 0; valueInd < numValues; valueInd++) { // Category inds for this input value across dimensions var categoryIndsPath = []; - for (var d=0; d < categoryIndsDims.length; d++) { + for(d = 0; d < categoryIndsDims.length; d++) { categoryIndsPath.push(categoryIndsDims[d][valueInd]); } @@ -149,7 +147,7 @@ module.exports = function calc(gd, trace) { count = counts[valueInd % counts.length]; // Update total count - totalCount+= count; + totalCount += count; // Path color var pathColorInfo = getMarkerColorInfo(valueInd); @@ -158,7 +156,7 @@ module.exports = function calc(gd, trace) { var pathKey = categoryIndsPath + '-' + pathColorInfo.rawColor; // Create / Update PathModel - if (pathModels[pathKey] === undefined) { + if(pathModels[pathKey] === undefined) { pathModels[pathKey] = createPathModel(categoryIndsPath, pathColorInfo.color, pathColorInfo.rawColor); @@ -175,16 +173,16 @@ module.exports = function calc(gd, trace) { }); - for (valueInd=0; valueInd < numValues; valueInd++) { + for(valueInd = 0; valueInd < numValues; valueInd++) { count = counts[valueInd % counts.length]; - for (d=0; d < dimensionModels.length; d++) { + for(d = 0; d < dimensionModels.length; d++) { var catInd = uniqueInfoDims[d].inds[valueInd]; var cats = dimensionModels[d].categories; - if (cats[catInd] === undefined) { + if(cats[catInd] === undefined) { var catLabel = trace.dimensions[d].catLabels[catInd]; var displayInd = trace.dimensions[d].catDisplayInds[catInd]; @@ -199,17 +197,6 @@ module.exports = function calc(gd, trace) { return wrap(createParcatsModel(dimensionModels, pathModels, totalCount)); }; - -// Utilities -// ========= -function getValue(arrayOrScalar, index) { - var value; - if(!Array.isArray(arrayOrScalar)) value = arrayOrScalar; - else if(index < arrayOrScalar.length) value = arrayOrScalar[index]; - return value; -} - - // Models // ====== @@ -239,9 +226,9 @@ function getValue(arrayOrScalar, index) { */ function createParcatsModel(dimensions, paths, count) { var maxCats = dimensions - .map(function(d) {return d.categories.length}) - .reduce(function(v1, v2) {return Math.max(v1, v2)}); - return {dimensions: dimensions, paths: paths, trace: undefined, maxCats: maxCats, count: count} + .map(function(d) {return d.categories.length;}) + .reduce(function(v1, v2) {return Math.max(v1, v2);}); + return {dimensions: dimensions, paths: paths, trace: undefined, maxCats: maxCats, count: count}; } // Dimension Model @@ -281,7 +268,7 @@ function createDimensionModel(dimensionInd, displayInd, dimensionLabel, count) { count: count, categories: [], dragX: null - } + }; } // Category Model @@ -324,7 +311,7 @@ function createCategoryModel(dimensionInd, categoryInd, displayInd, categoryLabe valueInds: [], count: 0, dragY: null - } + }; } /** @@ -337,7 +324,7 @@ function createCategoryModel(dimensionInd, categoryInd, displayInd, categoryLabe */ function updateCategoryModel(categoryModel, valueInd, count) { categoryModel.valueInds.push(valueInd); - categoryModel.count+= count; + categoryModel.count += count; } @@ -375,7 +362,7 @@ function createPathModel(categoryInds, color, rawColor) { rawColor: rawColor, valueInds: [], count: 0 - } + }; } /** @@ -388,7 +375,7 @@ function createPathModel(categoryInds, color, rawColor) { */ function updatePathModel(pathModel, valueInd, count) { pathModel.valueInds.push(valueInd); - pathModel.count+= count; + pathModel.count += count; } // Unique calculations @@ -423,11 +410,11 @@ function updatePathModel(pathModel, valueInd, count) { function getUniqueInfo(values, uniqueValues) { // Initialize uniqueValues if not specified - if (uniqueValues === undefined || uniqueValues === null) { + if(uniqueValues === undefined || uniqueValues === null) { uniqueValues = []; } else { // Shallow copy so append below doesn't alter input array - uniqueValues = uniqueValues.map(function(e){return e}); + uniqueValues = uniqueValues.map(function(e) {return e;}); } // Initialize Variables @@ -456,17 +443,17 @@ function getUniqueInfo(values, uniqueValues) { uniqueValueCounts[item]++; itemInd = uniqueValueInds[item]; } - inds.push(itemInd) + inds.push(itemInd); } // Build UniqueInfo - var uniqueCounts = uniqueValues.map(function (v) { return uniqueValueCounts[v] }); + var uniqueCounts = uniqueValues.map(function(v) { return uniqueValueCounts[v]; }); return { uniqueValues: uniqueValues, uniqueCounts: uniqueCounts, inds: inds - } + }; } @@ -477,11 +464,11 @@ function getUniqueInfo(values, uniqueValues) { * @param {Object} trace */ function validateDimensionDisplayInds(trace) { - var displayInds = trace.dimensions.map(function(dim) {return dim.displayInd}); - if (!isRangePermutation(displayInds)) { - trace.dimensions.forEach(function (dim, i){ + var displayInds = trace.dimensions.map(function(dim) {return dim.displayInd;}); + if(!isRangePermutation(displayInds)) { + trace.dimensions.forEach(function(dim, i) { dim.displayInd = i; - }) + }); } } @@ -500,21 +487,21 @@ function validateCategoryProperties(dim, uniqueInfoDim) { dim.catValues = uniqueDimVals; // Handle catDisplayInds - if (dim.catDisplayInds.length !== uniqueDimVals.length || !isRangePermutation(dim.catDisplayInds)) { - dim.catDisplayInds = uniqueDimVals.map(function(v, i) {return i}); + if(dim.catDisplayInds.length !== uniqueDimVals.length || !isRangePermutation(dim.catDisplayInds)) { + dim.catDisplayInds = uniqueDimVals.map(function(v, i) {return i;}); } // Handle catLabels - if (dim.catLabels === null || dim.catLabels === undefined) { + if(dim.catLabels === null || dim.catLabels === undefined) { dim.catLabels = []; } else { // Shallow copy to avoid modifying input array - dim.catLabels = dim.catLabels.map(function(v) {return v}); + dim.catLabels = dim.catLabels.map(function(v) {return v;}); } // Extend catLabels with elements from uniqueInfoDim.uniqueValues - for (var i=dim.catLabels.length; i < uniqueInfoDim.uniqueValues.length; i++) { - dim.catLabels.push(uniqueInfoDim.uniqueValues[i]) + for(var i = dim.catLabels.length; i < uniqueInfoDim.uniqueValues.length; i++) { + dim.catLabels.push(uniqueInfoDim.uniqueValues[i]); } } @@ -526,14 +513,14 @@ function validateCategoryProperties(dim, uniqueInfoDim) { function isRangePermutation(inds) { var indsSpecified = new Array(inds.length); - for (var i=0; i < inds.length; i++) { + for(var i = 0; i < inds.length; i++) { // Check for out of bounds - if (inds[i] < 0 || inds[i] >= inds.length) { + if(inds[i] < 0 || inds[i] >= inds.length) { return false; } // Check for collisions with already specified index - if (indsSpecified[inds[i]] !== undefined) { + if(indsSpecified[inds[i]] !== undefined) { return false; } @@ -541,35 +528,5 @@ function isRangePermutation(inds) { } // Nothing out of bounds and no collisions. We have a permutation - return true -} - -/** - * Determine whether two arrays are permutations of each other - * This is accomplished by sorting both arrays lexicographically and checking element equality - * @param {Array} a1 - * @param {Array} a2 - */ -function arePermutations(a1, a2) { - - // Check for equal length - if (a1 === null || a2 === null || a1.length !== a2.length) { - return false - } else { - var a1_sorted = a1.map(function(v){return v}); - a1_sorted.sort(); - - var a2_sorted = a2.map(function(v){return v}); - a2_sorted.sort(); - - for(var i=0; i < a1_sorted.length; i++) { - if (a1_sorted[i] !== a2_sorted[i]) { - // Elements not equal - return false; - } - } - - // Same number of elemenets and all elements equal - return true; - } + return true; } diff --git a/src/traces/parcats/constants.js b/src/traces/parcats/constants.js index 4036485d788..3d24dff2956 100644 --- a/src/traces/parcats/constants.js +++ b/src/traces/parcats/constants.js @@ -12,49 +12,7 @@ module.exports = { maxDimensionCount: 12, defaultColor: 'lightgray', - - // overdrag: 45, - // verticalPadding: 2, // otherwise, horizontal lines on top or bottom are of lower width - // tickDistance: 50, - // canvasPixelRatio: 1, - // blockLineCount: 5000, - // scatter: false, - // layers: ['contextLineLayer', 'focusLineLayer', 'pickLineLayer'], - // axisTitleOffset: 28, - // axisExtentOffset: 10, - // bar: { - // width: 4, // Visible width of the filter bar - // capturewidth: 10, // Mouse-sensitive width for interaction (Fitts law) - // fillcolor: 'magenta', // Color of the filter bar fill - // fillopacity: 1, // Filter bar fill opacity - // strokecolor: 'white', // Color of the filter bar side lines - // strokeopacity: 1, // Filter bar side stroke opacity - // strokewidth: 1, // Filter bar side stroke width in pixels - // handleheight: 16, // Height of the filter bar vertical resize areas on top and bottom - // handleopacity: 1, // Opacity of the filter bar vertical resize areas on top and bottom - // handleoverlap: 0 // A larger than 0 value causes overlaps with the filter bar, represented as pixels.' - // }, cn: { className: 'parcats' - // axisExtentText: 'axis-extent-text', - // parcoordsLineLayers: 'parcoords-line-layers', - // parcoordsLineLayer: 'parcoords-lines', - // parcoords: 'parcoords', - // parcoordsControlView: 'parcoords-control-view', - // yAxis: 'y-axis', - // axisOverlays: 'axis-overlays', - // axis: 'axis', - // axisHeading: 'axis-heading', - // axisTitle: 'axis-title', - // axisExtent: 'axis-extent', - // axisExtentTop: 'axis-extent-top', - // axisExtentTopText: 'axis-extent-top-text', - // axisExtentBottom: 'axis-extent-bottom', - // axisExtentBottomText: 'axis-extent-bottom-text', - // axisBrush: 'axis-brush' - }, - // id: { - // filterBarPattern: 'filter-bar-pattern' - // - // } + } }; diff --git a/src/traces/parcats/defaults.js b/src/traces/parcats/defaults.js index 2fee8302855..09bc1251664 100644 --- a/src/traces/parcats/defaults.js +++ b/src/traces/parcats/defaults.js @@ -11,15 +11,13 @@ var Lib = require('../../lib'); var attributes = require('./attributes'); var parcatConstants = require('./constants'); -var hasColorscale = require('../../components/colorscale/has_colorscale'); -var colorscaleDefaults = require('../../components/colorscale/defaults'); var colorbarDefaults = require('../../components/colorbar/defaults'); function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce) { coerce('marker.color', defaultColor); - if (traceIn.marker) { + if(traceIn.marker) { coerce('marker.cmin'); coerce('marker.cmax'); coerce('marker.cauto'); @@ -55,7 +53,7 @@ function dimensionsDefaults(traceIn, traceOut) { } // Dimension level - var values = coerce('values'); + coerce('values'); coerce('label'); coerce('displayInd', i); @@ -97,13 +95,12 @@ function dimensionsDefaults(traceIn, traceOut) { } module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - console.log(traceIn); function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var dimensions = dimensionsDefaults(traceIn, traceOut); + dimensionsDefaults(traceIn, traceOut); coerce('domain.x'); coerce('domain.y'); @@ -115,6 +112,4 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('bundlecolors'); coerce('sortpaths'); coerce('counts'); - - console.log(['dimensionsDefaults', traceIn, traceOut]); }; diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index 4d98819a81d..9507f638f0d 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -8,7 +8,6 @@ 'use strict'; -// var c = require('./constants'); var d3 = require('d3'); var Plotly = require('../../plot_api/plot_api'); var Fx = require('../../components/fx'); @@ -19,8 +18,6 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { var viewModels = parcatsModels.map(createParcatsViewModel.bind(0, graphDiv, layout)); - console.log(['viewModels', viewModels]); - // Get (potentially empty) parcatslayer selection with bound data to single element array var layerSelection = svg.selectAll('g.parcatslayer').data([null]); @@ -42,7 +39,7 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { // Update properties for each trace traceSelection - .attr('transform', function (d) { + .attr('transform', function(d) { return 'translate(' + d.x + ', ' + d.y + ')'; }); @@ -58,14 +55,14 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { // Get paths selection var pathSelection = pathsSelection .selectAll('path.path') - .data(function (d) { - return d.paths + .data(function(d) { + return d.paths; }, key); // Update existing path colors pathSelection .attr('fill', function(d) { - return d.model.color + return d.model.color; }); // Create paths @@ -75,7 +72,7 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { .attr('class', 'path') .attr('stroke-opacity', 0) .attr('fill', function(d) { - return d.model.color + return d.model.color; }) .attr('fill-opacity', 0); @@ -83,12 +80,12 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { // Set path geometry pathSelection - .attr('d', function (d) { - return d.svgD + .attr('d', function(d) { + return d.svgD; }); // sort paths - if (!pathSelectionEnter.empty()) { + if(!pathSelectionEnter.empty()) { // Only sort paths if there has been a change. // Otherwise paths are already sorted or a hover operation may be in progress pathSelection.sort(compareRawColor); @@ -113,8 +110,8 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { // Get dimension selection var dimensionSelection = dimensionsSelection .selectAll('g.dimension') - .data(function (d) { - return d.dimensions + .data(function(d) { + return d.dimensions; }, key); // Create dimension groups @@ -123,7 +120,7 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { .attr('class', 'dimension'); // Update dimension group transforms - dimensionSelection.attr('transform', function (d) { + dimensionSelection.attr('transform', function(d) { return 'translate(' + d.x + ', 0)'; }); @@ -133,7 +130,7 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { // Get category selection var categorySelection = dimensionSelection .selectAll('g.category') - .data(function (d) { + .data(function(d) { return d.categories; }, key); @@ -145,8 +142,8 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { // Update category transforms categorySelection - .attr('transform', function (d) { - return 'translate(0, ' + d.y + ')' + .attr('transform', function(d) { + return 'translate(0, ' + d.y + ')'; }); @@ -160,10 +157,10 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { // Update rectangle categorySelection.select('rect.catrect') .attr('fill', 'none') - .attr('width', function (d) { + .attr('width', function(d) { return d.width; }) - .attr('height', function (d) { + .attr('height', function(d) { return d.height; }); @@ -174,17 +171,17 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { .selectAll('rect.bandrect') .data( /** @param {CategoryViewModel} catViewModel*/ - function (catViewModel) { + function(catViewModel) { return catViewModel.bands; }, key); // Raise all update bands to the top so that fading enter/exit bands will be behind - bandSelection.each(function() {Lib.raiseToTop(this)}); + bandSelection.each(function() {Lib.raiseToTop(this);}); // Update band color bandSelection .attr('fill', function(d) { - return d.color + return d.color; }); var bandsSelectionEnter = bandSelection.enter() @@ -193,21 +190,21 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { .attr('cursor', 'move') .attr('stroke-opacity', 0) .attr('fill', function(d) { - return d.color + return d.color; }) .attr('fill-opacity', 0); bandSelection - .attr('fill', function (d) { + .attr('fill', function(d) { return d.color; }) - .attr('width', function (d) { + .attr('width', function(d) { return d.width; }) - .attr('height', function (d) { + .attr('height', function(d) { return d.height; }) - .attr('y', function (d) { + .attr('y', function(d) { return d.y; }); @@ -224,13 +221,13 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { // Update category label categorySelection.select('text.catlabel') .attr('text-anchor', - function (d) { - if (catInRightDim(d)) { + function(d) { + if(catInRightDim(d)) { // Place label to the right of category return 'start'; } else { // Place label to the left of category - return 'end'; + return 'end'; } }) .attr('alignment-baseline', 'middle') @@ -243,8 +240,8 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { .attr('font-size', 10) .style('fill', 'rgb(0, 0, 0)') .attr('x', - function (d) { - if (catInRightDim(d)) { + function(d) { + if(catInRightDim(d)) { // Place label to the right of category return d.width + 5; } else { @@ -252,10 +249,10 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { return -5; } }) - .attr('y', function (d) { + .attr('y', function(d) { return d.height / 2; }) - .text(function (d) { + .text(function(d) { return d.model.categoryLabel; }); @@ -270,16 +267,16 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { .attr('alignment-baseline', 'baseline') .attr('cursor', 'ew-resize') .attr('font-size', 14) - .attr('x', function (d) { + .attr('x', function(d) { return d.width / 2; }) .attr('y', -5) - .text(function (d, i) { - if (i === 0) { + .text(function(d, i) { + if(i === 0) { // Add dimension label above topmost category return d.parcatsViewModel.model.dimensions[d.model.dimensionInd].dimensionLabel; } else { - return null + return null; } }); @@ -287,8 +284,8 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { // categorySelection.select('rect.catrect') categorySelection.selectAll('rect.bandrect') .on('mouseover', mouseoverCategoryBand) - .on('mouseout', function (d) { - mouseoutCategory(d.parcatsViewModel) + .on('mouseout', function(d) { + mouseoutCategory(d.parcatsViewModel); }); // Remove unused categories @@ -296,19 +293,19 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { // Setup drag dimensionSelection.call(d3.behavior.drag() - .origin(function (d) { - return {x: d.x, y: 0} + .origin(function(d) { + return {x: d.x, y: 0}; }) - .on("dragstart", dragDimensionStart) - .on("drag", dragDimension) - .on("dragend", dragDimensionEnd)); + .on('dragstart', dragDimensionStart) + .on('drag', dragDimension) + .on('dragend', dragDimensionEnd)); // Save off selections to view models - traceSelection.each(function (d) { + traceSelection.each(function(d) { d.traceSelection = d3.select(this); d.pathSelection = d3.select(this).selectAll('g.paths').selectAll('path.path'); - d.dimensionSelection = d3.select(this).selectAll('g.dimensions').selectAll('g.dimension') + d.dimensionSelection = d3.select(this).selectAll('g.dimensions').selectAll('g.dimension'); }); // Remove any orphan traces @@ -324,7 +321,6 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { * @param {Layout} layout */ module.exports = function(graphDiv, svg, parcatsModels, layout) { - console.log(['parcats', parcatsModels, layout]); performPlot(parcatsModels, graphDiv, layout, svg); }; @@ -338,23 +334,23 @@ function key(d) { /** True if a category view model is in the right-most display dimension * @param {CategoryViewModel} d */ - function catInRightDim(d) { - var numDims = d.parcatsViewModel.dimensions.length, - leftDimInd = d.parcatsViewModel.dimensions[numDims - 1].model.dimensionInd; - return d.model.dimensionInd === leftDimInd; - } +function catInRightDim(d) { + var numDims = d.parcatsViewModel.dimensions.length, + leftDimInd = d.parcatsViewModel.dimensions[numDims - 1].model.dimensionInd; + return d.model.dimensionInd === leftDimInd; +} /** * @param {PathViewModel} a * @param {PathViewModel} b */ function compareRawColor(a, b) { - if (a.model.rawColor > b.model.rawColor) { - return 1 - } else if (a.model.rawColor < b.model.rawColor){ - return -1 + if(a.model.rawColor > b.model.rawColor) { + return 1; + } else if(a.model.rawColor < b.model.rawColor) { + return -1; } else { - return 0 + return 0; } } @@ -364,10 +360,10 @@ function compareRawColor(a, b) { */ function mouseoverPath(d) { - if (!d.parcatsViewModel.dragDimension) { + if(!d.parcatsViewModel.dragDimension) { // We're not currently dragging - if (d.parcatsViewModel.hovermode !== 'none') { + if(d.parcatsViewModel.hovermode !== 'none') { // Raise path to top Lib.raiseToTop(this); @@ -379,7 +375,7 @@ function mouseoverPath(d) { d.parcatsViewModel.graphDiv.emit('plotly_hover', {points: points, event: d3.event}); // Handle tooltip - if (d.parcatsViewModel.tooltip) { + if(d.parcatsViewModel.tooltip) { // Mouse var hoverX = d3.mouse(this)[0]; @@ -395,13 +391,13 @@ function mouseoverPath(d) { pathCenterY, dimInd; - for (dimInd = 0; dimInd < (d.leftXs.length - 1); dimInd++) { - if (d.leftXs[dimInd] + d.dimWidths[dimInd] - 2 <= hoverX && hoverX <= d.leftXs[dimInd + 1] + 2) { + for(dimInd = 0; dimInd < (d.leftXs.length - 1); dimInd++) { + if(d.leftXs[dimInd] + d.dimWidths[dimInd] - 2 <= hoverX && hoverX <= d.leftXs[dimInd + 1] + 2) { var leftDim = d.parcatsViewModel.dimensions[dimInd]; var rightDim = d.parcatsViewModel.dimensions[dimInd + 1]; pathCenterX = (leftDim.x + leftDim.width + rightDim.x) / 2; pathCenterY = (d.topYs[dimInd] + d.topYs[dimInd + 1] + d.height) / 2; - break + break; } } @@ -411,7 +407,7 @@ function mouseoverPath(d) { var textColor = tinycolor.mostReadable(d.model.color, ['black', 'white']); - var tooltip = Fx.customHovers({ + Fx.customHovers({ x: hoverCenterX - rootBBox.left + graphDivBBox.left, y: hoverCenterY - rootBBox.top + graphDivBBox.top, text: [ @@ -440,7 +436,7 @@ function mouseoverPath(d) { * @param {PathViewModel} d */ function mouseoutPath(d) { - if (!d.parcatsViewModel.dragDimension) { + if(!d.parcatsViewModel.dragDimension) { // We're not currently dragging stylePathsNoHover(d3.select(this)); @@ -462,12 +458,12 @@ function buildPointsArrayForPath(d) { var points = []; var curveNumber = getTraceIndex(d.parcatsViewModel); - for (var i = 0; i < d.model.valueInds.length; i++) { + for(var i = 0; i < d.model.valueInds.length; i++) { var pointNumber = d.model.valueInds[i]; points.push({ curveNumber: curveNumber, pointNumber: pointNumber - }) + }); } return points; } @@ -477,7 +473,7 @@ function buildPointsArrayForPath(d) { * @param {PathViewModel} d */ function clickPath(d) { - if (d.parcatsViewModel.hovermode !== 'none') { + if(d.parcatsViewModel.hovermode !== 'none') { var points = buildPointsArrayForPath(d); d.parcatsViewModel.graphDiv.emit('plotly_click', {points: points, event: d3.event}); } @@ -486,7 +482,7 @@ function clickPath(d) { function stylePathsNoHover(pathSelection) { pathSelection .attr('fill', function(d) { - return d.model.color + return d.model.color; }) .attr('fill-opacity', 0.6) .attr('stroke', 'lightgray') @@ -521,7 +517,7 @@ function styleCategoriesNoHover(categorySelection) { function styleBandsHover(bandsSelection) { bandsSelection .attr('stroke', 'black') - .attr('stroke-width', 1.5) + .attr('stroke-width', 1.5); } function styleBandsNoHover(bandsSelection) { @@ -548,7 +544,7 @@ function selectPathsThroughCategoryBandColor(catBandViewModel) { function(pathViewModel) { return pathViewModel.model.categoryInds[dimInd] === catInd && pathViewModel.model.color === catBandViewModel.color; - }) + }); } @@ -568,7 +564,7 @@ function styleForCategoryHovermode(bandElement) { bandSel.each(function(bvm) { var paths = selectPathsThroughCategoryBandColor(bvm); stylePathsHover(paths); - paths.each(function(d) { + paths.each(function() { // Raise path to top Lib.raiseToTop(this); }); @@ -590,7 +586,7 @@ function styleForColorHovermode(bandElement) { var bandViewModel = d3.select(bandElement).datum(); var catPaths = selectPathsThroughCategoryBandColor(bandViewModel); stylePathsHover(catPaths); - catPaths.each(function(d) { + catPaths.each(function() { // Raise path to top Lib.raiseToTop(this); }); @@ -598,11 +594,11 @@ function styleForColorHovermode(bandElement) { // Style category for drag d3.select(bandElement.parentNode) .selectAll('rect.bandrect') - .filter(function(b) {return b.color === bandViewModel.color}) - .each(function(b) { + .filter(function(b) {return b.color === bandViewModel.color;}) + .each(function() { Lib.raiseToTop(this); styleBandsHover(d3.select(this)); - }) + }); } @@ -626,7 +622,7 @@ function emitPointsEventCategoryHovermode(bandElement, eventName, event) { paths.each(function(pathViewModel) { // Extend points array Array.prototype.push.apply(points, buildPointsArrayForPath(pathViewModel)); - }) + }); }); gd.emit(eventName, {points: points, event: event}); @@ -679,7 +675,7 @@ function createTooltipForCategoryHovermode(rootBBox, bandElement) { var hoverCenterX, tooltipIdealAlign; - if (parcatsViewModel.dimensions.length > 1 && + if(parcatsViewModel.dimensions.length > 1 && dimensionModel.displayInd === parcatsViewModel.dimensions.length - 1) { // right most dimension @@ -738,7 +734,7 @@ function createTooltipForColorHovermode(rootBBox, bandElement) { var hoverCenterX, tooltipIdealAlign; - if (parcatsViewModel.dimensions.length > 1 && + if(parcatsViewModel.dimensions.length > 1 && dimensionModel.displayInd === parcatsViewModel.dimensions.length - 1) { // right most dimension hoverCenterX = bandBoundingBox.left; @@ -756,9 +752,9 @@ function createTooltipForColorHovermode(rootBBox, bandElement) { var bandColorCount = 0; bandViewModel.categoryViewModel.bands.forEach(function(b) { - if (b.color === bandViewModel.color) { - bandColorCount += b.count; - } + if(b.color === bandViewModel.color) { + bandColorCount += b.count; + } }); var catCount = catViewModel.model.count; @@ -767,7 +763,7 @@ function createTooltipForColorHovermode(rootBBox, bandElement) { parcatsViewModel.pathSelection.each( /** @param {PathViewModel} pathViewModel */ function(pathViewModel) { - if (pathViewModel.model.color === bandViewModel.color) { + if(pathViewModel.model.color === bandViewModel.color) { colorCount += pathViewModel.model.count; } }); @@ -778,11 +774,11 @@ function createTooltipForColorHovermode(rootBBox, bandElement) { var pColorAndCatValue = (bandColorCount / totalCount).toFixed(3); var pColorAndCatRow = pColorAndCatLable + pColorAndCatValue; - var pCatGivenColorLabel = 'P(' + catLabel + ' | color): '; + var pCatGivenColorLabel = 'P(' + catLabel + ' | color): '; var pCatGivenColorValue = (bandColorCount / colorCount).toFixed(3); var pCatGivenColorRow = pCatGivenColorLabel + pCatGivenColorValue; - var pColorGivenCatLabel = 'P(color | ' + catLabel + '): '; + var pColorGivenCatLabel = 'P(color | ' + catLabel + '): '; var pColorGivenCatValue = (bandColorCount / catCount).toFixed(3); var pColorGivenCatRow = pColorGivenCatLabel + pColorGivenCatValue; @@ -804,7 +800,7 @@ function createTooltipForColorHovermode(rootBBox, bandElement) { fontColor: textColor, fontSize: 10, idealAlign: tooltipIdealAlign - } + }; } /** @@ -812,12 +808,12 @@ function createTooltipForColorHovermode(rootBBox, bandElement) { * @param {CategoryBandViewModel} bandViewModel */ function mouseoverCategoryBand(bandViewModel) { - if (!bandViewModel.parcatsViewModel.dragDimension) { + if(!bandViewModel.parcatsViewModel.dragDimension) { // We're not currently dragging // Mouse var mouseY = d3.mouse(this)[1]; - if (mouseY < -1) { + if(mouseY < -1) { // Hover is above above the category rectangle (probably the dimension title text) return; } @@ -832,25 +828,25 @@ function mouseoverCategoryBand(bandViewModel) { var bandElement = this; // Handle style and events - if (hovermode === 'category') { + if(hovermode === 'category') { styleForCategoryHovermode(bandElement); emitPointsEventCategoryHovermode(bandElement, 'plotly_hover', d3.event); - } else if (hovermode === 'color') { + } else if(hovermode === 'color') { styleForColorHovermode(bandElement); emitPointsEventColorHovermode(bandElement, 'plotly_hover', d3.event); } // Handle tooltip var hoverTooltip; - if (showTooltip) { - if (hovermode === 'category') { + if(showTooltip) { + if(hovermode === 'category') { hoverTooltip = createTooltipForCategoryHovermode(rootBBox, bandElement); - } else if (hovermode === 'color') { + } else if(hovermode === 'color') { hoverTooltip = createTooltipForColorHovermode(rootBBox, bandElement); } } - if (hoverTooltip) { + if(hoverTooltip) { Fx.loneHover(hoverTooltip, { container: fullLayout._hoverlayer.node(), outerContainer: fullLayout._paper.node(), @@ -866,7 +862,7 @@ function mouseoverCategoryBand(bandViewModel) { */ function mouseoutCategory(parcatsViewModel) { - if (!parcatsViewModel.dragDimension) { + if(!parcatsViewModel.dragDimension) { // We're not dragging anything // Reset unhovered styles @@ -891,7 +887,7 @@ function dragDimensionStart(d) { // Save off initial drag indexes for dimension d.dragDimensionDisplayInd = d.model.displayInd; - d.initialDragDimensionDisplayInds = d.parcatsViewModel.model.dimensions.map(function(d) {return d.displayInd}); + d.initialDragDimensionDisplayInds = d.parcatsViewModel.model.dimensions.map(function(d) {return d.displayInd;}); d.dragHasMoved = false; // Check for category hit @@ -901,18 +897,18 @@ function dragDimensionStart(d) { .select('rect.catrect') .each( /** @param {CategoryViewModel} catViewModel */ - function (catViewModel) { + function(catViewModel) { var catMouseX = d3.mouse(this)[0]; var catMouseY = d3.mouse(this)[1]; - if (-2 <= catMouseX && catMouseX <= catViewModel.width + 2 && + if(-2 <= catMouseX && catMouseX <= catViewModel.width + 2 && -2 <= catMouseY && catMouseY <= catViewModel.height + 2) { // Save off initial drag indexes for categories d.dragCategoryDisplayInd = catViewModel.model.displayInd; - d.initialDragCategoryDisplayInds = d.model.categories.map(function (c) { - return c.displayInd + d.initialDragCategoryDisplayInds = d.model.categories.map(function(c) { + return c.displayInd; }); // Initialize categories dragY to be the current y position @@ -926,10 +922,10 @@ function dragDimensionStart(d) { .selectAll('rect.bandrect') /** @param {CategoryBandViewModel} bandViewModel */ .each(function(bandViewModel) { - if (bandViewModel.y < catMouseY && catMouseY <= bandViewModel.y + bandViewModel.height) { + if(bandViewModel.y < catMouseY && catMouseY <= bandViewModel.y + bandViewModel.height) { d.potentialClickBand = this; } - }) + }); } }); @@ -947,8 +943,8 @@ function dragDimensionStart(d) { function dragDimension(d) { d.dragHasMoved = true; - if (d.dragDimensionDisplayInd === null) { - return + if(d.dragDimensionDisplayInd === null) { + return; } var dragDimInd = d.dragDimensionDisplayInd, @@ -959,7 +955,7 @@ function dragDimension(d) { .dimensions[dragDimInd]; // Update category - if (d.dragCategoryDisplayInd !== null) { + if(d.dragCategoryDisplayInd !== null) { var dragCategory = dragDimension.categories[d.dragCategoryDisplayInd]; @@ -975,9 +971,9 @@ function dragDimension(d) { var catBelow = dimCategoryViews[catDisplayInd + 1]; // Check for overlap above - if (catAbove !== undefined) { + if(catAbove !== undefined) { - if (categoryY < (catAbove.y + catAbove.height/2.0)) { + if(categoryY < (catAbove.y + catAbove.height / 2.0)) { // Swap display inds dragCategory.model.displayInd = catAbove.model.displayInd; @@ -985,9 +981,9 @@ function dragDimension(d) { } } - if (catBelow !== undefined) { + if(catBelow !== undefined) { - if ((categoryY + dragCategory.height) > (catBelow.y + catBelow.height/2.0)) { + if((categoryY + dragCategory.height) > (catBelow.y + catBelow.height / 2.0)) { // Swap display inds dragCategory.model.displayInd = catBelow.model.displayInd; @@ -1006,8 +1002,8 @@ function dragDimension(d) { var prevDimension = d.parcatsViewModel.dimensions[prevDimInd]; var nextDimension = d.parcatsViewModel.dimensions[nextDimInd]; - if (prevDimension !== undefined) { - if (dragDimension.model.dragX < (prevDimension.x + prevDimension.width)) { + if(prevDimension !== undefined) { + if(dragDimension.model.dragX < (prevDimension.x + prevDimension.width)) { // Swap display inds dragDimension.model.displayInd = prevDimension.model.displayInd; @@ -1015,8 +1011,8 @@ function dragDimension(d) { } } - if (nextDimension !== undefined) { - if ((dragDimension.model.dragX + dragDimension.width) > nextDimension.x) { + if(nextDimension !== undefined) { + if((dragDimension.model.dragX + dragDimension.width) > nextDimension.x) { // Swap display inds dragDimension.model.displayInd = nextDimension.model.displayInd; @@ -1043,8 +1039,8 @@ function dragDimension(d) { */ function dragDimensionEnd(d) { - if (d.dragDimensionDisplayInd === null) { - return + if(d.dragDimensionDisplayInd === null) { + return; } d3.select(this).selectAll('text').attr('font-weight', 'normal'); @@ -1055,39 +1051,39 @@ function dragDimensionEnd(d) { var traceInd = getTraceIndex(d.parcatsViewModel); // ### Handle dimension reordering ### - var finalDragDimensionDisplayInds = d.parcatsViewModel.model.dimensions.map(function(d) {return d.displayInd}); + var finalDragDimensionDisplayInds = d.parcatsViewModel.model.dimensions.map(function(d) {return d.displayInd;}); var anyDimsReordered = d.initialDragDimensionDisplayInds.some(function(initDimDisplay, dimInd) { - return initDimDisplay !== finalDragDimensionDisplayInds[dimInd] + return initDimDisplay !== finalDragDimensionDisplayInds[dimInd]; }); - if (anyDimsReordered) { + if(anyDimsReordered) { finalDragDimensionDisplayInds.forEach(function(finalDimDisplay, dimInd) { restyleData['dimensions[' + dimInd + '].displayInd'] = finalDimDisplay; - }) + }); } // ### Handle category reordering ### var anyCatsReordered = false; - if (d.dragCategoryDisplayInd !== null) { - var finalDragCategoryDisplayInds = d.model.categories.map(function (c) { - return c.displayInd + if(d.dragCategoryDisplayInd !== null) { + var finalDragCategoryDisplayInds = d.model.categories.map(function(c) { + return c.displayInd; }); - anyCatsReordered = d.initialDragCategoryDisplayInds.some(function (initCatDisplay, catInd) { + anyCatsReordered = d.initialDragCategoryDisplayInds.some(function(initCatDisplay, catInd) { return initCatDisplay !== finalDragCategoryDisplayInds[catInd]; }); - if (anyCatsReordered) { + if(anyCatsReordered) { restyleData['dimensions[' + d.model.dimensionInd + '].catDisplayInds'] = [finalDragCategoryDisplayInds]; } } // Handle potential click event // ---------------------------- - if (!d.dragHasMoved && d.potentialClickBand) { - if (d.parcatsViewModel.hovermode === 'color') { + if(!d.dragHasMoved && d.potentialClickBand) { + if(d.parcatsViewModel.hovermode === 'color') { emitPointsEventColorHovermode(d.potentialClickBand, 'plotly_click', d3.event.sourceEvent); - } else if (d.parcatsViewModel.hovermode === 'category') { + } else if(d.parcatsViewModel.hovermode === 'category') { emitPointsEventCategoryHovermode(d.potentialClickBand, 'plotly_click', d3.event.sourceEvent); } } @@ -1095,7 +1091,7 @@ function dragDimensionEnd(d) { // Nullify drag states // ------------------- d.model.dragX = null; - if (d.dragCategoryDisplayInd !== null) { + if(d.dragCategoryDisplayInd !== null) { var dragCategory = d.parcatsViewModel .dimensions[d.dragDimensionDisplayInd] .categories[d.dragCategoryDisplayInd]; @@ -1125,11 +1121,9 @@ function dragDimensionEnd(d) { updateSvgCategories(d.parcatsViewModel, true); updateSvgPaths(d.parcatsViewModel, true); }) - .each("end", function() { - if (anyDimsReordered || anyCatsReordered) { + .each('end', function() { + if(anyDimsReordered || anyCatsReordered) { // Perform restyle if the order of categories or dimensions changed - console.log('Do Plotly.restyle!'); - console.log([d.parcatsViewModel.graphDiv, restyleData, [traceInd]]); Plotly.restyle(d.parcatsViewModel.graphDiv, restyleData, [traceInd]); } }); @@ -1142,8 +1136,8 @@ function dragDimensionEnd(d) { function getTraceIndex(parcatsViewModel) { var traceInd; var allTraces = parcatsViewModel.graphDiv._fullData; - for (var i=0; i < allTraces.length; i++) { - if (parcatsViewModel.key === allTraces[i].uid) { + for(var i = 0; i < allTraces.length; i++) { + if(parcatsViewModel.key === allTraces[i].uid) { traceInd = i; break; } @@ -1157,22 +1151,22 @@ function getTraceIndex(parcatsViewModel) { */ function updateSvgPaths(parcatsViewModel, hasTransition) { - if (hasTransition === undefined) { + if(hasTransition === undefined) { hasTransition = false; } function transition(selection) { - return hasTransition? selection.transition(): selection + return hasTransition ? selection.transition() : selection; } // Update binding parcatsViewModel.pathSelection.data(function(d) { - return d.paths + return d.paths; }, key); // Update paths transition(parcatsViewModel.pathSelection).attr('d', function(d) { - return d.svgD + return d.svgD; }); } @@ -1182,12 +1176,12 @@ function updateSvgPaths(parcatsViewModel, hasTransition) { */ function updateSvgCategories(parcatsViewModel, hasTransition) { - if (hasTransition === undefined) { + if(hasTransition === undefined) { hasTransition = false; } function transition(selection) { - return hasTransition? selection.transition(): selection + return hasTransition ? selection.transition() : selection; } // Update binding @@ -1197,7 +1191,7 @@ function updateSvgCategories(parcatsViewModel, hasTransition) { var categorySelection = parcatsViewModel.dimensionSelection .selectAll('g.category') - .data(function(d){return d.categories;}, key); + .data(function(d) {return d.categories;}, key); // Update dimension position transition(parcatsViewModel.dimensionSelection) @@ -1208,7 +1202,7 @@ function updateSvgCategories(parcatsViewModel, hasTransition) { // Update category position transition(categorySelection) .attr('transform', function(d) { - return 'translate(0, ' + d.y + ')' + return 'translate(0, ' + d.y + ')'; }); var dimLabelSelection = categorySelection.select('.dimlabel'); @@ -1217,11 +1211,11 @@ function updateSvgCategories(parcatsViewModel, hasTransition) { // Only the top-most display category should have the dimension label dimLabelSelection .text(function(d, i) { - if (i === 0) { + if(i === 0) { // Add dimension label above topmost category return d.parcatsViewModel.model.dimensions[d.model.dimensionInd].dimensionLabel; } else { - return null + return null; } }); @@ -1231,18 +1225,18 @@ function updateSvgCategories(parcatsViewModel, hasTransition) { var catLabelSelection = categorySelection.select('.catlabel'); catLabelSelection .attr('text-anchor', - function (d) { - if (catInRightDim(d)) { + function(d) { + if(catInRightDim(d)) { // Place label to the right of category return 'start'; } else { // Place label to the left of category - return 'end'; + return 'end'; } }) .attr('x', - function (d) { - if (catInRightDim(d)) { + function(d) { + if(catInRightDim(d)) { // Place label to the right of category return d.width + 5; } else { @@ -1257,7 +1251,7 @@ function updateSvgCategories(parcatsViewModel, hasTransition) { .selectAll('rect.bandrect') .data( /** @param {CategoryViewModel} catViewModel*/ - function (catViewModel) { + function(catViewModel) { return catViewModel.bands; }, key); @@ -1267,31 +1261,31 @@ function updateSvgCategories(parcatsViewModel, hasTransition) { .attr('cursor', 'move') .attr('stroke-opacity', 0) .attr('fill', function(d) { - return d.color + return d.color; }) .attr('fill-opacity', 0); bandSelection - .attr('fill', function (d) { + .attr('fill', function(d) { return d.color; }) - .attr('width', function (d) { + .attr('width', function(d) { return d.width; }) - .attr('height', function (d) { + .attr('height', function(d) { return d.height; }) - .attr('y', function (d) { + .attr('y', function(d) { return d.y; }); styleBandsNoHover(bandsSelectionEnter); // Raise bands to the top - bandSelection.each(function() {Lib.raiseToTop(this)}); + bandSelection.each(function() {Lib.raiseToTop(this);}); // Remove unused bands - bandSelection.exit().remove() + bandSelection.exit().remove(); } /** @@ -1318,13 +1312,13 @@ function createParcatsViewModel(graphDiv, layout, wrappedParcatsModel) { figureHeight = layout.height, traceWidth = Math.floor(figureWidth * (domain.x[1] - domain.x[0])), traceHeight = Math.floor(figureHeight * (domain.y[1] - domain.y[0])), - traceX = domain.x[0] * figureWidth + margin['l'], - traceY = layout.height - domain.y[1] * layout.height + margin['t']; + traceX = domain.x[0] * figureWidth + margin.l, + traceY = layout.height - domain.y[1] * layout.height + margin.t; // Handle path shape // ----------------- var pathShape; - if (trace.marker && trace.marker.shape){ + if(trace.marker && trace.marker.shape) { pathShape = trace.marker.shape; } else { pathShape = 'curved'; @@ -1355,14 +1349,14 @@ function createParcatsViewModel(graphDiv, layout, wrappedParcatsModel) { }; // Update dimension view models if we have at least 1 dimension - if (parcatsModel.dimensions) { + if(parcatsModel.dimensions) { updateDimensionViewModels(parcatsViewModel); // Update path view models if we have at least 2 dimensions updatePathViewModels(parcatsViewModel); } // Inside a categories view model - return parcatsViewModel + return parcatsViewModel; } /** @@ -1387,7 +1381,7 @@ function buildSvgPath(leftXPositions, pathYs, dimWidths, pathHeight, curvature) refInterpolator, d; - for (d = 0; d < dimWidths.length - 1; d++) { + for(d = 0; d < dimWidths.length - 1; d++) { refInterpolator = d3.interpolateNumber(dimWidths[d] + leftXPositions[d], leftXPositions[d + 1]); xRefPoints1.push(refInterpolator(curvature)); xRefPoints2.push(refInterpolator(1 - curvature)); @@ -1400,12 +1394,12 @@ function buildSvgPath(leftXPositions, pathYs, dimWidths, pathHeight, curvature) svgD += 'l' + dimWidths[0] + ',0 '; // Horizontal line to right edge - for (d = 1; d < dimWidths.length; d++) { + for(d = 1; d < dimWidths.length; d++) { // Curve to left edge of category - svgD += 'C' + xRefPoints1[d-1] + ',' + pathYs[d-1] - + ' ' + xRefPoints2[d-1] + ',' + pathYs[d] - + ' ' + leftXPositions[d] + ',' + pathYs[d]; + svgD += 'C' + xRefPoints1[d - 1] + ',' + pathYs[d - 1] + + ' ' + xRefPoints2[d - 1] + ',' + pathYs[d] + + ' ' + leftXPositions[d] + ',' + pathYs[d]; // svgD += 'L' + leftXPositions[d] + ',' + pathYs[d]; @@ -1419,12 +1413,12 @@ function buildSvgPath(leftXPositions, pathYs, dimWidths, pathHeight, curvature) // Line to left edge of right-most category svgD += 'l -' + dimWidths[dimWidths.length - 1] + ',0 '; - for (d = dimWidths.length - 2; d >= 0; d--) { + for(d = dimWidths.length - 2; d >= 0; d--) { // Curve to right edge of category - svgD += 'C' + xRefPoints2[d] + ',' + (pathYs[d+1] + pathHeight) - + ' ' + xRefPoints1[d] + ',' + (pathYs[d] + pathHeight) - + ' ' + (leftXPositions[d] + dimWidths[d]) + ',' + (pathYs[d] + pathHeight); + svgD += 'C' + xRefPoints2[d] + ',' + (pathYs[d + 1] + pathHeight) + + ' ' + xRefPoints1[d] + ',' + (pathYs[d] + pathHeight) + + ' ' + (leftXPositions[d] + dimWidths[d]) + ',' + (pathYs[d] + pathHeight); // svgD += 'L' + (leftXPositions[d] + dimWidths[d]) + ',' + (pathYs[d] + pathHeight); @@ -1451,45 +1445,45 @@ function updatePathViewModels(parcatsViewModel) { var dimensionViewModels = parcatsViewModel.dimensions; var parcatsModel = parcatsViewModel.model; var nextYPositions = dimensionViewModels.map( - function (d) { + function(d) { return d.categories.map( - function (c) { - return c.y - }) + function(c) { + return c.y; + }); }); // Array from category index to category display index for each true dimension index var catToDisplayIndPerDim = parcatsViewModel.model.dimensions.map( - function (d) { - return d.categories.map(function(c) {return c.displayInd}) + function(d) { + return d.categories.map(function(c) {return c.displayInd;}); }); // Array from true dimension index to dimension display index - var dimToDisplayInd = parcatsViewModel.model.dimensions.map(function(d) {return d.displayInd}); - var displayToDimInd = parcatsViewModel.dimensions.map(function(d) {return d.model.dimensionInd}); + var dimToDisplayInd = parcatsViewModel.model.dimensions.map(function(d) {return d.displayInd;}); + var displayToDimInd = parcatsViewModel.dimensions.map(function(d) {return d.model.dimensionInd;}); // Array of the x position of the left edge of the rectangles for each dimension var leftXPositions = dimensionViewModels.map( - function (d) { - return d.x + function(d) { + return d.x; }); // Compute dimension widths - var dimWidths = dimensionViewModels.map(function(d) {return d.width}); + var dimWidths = dimensionViewModels.map(function(d) {return d.width;}); // Build sorted Array of PathModel objects var pathModels = []; - for (var p in parcatsModel.paths) { - if (parcatsModel.paths.hasOwnProperty(p)) { - pathModels.push(parcatsModel.paths[p]) + for(var p in parcatsModel.paths) { + if(parcatsModel.paths.hasOwnProperty(p)) { + pathModels.push(parcatsModel.paths[p]); } } // Compute category display inds to use for sorting paths function pathDisplayCategoryInds(pathModel) { - var dimensionInds = pathModel.categoryInds.map(function(catInd, dimInd){return catToDisplayIndPerDim[dimInd][catInd]}); + var dimensionInds = pathModel.categoryInds.map(function(catInd, dimInd) {return catToDisplayIndPerDim[dimInd][catInd];}); var displayInds = displayToDimInd.map(function(dimInd) { - return dimensionInds[dimInd] + return dimensionInds[dimInd]; }); return displayInds; } @@ -1502,7 +1496,7 @@ function updatePathViewModels(parcatsViewModel) { var sortArray2 = pathDisplayCategoryInds(v2); // Handle path sort order - if (parcatsViewModel.sortpaths === 'backward') { + if(parcatsViewModel.sortpaths === 'backward') { sortArray1.reverse(); sortArray2.reverse(); } @@ -1512,17 +1506,17 @@ function updatePathViewModels(parcatsViewModel) { sortArray2.push(v2.valueInds[0]); // Handle color bundling - if (parcatsViewModel.bundlecolors) { + if(parcatsViewModel.bundlecolors) { // Prepend sort array with the raw color value sortArray1.unshift(v1.rawColor); sortArray2.unshift(v2.rawColor); } // colors equal, sort by display categories - if (sortArray1 < sortArray2) { + if(sortArray1 < sortArray2) { return -1; } - if (sortArray1 > sortArray2) { + if(sortArray1 > sortArray2) { return 1; } @@ -1534,15 +1528,15 @@ function updatePathViewModels(parcatsViewModel) { totalCount = dimensionViewModels[0].model.count, totalHeight = dimensionViewModels[0].categories .map(function(c) { - return c.height}).reduce( - function(v1, v2) {return v1+v2}); + return c.height;}).reduce( + function(v1, v2) {return v1 + v2;}); - for(var pathNumber=0; pathNumber < pathModels.length; pathNumber++) { + for(var pathNumber = 0; pathNumber < pathModels.length; pathNumber++) { var pathModel = pathModels[pathNumber]; var pathHeight; - if (totalCount > 0) { + if(totalCount > 0) { pathHeight = totalHeight * (pathModel.count / totalCount); } else { pathHeight = 0; @@ -1550,7 +1544,7 @@ function updatePathViewModels(parcatsViewModel) { // Build path y coords var pathYs = new Array(nextYPositions.length); - for (var d=0; d < pathModel.categoryInds.length; d++) { + for(var d = 0; d < pathModel.categoryInds.length; d++) { var catInd = pathModel.categoryInds[d]; var catDisplayInd = catToDisplayIndPerDim[d][catInd]; var dimDisplayInd = dimToDisplayInd[d]; @@ -1562,11 +1556,11 @@ function updatePathViewModels(parcatsViewModel) { // Update category color information var catViewModle = parcatsViewModel.dimensions[dimDisplayInd].categories[catDisplayInd]; var numBands = catViewModle.bands.length; - var lastCatBand = catViewModle.bands[numBands-1]; + var lastCatBand = catViewModle.bands[numBands - 1]; - if (lastCatBand === undefined || pathModel.rawColor !== lastCatBand.rawColor) { + if(lastCatBand === undefined || pathModel.rawColor !== lastCatBand.rawColor) { // Create a new band - var bandY = lastCatBand === undefined? 0: lastCatBand.y + lastCatBand.height; + var bandY = lastCatBand === undefined ? 0 : lastCatBand.y + lastCatBand.height; catViewModle.bands.push({ key: bandY, color: pathModel.color, @@ -1580,7 +1574,7 @@ function updatePathViewModels(parcatsViewModel) { }); } else { // Extend current band - var currentBand = catViewModle.bands[numBands-1]; + var currentBand = catViewModle.bands[numBands - 1]; currentBand.height += pathHeight; currentBand.count += pathModel.count; } @@ -1588,7 +1582,7 @@ function updatePathViewModels(parcatsViewModel) { // build svg path var svgD; - if (parcatsViewModel.pathShape === 'curved') { + if(parcatsViewModel.pathShape === 'curved') { svgD = buildSvgPath(leftXPositions, pathYs, dimWidths, pathHeight, 0.5); } else { svgD = buildSvgPath(leftXPositions, pathYs, dimWidths, pathHeight, 0); @@ -1603,10 +1597,10 @@ function updatePathViewModels(parcatsViewModel) { dimWidths: dimWidths, svgD: svgD, parcatsViewModel: parcatsViewModel - } + }; } - parcatsViewModel['paths'] = pathViewModels; + parcatsViewModel.paths = pathViewModels; // * @property key // * Unique key for this model @@ -1627,22 +1621,22 @@ function updatePathViewModels(parcatsViewModel) { function updateDimensionViewModels(parcatsViewModel) { // Compute dimension ordering - var dimensionsIndInfo = parcatsViewModel.model.dimensions.map(function(d, i) { - return {displayInd: d.displayInd, dimensionInd: d.dimensionInd} + var dimensionsIndInfo = parcatsViewModel.model.dimensions.map(function(d) { + return {displayInd: d.displayInd, dimensionInd: d.dimensionInd}; }); dimensionsIndInfo.sort(function(a, b) { - return a.displayInd - b.displayInd + return a.displayInd - b.displayInd; }); var dimensions = []; - for (var displayInd in dimensionsIndInfo) { + for(var displayInd in dimensionsIndInfo) { var dimensionInd = dimensionsIndInfo[displayInd].dimensionInd; var dimModel = parcatsViewModel.model.dimensions[dimensionInd]; dimensions.push(createDimensionViewModel(parcatsViewModel, dimModel)); } - parcatsViewModel['dimensions'] = dimensions; + parcatsViewModel.dimensions = dimensions; } /** @@ -1666,13 +1660,13 @@ function createDimensionViewModel(parcatsViewModel, dimensionModel) { dimX0, dimX; - if (numDimensions > 1) { - dimDx = (parcatsViewModel.width - 2*categoryLabelPad - dimWidth) / (numDimensions - 1); + if(numDimensions > 1) { + dimDx = (parcatsViewModel.width - 2 * categoryLabelPad - dimWidth) / (numDimensions - 1); } else { - dimDx = 0 + dimDx = 0; } dimX0 = categoryLabelPad; - dimX = dimX0 + dimDx*displayInd; + dimX = dimX0 + dimDx * displayInd; // Compute categories var categories = [], @@ -1691,19 +1685,19 @@ function createDimensionViewModel(parcatsViewModel, dimensionModel) { var nextCatY = (maxCats - numCats) * catSpacing / 2.0; // Compute category ordering - var categoryIndInfo = dimensionModel.categories.map(function(c, i) { - return {displayInd: c.displayInd, categoryInd: c.categoryInd} + var categoryIndInfo = dimensionModel.categories.map(function(c) { + return {displayInd: c.displayInd, categoryInd: c.categoryInd}; }); categoryIndInfo.sort(function(a, b) { - return a.displayInd - b.displayInd + return a.displayInd - b.displayInd; }); - for (catDisplayInd = 0; catDisplayInd < numCats; catDisplayInd++) { + for(catDisplayInd = 0; catDisplayInd < numCats; catDisplayInd++) { catInd = categoryIndInfo[catDisplayInd].categoryInd; nextCatModel = dimensionModel.categories[catInd]; - if (totalCount > 0) { + if(totalCount > 0) { nextCatHeight = (nextCatModel.count / totalCount) * totalHeight; } else { nextCatHeight = 0; @@ -1714,18 +1708,18 @@ function createDimensionViewModel(parcatsViewModel, dimensionModel) { model: nextCatModel, width: dimWidth, height: nextCatHeight, - y: nextCatModel.dragY !== null? nextCatModel.dragY: nextCatY, + y: nextCatModel.dragY !== null ? nextCatModel.dragY : nextCatY, bands: [], parcatsViewModel: parcatsViewModel }; nextCatY = nextCatY + nextCatHeight + catSpacing; - categories.push(nextCat) + categories.push(nextCat); } return { key: dimensionModel.dimensionInd, - x: dimensionModel.dragX !== null? dimensionModel.dragX: dimX, + x: dimensionModel.dragX !== null ? dimensionModel.dragX : dimX, y: 0, width: dimWidth, model: dimensionModel, @@ -1737,7 +1731,7 @@ function createDimensionViewModel(parcatsViewModel, dimensionModel) { initialDragCategoryDisplayInds: null, dragHasMoved: null, potentialClickBand: null - } + }; } // JSDoc typedefs diff --git a/src/traces/parcats/plot.js b/src/traces/parcats/plot.js index 35a37311bf8..5dad5365b6f 100644 --- a/src/traces/parcats/plot.js +++ b/src/traces/parcats/plot.js @@ -18,7 +18,6 @@ var parcats = require('./parcats'); * @param {Array.} parcatsModels */ module.exports = function plot(graphDiv, parcatsModels, transitionOpts, makeOnCompleteCallback) { - console.log(['plot', parcatsModels, transitionOpts, makeOnCompleteCallback]); var fullLayout = graphDiv._fullLayout, svg = fullLayout._paper, size = fullLayout._size; diff --git a/test/jasmine/tests/parcats_test.js b/test/jasmine/tests/parcats_test.js index 7c0d9263f85..08b166902e7 100644 --- a/test/jasmine/tests/parcats_test.js +++ b/test/jasmine/tests/parcats_test.js @@ -1,21 +1,25 @@ var Plotly = require('@lib/index'); var Lib = require('@src/lib'); - var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var failTest = require('../assets/fail_test'); -var click = require('../assets/click'); -var getClientPosition = require('../assets/get_client_position'); var mouseEvent = require('../assets/mouse_event'); var delay = require('../assets/delay'); -var customAssertions = require('../assets/custom_assertions'); -var assertHoverLabelStyle = customAssertions.assertHoverLabelStyle; -var assertHoverLabelContent = customAssertions.assertHoverLabelContent; - var CALLBACK_DELAY = 500; +// Testing constants +// ================= +var basic_mock = Lib.extendDeep({}, require('@mocks/parcats_basic.json')); +var margin = basic_mock.layout.margin; +var domain = basic_mock.data[0].domain; + +var categoryLabelPad = 40, + dimWidth = 16, + catSpacing = 8, + dimDx = (256 - 2 * categoryLabelPad - dimWidth) / 2; + // Validation helpers // ================== function checkDimensionCalc(gd, dimInd, dimProps) { @@ -23,7 +27,7 @@ function checkDimensionCalc(gd, dimInd, dimProps) { var calcdata = gd.calcdata[0][0]; var dimension = calcdata.dimensions[dimInd]; for(var dimProp in dimProps) { - if (dimProps.hasOwnProperty(dimProp)) { + if(dimProps.hasOwnProperty(dimProp)) { expect(dimension[dimProp]).toEqual(dimProps[dimProp]); } } @@ -35,7 +39,7 @@ function checkCategoryCalc(gd, dimInd, catInd, catProps) { var dimension = calcdata.dimensions[dimInd]; var category = dimension.categories[catInd]; for(var catProp in catProps) { - if (catProps.hasOwnProperty(catProp)) { + if(catProps.hasOwnProperty(catProp)) { expect(category[catProp]).toEqual(catProps[catProp]); } } @@ -65,14 +69,14 @@ function checkParcatsModelView(gd) { expect(parcatsViewModel.dimensions[1].x).toEqual(categoryLabelPad + dimDx); expect(parcatsViewModel.dimensions[1].y).toEqual(0); - expect(parcatsViewModel.dimensions[2].x).toEqual(categoryLabelPad + 2*dimDx); + expect(parcatsViewModel.dimensions[2].x).toEqual(categoryLabelPad + 2 * dimDx); expect(parcatsViewModel.dimensions[2].y).toEqual(0); // Check location of categories /** @param {Array.} categories */ function checkCategoryPositions(categories) { var nextY = (3 - categories.length) * catSpacing / 2; - for (var c=0; c Date: Tue, 7 Aug 2018 13:23:37 -0400 Subject: [PATCH 04/25] Remove customHovers, replace with loneHover This was a relic of an older attempt to display a tooltip per color for the hover node. It worked, but was pretty unwieldy. --- src/components/fx/hover.js | 76 ----------------------------------- src/traces/parcats/parcats.js | 2 +- 2 files changed, 1 insertion(+), 77 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index c2e616b253b..ff8175c69c5 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -153,82 +153,6 @@ exports.loneHover = function loneHover(hoverItem, opts) { return hoverLabel.node(); }; -// TODO: replace loneHover? -exports.customHovers = function customHovers(hoverItems, opts) { - - if(!Array.isArray(hoverItems)) { - hoverItems = [hoverItems]; - } - - var pointsData = hoverItems.map(function(hoverItem) { - return { - color: hoverItem.color || Color.defaultLine, - x0: hoverItem.x0 || hoverItem.x || 0, - x1: hoverItem.x1 || hoverItem.x || 0, - y0: hoverItem.y0 || hoverItem.y || 0, - y1: hoverItem.y1 || hoverItem.y || 0, - xLabel: hoverItem.xLabel, - yLabel: hoverItem.yLabel, - zLabel: hoverItem.zLabel, - text: hoverItem.text, - name: hoverItem.name, - idealAlign: hoverItem.idealAlign, - - // optional extra bits of styling - borderColor: hoverItem.borderColor, - fontFamily: hoverItem.fontFamily, - fontSize: hoverItem.fontSize, - fontColor: hoverItem.fontColor, - - // filler to make createHoverText happy - trace: { - index: 0, - hoverinfo: '' - }, - xa: {_offset: 0}, - ya: {_offset: 0}, - index: 0 - }; - }); - - - var container3 = d3.select(opts.container), - outerContainer3 = opts.outerContainer ? - d3.select(opts.outerContainer) : container3; - - var fullOpts = { - hovermode: 'closest', - rotateLabels: false, - bgColor: opts.bgColor || Color.background, - container: container3, - outerContainer: outerContainer3 - }; - - var hoverLabel = createHoverText(pointsData, fullOpts, opts.gd); - - // Fix vertical overlap - var tooltipSpacing = 5; - var lastBottomY = 0; - hoverLabel - .sort(function(a, b) {return a.y0 - b.y0;}) - .each(function(d) { - var topY = d.y0 - d.by / 2; - - if((topY - tooltipSpacing) < lastBottomY) { - d.offset = (lastBottomY - topY) + tooltipSpacing; - } else { - d.offset = 0; - } - - lastBottomY = topY + d.by + d.offset; - }); - - - alignHoverText(hoverLabel, fullOpts.rotateLabels); - - return hoverLabel.node(); -}; - // The actual implementation is here: function _hover(gd, evt, subplot, noHoverEvent) { if(!subplot) subplot = 'xy'; diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index 9507f638f0d..39a141d219c 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -407,7 +407,7 @@ function mouseoverPath(d) { var textColor = tinycolor.mostReadable(d.model.color, ['black', 'white']); - Fx.customHovers({ + Fx.loneHover({ x: hoverCenterX - rootBBox.left + graphDivBBox.left, y: hoverCenterY - rootBBox.top + graphDivBBox.top, text: [ From 0c9c75f6de7699c0dc83b3f203544f6505db7318 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 7 Aug 2018 14:32:33 -0400 Subject: [PATCH 05/25] Bring back customHovers --- src/components/fx/hover.js | 76 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index ff8175c69c5..c2e616b253b 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -153,6 +153,82 @@ exports.loneHover = function loneHover(hoverItem, opts) { return hoverLabel.node(); }; +// TODO: replace loneHover? +exports.customHovers = function customHovers(hoverItems, opts) { + + if(!Array.isArray(hoverItems)) { + hoverItems = [hoverItems]; + } + + var pointsData = hoverItems.map(function(hoverItem) { + return { + color: hoverItem.color || Color.defaultLine, + x0: hoverItem.x0 || hoverItem.x || 0, + x1: hoverItem.x1 || hoverItem.x || 0, + y0: hoverItem.y0 || hoverItem.y || 0, + y1: hoverItem.y1 || hoverItem.y || 0, + xLabel: hoverItem.xLabel, + yLabel: hoverItem.yLabel, + zLabel: hoverItem.zLabel, + text: hoverItem.text, + name: hoverItem.name, + idealAlign: hoverItem.idealAlign, + + // optional extra bits of styling + borderColor: hoverItem.borderColor, + fontFamily: hoverItem.fontFamily, + fontSize: hoverItem.fontSize, + fontColor: hoverItem.fontColor, + + // filler to make createHoverText happy + trace: { + index: 0, + hoverinfo: '' + }, + xa: {_offset: 0}, + ya: {_offset: 0}, + index: 0 + }; + }); + + + var container3 = d3.select(opts.container), + outerContainer3 = opts.outerContainer ? + d3.select(opts.outerContainer) : container3; + + var fullOpts = { + hovermode: 'closest', + rotateLabels: false, + bgColor: opts.bgColor || Color.background, + container: container3, + outerContainer: outerContainer3 + }; + + var hoverLabel = createHoverText(pointsData, fullOpts, opts.gd); + + // Fix vertical overlap + var tooltipSpacing = 5; + var lastBottomY = 0; + hoverLabel + .sort(function(a, b) {return a.y0 - b.y0;}) + .each(function(d) { + var topY = d.y0 - d.by / 2; + + if((topY - tooltipSpacing) < lastBottomY) { + d.offset = (lastBottomY - topY) + tooltipSpacing; + } else { + d.offset = 0; + } + + lastBottomY = topY + d.by + d.offset; + }); + + + alignHoverText(hoverLabel, fullOpts.rotateLabels); + + return hoverLabel.node(); +}; + // The actual implementation is here: function _hover(gd, evt, subplot, noHoverEvent) { if(!subplot) subplot = 'xy'; From 7b751009fc9804272316f0bb539ed0386c0858bd Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 7 Aug 2018 14:44:44 -0400 Subject: [PATCH 06/25] Renamed `parcats.marker` -> `parcats.line` Renamed shape categories to `linear` and `hspline` and made `linear` the default. --- src/traces/parcats/attributes.js | 14 ++++++------ src/traces/parcats/calc.js | 18 +++++++-------- src/traces/parcats/defaults.js | 22 +++++++++---------- src/traces/parcats/parcats.js | 10 ++++----- test/image/mocks/parcats_bundled.json | 2 +- .../image/mocks/parcats_bundled_reversed.json | 2 +- test/image/mocks/parcats_unbundled.json | 4 ++-- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index 3962dbf21b9..e5a36072815 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -12,19 +12,19 @@ var extendFlat = require('../../lib/extend').extendFlat; var colorAttributes = require('../../components/colorscale/attributes'); var scatterAttrs = require('../scatter/attributes'); -var scatterMarkerAttrs = scatterAttrs.marker; +var scatterLineAttrs = scatterAttrs.line; var colorbarAttrs = require('../../components/colorbar/attributes'); -var marker = extendFlat({ +var line = extendFlat({ editType: 'calc' -}, colorAttributes('marker', {editType: 'calc'}), +}, colorAttributes('line', {editType: 'calc'}), { - showscale: scatterMarkerAttrs.showscale, + showscale: scatterLineAttrs.showscale, colorbar: colorbarAttrs, shape: { valType: 'enumerated', - values: ['straight', 'curved'], - dflt: 'curved', + values: ['linear', 'hspline'], + dflt: 'linear', role: 'info', editType: 'plot', description: 'Sets the shape of the paths'}, @@ -166,7 +166,7 @@ module.exports = { description: 'The dimensions (variables) of the parallel categories diagram.' }, - marker: marker, + line: line, counts: { valType: 'number', min: 0, diff --git a/src/traces/parcats/calc.js b/src/traces/parcats/calc.js index 4384e213c24..f3189a087b3 100644 --- a/src/traces/parcats/calc.js +++ b/src/traces/parcats/calc.js @@ -71,15 +71,15 @@ module.exports = function calc(gd, trace) { // Handle path colors // ------------------ - var marker = trace.marker; + var line = trace.line; var markerColorscale; // Process colorscale - if(marker) { - if(hasColorscale(trace, 'marker')) { - colorscaleCalc(trace, trace.marker.color, 'marker', 'c'); + if(line) { + if(hasColorscale(trace, 'line')) { + colorscaleCalc(trace, trace.line.color, 'line', 'c'); } - markerColorscale = Drawing.tryColorscale(marker); + markerColorscale = Drawing.tryColorscale(line); } else { markerColorscale = Lib.identity; } @@ -87,12 +87,12 @@ module.exports = function calc(gd, trace) { // Build color generation function function getMarkerColorInfo(index) { var value; - if(!marker) { + if(!line) { value = parcatConstants.defaultColor; - } else if(Array.isArray(marker.color)) { - value = marker.color[index % marker.color.length]; + } else if(Array.isArray(line.color)) { + value = line.color[index % line.color.length]; } else { - value = marker.color; + value = line.color; } return {color: markerColorscale(value), rawColor: value}; diff --git a/src/traces/parcats/defaults.js b/src/traces/parcats/defaults.js index 09bc1251664..f1184227aca 100644 --- a/src/traces/parcats/defaults.js +++ b/src/traces/parcats/defaults.js @@ -15,16 +15,16 @@ var colorbarDefaults = require('../../components/colorbar/defaults'); function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce) { - coerce('marker.color', defaultColor); - - if(traceIn.marker) { - coerce('marker.cmin'); - coerce('marker.cmax'); - coerce('marker.cauto'); - coerce('marker.colorscale'); - coerce('marker.showscale'); - coerce('marker.shape'); - colorbarDefaults(traceIn.marker, traceOut.marker, layout); + coerce('line.color', defaultColor); + + if(traceIn.line) { + coerce('line.cmin'); + coerce('line.cmax'); + coerce('line.cauto'); + coerce('line.colorscale'); + coerce('line.showscale'); + coerce('line.shape'); + colorbarDefaults(traceIn.line, traceOut.line, layout); } } @@ -64,7 +64,7 @@ function dimensionsDefaults(traceIn, traceOut) { // Pass through catValues, catorder, and catlabels (validated in calc since this is where unique info is available) - // pass through marker (color, line) + // pass through line (color) // Pass through font commonLength = Math.min(commonLength, dimensionOut.values.length); diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index 39a141d219c..de646dc73dc 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -1318,10 +1318,10 @@ function createParcatsViewModel(graphDiv, layout, wrappedParcatsModel) { // Handle path shape // ----------------- var pathShape; - if(trace.marker && trace.marker.shape) { - pathShape = trace.marker.shape; + if(trace.line && trace.line.shape) { + pathShape = trace.line.shape; } else { - pathShape = 'curved'; + pathShape = 'linear'; } // Initialize parcatsViewModel @@ -1582,7 +1582,7 @@ function updatePathViewModels(parcatsViewModel) { // build svg path var svgD; - if(parcatsViewModel.pathShape === 'curved') { + if(parcatsViewModel.pathShape === 'hspline') { svgD = buildSvgPath(leftXPositions, pathYs, dimWidths, pathHeight, 0.5); } else { svgD = buildSvgPath(leftXPositions, pathYs, dimWidths, pathHeight, 0); @@ -1791,7 +1791,7 @@ function createDimensionViewModel(parcatsViewModel, dimensionModel) { * If 'forward' then sort paths based on dimensions from left to right. If 'backward' sort based on dimensions * from right to left * @property {String} pathShape - * The shape of the paths. Either 'straight' or 'curved'. + * The shape of the paths. Either 'linear' or 'hspline'. * @property {DimensionViewModel|null} dragDimension * Dimension currently being dragged. Null if no drag in progress * @property {Margin} margin diff --git a/test/image/mocks/parcats_bundled.json b/test/image/mocks/parcats_bundled.json index 8f923c82b80..118c56234c3 100644 --- a/test/image/mocks/parcats_bundled.json +++ b/test/image/mocks/parcats_bundled.json @@ -7,7 +7,7 @@ {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"]}, {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}], "bundlecolors": true, - "marker": { + "line": { "color": [0, 0, 1, 1, 0, 1, 0, 0, 0] } } diff --git a/test/image/mocks/parcats_bundled_reversed.json b/test/image/mocks/parcats_bundled_reversed.json index ee6b6c34d1a..116945f9fb1 100644 --- a/test/image/mocks/parcats_bundled_reversed.json +++ b/test/image/mocks/parcats_bundled_reversed.json @@ -8,7 +8,7 @@ {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}], "bundlecolors": true, "sortpaths": "backward", - "marker": { + "line": { "color": [0, 0, 1, 1, 0, 1, 0, 0, 0] } } diff --git a/test/image/mocks/parcats_unbundled.json b/test/image/mocks/parcats_unbundled.json index d857574df4a..d4a782a8ca5 100644 --- a/test/image/mocks/parcats_unbundled.json +++ b/test/image/mocks/parcats_unbundled.json @@ -7,9 +7,9 @@ {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"]}, {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}], "bundlecolors": false, - "marker": { + "line": { "color": [0, 0, 1, 1, 0, 1, 0, 0, 0], - "shape": "straight" + "shape": "hspline" } } ], From d2c5ae8ee74ebc39421309bc0d2ec542a75a3dc1 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 8 Aug 2018 14:44:16 -0400 Subject: [PATCH 07/25] Use plots/domain and handleDomainDefaults --- src/traces/parcats/attributes.js | 34 ++------------------------------ src/traces/parcats/defaults.js | 4 ++-- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index e5a36072815..d05820ca08c 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -10,7 +10,7 @@ var extendFlat = require('../../lib/extend').extendFlat; var colorAttributes = require('../../components/colorscale/attributes'); - +var domainAttrs = require('../../plots/domain').attributes; var scatterAttrs = require('../scatter/attributes'); var scatterLineAttrs = scatterAttrs.line; var colorbarAttrs = require('../../components/colorbar/attributes'); @@ -31,37 +31,7 @@ var line = extendFlat({ }); module.exports = { - domain: { - x: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1, editType: 'calc'}, - {valType: 'number', min: 0, max: 1, editType: 'calc'} - ], - dflt: [0, 1], - editType: 'calc', - description: [ - 'Sets the horizontal domain of this `parcats` trace', - '(in plot fraction).' - ].join(' ') - }, - y: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1, editType: 'calc'}, - {valType: 'number', min: 0, max: 1, editType: 'calc'} - ], - dflt: [0, 1], - editType: 'calc', - description: [ - 'Sets the vertical domain of this `parcats` trace', - '(in plot fraction).' - ].join(' ') - }, - editType: 'calc' - }, + domain: domainAttrs({name: 'parcats', trace: true, editType: 'calc'}), tooltip: { valType: 'boolean', diff --git a/src/traces/parcats/defaults.js b/src/traces/parcats/defaults.js index f1184227aca..cb0b7e78c1b 100644 --- a/src/traces/parcats/defaults.js +++ b/src/traces/parcats/defaults.js @@ -12,6 +12,7 @@ var Lib = require('../../lib'); var attributes = require('./attributes'); var parcatConstants = require('./constants'); var colorbarDefaults = require('../../components/colorbar/defaults'); +var handleDomainDefaults = require('../../plots/domain').defaults; function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce) { @@ -102,8 +103,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout dimensionsDefaults(traceIn, traceOut); - coerce('domain.x'); - coerce('domain.y'); + handleDomainDefaults(traceOut, layout, coerce); markerDefaults(traceIn, traceOut, defaultColor, layout, coerce); From a34dafaf6f344e4b3936626b1c144ab8969877da Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 8 Aug 2018 15:06:00 -0400 Subject: [PATCH 08/25] Rename displayInd -> displayindex --- src/traces/parcats/attributes.js | 2 +- src/traces/parcats/calc.js | 6 +++--- src/traces/parcats/defaults.js | 2 +- src/traces/parcats/parcats.js | 2 +- test/image/mocks/parcats_reordered.json | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index d05820ca08c..8223685775e 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -123,7 +123,7 @@ module.exports = { 'will be truncated). Each value must an element of `catValues`.' ].join(' ') }, - displayInd: { + displayindex: { valType: 'integer', role: 'info', editType: 'calc', diff --git a/src/traces/parcats/calc.js b/src/traces/parcats/calc.js index f3189a087b3..9b1d937e346 100644 --- a/src/traces/parcats/calc.js +++ b/src/traces/parcats/calc.js @@ -169,7 +169,7 @@ module.exports = function calc(gd, trace) { // Array of DimensionModel objects var dimensionModels = trace.dimensions.map(function(di, i) { - return createDimensionModel(i, di.displayInd, di.label, totalCount); + return createDimensionModel(i, di.displayindex, di.label, totalCount); }); @@ -464,10 +464,10 @@ function getUniqueInfo(values, uniqueValues) { * @param {Object} trace */ function validateDimensionDisplayInds(trace) { - var displayInds = trace.dimensions.map(function(dim) {return dim.displayInd;}); + var displayInds = trace.dimensions.map(function(dim) {return dim.displayindex;}); if(!isRangePermutation(displayInds)) { trace.dimensions.forEach(function(dim, i) { - dim.displayInd = i; + dim.displayindex = i; }); } } diff --git a/src/traces/parcats/defaults.js b/src/traces/parcats/defaults.js index cb0b7e78c1b..1e42f31f810 100644 --- a/src/traces/parcats/defaults.js +++ b/src/traces/parcats/defaults.js @@ -56,7 +56,7 @@ function dimensionsDefaults(traceIn, traceOut) { // Dimension level coerce('values'); coerce('label'); - coerce('displayInd', i); + coerce('displayindex', i); // Category level coerce('catDisplayInds'); diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index de646dc73dc..3e262e36ab6 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -1058,7 +1058,7 @@ function dragDimensionEnd(d) { if(anyDimsReordered) { finalDragDimensionDisplayInds.forEach(function(finalDimDisplay, dimInd) { - restyleData['dimensions[' + dimInd + '].displayInd'] = finalDimDisplay; + restyleData['dimensions[' + dimInd + '].displayindex'] = finalDimDisplay; }); } diff --git a/test/image/mocks/parcats_reordered.json b/test/image/mocks/parcats_reordered.json index 7d61464e431..65169e6ad5b 100644 --- a/test/image/mocks/parcats_reordered.json +++ b/test/image/mocks/parcats_reordered.json @@ -3,10 +3,10 @@ {"type": "parcats", "domain": {"x": [0.125, 0.625],"y": [0.25, 0.75]}, "dimensions": [ - {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1], "displayInd": 0}, - {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"], "displayInd": 2, + {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1], "displayindex": 0}, + {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"], "displayindex": 2, "catDisplayInds": [1, 2, 0], "CatValues": ["A", "B", "C"]}, - {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11], "displayInd": 1}]} + {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11], "displayindex": 1}]} ], "layout": { "height": 602, From 71e212bd0b341aaa05aa59cdf6b7492e8472bb43 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 8 Aug 2018 15:39:57 -0400 Subject: [PATCH 09/25] Convert to simplified colorbar logic Added colorscale mock --- src/traces/parcats/colorbar.js | 52 -------------------------- src/traces/parcats/index.js | 6 ++- test/image/mocks/parcats_colorbar.json | 24 ++++++++++++ 3 files changed, 29 insertions(+), 53 deletions(-) delete mode 100644 src/traces/parcats/colorbar.js create mode 100644 test/image/mocks/parcats_colorbar.json diff --git a/src/traces/parcats/colorbar.js b/src/traces/parcats/colorbar.js deleted file mode 100644 index 34311390879..00000000000 --- a/src/traces/parcats/colorbar.js +++ /dev/null @@ -1,52 +0,0 @@ -/** -* Copyright 2012-2017, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Plots = require('../../plots/plots'); -var Colorscale = require('../../components/colorscale'); -var drawColorbar = require('../../components/colorbar/draw'); - - -module.exports = function colorbar(gd, cd) { - var trace = cd[0].trace, - marker = trace.marker, - cbId = 'cb' + trace.uid; - - gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); - - if((marker === undefined) || !marker.showscale) { - Plots.autoMargin(gd, cbId); - return; - } - - var vals = marker.color, - cmin = marker.cmin, - cmax = marker.cmax; - - if(!isNumeric(cmin)) cmin = Lib.aggNums(Math.min, null, vals); - if(!isNumeric(cmax)) cmax = Lib.aggNums(Math.max, null, vals); - - var cb = cd[0].t.cb = drawColorbar(gd, cbId); - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - marker.colorscale, - cmin, - cmax - ), - { noNumericCheck: true } - ); - - cb.fillcolor(sclFunc) - .filllevels({start: cmin, end: cmax, size: (cmax - cmin) / 254}) - .options(marker.colorbar)(); -}; diff --git a/src/traces/parcats/index.js b/src/traces/parcats/index.js index 868245feea7..a8e9de4dbde 100644 --- a/src/traces/parcats/index.js +++ b/src/traces/parcats/index.js @@ -14,7 +14,11 @@ Parcats.attributes = require('./attributes'); Parcats.supplyDefaults = require('./defaults'); Parcats.calc = require('./calc'); Parcats.plot = require('./plot'); -Parcats.colorbar = require('./colorbar'); +Parcats.colorbar = { + container: 'line', + min: 'cmin', + max: 'cmax' +}; Parcats.moduleType = 'trace'; Parcats.name = 'parcats'; diff --git a/test/image/mocks/parcats_colorbar.json b/test/image/mocks/parcats_colorbar.json new file mode 100644 index 00000000000..b0f06f39bfe --- /dev/null +++ b/test/image/mocks/parcats_colorbar.json @@ -0,0 +1,24 @@ +{ + "data": [ + {"type": "parcats", + "domain": {"x": [0.125, 0.625],"y": [0.25, 0.75]}, + "dimensions":[ + {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1, 2, 3, 1, 2, 3, 1, 2, 1, 2, 1, 1]}, + {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C", "B", "C", "B", "C", "B", "C", "B", "C", "B", "A", "A"]}, + {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11]}], + "bundlecolors": true, + "line": { + "color": [0, 0, 1, 1, 0, 1, 0, 0, 0, 2, 2, 2, 2, 1, 2, 1, 0, 2, 1, 2], + "shape": "linear", + "showscale": true, + "colorscale": "Viridis" + } + } + ], + "layout": { + "height": 602, + "width": 592, + "margin": { + "l": 40, "r": 40, "t": 50, "b": 40 + }} +} From 6797a8354bcab0277af4e7d4cca9238bb19e415f Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Thu, 9 Aug 2018 15:18:28 -0400 Subject: [PATCH 10/25] Remove maxDimensionCount check --- src/traces/parcats/constants.js | 1 - src/traces/parcats/defaults.js | 5 ----- 2 files changed, 6 deletions(-) diff --git a/src/traces/parcats/constants.js b/src/traces/parcats/constants.js index 3d24dff2956..ae9a9e73d61 100644 --- a/src/traces/parcats/constants.js +++ b/src/traces/parcats/constants.js @@ -10,7 +10,6 @@ module.exports = { - maxDimensionCount: 12, defaultColor: 'lightgray', cn: { className: 'parcats' diff --git a/src/traces/parcats/defaults.js b/src/traces/parcats/defaults.js index 1e42f31f810..4d16640fbea 100644 --- a/src/traces/parcats/defaults.js +++ b/src/traces/parcats/defaults.js @@ -36,11 +36,6 @@ function dimensionsDefaults(traceIn, traceOut) { var dimensionIn, dimensionOut, i; var commonLength = Infinity; - if(dimensionsIn.length > parcatConstants.maxDimensionCount) { - Lib.log('parcats traces support up to ' + parcatConstants.maxDimensionCount + ' dimensions at the moment'); - dimensionsIn.splice(parcatConstants.maxDimensionCount); - } - function coerce(attr, dflt) { return Lib.coerce(dimensionIn, dimensionOut, attributes.dimensions, attr, dflt); } From 2c14168928ccf500dafa3f44ed555944b3c20374 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Thu, 9 Aug 2018 15:59:26 -0400 Subject: [PATCH 11/25] Cleanup supplyDefaults and add visible dimension property (property isn't wired up properly yet) --- src/traces/parcats/attributes.js | 9 ++- src/traces/parcats/defaults.js | 97 +++++++++++++------------------- 2 files changed, 46 insertions(+), 60 deletions(-) diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index 8223685775e..359f30988b5 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -133,7 +133,14 @@ module.exports = { ].join(' ') }, editType: 'calc', - description: 'The dimensions (variables) of the parallel categories diagram.' + description: 'The dimensions (variables) of the parallel categories diagram.', + visible: { + valType: 'boolean', + dflt: true, + role: 'info', + editType: 'calc', + description: 'Shows the dimension when set to `true` (the default). Hides the dimension for `false`.' + }, }, line: line, diff --git a/src/traces/parcats/defaults.js b/src/traces/parcats/defaults.js index 4d16640fbea..c1e92c98298 100644 --- a/src/traces/parcats/defaults.js +++ b/src/traces/parcats/defaults.js @@ -9,85 +9,55 @@ 'use strict'; var Lib = require('../../lib'); -var attributes = require('./attributes'); -var parcatConstants = require('./constants'); -var colorbarDefaults = require('../../components/colorbar/defaults'); +var hasColorscale = require('../../components/colorscale/has_colorscale'); +var colorscaleDefaults = require('../../components/colorscale/defaults'); var handleDomainDefaults = require('../../plots/domain').defaults; +var handleArrayContainerDefaults = require('../../plots/array_container_defaults'); -function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce) { +var attributes = require('./attributes'); +var mergeLength = require('../parcoords/merge_length'); - coerce('line.color', defaultColor); +function handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce) { if(traceIn.line) { - coerce('line.cmin'); - coerce('line.cmax'); - coerce('line.cauto'); - coerce('line.colorscale'); - coerce('line.showscale'); coerce('line.shape'); - colorbarDefaults(traceIn.line, traceOut.line, layout); } -} - -function dimensionsDefaults(traceIn, traceOut) { - var dimensionsIn = traceIn.dimensions || [], - dimensionsOut = traceOut.dimensions = []; - var dimensionIn, dimensionOut, i; - var commonLength = Infinity; + var lineColor = coerce('line.color', defaultColor); + if(hasColorscale(traceIn, 'line') && Lib.isArrayOrTypedArray(lineColor)) { + if(lineColor.length) { + coerce('line.colorscale'); + colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'line.', cLetter: 'c'}); + return lineColor.length; + } + else { + traceOut.line.color = defaultColor; + } + } + return Infinity; +} +function dimensionDefaults(dimensionIn, dimensionOut) { function coerce(attr, dflt) { return Lib.coerce(dimensionIn, dimensionOut, attributes.dimensions, attr, dflt); } - for(i = 0; i < dimensionsIn.length; i++) { - dimensionIn = dimensionsIn[i]; - dimensionOut = {}; - - if(!Lib.isPlainObject(dimensionIn)) { - continue; - } + var values = coerce('values'); + var visible = coerce('visible'); + if(!(values && values.length)) { + visible = dimensionOut.visible = false; + } + if(visible) { // Dimension level - coerce('values'); coerce('label'); - coerce('displayindex', i); + coerce('displayindex', dimensionOut._index); // Category level coerce('catDisplayInds'); coerce('catValues'); coerce('catLabels'); - - // Pass through catValues, catorder, and catlabels (validated in calc since this is where unique info is available) - - // pass through line (color) - // Pass through font - - commonLength = Math.min(commonLength, dimensionOut.values.length); - - // dimensionOut._index = i; - dimensionsOut.push(dimensionOut); - } - - if(isFinite(commonLength)) { - for(i = 0; i < dimensionsOut.length; i++) { - dimensionOut = dimensionsOut[i]; - if(dimensionOut.values.length > commonLength) { - dimensionOut.values = dimensionOut.values.slice(0, commonLength); - } - } } - - // handle dimension order - // If specified for all dimensions and no collisions or holes keep, otherwise discard - - // Pass through value colors - // Pass through opacity - - // Pass through dimension font - // Pass through category font - - return dimensionsOut; } module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { @@ -96,11 +66,20 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - dimensionsDefaults(traceIn, traceOut); + var dimensions = handleArrayContainerDefaults(traceIn, traceOut, { + name: 'dimensions', + handleItemDefaults: dimensionDefaults + }); + + var len = handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); handleDomainDefaults(traceOut, layout, coerce); - markerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + if(!Array.isArray(dimensions) || !dimensions.length) { + traceOut.visible = false; + } + + mergeLength(traceOut, dimensions, 'values', len); coerce('hovermode'); coerce('tooltip'); From c8e3cc9456a628e8b58f40571943e1417609602a Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 10 Aug 2018 15:32:51 -0400 Subject: [PATCH 12/25] Added support for dimensions with visible=false --- src/traces/parcats/calc.js | 44 +++++++++++-------- src/traces/parcats/parcats.js | 5 ++- .../mocks/parcats_invisible_dimension.json | 18 ++++++++ 3 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 test/image/mocks/parcats_invisible_dimension.json diff --git a/src/traces/parcats/calc.js b/src/traces/parcats/calc.js index 9b1d937e346..007232e9d5c 100644 --- a/src/traces/parcats/calc.js +++ b/src/traces/parcats/calc.js @@ -18,6 +18,9 @@ var parcatConstants = require('./constants'); var Drawing = require('../../components/drawing'); var Lib = require('../../lib'); + +function visible(dimension) { return !('visible' in dimension) || dimension.visible; } + // Exports // ======= /** @@ -32,22 +35,18 @@ module.exports = function calc(gd, trace) { // Process inputs // -------------- - if(trace.dimensions.length === 0) { - // No dimensions specified. Nothing to compute + if(trace.dimensions.filter(visible).length === 0) { + // No visible dimensions specified. Nothing to compute return []; } // Compute unique information // -------------------------- // UniqueInfo per dimension - var uniqueInfoDims = trace.dimensions.map(function(dim) { + var uniqueInfoDims = trace.dimensions.filter(visible).map(function(dim) { return getUniqueInfo(dim.values, dim.catValues); }); - // Number of values and counts - // --------------------------- - var numValues = trace.dimensions[0].values.length; - // Process counts // -------------- var counts, @@ -65,7 +64,7 @@ module.exports = function calc(gd, trace) { // Validate category display order // ------------------------------- - trace.dimensions.forEach(function(dim, dimInd) { + trace.dimensions.filter(visible).forEach(function(dim, dimInd) { validateCategoryProperties(dim, uniqueInfoDims[dimInd]); }); @@ -122,6 +121,10 @@ module.exports = function calc(gd, trace) { // Category order logic // 1) + // Number of values and counts + // --------------------------- + var numValues = trace.dimensions.filter(visible)[0].values.length; + // Build path info // --------------- // Mapping from category inds to PathModel objects @@ -168,8 +171,8 @@ module.exports = function calc(gd, trace) { // --------------------- // Array of DimensionModel objects - var dimensionModels = trace.dimensions.map(function(di, i) { - return createDimensionModel(i, di.displayindex, di.label, totalCount); + var dimensionModels = trace.dimensions.filter(visible).map(function(di, i) { + return createDimensionModel(i, di._index, di.displayindex, di.label, totalCount); }); @@ -178,13 +181,13 @@ module.exports = function calc(gd, trace) { count = counts[valueInd % counts.length]; for(d = 0; d < dimensionModels.length; d++) { + var containerInd = dimensionModels[d].containerInd; var catInd = uniqueInfoDims[d].inds[valueInd]; var cats = dimensionModels[d].categories; - if(cats[catInd] === undefined) { - var catLabel = trace.dimensions[d].catLabels[catInd]; - var displayInd = trace.dimensions[d].catDisplayInds[catInd]; + var catLabel = trace.dimensions[containerInd].catLabels[catInd]; + var displayInd = trace.dimensions[containerInd].catDisplayInds[catInd]; cats[catInd] = createCategoryModel(d, catInd, displayInd, catLabel); } @@ -226,6 +229,7 @@ module.exports = function calc(gd, trace) { */ function createParcatsModel(dimensions, paths, count) { var maxCats = dimensions + .filter(visible) .map(function(d) {return d.categories.length;}) .reduce(function(v1, v2) {return Math.max(v1, v2);}); return {dimensions: dimensions, paths: paths, trace: undefined, maxCats: maxCats, count: count}; @@ -238,7 +242,10 @@ function createParcatsModel(dimensions, paths, count) { * Object containing calculated information about a single dimension * * @property {Number} dimensionInd - * The index of this dimension + * The index of this dimension among the *visible* dimensions + * @property {Number} containerInd + * The index of this dimension in the original dimensions container, + * irrespective of dimension visibility * @property {Number} displayInd * The display index of this dimension (where 0 is the left most dimension) * @property {String} dimensionLabel @@ -253,16 +260,17 @@ function createParcatsModel(dimensions, paths, count) { /** * Create and new DimensionModel object with an empty categories array * @param {Number} dimensionInd + * @param {Number} containerInd * @param {Number} displayInd - * The display index of this dimension (where 0 is the left most dimension) * @param {String} dimensionLabel * @param {Number} count * Total number of input values * @return {DimensionModel} */ -function createDimensionModel(dimensionInd, displayInd, dimensionLabel, count) { +function createDimensionModel(dimensionInd, containerInd, displayInd, dimensionLabel, count) { return { dimensionInd: dimensionInd, + containerInd: containerInd, displayInd: displayInd, dimensionLabel: dimensionLabel, count: count, @@ -464,9 +472,9 @@ function getUniqueInfo(values, uniqueValues) { * @param {Object} trace */ function validateDimensionDisplayInds(trace) { - var displayInds = trace.dimensions.map(function(dim) {return dim.displayindex;}); + var displayInds = trace.dimensions.filter(visible).map(function(dim) {return dim.displayindex;}); if(!isRangePermutation(displayInds)) { - trace.dimensions.forEach(function(dim, i) { + trace.dimensions.filter(visible).forEach(function(dim, i) { dim.displayindex = i; }); } diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index 3e262e36ab6..c26ab33aca8 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -1058,7 +1058,8 @@ function dragDimensionEnd(d) { if(anyDimsReordered) { finalDragDimensionDisplayInds.forEach(function(finalDimDisplay, dimInd) { - restyleData['dimensions[' + dimInd + '].displayindex'] = finalDimDisplay; + var containerInd = d.parcatsViewModel.model.dimensions[dimInd].containerInd; + restyleData['dimensions[' + containerInd + '].displayindex'] = finalDimDisplay; }); } @@ -1074,7 +1075,7 @@ function dragDimensionEnd(d) { }); if(anyCatsReordered) { - restyleData['dimensions[' + d.model.dimensionInd + '].catDisplayInds'] = [finalDragCategoryDisplayInds]; + restyleData['dimensions[' + d.model.containerInd + '].catDisplayInds'] = [finalDragCategoryDisplayInds]; } } diff --git a/test/image/mocks/parcats_invisible_dimension.json b/test/image/mocks/parcats_invisible_dimension.json new file mode 100644 index 00000000000..159c1ff237e --- /dev/null +++ b/test/image/mocks/parcats_invisible_dimension.json @@ -0,0 +1,18 @@ +{ + "data": [ + {"type": "parcats", + "domain": {"x": [0.125, 0.625],"y": [0.25, 0.75]}, + "dimensions":[ + {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1]}, + {"label": "Two", + "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"], + "visible": false}, + {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}]} + ], + "layout": { + "height": 602, + "width": 592, + "margin": { + "l": 40, "r": 40, "t": 50, "b": 40 + }} +} From 6a5c20e8a62eddbd59ba906c19ea4853cf63107c Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 10 Aug 2018 15:33:23 -0400 Subject: [PATCH 13/25] Fixed failing test (needed to rename displayInd -> displayindex) --- test/jasmine/tests/parcats_test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/jasmine/tests/parcats_test.js b/test/jasmine/tests/parcats_test.js index 08b166902e7..183e3300846 100644 --- a/test/jasmine/tests/parcats_test.js +++ b/test/jasmine/tests/parcats_test.js @@ -400,7 +400,7 @@ describe('Dimension reordered parcats trace', function() { it('should recover from bad display order specification', function(done) { // Define bad display indexes [0, 2, 0] - mock.data[0].dimensions[2].displayInd = 0; + mock.data[0].dimensions[2].displayindex = 0; // catDisplayInds for dimension 1 as [0, 2, 0] mock.data[0].dimensions[1].catDisplayInds[0] = 0; @@ -599,9 +599,9 @@ describe('Drag to reordered dimensions and categories', function() { // ------------------------------------------- expect(restyleCallback).toHaveBeenCalledTimes(1); expect(restyleCallback).toHaveBeenCalledWith([ - {'dimensions[0].displayInd': 0, - 'dimensions[1].displayInd': 2, - 'dimensions[2].displayInd': 1}, + {'dimensions[0].displayindex': 0, + 'dimensions[1].displayindex': 2, + 'dimensions[2].displayindex': 1}, [0]]); restyleCallback.calls.reset(); @@ -734,9 +734,9 @@ describe('Drag to reordered dimensions and categories', function() { // ------------------------------------------- expect(restyleCallback).toHaveBeenCalledTimes(1); expect(restyleCallback).toHaveBeenCalledWith([ - {'dimensions[0].displayInd': 0, - 'dimensions[1].displayInd': 2, - 'dimensions[2].displayInd': 1, + {'dimensions[0].displayindex': 0, + 'dimensions[1].displayindex': 2, + 'dimensions[2].displayindex': 1, 'dimensions[1].catDisplayInds': [[ 1, 2, 0 ]]}, [0]]); From 22346d02462d1733fe78827de702ef8cb6d4f24e Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 10 Aug 2018 15:51:14 -0400 Subject: [PATCH 14/25] Added mock with color hovermode --- test/image/mocks/parcats_hovermode_color.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/image/mocks/parcats_hovermode_color.json diff --git a/test/image/mocks/parcats_hovermode_color.json b/test/image/mocks/parcats_hovermode_color.json new file mode 100644 index 00000000000..a52bf406e27 --- /dev/null +++ b/test/image/mocks/parcats_hovermode_color.json @@ -0,0 +1,23 @@ +{ + "data": [ + {"type": "parcats", + "domain": {"x": [0.125, 0.625],"y": [0.25, 0.75]}, + "hovermode": "color", + "dimensions":[ + {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1]}, + {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"]}, + {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}], + "bundlecolors": false, + "line": { + "color": [0, 0, 1, 1, 0, 1, 0, 0, 0], + "shape": "hspline" + } + } + ], + "layout": { + "height": 602, + "width": 592, + "margin": { + "l": 40, "r": 40, "t": 50, "b": 40 + }} +} From 3680084f9d1a4690511773dc435c4c1f8402fc9c Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Wed, 15 Aug 2018 13:28:52 -0400 Subject: [PATCH 15/25] Replace tooltip with hoverinfo More consistent with other traces, and now it's possible to display only probabilities, only counts, both, none (with hover effects), or skip (not hover effects). --- src/traces/parcats/attributes.js | 16 +- src/traces/parcats/defaults.js | 1 - src/traces/parcats/parcats.js | 216 ++++++++++-------- test/image/mocks/parcats_hovermode_color.json | 1 + 4 files changed, 130 insertions(+), 104 deletions(-) diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index 359f30988b5..fa5b654ae01 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -9,6 +9,7 @@ 'use strict'; var extendFlat = require('../../lib/extend').extendFlat; +var plotAttrs = require('../../plots/attributes'); var colorAttributes = require('../../components/colorscale/attributes'); var domainAttrs = require('../../plots/domain').attributes; var scatterAttrs = require('../scatter/attributes'); @@ -32,18 +33,13 @@ var line = extendFlat({ module.exports = { domain: domainAttrs({name: 'parcats', trace: true, editType: 'calc'}), - - tooltip: { - valType: 'boolean', - dflt: true, - role: 'info', - editType: 'plot', - description: 'Shows a tooltip when hover mode is `category` or `color`.' - }, - + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { + flags: ['count', 'probability'], + editType: 'plot' + }), hovermode: { valType: 'enumerated', - values: ['none', 'category', 'color'], + values: ['category', 'color'], dflt: 'category', role: 'info', editType: 'plot', diff --git a/src/traces/parcats/defaults.js b/src/traces/parcats/defaults.js index c1e92c98298..6e83786356d 100644 --- a/src/traces/parcats/defaults.js +++ b/src/traces/parcats/defaults.js @@ -82,7 +82,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout mergeLength(traceOut, dimensions, 'values', len); coerce('hovermode'); - coerce('tooltip'); coerce('bundlecolors'); coerce('sortpaths'); coerce('counts'); diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index c26ab33aca8..635a602fbe5 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -363,7 +363,8 @@ function mouseoverPath(d) { if(!d.parcatsViewModel.dragDimension) { // We're not currently dragging - if(d.parcatsViewModel.hovermode !== 'none') { + if(d.parcatsViewModel.hoverinfoItems.indexOf('skip') === -1) { + // hoverinfo is not skip, so we at least style the paths and emit interaction events // Raise path to top Lib.raiseToTop(this); @@ -374,13 +375,14 @@ function mouseoverPath(d) { var points = buildPointsArrayForPath(d); d.parcatsViewModel.graphDiv.emit('plotly_hover', {points: points, event: d3.event}); - // Handle tooltip - if(d.parcatsViewModel.tooltip) { + // Handle hover label + if(d.parcatsViewModel.hoverinfoItems.indexOf('none') === -1) { + // hoverinfo is a combination of 'count' and 'probability' // Mouse var hoverX = d3.mouse(this)[0]; - // Tooltip + // Label var gd = d.parcatsViewModel.graphDiv; var fullLayout = gd._fullLayout; var rootBBox = fullLayout._paperdiv.node().getBoundingClientRect(); @@ -407,14 +409,21 @@ function mouseoverPath(d) { var textColor = tinycolor.mostReadable(d.model.color, ['black', 'white']); + // Build hover text + var hovertextParts = []; + if (d.parcatsViewModel.hoverinfoItems.indexOf('count') !== -1) { + hovertextParts.push(['Count:', d.model.count].join(' ')); + } + if (d.parcatsViewModel.hoverinfoItems.indexOf('probability') !== -1) { + hovertextParts.push(['P:', (d.model.count / d.parcatsViewModel.model.count).toFixed(3)].join(' ')); + } + + var hovertext = hovertextParts.join('
'); + Fx.loneHover({ x: hoverCenterX - rootBBox.left + graphDivBBox.left, y: hoverCenterY - rootBBox.top + graphDivBBox.top, - text: [ - ['Count:', d.model.count].join(' '), - ['P:', (d.model.count / d.parcatsViewModel.model.count).toFixed(3)].join(' ') - ].join('
'), - + text: hovertext, color: d.model.color, borderColor: 'black', fontFamily: 'Monaco, "Courier New", monospace', @@ -440,7 +449,7 @@ function mouseoutPath(d) { // We're not currently dragging stylePathsNoHover(d3.select(this)); - // Remove tooltip + // Remove and hover label Fx.loneUnhover(d.parcatsViewModel.graphDiv._fullLayout._hoverlayer.node()); // Restore path order @@ -651,7 +660,7 @@ function emitPointsEventColorHovermode(bandElement, eventName, event) { } /** - * Create tooltip for a band element's category (for use when hovermode === 'category') + * Create hover label for a band element's category (for use when hovermode === 'category') * * @param {ClientRect} rootBBox * Client bounding box for root of figure @@ -659,8 +668,8 @@ function emitPointsEventColorHovermode(bandElement, eventName, event) { * HTML element for band * */ -function createTooltipForCategoryHovermode(rootBBox, bandElement) { - // Build tooltips +function createHoverLabelForCategoryHovermode(rootBBox, bandElement) { + // Selections var rectSelection = d3.select(bandElement.parentNode).select('rect.catrect'); var rectBoundingBox = rectSelection.node().getBoundingClientRect(); @@ -673,44 +682,47 @@ function createTooltipForCategoryHovermode(rootBBox, bandElement) { // Positions var hoverCenterY = rectBoundingBox.top + rectBoundingBox.height / 2; var hoverCenterX, - tooltipIdealAlign; + hoverLabelIdealAlign; if(parcatsViewModel.dimensions.length > 1 && dimensionModel.displayInd === parcatsViewModel.dimensions.length - 1) { // right most dimension hoverCenterX = rectBoundingBox.left; - tooltipIdealAlign = 'left'; + hoverLabelIdealAlign = 'left'; } else { hoverCenterX = rectBoundingBox.left + rectBoundingBox.width; - tooltipIdealAlign = 'right'; + hoverLabelIdealAlign = 'right'; } - var countStr = ['Count:', catViewModel.model.count].join(' '); - var pStr = ['P(' + catViewModel.model.categoryLabel + '):', - (catViewModel.model.count / catViewModel.parcatsViewModel.model.count).toFixed(3)].join(' '); + // Hover label text + var hoverinfoParts = []; + if (catViewModel.parcatsViewModel.hoverinfoItems.indexOf('count') !== -1) { + hoverinfoParts.push(['Count:', catViewModel.model.count].join(' ')) + } + if (catViewModel.parcatsViewModel.hoverinfoItems.indexOf('probability') !== -1) { + hoverinfoParts.push([ + 'P(' + catViewModel.model.categoryLabel + '):', + (catViewModel.model.count / catViewModel.parcatsViewModel.model.count).toFixed(3)].join(' ')) + } + var hovertext = hoverinfoParts.join('
'); return { x: hoverCenterX - rootBBox.left, y: hoverCenterY - rootBBox.top, - // name: 'NAME', - text: [ - countStr, - pStr - ].join('
'), - + text: hovertext, color: 'lightgray', borderColor: 'black', fontFamily: 'Monaco, "Courier New", monospace', fontSize: 12, fontColor: 'black', - idealAlign: tooltipIdealAlign + idealAlign: hoverLabelIdealAlign }; } /** - * Create tooltip for a band element's category (for use when hovermode === 'category') + * Create hover label for a band element's category (for use when hovermode === 'category') * * @param {ClientRect} rootBBox * Client bounding box for root of figure @@ -718,7 +730,7 @@ function createTooltipForCategoryHovermode(rootBBox, bandElement) { * HTML element for band * */ -function createTooltipForColorHovermode(rootBBox, bandElement) { +function createHoverLabelForColorHovermode(rootBBox, bandElement) { var bandBoundingBox = bandElement.getBoundingClientRect(); @@ -733,15 +745,15 @@ function createTooltipForColorHovermode(rootBBox, bandElement) { var hoverCenterY = bandBoundingBox.y + bandBoundingBox.height / 2; var hoverCenterX, - tooltipIdealAlign; + hoverLabelIdealAlign; if(parcatsViewModel.dimensions.length > 1 && dimensionModel.displayInd === parcatsViewModel.dimensions.length - 1) { // right most dimension hoverCenterX = bandBoundingBox.left; - tooltipIdealAlign = 'left'; + hoverLabelIdealAlign = 'left'; } else { hoverCenterX = bandBoundingBox.left + bandBoundingBox.width; - tooltipIdealAlign = 'right'; + hoverLabelIdealAlign = 'right'; } // Labels @@ -768,19 +780,29 @@ function createTooltipForColorHovermode(rootBBox, bandElement) { } }); - // Create talbe to align probability terms - var countStr = ['Count:', bandColorCount].join(' '); - var pColorAndCatLable = 'P(color ∩ ' + catLabel + '): '; - var pColorAndCatValue = (bandColorCount / totalCount).toFixed(3); - var pColorAndCatRow = pColorAndCatLable + pColorAndCatValue; - - var pCatGivenColorLabel = 'P(' + catLabel + ' | color): '; - var pCatGivenColorValue = (bandColorCount / colorCount).toFixed(3); - var pCatGivenColorRow = pCatGivenColorLabel + pCatGivenColorValue; + // Hover label text + var hoverinfoParts = []; + if (catViewModel.parcatsViewModel.hoverinfoItems.indexOf('count') !== -1) { + hoverinfoParts.push(['Count:', bandColorCount].join(' ')) + } + if (catViewModel.parcatsViewModel.hoverinfoItems.indexOf('probability') !== -1) { + var pColorAndCatLable = 'P(color ∩ ' + catLabel + '): '; + var pColorAndCatValue = (bandColorCount / totalCount).toFixed(3); + var pColorAndCatRow = pColorAndCatLable + pColorAndCatValue; + hoverinfoParts.push(pColorAndCatRow); + + var pCatGivenColorLabel = 'P(' + catLabel + ' | color): '; + var pCatGivenColorValue = (bandColorCount / colorCount).toFixed(3); + var pCatGivenColorRow = pCatGivenColorLabel + pCatGivenColorValue; + hoverinfoParts.push(pCatGivenColorRow); + + var pColorGivenCatLabel = 'P(color | ' + catLabel + '): '; + var pColorGivenCatValue = (bandColorCount / catCount).toFixed(3); + var pColorGivenCatRow = pColorGivenCatLabel + pColorGivenCatValue; + hoverinfoParts.push(pColorGivenCatRow); + } - var pColorGivenCatLabel = 'P(color | ' + catLabel + '): '; - var pColorGivenCatValue = (bandColorCount / catCount).toFixed(3); - var pColorGivenCatRow = pColorGivenCatLabel + pColorGivenCatValue; + var hovertext = hoverinfoParts.join('
'); // Compute text color var textColor = tinycolor.mostReadable(bandViewModel.color, ['black', 'white']); @@ -789,17 +811,13 @@ function createTooltipForColorHovermode(rootBBox, bandElement) { x: hoverCenterX - rootBBox.left, y: hoverCenterY - rootBBox.top, // name: 'NAME', - text: [ - countStr, - pColorAndCatRow, pCatGivenColorRow, pColorGivenCatRow - ].join('
'), - + text: hovertext, color: bandViewModel.color, borderColor: 'black', fontFamily: 'Monaco, "Courier New", monospace', fontColor: textColor, fontSize: 10, - idealAlign: tooltipIdealAlign + idealAlign: hoverLabelIdealAlign }; } @@ -811,47 +829,50 @@ function mouseoverCategoryBand(bandViewModel) { if(!bandViewModel.parcatsViewModel.dragDimension) { // We're not currently dragging - // Mouse - var mouseY = d3.mouse(this)[1]; - if(mouseY < -1) { - // Hover is above above the category rectangle (probably the dimension title text) - return; - } + if(bandViewModel.parcatsViewModel.hoverinfoItems.indexOf('skip') === -1) { + // hoverinfo is not skip, so we at least style the bands and emit interaction events - var gd = bandViewModel.parcatsViewModel.graphDiv; - var fullLayout = gd._fullLayout; - var rootBBox = fullLayout._paperdiv.node().getBoundingClientRect(); - var hovermode = bandViewModel.parcatsViewModel.hovermode; - var showTooltip = bandViewModel.parcatsViewModel.tooltip; - - /** @type {HTMLElement} */ - var bandElement = this; - - // Handle style and events - if(hovermode === 'category') { - styleForCategoryHovermode(bandElement); - emitPointsEventCategoryHovermode(bandElement, 'plotly_hover', d3.event); - } else if(hovermode === 'color') { - styleForColorHovermode(bandElement); - emitPointsEventColorHovermode(bandElement, 'plotly_hover', d3.event); - } + // Mouse + var mouseY = d3.mouse(this)[1]; + if (mouseY < -1) { + // Hover is above above the category rectangle (probably the dimension title text) + return; + } - // Handle tooltip - var hoverTooltip; - if(showTooltip) { - if(hovermode === 'category') { - hoverTooltip = createTooltipForCategoryHovermode(rootBBox, bandElement); - } else if(hovermode === 'color') { - hoverTooltip = createTooltipForColorHovermode(rootBBox, bandElement); + var gd = bandViewModel.parcatsViewModel.graphDiv; + var fullLayout = gd._fullLayout; + var rootBBox = fullLayout._paperdiv.node().getBoundingClientRect(); + var hovermode = bandViewModel.parcatsViewModel.hovermode; + + /** @type {HTMLElement} */ + var bandElement = this; + + // Handle style and events + if (hovermode === 'category') { + styleForCategoryHovermode(bandElement); + emitPointsEventCategoryHovermode(bandElement, 'plotly_hover', d3.event); + } else if (hovermode === 'color') { + styleForColorHovermode(bandElement); + emitPointsEventColorHovermode(bandElement, 'plotly_hover', d3.event); } - } - if(hoverTooltip) { - Fx.loneHover(hoverTooltip, { - container: fullLayout._hoverlayer.node(), - outerContainer: fullLayout._paper.node(), - gd: gd - }); + // Handle hover label + if(bandViewModel.parcatsViewModel.hoverinfoItems.indexOf('none') === -1) { + var hoverItem; + if (hovermode === 'category') { + hoverItem = createHoverLabelForCategoryHovermode(rootBBox, bandElement); + } else if (hovermode === 'color') { + hoverItem = createHoverLabelForColorHovermode(rootBBox, bandElement); + } + + if (hoverItem) { + Fx.loneHover(hoverItem, { + container: fullLayout._hoverlayer.node(), + outerContainer: fullLayout._paper.node(), + gd: gd + }); + } + } } } } @@ -870,7 +891,7 @@ function mouseoutCategory(parcatsViewModel) { styleCategoriesNoHover(parcatsViewModel.dimensionSelection.selectAll('g.category')); styleBandsNoHover(parcatsViewModel.dimensionSelection.selectAll('g.category').selectAll('rect.bandrect')); - // Remove tooltip + // Remove hover label Fx.loneUnhover(parcatsViewModel.graphDiv._fullLayout._hoverlayer.node()); // Restore path order @@ -932,7 +953,7 @@ function dragDimensionStart(d) { // Update toplevel drag dimension d.parcatsViewModel.dragDimension = d; - // Remove any tooltip if any + // Remove hover label if any Fx.loneUnhover(d.parcatsViewModel.graphDiv._fullLayout._hoverlayer.node()); } @@ -1325,8 +1346,17 @@ function createParcatsViewModel(graphDiv, layout, wrappedParcatsModel) { pathShape = 'linear'; } - // Initialize parcatsViewModel - // paths and dimensions are missing at this point + // Handle hover info + // ----------------- + var hoverinfoItems; + if (trace.hoverinfo === 'all') { + hoverinfoItems = ['count', 'probability']; + } else { + hoverinfoItems = trace.hoverinfo.split('+'); + } + + // Construct parcatsViewModel + // -------------------------- var parcatsViewModel = { key: trace.uid, model: parcatsModel, @@ -1335,7 +1365,7 @@ function createParcatsViewModel(graphDiv, layout, wrappedParcatsModel) { width: traceWidth, height: traceHeight, hovermode: trace.hovermode, - tooltip: trace.tooltip, + hoverinfoItems: hoverinfoItems, bundlecolors: trace.bundlecolors, sortpaths: trace.sortpaths, pathShape: pathShape, @@ -1784,8 +1814,8 @@ function createDimensionViewModel(parcatsViewModel, dimensionModel) { * Y position of this trace with respect to the Figure (pixels) * @property {String} hovermode * Hover mode. One of: 'none', 'category', or 'color' - * @property {Boolean} tooltip - * Whether to display a tooltip for the 'category' or 'color' hovermodes (has no effect if 'hovermode' is 'none') + * @property {Array.} hoverinfoItems + * Info to display on hover. Array with a combination of 'counts' and/or 'probabilities', or 'none', or 'skip' * @property {Boolean} bundlecolors * Whether paths should be sorted so that like colors are bundled together as they pass through categories * @property {String} sortpaths diff --git a/test/image/mocks/parcats_hovermode_color.json b/test/image/mocks/parcats_hovermode_color.json index a52bf406e27..c4726048523 100644 --- a/test/image/mocks/parcats_hovermode_color.json +++ b/test/image/mocks/parcats_hovermode_color.json @@ -3,6 +3,7 @@ {"type": "parcats", "domain": {"x": [0.125, 0.625],"y": [0.25, 0.75]}, "hovermode": "color", + "hoverinfo": "probability", "dimensions":[ {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1]}, {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"]}, From 2d07f4d73f203be5055b9af345a0bfb0fe83bf1d Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Wed, 15 Aug 2018 15:44:37 -0400 Subject: [PATCH 16/25] Added `arrangement` property that is very similar to the sankey trace There are three arrangement modes: - `perpendicular` (default): categories only drag vertically, dimension labels drag horizontally. - `freeform`: category labels can drag vertically and horizontally (in which case they pull the dimension along with them). Here dragging a category can reorder the categories and dimensions. - `fixed`: dragging of dimensions and categories is disabled. --- src/traces/parcats/attributes.js | 12 +++ src/traces/parcats/defaults.js | 1 + src/traces/parcats/parcats.js | 79 +++++++++++++++----- test/image/mocks/parcats_basic_freeform.json | 17 +++++ test/jasmine/tests/parcats_test.js | 2 +- 5 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 test/image/mocks/parcats_basic_freeform.json diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index fa5b654ae01..5c1d8f07793 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -45,6 +45,18 @@ module.exports = { editType: 'plot', description: 'Sets the hover mode of the parcats diagram' }, + arrangement: { + valType: 'enumerated', + values: ['perpendicular', 'freeform', 'fixed'], + dflt: 'perpendicular', + role: 'style', + editType: 'plot', + description: [ + 'If value is `perpendicular`, the categories can only move along a line perpendicular to the paths.', + 'If value is `freeform`, the categories can freely move on the plane.', + 'If value is `fixed`, the categories and dimensions are stationary.' + ].join(' ') + }, bundlecolors: { valType: 'boolean', dflt: true, diff --git a/src/traces/parcats/defaults.js b/src/traces/parcats/defaults.js index 6e83786356d..1d32b517b3c 100644 --- a/src/traces/parcats/defaults.js +++ b/src/traces/parcats/defaults.js @@ -82,6 +82,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout mergeLength(traceOut, dimensions, 'values', len); coerce('hovermode'); + coerce('arrangement'); coerce('bundlecolors'); coerce('sortpaths'); coerce('counts'); diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index 635a602fbe5..fe7b0380391 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -187,7 +187,6 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { var bandsSelectionEnter = bandSelection.enter() .append('rect') .attr('class', 'bandrect') - .attr('cursor', 'move') .attr('stroke-opacity', 0) .attr('fill', function(d) { return d.color; @@ -206,6 +205,17 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { }) .attr('y', function(d) { return d.y; + }) + .attr('cursor', + /** @param {CategoryBandViewModel} bandModel*/ + function(bandModel) { + if (bandModel.parcatsViewModel.arrangement === 'fixed') { + return 'default' + } else if (bandModel.parcatsViewModel.arrangement === 'perpendicular') { + return 'ns-resize' + } else { + return 'move' + } }); styleBandsNoHover(bandsSelectionEnter); @@ -265,7 +275,15 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { categorySelection.select('text.dimlabel') .attr('text-anchor', 'middle') .attr('alignment-baseline', 'baseline') - .attr('cursor', 'ew-resize') + .attr('cursor', + /** @param {CategoryViewModel} catModel*/ + function(catModel) { + if (catModel.parcatsViewModel.arrangement === 'fixed') { + return 'default'; + } else { + return 'ew-resize' + } + }) .attr('font-size', 14) .attr('x', function(d) { return d.width / 2; @@ -906,6 +924,11 @@ function mouseoutCategory(parcatsViewModel) { */ function dragDimensionStart(d) { + // Check if dragging is supported + if (d.parcatsViewModel.arrangement === 'fixed') { + return + } + // Save off initial drag indexes for dimension d.dragDimensionDisplayInd = d.model.displayInd; d.initialDragDimensionDisplayInds = d.parcatsViewModel.model.dimensions.map(function(d) {return d.displayInd;}); @@ -962,6 +985,12 @@ function dragDimensionStart(d) { * @param {DimensionViewModel} d */ function dragDimension(d) { + + // Check if dragging is supported + if (d.parcatsViewModel.arrangement === 'fixed') { + return + } + d.dragHasMoved = true; if(d.dragDimensionDisplayInd === null) { @@ -1017,32 +1046,34 @@ function dragDimension(d) { } // Update dimension position - dragDimension.model.dragX = d3.event.x; + if (d.dragCategoryDisplayInd === null || d.parcatsViewModel.arrangement === 'freeform') { + dragDimension.model.dragX = d3.event.x; - // Check for dimension swaps - var prevDimension = d.parcatsViewModel.dimensions[prevDimInd]; - var nextDimension = d.parcatsViewModel.dimensions[nextDimInd]; + // Check for dimension swaps + var prevDimension = d.parcatsViewModel.dimensions[prevDimInd]; + var nextDimension = d.parcatsViewModel.dimensions[nextDimInd]; - if(prevDimension !== undefined) { - if(dragDimension.model.dragX < (prevDimension.x + prevDimension.width)) { + if (prevDimension !== undefined) { + if (dragDimension.model.dragX < (prevDimension.x + prevDimension.width)) { - // Swap display inds - dragDimension.model.displayInd = prevDimension.model.displayInd; - prevDimension.model.displayInd = dragDimInd; + // Swap display inds + dragDimension.model.displayInd = prevDimension.model.displayInd; + prevDimension.model.displayInd = dragDimInd; + } } - } - if(nextDimension !== undefined) { - if((dragDimension.model.dragX + dragDimension.width) > nextDimension.x) { + if (nextDimension !== undefined) { + if ((dragDimension.model.dragX + dragDimension.width) > nextDimension.x) { - // Swap display inds - dragDimension.model.displayInd = nextDimension.model.displayInd; - nextDimension.model.displayInd = d.dragDimensionDisplayInd; + // Swap display inds + dragDimension.model.displayInd = nextDimension.model.displayInd; + nextDimension.model.displayInd = d.dragDimensionDisplayInd; + } } - } - // Update drag display index - d.dragDimensionDisplayInd = dragDimension.model.displayInd; + // Update drag display index + d.dragDimensionDisplayInd = dragDimension.model.displayInd; + } // Update view models updateDimensionViewModels(d.parcatsViewModel); @@ -1060,6 +1091,11 @@ function dragDimension(d) { */ function dragDimensionEnd(d) { + // Check if dragging is supported + if (d.parcatsViewModel.arrangement === 'fixed') { + return + } + if(d.dragDimensionDisplayInd === null) { return; } @@ -1366,6 +1402,7 @@ function createParcatsViewModel(graphDiv, layout, wrappedParcatsModel) { height: traceHeight, hovermode: trace.hovermode, hoverinfoItems: hoverinfoItems, + arrangement: trace.arrangement, bundlecolors: trace.bundlecolors, sortpaths: trace.sortpaths, pathShape: pathShape, @@ -1816,6 +1853,8 @@ function createDimensionViewModel(parcatsViewModel, dimensionModel) { * Hover mode. One of: 'none', 'category', or 'color' * @property {Array.} hoverinfoItems * Info to display on hover. Array with a combination of 'counts' and/or 'probabilities', or 'none', or 'skip' + * @property {String} arrangement + * Category arrangement. One of: 'perpendicular', 'freeform', or 'fixed' * @property {Boolean} bundlecolors * Whether paths should be sorted so that like colors are bundled together as they pass through categories * @property {String} sortpaths diff --git a/test/image/mocks/parcats_basic_freeform.json b/test/image/mocks/parcats_basic_freeform.json new file mode 100644 index 00000000000..4f0b54472f8 --- /dev/null +++ b/test/image/mocks/parcats_basic_freeform.json @@ -0,0 +1,17 @@ +{ + "data": [ + {"type": "parcats", + "domain": {"x": [0.125, 0.625],"y": [0.25, 0.75]}, + "arrangement": "freeform", + "dimensions":[ + {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1]}, + {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"]}, + {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}]} + ], + "layout": { + "height": 602, + "width": 592, + "margin": { + "l": 40, "r": 40, "t": 50, "b": 40 + }} +} diff --git a/test/jasmine/tests/parcats_test.js b/test/jasmine/tests/parcats_test.js index 183e3300846..41876f6b845 100644 --- a/test/jasmine/tests/parcats_test.js +++ b/test/jasmine/tests/parcats_test.js @@ -501,7 +501,7 @@ describe('Drag to reordered dimensions and categories', function() { // -------- beforeEach(function() { gd = createGraphDiv(); - mock = Lib.extendDeep({}, require('@mocks/parcats_basic.json')); + mock = Lib.extendDeep({}, require('@mocks/parcats_basic_freeform.json')); }); afterEach(destroyGraphDiv); From 7f90fc10b2dd206b320ee70634d23175707d381b Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Thu, 16 Aug 2018 15:17:39 -0400 Subject: [PATCH 17/25] WIP towards categoryorder/categoryarray/categorylabels Not working yet, just a checkpoint --- src/traces/parcats/attributes.js | 32 ++++++++----- src/traces/parcats/calc.js | 76 ++++++++++++------------------- src/traces/parcats/defaults.js | 10 +++-- src/traces/parcats/parcats.js | 77 +++++++++++--------------------- 4 files changed, 83 insertions(+), 112 deletions(-) diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index 5c1d8f07793..c3aab107edd 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -93,31 +93,43 @@ module.exports = { editType: 'calc', description: 'The shown name of the dimension.' }, - catDisplayInds: { - valType: 'data_array', + categoryorder: { + valType: 'enumerated', + values: [ + 'trace', 'category ascending', 'category descending', 'array' + ], + dflt: 'trace', role: 'info', editType: 'calc', - dflt: [], description: [ - '' + 'Specifies the ordering logic for the categories in the dimension.', + '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 *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`.' ].join(' ') }, - catValues: { + categoryarray: { valType: 'data_array', role: 'info', editType: 'calc', - dflt: [], description: [ - '' + 'Sets the order in which categories in this dimension appear.', + 'Only has an effect if `categoryorder` is set to *array*.', + 'Used with `categoryorder`.' ].join(' ') }, - catLabels: { + categorylabels: { valType: 'data_array', role: 'info', editType: 'calc', - dflt: [], description: [ - '' + 'Sets alternative labels for the categories in this dimension.', + 'Only has an effect if `categoryorder` is set to *array*.', + 'Should be an array the same length as `categoryarray`', + 'Used with `categoryorder`.' ].join(' ') }, values: { diff --git a/src/traces/parcats/calc.js b/src/traces/parcats/calc.js index 007232e9d5c..00137dd33d0 100644 --- a/src/traces/parcats/calc.js +++ b/src/traces/parcats/calc.js @@ -14,7 +14,7 @@ var wrap = require('../../lib/gup').wrap; var hasColorscale = require('../../components/colorscale/has_colorscale'); var colorscaleCalc = require('../../components/colorscale/calc'); var parcatConstants = require('./constants'); - +var filterUnique = require('../../lib/filter_unique.js'); var Drawing = require('../../components/drawing'); var Lib = require('../../lib'); @@ -44,7 +44,22 @@ module.exports = function calc(gd, trace) { // -------------------------- // UniqueInfo per dimension var uniqueInfoDims = trace.dimensions.filter(visible).map(function(dim) { - return getUniqueInfo(dim.values, dim.catValues); + var categoryValues; + if (dim.categoryorder === 'trace') { + // Use order of first occurrence in trace + categoryValues = null; + } else if (dim.categoryorder === 'array') { + // Use categories specified in `categoryarray` first, then add extra to the end in trace order + categoryValues = dim.categoryarray; + } else { + // Get all categories up front so we can order them + // Should we check for numbers as sort numerically? + categoryValues = filterUnique(dim.values).sort(); + if (dim.categoryorder === 'category descending') { + categoryValues = categoryValues.reverse(); + } + } + return getUniqueInfo(dim.values, categoryValues); }); // Process counts @@ -97,30 +112,6 @@ module.exports = function calc(gd, trace) { return {color: markerColorscale(value), rawColor: value}; } - // Build/Validate category labels/order - // ------------------------------------ - // properties: catValues, catorder, catlabels - // - // 1) if catValues and catorder are specified - // a) cat order must be the same length with no collisions or holes, otherwise it is discarded - // b) Additional categories in data that are not specified are appended to catValues, and next indexes are - // appended to catorder - // c) catValues updated in data/_fullData - // 2) if catorder but not catValues is specified - // a) catorder must be same length as inferred catValues with no collisions or holes - // otherwise it is discarded and set to 0 to catValues.length - // 3) if catValues but not catorder is specified - // a) Append unspecified values to catValues - // b) set carorder to 0 to catValues.length - // 4) if neither are specified - // a) Set catValues to unique catValues - // b) Set carorder to 0 to catValues.length - // - // uniqueInfoDims[0].uniqueValues - - // Category order logic - // 1) - // Number of values and counts // --------------------------- var numValues = trace.dimensions.filter(visible)[0].values.length; @@ -186,10 +177,8 @@ module.exports = function calc(gd, trace) { var cats = dimensionModels[d].categories; if(cats[catInd] === undefined) { - var catLabel = trace.dimensions[containerInd].catLabels[catInd]; - var displayInd = trace.dimensions[containerInd].catDisplayInds[catInd]; - - cats[catInd] = createCategoryModel(d, catInd, displayInd, catLabel); + var catLabel = trace.dimensions[containerInd].categorylabels[catInd]; + cats[catInd] = createCategoryModel(d, catInd, catLabel); } updateCategoryModel(cats[catInd], valueInd, count); @@ -289,8 +278,6 @@ function createDimensionModel(dimensionInd, containerInd, displayInd, dimensionL * The index of this categories dimension * @property {Number} categoryInd * The index of this category - * @property {Number} displayInd - * The display index of this category (where 0 is the topmost category) * @property {String} categoryLabel * The name of this category * @property {Array} valueInds @@ -314,7 +301,6 @@ function createCategoryModel(dimensionInd, categoryInd, displayInd, categoryLabe return { dimensionInd: dimensionInd, categoryInd: categoryInd, - displayInd: displayInd, categoryLabel: categoryLabel, valueInds: [], count: 0, @@ -489,27 +475,21 @@ function validateDimensionDisplayInds(trace) { * @param {UniqueInfo} uniqueInfoDim */ function validateCategoryProperties(dim, uniqueInfoDim) { - var uniqueDimVals = uniqueInfoDim.uniqueValues; - // Update catValues - dim.catValues = uniqueDimVals; - - // Handle catDisplayInds - if(dim.catDisplayInds.length !== uniqueDimVals.length || !isRangePermutation(dim.catDisplayInds)) { - dim.catDisplayInds = uniqueDimVals.map(function(v, i) {return i;}); - } + // Update categoryarray + dim.categoryarray = uniqueInfoDim.uniqueValues; - // Handle catLabels - if(dim.catLabels === null || dim.catLabels === undefined) { - dim.catLabels = []; + // Handle categorylabels + if(dim.categorylabels === null || dim.categorylabels === undefined) { + dim.categorylabels = []; } else { // Shallow copy to avoid modifying input array - dim.catLabels = dim.catLabels.map(function(v) {return v;}); + dim.categorylabels = dim.categorylabels.slice(); } - // Extend catLabels with elements from uniqueInfoDim.uniqueValues - for(var i = dim.catLabels.length; i < uniqueInfoDim.uniqueValues.length; i++) { - dim.catLabels.push(uniqueInfoDim.uniqueValues[i]); + // Extend categorylabels with elements from uniqueInfoDim.uniqueValues + for(var i = dim.categorylabels.length; i < uniqueInfoDim.uniqueValues.length; i++) { + dim.categorylabels.push(uniqueInfoDim.uniqueValues[i]); } } diff --git a/src/traces/parcats/defaults.js b/src/traces/parcats/defaults.js index 1d32b517b3c..baccbdb5724 100644 --- a/src/traces/parcats/defaults.js +++ b/src/traces/parcats/defaults.js @@ -14,6 +14,7 @@ var colorscaleDefaults = require('../../components/colorscale/defaults'); var handleDomainDefaults = require('../../plots/domain').defaults; var handleArrayContainerDefaults = require('../../plots/array_container_defaults'); +var handleCategoryOrderDefaults = require('../../plots/cartesian/category_order_defaults'); var attributes = require('./attributes'); var mergeLength = require('../parcoords/merge_length'); @@ -54,9 +55,12 @@ function dimensionDefaults(dimensionIn, dimensionOut) { coerce('displayindex', dimensionOut._index); // Category level - coerce('catDisplayInds'); - coerce('catValues'); - coerce('catLabels'); + // TODO: Make categoryorder and categoryarray consistent + // If valid array, set order to 'array' + // If order is 'array' but array is invalid set order to 'trace' + coerce('categoryorder'); + coerce('categoryarray'); + coerce('categorylabels'); } } diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index fe7b0380391..4022e2a4056 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -1014,11 +1014,11 @@ function dragDimension(d) { var categoryY = dragCategory.model.dragY; // Check for category drag swaps - var catDisplayInd = dragCategory.model.displayInd; + var categoryInd = dragCategory.model.categoryInd; var dimCategoryViews = dragDimension.categories; - var catAbove = dimCategoryViews[catDisplayInd - 1]; - var catBelow = dimCategoryViews[catDisplayInd + 1]; + var catAbove = dimCategoryViews[categoryInd - 1]; + var catBelow = dimCategoryViews[categoryInd + 1]; // Check for overlap above if(catAbove !== undefined) { @@ -1027,7 +1027,7 @@ function dragDimension(d) { // Swap display inds dragCategory.model.displayInd = catAbove.model.displayInd; - catAbove.model.displayInd = catDisplayInd; + catAbove.model.displayInd = categoryInd; } } @@ -1037,7 +1037,7 @@ function dragDimension(d) { // Swap display inds dragCategory.model.displayInd = catBelow.model.displayInd; - catBelow.model.displayInd = catDisplayInd; + catBelow.model.displayInd = categoryInd; } } @@ -1121,20 +1121,20 @@ function dragDimensionEnd(d) { } // ### Handle category reordering ### - var anyCatsReordered = false; - if(d.dragCategoryDisplayInd !== null) { - var finalDragCategoryDisplayInds = d.model.categories.map(function(c) { - return c.displayInd; - }); - - anyCatsReordered = d.initialDragCategoryDisplayInds.some(function(initCatDisplay, catInd) { - return initCatDisplay !== finalDragCategoryDisplayInds[catInd]; - }); - - if(anyCatsReordered) { - restyleData['dimensions[' + d.model.containerInd + '].catDisplayInds'] = [finalDragCategoryDisplayInds]; - } - } + // var anyCatsReordered = false; + // if(d.dragCategoryDisplayInd !== null) { + // var finalDragCategoryDisplayInds = d.model.categories.map(function(c) { + // return c.displayInd; + // }); + // + // anyCatsReordered = d.initialDragCategoryDisplayInds.some(function(initCatDisplay, catInd) { + // return initCatDisplay !== finalDragCategoryDisplayInds[catInd]; + // }); + // + // if(anyCatsReordered) { + // restyleData['dimensions[' + d.model.containerInd + '].catDisplayInds'] = [finalDragCategoryDisplayInds]; + // } + // } // Handle potential click event // ---------------------------- @@ -1520,12 +1520,6 @@ function updatePathViewModels(parcatsViewModel) { }); }); - // Array from category index to category display index for each true dimension index - var catToDisplayIndPerDim = parcatsViewModel.model.dimensions.map( - function(d) { - return d.categories.map(function(c) {return c.displayInd;}); - }); - // Array from true dimension index to dimension display index var dimToDisplayInd = parcatsViewModel.model.dimensions.map(function(d) {return d.displayInd;}); var displayToDimInd = parcatsViewModel.dimensions.map(function(d) {return d.model.dimensionInd;}); @@ -1547,21 +1541,12 @@ function updatePathViewModels(parcatsViewModel) { } } - // Compute category display inds to use for sorting paths - function pathDisplayCategoryInds(pathModel) { - var dimensionInds = pathModel.categoryInds.map(function(catInd, dimInd) {return catToDisplayIndPerDim[dimInd][catInd];}); - var displayInds = displayToDimInd.map(function(dimInd) { - return dimensionInds[dimInd]; - }); - return displayInds; - } - // Sort in ascending order by display index array pathModels.sort(function(v1, v2) { // Build display inds for each path - var sortArray1 = pathDisplayCategoryInds(v1); - var sortArray2 = pathDisplayCategoryInds(v2); + var sortArray1 = v1.categoryInds; + var sortArray2 = v2.categoryInds; // Handle path sort order if(parcatsViewModel.sortpaths === 'backward') { @@ -1614,15 +1599,14 @@ function updatePathViewModels(parcatsViewModel) { var pathYs = new Array(nextYPositions.length); for(var d = 0; d < pathModel.categoryInds.length; d++) { var catInd = pathModel.categoryInds[d]; - var catDisplayInd = catToDisplayIndPerDim[d][catInd]; var dimDisplayInd = dimToDisplayInd[d]; // Update next y position - pathYs[dimDisplayInd] = nextYPositions[dimDisplayInd][catDisplayInd]; - nextYPositions[dimDisplayInd][catDisplayInd] += pathHeight; + pathYs[dimDisplayInd] = nextYPositions[dimDisplayInd][catInd]; + nextYPositions[dimDisplayInd][catInd] += pathHeight; // Update category color information - var catViewModle = parcatsViewModel.dimensions[dimDisplayInd].categories[catDisplayInd]; + var catViewModle = parcatsViewModel.dimensions[dimDisplayInd].categories[catInd]; var numBands = catViewModle.bands.length; var lastCatBand = catViewModle.bands[numBands - 1]; @@ -1746,23 +1730,14 @@ function createDimensionViewModel(parcatsViewModel, dimensionModel) { nextCatHeight, nextCatModel, nextCat, - catInd, - catDisplayInd; + catInd; // Compute starting Y offset var nextCatY = (maxCats - numCats) * catSpacing / 2.0; // Compute category ordering - var categoryIndInfo = dimensionModel.categories.map(function(c) { - return {displayInd: c.displayInd, categoryInd: c.categoryInd}; - }); - - categoryIndInfo.sort(function(a, b) { - return a.displayInd - b.displayInd; - }); + for(catInd = 0; catInd < numCats; catInd++) { - for(catDisplayInd = 0; catDisplayInd < numCats; catDisplayInd++) { - catInd = categoryIndInfo[catDisplayInd].categoryInd; nextCatModel = dimensionModel.categories[catInd]; if(totalCount > 0) { From 5e60062e686b3bf8b78018e288ce6e61abd6ebca Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Fri, 17 Aug 2018 15:52:20 -0400 Subject: [PATCH 18/25] Full support for categoryorder, categoryarray, and categorylabels Mocks updated, but not tests yet. --- src/traces/parcats/calc.js | 12 +++- src/traces/parcats/defaults.js | 26 +++++-- src/traces/parcats/parcats.js | 92 +++++++++++++++++-------- test/image/mocks/parcats_reordered.json | 2 +- 4 files changed, 94 insertions(+), 38 deletions(-) diff --git a/src/traces/parcats/calc.js b/src/traces/parcats/calc.js index 00137dd33d0..0773c027dde 100644 --- a/src/traces/parcats/calc.js +++ b/src/traces/parcats/calc.js @@ -177,8 +177,9 @@ module.exports = function calc(gd, trace) { var cats = dimensionModels[d].categories; if(cats[catInd] === undefined) { + var catValue = trace.dimensions[containerInd].categoryarray[catInd]; var catLabel = trace.dimensions[containerInd].categorylabels[catInd]; - cats[catInd] = createCategoryModel(d, catInd, catLabel); + cats[catInd] = createCategoryModel(d, catInd, catValue, catLabel); } updateCategoryModel(cats[catInd], valueInd, count); @@ -278,8 +279,11 @@ function createDimensionModel(dimensionInd, containerInd, displayInd, dimensionL * The index of this categories dimension * @property {Number} categoryInd * The index of this category + * @property {Number} displayInd + * The display index of this category (where 0 is the topmost category) * @property {String} categoryLabel * The name of this category + * @property categoryValue: Raw value of the category * @property {Array} valueInds * Array of indices (into the original value array) of all samples in this category * @property {Number} count @@ -292,15 +296,17 @@ function createDimensionModel(dimensionInd, containerInd, displayInd, dimensionL * Create and return a new CategoryModel object * @param {Number} dimensionInd * @param {Number} categoryInd - * @param {Number} displayInd * The display index of this category (where 0 is the topmost category) + * @param {String} categoryValue * @param {String} categoryLabel * @return {CategoryModel} */ -function createCategoryModel(dimensionInd, categoryInd, displayInd, categoryLabel) { +function createCategoryModel(dimensionInd, categoryInd, categoryValue, categoryLabel) { return { dimensionInd: dimensionInd, categoryInd: categoryInd, + categoryValue: categoryValue, + displayInd: categoryInd, categoryLabel: categoryLabel, valueInds: [], count: 0, diff --git a/src/traces/parcats/defaults.js b/src/traces/parcats/defaults.js index baccbdb5724..4eb0e56553b 100644 --- a/src/traces/parcats/defaults.js +++ b/src/traces/parcats/defaults.js @@ -55,12 +55,26 @@ function dimensionDefaults(dimensionIn, dimensionOut) { coerce('displayindex', dimensionOut._index); // Category level - // TODO: Make categoryorder and categoryarray consistent - // If valid array, set order to 'array' - // If order is 'array' but array is invalid set order to 'trace' - coerce('categoryorder'); - coerce('categoryarray'); - coerce('categorylabels'); + var arrayIn = dimensionIn.categoryarray; + var isValidArray = (Array.isArray(arrayIn) && arrayIn.length > 0); + + var orderDefault; + if(isValidArray) orderDefault = 'array'; + var order = coerce('categoryorder', orderDefault); + + // coerce 'categoryarray' only in array order case + if(order === 'array') { + coerce('categoryarray'); + coerce('categorylabels'); + } else { + delete dimensionIn.categoryarray; + delete dimensionIn.categorylabels; + } + + // cannot set 'categoryorder' to 'array' with an invalid 'categoryarray' + if(!isValidArray && order === 'array') { + dimensionOut.categoryorder = 'trace'; + } } } diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index 4022e2a4056..dfef5d3f2ae 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -1014,11 +1014,11 @@ function dragDimension(d) { var categoryY = dragCategory.model.dragY; // Check for category drag swaps - var categoryInd = dragCategory.model.categoryInd; + var catDisplayInd = dragCategory.model.displayInd; var dimCategoryViews = dragDimension.categories; - var catAbove = dimCategoryViews[categoryInd - 1]; - var catBelow = dimCategoryViews[categoryInd + 1]; + var catAbove = dimCategoryViews[catDisplayInd - 1]; + var catBelow = dimCategoryViews[catDisplayInd + 1]; // Check for overlap above if(catAbove !== undefined) { @@ -1027,7 +1027,7 @@ function dragDimension(d) { // Swap display inds dragCategory.model.displayInd = catAbove.model.displayInd; - catAbove.model.displayInd = categoryInd; + catAbove.model.displayInd = catDisplayInd; } } @@ -1037,7 +1037,7 @@ function dragDimension(d) { // Swap display inds dragCategory.model.displayInd = catBelow.model.displayInd; - catBelow.model.displayInd = categoryInd; + catBelow.model.displayInd = catDisplayInd; } } @@ -1121,20 +1121,31 @@ function dragDimensionEnd(d) { } // ### Handle category reordering ### - // var anyCatsReordered = false; - // if(d.dragCategoryDisplayInd !== null) { - // var finalDragCategoryDisplayInds = d.model.categories.map(function(c) { - // return c.displayInd; - // }); - // - // anyCatsReordered = d.initialDragCategoryDisplayInds.some(function(initCatDisplay, catInd) { - // return initCatDisplay !== finalDragCategoryDisplayInds[catInd]; - // }); - // - // if(anyCatsReordered) { - // restyleData['dimensions[' + d.model.containerInd + '].catDisplayInds'] = [finalDragCategoryDisplayInds]; - // } - // } + var anyCatsReordered = false; + if(d.dragCategoryDisplayInd !== null) { + var finalDragCategoryDisplayInds = d.model.categories.map(function(c) { + return c.displayInd; + }); + + anyCatsReordered = d.initialDragCategoryDisplayInds.some(function(initCatDisplay, catInd) { + return initCatDisplay !== finalDragCategoryDisplayInds[catInd]; + }); + + if(anyCatsReordered) { + + // Sort a shallow copy of the category models by display index + var sortedCategoryModels = d.model.categories.slice().sort( + function (a, b) { return a.displayInd - b.displayInd }); + + // Get new categoryarray and categorylabels values + var newCategoryArray = sortedCategoryModels.map(function (v) { return v.categoryValue }); + var newCategoryLabels = sortedCategoryModels.map(function (v) { return v.categoryLabel }); + + restyleData['dimensions[' + d.model.containerInd + '].categoryarray'] = [newCategoryArray]; + restyleData['dimensions[' + d.model.containerInd + '].categorylabels'] = [newCategoryLabels]; + restyleData['dimensions[' + d.model.containerInd + '].categoryorder'] = ['array']; + } + } // Handle potential click event // ---------------------------- @@ -1520,6 +1531,12 @@ function updatePathViewModels(parcatsViewModel) { }); }); + // Array from category index to category display index for each true dimension index + var catToDisplayIndPerDim = parcatsViewModel.model.dimensions.map( + function(d) { + return d.categories.map(function(c) {return c.displayInd;}); + }); + // Array from true dimension index to dimension display index var dimToDisplayInd = parcatsViewModel.model.dimensions.map(function(d) {return d.displayInd;}); var displayToDimInd = parcatsViewModel.dimensions.map(function(d) {return d.model.dimensionInd;}); @@ -1541,12 +1558,21 @@ function updatePathViewModels(parcatsViewModel) { } } + // Compute category display inds to use for sorting paths + function pathDisplayCategoryInds(pathModel) { + var dimensionInds = pathModel.categoryInds.map(function(catInd, dimInd) {return catToDisplayIndPerDim[dimInd][catInd];}); + var displayInds = displayToDimInd.map(function(dimInd) { + return dimensionInds[dimInd]; + }); + return displayInds; + } + // Sort in ascending order by display index array pathModels.sort(function(v1, v2) { // Build display inds for each path - var sortArray1 = v1.categoryInds; - var sortArray2 = v2.categoryInds; + var sortArray1 = pathDisplayCategoryInds(v1); + var sortArray2 = pathDisplayCategoryInds(v2); // Handle path sort order if(parcatsViewModel.sortpaths === 'backward') { @@ -1599,14 +1625,15 @@ function updatePathViewModels(parcatsViewModel) { var pathYs = new Array(nextYPositions.length); for(var d = 0; d < pathModel.categoryInds.length; d++) { var catInd = pathModel.categoryInds[d]; + var catDisplayInd = catToDisplayIndPerDim[d][catInd]; var dimDisplayInd = dimToDisplayInd[d]; // Update next y position - pathYs[dimDisplayInd] = nextYPositions[dimDisplayInd][catInd]; - nextYPositions[dimDisplayInd][catInd] += pathHeight; + pathYs[dimDisplayInd] = nextYPositions[dimDisplayInd][catDisplayInd]; + nextYPositions[dimDisplayInd][catDisplayInd] += pathHeight; // Update category color information - var catViewModle = parcatsViewModel.dimensions[dimDisplayInd].categories[catInd]; + var catViewModle = parcatsViewModel.dimensions[dimDisplayInd].categories[catDisplayInd]; var numBands = catViewModle.bands.length; var lastCatBand = catViewModle.bands[numBands - 1]; @@ -1641,7 +1668,7 @@ function updatePathViewModels(parcatsViewModel) { } pathViewModels[pathNumber] = { - key: pathModel.categoryInds + '-' + pathModel.valueInds[0], + key: pathModel.valueInds[0], model: pathModel, height: pathHeight, leftXs: leftXPositions, @@ -1730,14 +1757,23 @@ function createDimensionViewModel(parcatsViewModel, dimensionModel) { nextCatHeight, nextCatModel, nextCat, - catInd; + catInd, + catDisplayInd; // Compute starting Y offset var nextCatY = (maxCats - numCats) * catSpacing / 2.0; // Compute category ordering - for(catInd = 0; catInd < numCats; catInd++) { + var categoryIndInfo = dimensionModel.categories.map(function(c) { + return {displayInd: c.displayInd, categoryInd: c.categoryInd}; + }); + + categoryIndInfo.sort(function(a, b) { + return a.displayInd - b.displayInd; + }); + for(catDisplayInd = 0; catDisplayInd < numCats; catDisplayInd++) { + catInd = categoryIndInfo[catDisplayInd].categoryInd; nextCatModel = dimensionModel.categories[catInd]; if(totalCount > 0) { @@ -1747,7 +1783,7 @@ function createDimensionViewModel(parcatsViewModel, dimensionModel) { } nextCat = { - key: catInd, + key: nextCatModel.valueInds[0], model: nextCatModel, width: dimWidth, height: nextCatHeight, diff --git a/test/image/mocks/parcats_reordered.json b/test/image/mocks/parcats_reordered.json index 65169e6ad5b..eeb089add90 100644 --- a/test/image/mocks/parcats_reordered.json +++ b/test/image/mocks/parcats_reordered.json @@ -5,7 +5,7 @@ "dimensions": [ {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1], "displayindex": 0}, {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"], "displayindex": 2, - "catDisplayInds": [1, 2, 0], "CatValues": ["A", "B", "C"]}, + "categoryarray": ["B", "A", "C"]}, {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11], "displayindex": 1}]} ], "layout": { From 66c90fa4b82d54bd7b84224bcba1e840e87f1a05 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Mon, 20 Aug 2018 11:44:08 -0400 Subject: [PATCH 19/25] Fixed tests to use new `categoryorder`, `categoryarray`, `categorylabels` construct --- src/traces/parcats/parcats.js | 2 +- test/jasmine/tests/parcats_test.js | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index dfef5d3f2ae..af564f7e50e 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -1143,7 +1143,7 @@ function dragDimensionEnd(d) { restyleData['dimensions[' + d.model.containerInd + '].categoryarray'] = [newCategoryArray]; restyleData['dimensions[' + d.model.containerInd + '].categorylabels'] = [newCategoryLabels]; - restyleData['dimensions[' + d.model.containerInd + '].categoryorder'] = ['array']; + restyleData['dimensions[' + d.model.containerInd + '].categoryorder'] = 'array'; } } diff --git a/test/jasmine/tests/parcats_test.js b/test/jasmine/tests/parcats_test.js index 41876f6b845..bf7eaefbf8b 100644 --- a/test/jasmine/tests/parcats_test.js +++ b/test/jasmine/tests/parcats_test.js @@ -366,22 +366,22 @@ describe('Dimension reordered parcats trace', function() { {dimensionInd: 1, displayInd: 2, dimensionLabel: 'Two'}); checkCategoryCalc(gd, 1, 0, { - categoryLabel: 'A', + categoryLabel: 'B', dimensionInd: 1, categoryInd: 0, - displayInd: 1}); + displayInd: 0}); checkCategoryCalc(gd, 1, 1, { - categoryLabel: 'B', + categoryLabel: 'A', dimensionInd: 1, categoryInd: 1, - displayInd: 2}); + displayInd: 1}); checkCategoryCalc(gd, 1, 2, { categoryLabel: 'C', dimensionInd: 1, categoryInd: 2, - displayInd: 0}); + displayInd: 2}); // ### Dimension 2 ### checkDimensionCalc(gd, 2, @@ -402,9 +402,6 @@ describe('Dimension reordered parcats trace', function() { // Define bad display indexes [0, 2, 0] mock.data[0].dimensions[2].displayindex = 0; - // catDisplayInds for dimension 1 as [0, 2, 0] - mock.data[0].dimensions[1].catDisplayInds[0] = 0; - Plotly.newPlot(gd, mock) .then(function() { @@ -440,12 +437,12 @@ describe('Dimension reordered parcats trace', function() { {dimensionInd: 1, displayInd: 1, dimensionLabel: 'Two'}); checkCategoryCalc(gd, 1, 0, { - categoryLabel: 'A', + categoryLabel: 'B', categoryInd: 0, displayInd: 0}); checkCategoryCalc(gd, 1, 1, { - categoryLabel: 'B', + categoryLabel: 'A', categoryInd: 1, displayInd: 1}); @@ -737,7 +734,9 @@ describe('Drag to reordered dimensions and categories', function() { {'dimensions[0].displayindex': 0, 'dimensions[1].displayindex': 2, 'dimensions[2].displayindex': 1, - 'dimensions[1].catDisplayInds': [[ 1, 2, 0 ]]}, + 'dimensions[1].categoryorder': 'array', + 'dimensions[1].categoryarray': [['C', 'A', 'B' ]], + 'dimensions[1].categorylabels': [['C', 'A', 'B' ]]}, [0]]); restyleCallback.calls.reset(); From 69f692209e4f9e56339abcbbe795b3b700e5f8d7 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Mon, 20 Aug 2018 14:33:19 -0400 Subject: [PATCH 20/25] Add 'dimension' hovermode that uses multi-hoverlabel logic --- src/components/fx/hover.js | 3 +- src/components/fx/index.js | 2 +- src/traces/parcats/attributes.js | 2 +- src/traces/parcats/parcats.js | 53 ++++++++++++++----- .../mocks/parcats_hovermode_dimension.json | 21 ++++++++ 5 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 test/image/mocks/parcats_hovermode_dimension.json diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index c2e616b253b..1b39c5dede9 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -153,8 +153,7 @@ exports.loneHover = function loneHover(hoverItem, opts) { return hoverLabel.node(); }; -// TODO: replace loneHover? -exports.customHovers = function customHovers(hoverItems, opts) { +exports.multiHovers = function multiHovers(hoverItems, opts) { if(!Array.isArray(hoverItems)) { hoverItems = [hoverItems]; diff --git a/src/components/fx/index.js b/src/components/fx/index.js index c495537e379..738c64a00e3 100644 --- a/src/components/fx/index.js +++ b/src/components/fx/index.js @@ -45,7 +45,7 @@ module.exports = { unhover: dragElement.unhover, loneHover: require('./hover').loneHover, - customHovers: require('./hover').customHovers, + multiHovers: require('./hover').multiHovers, loneUnhover: loneUnhover, click: require('./click') diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index c3aab107edd..e026760cd80 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -39,7 +39,7 @@ module.exports = { }), hovermode: { valType: 'enumerated', - values: ['category', 'color'], + values: ['category', 'color', 'dimension'], dflt: 'category', role: 'info', editType: 'plot', diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index af564f7e50e..ceba62b0929 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -500,7 +500,8 @@ function buildPointsArrayForPath(d) { * @param {PathViewModel} d */ function clickPath(d) { - if(d.parcatsViewModel.hovermode !== 'none') { + if (d.parcatsViewModel.hoverinfoItems.indexOf('skip') === -1) { + // hoverinfo it's skip, so interaction events aren't disabled var points = buildPointsArrayForPath(d); d.parcatsViewModel.graphDiv.emit('plotly_click', {points: points, event: d3.event}); } @@ -738,7 +739,6 @@ function createHoverLabelForCategoryHovermode(rootBBox, bandElement) { }; } - /** * Create hover label for a band element's category (for use when hovermode === 'category') * @@ -748,6 +748,31 @@ function createHoverLabelForCategoryHovermode(rootBBox, bandElement) { * HTML element for band * */ +function createHoverLabelForDimensionHovermode(rootBBox, bandElement) { + + var allHoverlabels = []; + + d3.select(bandElement.parentNode.parentNode) + .selectAll('g.category') + .select('rect.catrect') + .each(function() { + var bandNode = this; + allHoverlabels.push(createHoverLabelForCategoryHovermode(rootBBox, bandNode)); + }); + + console.log(allHoverlabels); + return allHoverlabels +} + +/** + * Create hover labels for a band element's category (for use when hovermode === 'dimension') + * + * @param {ClientRect} rootBBox + * Client bounding box for root of figure + * @param {HTMLElement} bandElement + * HTML element for band + * + */ function createHoverLabelForColorHovermode(rootBBox, bandElement) { var bandBoundingBox = bandElement.getBoundingClientRect(); @@ -866,25 +891,27 @@ function mouseoverCategoryBand(bandViewModel) { var bandElement = this; // Handle style and events - if (hovermode === 'category') { - styleForCategoryHovermode(bandElement); - emitPointsEventCategoryHovermode(bandElement, 'plotly_hover', d3.event); - } else if (hovermode === 'color') { + if (hovermode === 'color') { styleForColorHovermode(bandElement); emitPointsEventColorHovermode(bandElement, 'plotly_hover', d3.event); + } else { + styleForCategoryHovermode(bandElement); + emitPointsEventCategoryHovermode(bandElement, 'plotly_hover', d3.event); } // Handle hover label if(bandViewModel.parcatsViewModel.hoverinfoItems.indexOf('none') === -1) { - var hoverItem; + var hoverItems; if (hovermode === 'category') { - hoverItem = createHoverLabelForCategoryHovermode(rootBBox, bandElement); + hoverItems = createHoverLabelForCategoryHovermode(rootBBox, bandElement); } else if (hovermode === 'color') { - hoverItem = createHoverLabelForColorHovermode(rootBBox, bandElement); + hoverItems = createHoverLabelForColorHovermode(rootBBox, bandElement); + } else if (hovermode === 'dimension') { + hoverItems = createHoverLabelForDimensionHovermode(rootBBox, bandElement); } - if (hoverItem) { - Fx.loneHover(hoverItem, { + if (hoverItems) { + Fx.multiHovers(hoverItems, { container: fullLayout._hoverlayer.node(), outerContainer: fullLayout._paper.node(), gd: gd @@ -1152,7 +1179,7 @@ function dragDimensionEnd(d) { if(!d.dragHasMoved && d.potentialClickBand) { if(d.parcatsViewModel.hovermode === 'color') { emitPointsEventColorHovermode(d.potentialClickBand, 'plotly_click', d3.event.sourceEvent); - } else if(d.parcatsViewModel.hovermode === 'category') { + } else { emitPointsEventCategoryHovermode(d.potentialClickBand, 'plotly_click', d3.event.sourceEvent); } } @@ -1861,7 +1888,7 @@ function createDimensionViewModel(parcatsViewModel, dimensionModel) { * @property {Number} y * Y position of this trace with respect to the Figure (pixels) * @property {String} hovermode - * Hover mode. One of: 'none', 'category', or 'color' + * Hover mode. One of: 'none', 'category', 'color', or 'dimension' * @property {Array.} hoverinfoItems * Info to display on hover. Array with a combination of 'counts' and/or 'probabilities', or 'none', or 'skip' * @property {String} arrangement diff --git a/test/image/mocks/parcats_hovermode_dimension.json b/test/image/mocks/parcats_hovermode_dimension.json new file mode 100644 index 00000000000..47c896e2d02 --- /dev/null +++ b/test/image/mocks/parcats_hovermode_dimension.json @@ -0,0 +1,21 @@ +{ + "data": [ + {"type": "parcats", + "domain": {"x": [0.125, 0.625],"y": [0.25, 0.75]}, + "hovermode": "dimension", + "hoverinfo": "probability+count", + "dimensions":[ + {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1]}, + {"label": "Two", + "values": ["A", "A", "A", "B", "A", "A", "A", "A", "C"], + "categoryorder": "category descending"}, + {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}] + } + ], + "layout": { + "height": 602, + "width": 592, + "margin": { + "l": 40, "r": 40, "t": 50, "b": 40 + }} +} From f2aa9b9a9cbf713df4245b07b67f35366ef37777 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Tue, 21 Aug 2018 12:37:09 -0400 Subject: [PATCH 21/25] Added labelfont and categorylabelfont top-level attributes to the control font of dimension labels and category labels respectively --- src/traces/parcats/attributes.js | 19 +++++++++-------- src/traces/parcats/defaults.js | 16 +++++++++++++++ src/traces/parcats/parcats.js | 32 +++++++++++++++++++++++++---- test/image/mocks/parcats_fonts.json | 19 +++++++++++++++++ 4 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 test/image/mocks/parcats_fonts.json diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index e026760cd80..711606e6727 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -10,6 +10,7 @@ var extendFlat = require('../../lib/extend').extendFlat; var plotAttrs = require('../../plots/attributes'); +var fontAttrs = require('../../plots/font_attributes'); var colorAttributes = require('../../components/colorscale/attributes'); var domainAttrs = require('../../plots/domain').attributes; var scatterAttrs = require('../scatter/attributes'); @@ -75,15 +76,15 @@ module.exports = { 'If `backward` sort based on dimensions from right to left.' ].join(' ') }, - // labelfont: fontAttrs({ - // editType: 'calc', - // description: 'Sets the font for the `dimension` labels.' - // }), - // - // catfont: fontAttrs({ - // editType: 'calc', - // description: 'Sets the font for the `category` labels.' - // }), + labelfont: fontAttrs({ + editType: 'calc', + description: 'Sets the font for the `dimension` labels.' + }), + + categorylabelfont: fontAttrs({ + editType: 'calc', + description: 'Sets the font for the `category` labels.' + }), dimensions: { _isLinkedToArray: 'dimension', diff --git a/src/traces/parcats/defaults.js b/src/traces/parcats/defaults.js index 4eb0e56553b..c23b7b817c1 100644 --- a/src/traces/parcats/defaults.js +++ b/src/traces/parcats/defaults.js @@ -104,4 +104,20 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('bundlecolors'); coerce('sortpaths'); coerce('counts'); + + var labelfontDflt = { + family: layout.font.family, + size: Math.round(layout.font.size / 1.2), + color: layout.font.color + }; + + Lib.coerceFont(coerce, 'labelfont', labelfontDflt); + + var categoryfontDefault = { + family: layout.font.family, + size: Math.round(layout.font.size / 1.4), + color: layout.font.color + }; + + Lib.coerceFont(coerce, 'categorylabelfont', categoryfontDefault); }; diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index ceba62b0929..0851160dcd0 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -12,6 +12,7 @@ var d3 = require('d3'); var Plotly = require('../../plot_api/plot_api'); var Fx = require('../../components/fx'); var Lib = require('../../lib'); +var Drawing = require('../../components/drawing'); var tinycolor = require('tinycolor2'); function performPlot(parcatsModels, graphDiv, layout, svg) { @@ -247,7 +248,6 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { 'rgb(255, 255, 255) 1px 1px 2px, ' + 'rgb(255, 255, 255) 1px -1px 2px, ' + 'rgb(255, 255, 255) -1px -1px 2px') - .attr('font-size', 10) .style('fill', 'rgb(0, 0, 0)') .attr('x', function(d) { @@ -264,7 +264,12 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { }) .text(function(d) { return d.model.categoryLabel; - }); + }) + .each( + /** @param {CategoryViewModel} catModel*/ + function(catModel){ + Drawing.font(d3.select(this), catModel.parcatsViewModel.categorylabelfont); + }); // Initialize dimension label categoryGroupEnterSelection @@ -284,7 +289,6 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { return 'ew-resize' } }) - .attr('font-size', 14) .attr('x', function(d) { return d.width / 2; }) @@ -296,7 +300,12 @@ function performPlot(parcatsModels, graphDiv, layout, svg) { } else { return null; } - }); + }) + .each( + /** @param {CategoryViewModel} catModel*/ + function(catModel){ + Drawing.font(d3.select(this), catModel.parcatsViewModel.labelfont); + }); // Category hover // categorySelection.select('rect.catrect') @@ -1443,6 +1452,8 @@ function createParcatsViewModel(graphDiv, layout, wrappedParcatsModel) { arrangement: trace.arrangement, bundlecolors: trace.bundlecolors, sortpaths: trace.sortpaths, + labelfont: trace.labelfont, + categorylabelfont: trace.categorylabelfont, pathShape: pathShape, dragDimension: null, margin: margin, @@ -1868,6 +1879,15 @@ function createDimensionViewModel(parcatsViewModel, dimensionModel) { * Left margin */ +/** + * @typedef {Object} Font + * Object containing font information + * + * @property {Number} size: Font size + * @property {String} color: Font color + * @property {String} family: Font family + */ + /** * @typedef {Object} ParcatsViewModel * Object containing calculated parcats view information @@ -1898,6 +1918,10 @@ function createDimensionViewModel(parcatsViewModel, dimensionModel) { * @property {String} sortpaths * If 'forward' then sort paths based on dimensions from left to right. If 'backward' sort based on dimensions * from right to left + * @property {Font} labelfont + * Font for the dimension labels + * @property {Font} categorylabelfont + * Font for the category labels * @property {String} pathShape * The shape of the paths. Either 'linear' or 'hspline'. * @property {DimensionViewModel|null} dragDimension diff --git a/test/image/mocks/parcats_fonts.json b/test/image/mocks/parcats_fonts.json new file mode 100644 index 00000000000..0e7937512de --- /dev/null +++ b/test/image/mocks/parcats_fonts.json @@ -0,0 +1,19 @@ +{ + "data": [ + {"type": "parcats", + "domain": {"x": [0.125, 0.625],"y": [0.25, 0.75]}, + "dimensions":[ + {"label": "One", "values": [1, 1, 2, 1, 2, 1, 1, 2, 1]}, + {"label": "Two", "values": ["A", "B", "A", "B", "C", "C", "A", "B", "C"]}, + {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}], + "labelfont": {"family": "Rockwell", "size": 20, "color": "gray"}, + "categorylabelfont": {"family": "Arial", "size": 10, "color": "firebrick"} + } + ], + "layout": { + "height": 602, + "width": 592, + "margin": { + "l": 40, "r": 40, "t": 50, "b": 40 + }} +} From 4eb5317ccef551281ff89332b8c15b4d0e1463cf Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Tue, 21 Aug 2018 13:46:48 -0400 Subject: [PATCH 22/25] Review / cleanup attribute descriptions --- src/traces/parcats/attributes.js | 33 ++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/traces/parcats/attributes.js b/src/traces/parcats/attributes.js index 711606e6727..beaa37e82b3 100644 --- a/src/traces/parcats/attributes.js +++ b/src/traces/parcats/attributes.js @@ -29,7 +29,12 @@ var line = extendFlat({ dflt: 'linear', role: 'info', editType: 'plot', - description: 'Sets the shape of the paths'}, + description: [ + 'Sets the shape of the paths.', + 'If `linear`, paths are composed of straight lines.', + 'If `hspline`, paths are composed of horizontal curved splines' + ].join(' ') + } }); module.exports = { @@ -37,6 +42,7 @@ module.exports = { hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { flags: ['count', 'probability'], editType: 'plot' + // plotAttrs.hoverinfo description is appropriate }), hovermode: { valType: 'enumerated', @@ -44,7 +50,12 @@ module.exports = { dflt: 'category', role: 'info', editType: 'plot', - description: 'Sets the hover mode of the parcats diagram' + description: [ + 'Sets the hover mode of the parcats diagram.', + 'If `category`, hover interaction take place per category.', + 'If `color`, hover interactions take place per color per category.', + 'If `dimension`, hover interactions take across all categories per dimension.' + ].join(' ') }, arrangement: { valType: 'enumerated', @@ -53,9 +64,10 @@ module.exports = { role: 'style', editType: 'plot', description: [ - 'If value is `perpendicular`, the categories can only move along a line perpendicular to the paths.', - 'If value is `freeform`, the categories can freely move on the plane.', - 'If value is `fixed`, the categories and dimensions are stationary.' + 'Sets the drag interaction mode for categories and dimensions.', + 'If `perpendicular`, the categories can only move along a line perpendicular to the paths.', + 'If `freeform`, the categories can freely move on the plane.', + 'If `fixed`, the categories and dimensions are stationary.' ].join(' ') }, bundlecolors: { @@ -63,7 +75,7 @@ module.exports = { dflt: true, role: 'info', editType: 'plot', - description: 'Sort paths so that like colors are bundled together' + description: 'Sort paths so that like colors are bundled together within each category.' }, sortpaths: { valType: 'enumerated', @@ -72,8 +84,9 @@ module.exports = { role: 'info', editType: 'plot', description: [ - 'If `forward` then sort paths based on dimensions from left to right.', - 'If `backward` sort based on dimensions from right to left.' + 'Sets the path sorting algorithm.', + 'If `forward`, sort paths based on dimension categories from left to right.', + 'If `backward`, sort paths based on dimensions categories from right to left.' ].join(' ') }, labelfont: fontAttrs({ @@ -141,7 +154,7 @@ module.exports = { description: [ 'Dimension values. `values[n]` represents the category value of the `n`th point in the dataset,', 'therefore the `values` vector for all dimensions must be the same (longer vectors', - 'will be truncated). Each value must an element of `catValues`.' + 'will be truncated).' ].join(' ') }, displayindex: { @@ -161,7 +174,7 @@ module.exports = { role: 'info', editType: 'calc', description: 'Shows the dimension when set to `true` (the default). Hides the dimension for `false`.' - }, + } }, line: line, From a8973889231495d6f745d6e9722c05e267864121 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Tue, 21 Aug 2018 13:47:49 -0400 Subject: [PATCH 23/25] Add `counts` attribute to parcats_hovermode_dimension mock Makes the hoverlabel shifting logic more noticeable and shows off the `counts` attribute --- test/image/mocks/parcats_hovermode_dimension.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/image/mocks/parcats_hovermode_dimension.json b/test/image/mocks/parcats_hovermode_dimension.json index 47c896e2d02..b5b7d990844 100644 --- a/test/image/mocks/parcats_hovermode_dimension.json +++ b/test/image/mocks/parcats_hovermode_dimension.json @@ -9,7 +9,8 @@ {"label": "Two", "values": ["A", "A", "A", "B", "A", "A", "A", "A", "C"], "categoryorder": "category descending"}, - {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}] + {"label": "Three", "values": [11, 11, 11, 11, 11, 11, 11, 11, 11]}], + "counts": [11, 3, 5, 2, 1, 20, 5, 9, 1] } ], "layout": { From 98d76ee4c29712f3d25f76dae9c8e9dcca498c02 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Thu, 23 Aug 2018 13:24:33 -0400 Subject: [PATCH 24/25] Refactor dimension and category dragging tests and test arrangements Now there are tests for 'freeform', 'perpendicular', and 'fixed' arrangements for dragging the dimension label and category rectangle. --- src/traces/parcats/parcats.js | 1 - test/jasmine/tests/parcats_test.js | 680 +++++++++++++++++++++++------ 2 files changed, 549 insertions(+), 132 deletions(-) diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index 0851160dcd0..1cd49eec4e1 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -769,7 +769,6 @@ function createHoverLabelForDimensionHovermode(rootBBox, bandElement) { allHoverlabels.push(createHoverLabelForCategoryHovermode(rootBBox, bandNode)); }); - console.log(allHoverlabels); return allHoverlabels } diff --git a/test/jasmine/tests/parcats_test.js b/test/jasmine/tests/parcats_test.js index bf7eaefbf8b..ef9a0cc01fe 100644 --- a/test/jasmine/tests/parcats_test.js +++ b/test/jasmine/tests/parcats_test.js @@ -283,7 +283,11 @@ describe('Basic parcats trace', function() { it('should compute initial fullData properly', function(done) { Plotly.newPlot(gd, basic_mock) .then(function() { - // TODO: check that defaults are inferred properly + // Check that some of the defaults are computed properly + var fullTrace = gd._fullData[0]; + expect(fullTrace.arrangement).toBe('perpendicular'); + expect(fullTrace.bundlecolors).toBe(true); + expect(fullTrace.dimensions[1].visible).toBe(true); }) .catch(failTest) .then(done); @@ -485,7 +489,7 @@ describe('Dimension reordered parcats trace', function() { }); }); -describe('Drag to reordered dimensions and categories', function() { +describe('Drag to reordered dimensions', function() { // Variable declarations // --------------------- @@ -503,56 +507,168 @@ describe('Drag to reordered dimensions and categories', function() { afterEach(destroyGraphDiv); - it('It should support dragging dimension label to reorder dimensions', function(done) { + function getMousePositions(parcatsViewModel) { + // Compute Mouse positions + // ----------------------- + // Start mouse in the middle of the dimension label on the + // second dimensions (dimension display index 1) + var dragDimStartX = parcatsViewModel.dimensions[1].x; + var mouseStartY = parcatsViewModel.y - 5, + mouseStartX = parcatsViewModel.x + dragDimStartX + dimWidth / 2; + + // Pause mouse half-way between the original location of + // the first and second dimensions. Also move mosue + // downward a bit to make sure drag 'sticks' + var mouseMidY = parcatsViewModel.y + 50, + mouseMidX = mouseStartX + dimDx / 2; + + // End mouse drag in the middle of the original + // position of the dimension label of the third dimension + // (dimension display index 2) + var mouseEndY = parcatsViewModel.y + 100, + mouseEndX = parcatsViewModel.x + parcatsViewModel.dimensions[2].x + dimWidth / 2; + return { + mouseStartY: mouseStartY, + mouseStartX: mouseStartX, + mouseMidY: mouseMidY, + mouseMidX: mouseMidX, + mouseEndY: mouseEndY, + mouseEndX: mouseEndX + }; + } + + function checkInitialDimensions() { + checkDimensionCalc(gd, 0, + {dimensionInd: 0, displayInd: 0, dragX: null, dimensionLabel: 'One', count: 9}); + checkDimensionCalc(gd, 1, + {dimensionInd: 1, displayInd: 1, dragX: null, dimensionLabel: 'Two', count: 9}); + checkDimensionCalc(gd, 2, + {dimensionInd: 2, displayInd: 2, dragX: null, dimensionLabel: 'Three', count: 9}); + } + + function checkReorderedDimensions() { + checkDimensionCalc(gd, 0, + {dimensionInd: 0, displayInd: 0, dragX: null, dimensionLabel: 'One', count: 9}); + checkDimensionCalc(gd, 1, + {dimensionInd: 1, displayInd: 2, dragX: null, dimensionLabel: 'Two', count: 9}); + checkDimensionCalc(gd, 2, + {dimensionInd: 2, displayInd: 1, dragX: null, dimensionLabel: 'Three', count: 9}); + } + + function checkMidDragDimensions(dragDimStartX) { + checkDimensionCalc(gd, 0, + {dimensionInd: 0, displayInd: 0, dragX: null, dimensionLabel: 'One', count: 9}); + checkDimensionCalc(gd, 1, + {dimensionInd: 1, displayInd: 1, dragX: dragDimStartX + dimDx / 2, dimensionLabel: 'Two', count: 9}); + checkDimensionCalc(gd, 2, + {dimensionInd: 2, displayInd: 2, dragX: null, dimensionLabel: 'Three', count: 9}); + } + + it('should support dragging dimension label to reorder dimensions in freeform arrangement', function(done) { + + // Set arrangement + mock.data[0].arrangement = 'freeform'; + Plotly.newPlot(gd, mock) .then(function() { - restyleCallback = jasmine.createSpy('restyleCallback'); gd.on('plotly_restyle', restyleCallback); /** @type {ParcatsViewModel} */ var parcatsViewModel = d3.select('g.trace.parcats').datum(); - // Compute Mouse positions - // ----------------------- - // Start mouse in the middle of the dimension label on the - // second dimensions (dimension display index 1) var dragDimStartX = parcatsViewModel.dimensions[1].x; - var mouseStartY = parcatsViewModel.y - 5, - mouseStartX = parcatsViewModel.x + dragDimStartX + dimWidth / 2; + var pos = getMousePositions(parcatsViewModel); - // Pause mouse half-way between the original location of - // the first and second dimensions. Also move mosue - // downward a bit to make sure drag 'sticks' - var mouseMidY = parcatsViewModel.y + 50, - mouseMidX = mouseStartX + dimDx / 2; + // Check initial dimension order + // ----------------------------- + checkInitialDimensions(); - // End mouse drag in the middle of the original - // position of the dimension label of the third dimension - // (dimension display index 2) - var mouseEndY = parcatsViewModel.y + 100, - mouseEndX = parcatsViewModel.x + parcatsViewModel.dimensions[2].x + dimWidth / 2; + // Position mouse for start of drag + // -------------------------------- + mouseEvent('mousemove', pos.mouseStartX, pos.mouseStartY); - // Check initial dimension order + // Perform drag + // ------------ + mouseEvent('mousedown', pos.mouseStartX, pos.mouseStartY); + + // ### Pause at drag mid-point + mouseEvent('mousemove', pos.mouseMidX, pos.mouseMidY + // {buttons: 1} // Left click + ); + + // Make sure we're dragging the middle dimension + expect(parcatsViewModel.dragDimension.model.dimensionLabel).toEqual('Two'); + + // Make sure dimensions haven't changed order yet, but that + // we do have a drag in progress on the middle dimension + checkMidDragDimensions(dragDimStartX); + + // ### Move to drag end-point + mouseEvent('mousemove', pos.mouseEndX, pos.mouseEndY); + + // Make sure we're still dragging the middle dimension + expect(parcatsViewModel.dragDimension.model.dimensionLabel).toEqual('Two'); + + // End drag + // -------- + mouseEvent('mouseup', pos.mouseEndX, pos.mouseEndY); + + // Make sure we've cleared drag dimension + expect(parcatsViewModel.dragDimension).toEqual(null); + + // Check final dimension order // ----------------------------- - checkDimensionCalc(gd, 0, - {dimensionInd: 0, displayInd: 0, dragX: null, dimensionLabel: 'One', count: 9}); - checkDimensionCalc(gd, 1, - {dimensionInd: 1, displayInd: 1, dragX: null, dimensionLabel: 'Two', count: 9}); - checkDimensionCalc(gd, 2, - {dimensionInd: 2, displayInd: 2, dragX: null, dimensionLabel: 'Three', count: 9}); + checkReorderedDimensions(); + }) + .then(delay(CALLBACK_DELAY)) + .then(function() { + // Check that proper restyle event was emitted + // ------------------------------------------- + expect(restyleCallback).toHaveBeenCalledTimes(1); + expect(restyleCallback).toHaveBeenCalledWith([ + { + 'dimensions[0].displayindex': 0, + 'dimensions[1].displayindex': 2, + 'dimensions[2].displayindex': 1 + }, + [0]]); + restyleCallback.calls.reset(); + }) + .catch(failTest) + .then(done); + }); + it('should support dragging dimension label to reorder dimensions in perpendicular arrangement', function(done) { + + // Set arrangement + mock.data[0].arrangement = 'perpendicular'; + + Plotly.newPlot(gd, mock) + .then(function() { + restyleCallback = jasmine.createSpy('restyleCallback'); + gd.on('plotly_restyle', restyleCallback); + + /** @type {ParcatsViewModel} */ + var parcatsViewModel = d3.select('g.trace.parcats').datum(); + + var dragDimStartX = parcatsViewModel.dimensions[1].x; + var pos = getMousePositions(parcatsViewModel); + + // Check initial dimension order + // ----------------------------- + checkInitialDimensions(); // Position mouse for start of drag // -------------------------------- - mouseEvent('mousemove', mouseStartX, mouseStartY); + mouseEvent('mousemove', pos.mouseStartX, pos.mouseStartY); // Perform drag // ------------ - mouseEvent('mousedown', mouseStartX, mouseStartY); + mouseEvent('mousedown', pos.mouseStartX, pos.mouseStartY); // ### Pause at drag mid-point - mouseEvent('mousemove', mouseMidX, mouseMidY + mouseEvent('mousemove', pos.mouseMidX, pos.mouseMidY // {buttons: 1} // Left click ); @@ -561,34 +677,24 @@ describe('Drag to reordered dimensions and categories', function() { // Make sure dimensions haven't changed order yet, but that // we do have a drag in progress on the middle dimension - checkDimensionCalc(gd, 0, - {dimensionInd: 0, displayInd: 0, dragX: null, dimensionLabel: 'One', count: 9}); - checkDimensionCalc(gd, 1, - {dimensionInd: 1, displayInd: 1, dragX: dragDimStartX + dimDx / 2, dimensionLabel: 'Two', count: 9}); - checkDimensionCalc(gd, 2, - {dimensionInd: 2, displayInd: 2, dragX: null, dimensionLabel: 'Three', count: 9}); + checkMidDragDimensions(dragDimStartX); // ### Move to drag end-point - mouseEvent('mousemove', mouseEndX, mouseEndY); + mouseEvent('mousemove', pos.mouseEndX, pos.mouseEndY); // Make sure we're still dragging the middle dimension expect(parcatsViewModel.dragDimension.model.dimensionLabel).toEqual('Two'); // End drag // -------- - mouseEvent('mouseup', mouseEndX, mouseEndY); + mouseEvent('mouseup', pos.mouseEndX, pos.mouseEndY); // Make sure we've cleared drag dimension expect(parcatsViewModel.dragDimension).toEqual(null); // Check final dimension order // ----------------------------- - checkDimensionCalc(gd, 0, - {dimensionInd: 0, displayInd: 0, dragX: null, dimensionLabel: 'One', count: 9}); - checkDimensionCalc(gd, 1, - {dimensionInd: 1, displayInd: 2, dragX: null, dimensionLabel: 'Two', count: 9}); - checkDimensionCalc(gd, 2, - {dimensionInd: 2, displayInd: 1, dragX: null, dimensionLabel: 'Three', count: 9}); + checkReorderedDimensions(); }) .then(delay(CALLBACK_DELAY)) .then(function() { @@ -596,10 +702,12 @@ describe('Drag to reordered dimensions and categories', function() { // ------------------------------------------- expect(restyleCallback).toHaveBeenCalledTimes(1); expect(restyleCallback).toHaveBeenCalledWith([ - {'dimensions[0].displayindex': 0, + { + 'dimensions[0].displayindex': 0, 'dimensions[1].displayindex': 2, - 'dimensions[2].displayindex': 1}, - [0]]); + 'dimensions[2].displayindex': 1 + }, + [0]]); restyleCallback.calls.reset(); }) @@ -607,7 +715,210 @@ describe('Drag to reordered dimensions and categories', function() { .then(done); }); - it('It should support dragging category to reorder categories and dimensions', function(done) { + it('should NOT support dragging dimension label to reorder dimensions in fixed arrangement', function(done) { + + // Set arrangement + mock.data[0].arrangement = 'fixed'; + + Plotly.newPlot(gd, mock) + .then(function() { + console.log(gd.data); + restyleCallback = jasmine.createSpy('restyleCallback'); + gd.on('plotly_restyle', restyleCallback); + + /** @type {ParcatsViewModel} */ + var parcatsViewModel = d3.select('g.trace.parcats').datum(); + + var dragDimStartX = parcatsViewModel.dimensions[1].x; + var pos = getMousePositions(parcatsViewModel); + + // Check initial dimension order + // ----------------------------- + checkInitialDimensions(); + + // Position mouse for start of drag + // -------------------------------- + mouseEvent('mousemove', pos.mouseStartX, pos.mouseStartY); + + // Perform drag + // ------------ + mouseEvent('mousedown', pos.mouseStartX, pos.mouseStartY); + + // ### Pause at drag mid-point + mouseEvent('mousemove', pos.mouseMidX, pos.mouseMidY + // {buttons: 1} // Left click + ); + + // Make sure we're not dragging any dimension + expect(parcatsViewModel.dragDimension).toEqual(null); + + // Make sure dimensions haven't changed order yet + checkInitialDimensions(); + + // ### Move to drag end-point + mouseEvent('mousemove', pos.mouseEndX, pos.mouseEndY); + + // Make sure we're still not dragging a dimension + expect(parcatsViewModel.dragDimension).toEqual(null); + + // End drag + // -------- + mouseEvent('mouseup', pos.mouseEndX, pos.mouseEndY); + + // Make sure dimensions haven't changed + // ------------------------------------ + checkInitialDimensions(); + }) + .then(delay(CALLBACK_DELAY)) + .then(function() { + // Check that no restyle event was emitted + // --------------------------------------- + expect(restyleCallback).toHaveBeenCalledTimes(0); + restyleCallback.calls.reset(); + }) + .catch(failTest) + .then(done); + }); +}); + +describe('Drag to reordered categories', function() { + + // Variable declarations + // --------------------- + // ### Trace level ### + var gd, + restyleCallback, + mock; + + // Fixtures + // -------- + beforeEach(function() { + gd = createGraphDiv(); + mock = Lib.extendDeep({}, require('@mocks/parcats_basic_freeform.json')); + }); + + afterEach(destroyGraphDiv); + + function getDragPositions(parcatsViewModel) { + var dragDimStartX = parcatsViewModel.dimensions[1].x; + + var mouseStartY = parcatsViewModel.y + parcatsViewModel.dimensions[1].categories[2].y, + mouseStartX = parcatsViewModel.x + dragDimStartX + dimWidth / 2; + + // Pause mouse half-way between the original location of + // the first and second dimensions. Also move mouse + // upward enough to swap position with middle category + var mouseMidY = parcatsViewModel.y + parcatsViewModel.dimensions[1].categories[1].y, + mouseMidX = mouseStartX + dimDx / 2; + + // End mouse drag in the middle of the original + // position of the dimension label of the third dimension + // (dimension display index 2), and at the height of the original top category + var mouseEndY = parcatsViewModel.y, + mouseEndX = parcatsViewModel.x + parcatsViewModel.dimensions[2].x + dimWidth / 2; + return { + dragDimStartX: dragDimStartX, + mouseStartY: mouseStartY, + mouseStartX: mouseStartX, + mouseMidY: mouseMidY, + mouseMidX: mouseMidX, + mouseEndY: mouseEndY, + mouseEndX: mouseEndX + }; + } + + function checkInitialDimensions() { + checkDimensionCalc(gd, 0, + {dimensionInd: 0, displayInd: 0, dragX: null, dimensionLabel: 'One', count: 9}); + checkDimensionCalc(gd, 1, + {dimensionInd: 1, displayInd: 1, dragX: null, dimensionLabel: 'Two', count: 9}); + checkDimensionCalc(gd, 2, + {dimensionInd: 2, displayInd: 2, dragX: null, dimensionLabel: 'Three', count: 9}); + } + + function checkMidDragDimensions(dragDimStartX) { + checkDimensionCalc(gd, 0, + {dimensionInd: 0, displayInd: 0, dragX: null, dimensionLabel: 'One', count: 9}); + checkDimensionCalc(gd, 1, + {dimensionInd: 1, displayInd: 1, dragX: dragDimStartX + dimDx / 2, dimensionLabel: 'Two', count: 9}); + checkDimensionCalc(gd, 2, + {dimensionInd: 2, displayInd: 2, dragX: null, dimensionLabel: 'Three', count: 9}); + } + + function checkInitialCategories() { + checkCategoryCalc(gd, 1, 0, { + categoryLabel: 'A', + categoryInd: 0, + displayInd: 0 + }); + + checkCategoryCalc(gd, 1, 1, { + categoryLabel: 'B', + categoryInd: 1, + displayInd: 1 + }); + + checkCategoryCalc(gd, 1, 2, { + categoryLabel: 'C', + categoryInd: 2, + displayInd: 2 + }); + } + + function checkMidDragCategories() { + checkCategoryCalc(gd, 1, 0, { + categoryLabel: 'A', + categoryInd: 0, + displayInd: 0 + }); + + checkCategoryCalc(gd, 1, 1, { + categoryLabel: 'B', + categoryInd: 1, + displayInd: 2 + }); + + checkCategoryCalc(gd, 1, 2, { + categoryLabel: 'C', + categoryInd: 2, + displayInd: 1 + }); + } + + function checkFinalDimensions() { + checkDimensionCalc(gd, 0, + {dimensionInd: 0, displayInd: 0, dragX: null, dimensionLabel: 'One', count: 9}); + checkDimensionCalc(gd, 1, + {dimensionInd: 1, displayInd: 2, dragX: null, dimensionLabel: 'Two', count: 9}); + checkDimensionCalc(gd, 2, + {dimensionInd: 2, displayInd: 1, dragX: null, dimensionLabel: 'Three', count: 9}); + } + + function checkFinalCategories() { + checkCategoryCalc(gd, 1, 0, { + categoryLabel: 'A', + categoryInd: 0, + displayInd: 1 + }); + + checkCategoryCalc(gd, 1, 1, { + categoryLabel: 'B', + categoryInd: 1, + displayInd: 2 + }); + + checkCategoryCalc(gd, 1, 2, { + categoryLabel: 'C', + categoryInd: 2, + displayInd: 0 + }); + } + + it('should support dragging category to reorder categories and dimensions in freeform arrangement', function(done) { + + // Set arrangement + mock.data[0].arrangement = 'freeform'; + Plotly.newPlot(gd, mock) .then(function() { @@ -621,108 +932,56 @@ describe('Drag to reordered dimensions and categories', function() { // ----------------------- // Start mouse in the middle of the lowest category // second dimensions (dimension display index 1) - var dragDimStartX = parcatsViewModel.dimensions[1].x; - - var mouseStartY = parcatsViewModel.y + parcatsViewModel.dimensions[1].categories[2].y, - mouseStartX = parcatsViewModel.x + dragDimStartX + dimWidth / 2; - - // Pause mouse half-way between the original location of - // the first and second dimensions. Also move mouse - // upward enough to swap position with middle category - var mouseMidY = parcatsViewModel.y + parcatsViewModel.dimensions[1].categories[1].y, - mouseMidX = mouseStartX + dimDx / 2; - - // End mouse drag in the middle of the original - // position of the dimension label of the third dimension - // (dimension display index 2), and at the height of the original top category - var mouseEndY = parcatsViewModel.y, - mouseEndX = parcatsViewModel.x + parcatsViewModel.dimensions[2].x + dimWidth / 2; + var pos = getDragPositions(parcatsViewModel); // Check initial dimension order // ----------------------------- - checkDimensionCalc(gd, 0, - {dimensionInd: 0, displayInd: 0, dragX: null, dimensionLabel: 'One', count: 9}); - checkDimensionCalc(gd, 1, - {dimensionInd: 1, displayInd: 1, dragX: null, dimensionLabel: 'Two', count: 9}); - checkDimensionCalc(gd, 2, - {dimensionInd: 2, displayInd: 2, dragX: null, dimensionLabel: 'Three', count: 9}); + checkInitialDimensions(); + + // Check initial categories + // ------------------------ + checkInitialCategories(); // Position mouse for start of drag // -------------------------------- - mouseEvent('mousemove', mouseStartX, mouseStartY); + mouseEvent('mousemove', pos.mouseStartX, pos.mouseStartY); // Perform drag // ------------ - mouseEvent('mousedown', mouseStartX, mouseStartY); + mouseEvent('mousedown', pos.mouseStartX, pos.mouseStartY); // ### Pause at drag mid-point - mouseEvent('mousemove', mouseMidX, mouseMidY); + mouseEvent('mousemove', pos.mouseMidX, pos.mouseMidY); // Make sure we're dragging the middle dimension expect(parcatsViewModel.dragDimension.model.dimensionLabel).toEqual('Two'); // Make sure dimensions haven't changed order yet, but that // we do have a drag in progress on the middle dimension - checkDimensionCalc(gd, 0, - {dimensionInd: 0, displayInd: 0, dragX: null, dimensionLabel: 'One', count: 9}); - checkDimensionCalc(gd, 1, - {dimensionInd: 1, displayInd: 1, dragX: dragDimStartX + dimDx / 2, dimensionLabel: 'Two', count: 9}); - checkDimensionCalc(gd, 2, - {dimensionInd: 2, displayInd: 2, dragX: null, dimensionLabel: 'Three', count: 9}); + checkMidDragDimensions(pos.dragDimStartX); // Make sure categories in dimension 1 have changed already - checkCategoryCalc(gd, 1, 0, { - categoryLabel: 'A', - categoryInd: 0, - displayInd: 0}); - - checkCategoryCalc(gd, 1, 1, { - categoryLabel: 'B', - categoryInd: 1, - displayInd: 2}); - - checkCategoryCalc(gd, 1, 2, { - categoryLabel: 'C', - categoryInd: 2, - displayInd: 1}); + checkMidDragCategories(); // ### Move to drag end-point - mouseEvent('mousemove', mouseEndX, mouseEndY); + mouseEvent('mousemove', pos.mouseEndX, pos.mouseEndY); // Make sure we're still dragging the middle dimension expect(parcatsViewModel.dragDimension.model.dimensionLabel).toEqual('Two'); // End drag // -------- - mouseEvent('mouseup', mouseEndX, mouseEndY); + mouseEvent('mouseup', pos.mouseEndX, pos.mouseEndY); // Make sure we've cleared drag dimension expect(parcatsViewModel.dragDimension).toEqual(null); // Check final dimension order // ----------------------------- - checkDimensionCalc(gd, 0, - {dimensionInd: 0, displayInd: 0, dragX: null, dimensionLabel: 'One', count: 9}); - checkDimensionCalc(gd, 1, - {dimensionInd: 1, displayInd: 2, dragX: null, dimensionLabel: 'Two', count: 9}); - checkDimensionCalc(gd, 2, - {dimensionInd: 2, displayInd: 1, dragX: null, dimensionLabel: 'Three', count: 9}); + checkFinalDimensions(); // Make sure categories in dimension 1 have changed already - checkCategoryCalc(gd, 1, 0, { - categoryLabel: 'A', - categoryInd: 0, - displayInd: 1}); - - checkCategoryCalc(gd, 1, 1, { - categoryLabel: 'B', - categoryInd: 1, - displayInd: 2}); - - checkCategoryCalc(gd, 1, 2, { - categoryLabel: 'C', - categoryInd: 2, - displayInd: 0}); + checkFinalCategories(); }) .then(delay(CALLBACK_DELAY)) @@ -744,27 +1003,186 @@ describe('Drag to reordered dimensions and categories', function() { .catch(failTest) .then(done); }); -}); -// To Test -// ------- -// ### Drag to reorder categories and dimensions -// - Models before / after -// - View models before / after -// - SVG before / after (include cat/dim labels) -// - Emit restyle event + it('should support dragging category to reorder categories only in perpendicular arrangement', function(done) { + + // Set arrangement + mock.data[0].arrangement = 'perpendicular'; + + Plotly.newPlot(gd, mock) + .then(function() { + + restyleCallback = jasmine.createSpy('restyleCallback'); + gd.on('plotly_restyle', restyleCallback); + + /** @type {ParcatsViewModel} */ + var parcatsViewModel = d3.select('g.trace.parcats').datum(); + + // Compute Mouse positions + // ----------------------- + var pos = getDragPositions(parcatsViewModel); + + // Check initial dimension order + // ----------------------------- + checkInitialDimensions(); + + // Check initial categories + // ------------------------ + checkInitialCategories(); + + // Position mouse for start of drag + // -------------------------------- + mouseEvent('mousemove', pos.mouseStartX, pos.mouseStartY); + + // Perform drag + // ------------ + mouseEvent('mousedown', pos.mouseStartX, pos.mouseStartY); + + // ### Pause at drag mid-point + mouseEvent('mousemove', pos.mouseMidX, pos.mouseMidY); + + // Make sure we're dragging the middle dimension + expect(parcatsViewModel.dragDimension.model.dimensionLabel).toEqual('Two'); + + // Make sure dimensions haven't changed order or position + checkInitialDimensions(); + + // Make sure categories in dimension 1 have changed already + checkMidDragCategories(); + + // ### Move to drag end-point + mouseEvent('mousemove', pos.mouseEndX, pos.mouseEndY); + + // Make sure we're still dragging the middle dimension + expect(parcatsViewModel.dragDimension.model.dimensionLabel).toEqual('Two'); -// ### Restyle / Animate + // End drag + // -------- + mouseEvent('mouseup', pos.mouseEndX, pos.mouseEndY); -// ### Events -// - Hover events (category, path) -// - Click events (category, path) + // Make sure we've cleared drag dimension + expect(parcatsViewModel.dragDimension).toEqual(null); -// ### Hover mode -// - SVG display for each category hover mode -// - SVG display for path hover mode -// - Test tooltip message with counts set + // Check final dimension order + // --------------------------- + // Dimension order should not have changed + checkInitialDimensions(); + // Make sure categories in dimension 1 have changed already + checkFinalCategories(); + }) + .then(delay(CALLBACK_DELAY)) + .then(function() { + // Check that proper restyle event was emitted + // ------------------------------------------- + expect(restyleCallback).toHaveBeenCalledTimes(1); + expect(restyleCallback).toHaveBeenCalledWith([ + { + 'dimensions[1].categoryorder': 'array', + 'dimensions[1].categoryarray': [['C', 'A', 'B' ]], + 'dimensions[1].categorylabels': [['C', 'A', 'B' ]]}, + [0]]); + + restyleCallback.calls.reset(); + }) + .catch(failTest) + .then(done); + }); + + it('should NOT support dragging category to reorder categories or dimensions in fixed arrangement', function(done) { + + // Set arrangement + mock.data[0].arrangement = 'fixed'; + + Plotly.newPlot(gd, mock) + .then(function() { + + restyleCallback = jasmine.createSpy('restyleCallback'); + gd.on('plotly_restyle', restyleCallback); + + /** @type {ParcatsViewModel} */ + var parcatsViewModel = d3.select('g.trace.parcats').datum(); + + // Compute Mouse positions + // ----------------------- + var pos = getDragPositions(parcatsViewModel); + + // Check initial dimension order + // ----------------------------- + checkInitialDimensions(); + + // Check initial categories + // ------------------------ + checkInitialCategories(); + + // Position mouse for start of drag + // -------------------------------- + mouseEvent('mousemove', pos.mouseStartX, pos.mouseStartY); + + // Perform drag + // ------------ + mouseEvent('mousedown', pos.mouseStartX, pos.mouseStartY); + + // ### Pause at drag mid-point + mouseEvent('mousemove', pos.mouseMidX, pos.mouseMidY); + + // Make sure we're not dragging a dimension + expect(parcatsViewModel.dragDimension).toEqual(null); + + // Make sure dimensions and categories haven't changed order + checkInitialDimensions(); + checkInitialCategories(); + + // ### Move to drag end-point + mouseEvent('mousemove', pos.mouseEndX, pos.mouseEndY); + + // Make sure we're still not dragging a dimension + expect(parcatsViewModel.dragDimension).toEqual(null); + + // End drag + // -------- + mouseEvent('mouseup', pos.mouseEndX, pos.mouseEndY); + + // Check final dimension order + // --------------------------- + // Dimension and category order should not have changed + checkInitialDimensions(); + checkInitialCategories(); + }) + .then(delay(CALLBACK_DELAY)) + .then(function() { + // Check that no restyle event was emitted + // --------------------------------------- + expect(restyleCallback).toHaveBeenCalledTimes(0); + restyleCallback.calls.reset(); + }) + .catch(failTest) + .then(done); + }); +}); + +// To Test +// ------- +// ### Hovering +// - [ ] Path hover label +// - [ ] Category hover label for 'category', 'color', and 'dimension', `hovermode` +// - [ ] No category hover label for 'none', 'skip' `hovermode +// - [ ] Events emitted on path hover +// - [ ] Events emitted on category hover in 'category', 'color', 'dimension', and 'none' `hovermode` +// - [ ] No events emitted on category or path in 'skip' `hovermode` +// In each case, check hoverinfo text +// +// ### Clicking +// - [ ] Path click events fired unless `hovermode` is 'skip' +// - [ ] Category/color click events fired unless `hovermode` is 'skip' +// +// ### Test that properties have the desired effect on models +// - [ ] visible +// - [ ] counts +// - [ ] bundlecolors +// - [ ] sortpaths +// +// // ### Test Font styles ### // ### Test visible From 66f21fe1065f3b9c88eb9f58b9ba25d0ffe705e1 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Thu, 23 Aug 2018 15:27:47 -0400 Subject: [PATCH 25/25] Add tests for clicking on category and path with/without hoverinfo skip --- src/traces/parcats/parcats.js | 12 +- test/jasmine/tests/parcats_test.js | 179 +++++++++++++++++++++++++++-- 2 files changed, 179 insertions(+), 12 deletions(-) diff --git a/src/traces/parcats/parcats.js b/src/traces/parcats/parcats.js index 1cd49eec4e1..ddcdaf8757c 100644 --- a/src/traces/parcats/parcats.js +++ b/src/traces/parcats/parcats.js @@ -1184,11 +1184,13 @@ function dragDimensionEnd(d) { // Handle potential click event // ---------------------------- - if(!d.dragHasMoved && d.potentialClickBand) { - if(d.parcatsViewModel.hovermode === 'color') { - emitPointsEventColorHovermode(d.potentialClickBand, 'plotly_click', d3.event.sourceEvent); - } else { - emitPointsEventCategoryHovermode(d.potentialClickBand, 'plotly_click', d3.event.sourceEvent); + if(d.parcatsViewModel.hoverinfoItems.indexOf('skip') === -1) { + if (!d.dragHasMoved && d.potentialClickBand) { + if (d.parcatsViewModel.hovermode === 'color') { + emitPointsEventColorHovermode(d.potentialClickBand, 'plotly_click', d3.event.sourceEvent); + } else { + emitPointsEventCategoryHovermode(d.potentialClickBand, 'plotly_click', d3.event.sourceEvent); + } } } diff --git a/test/jasmine/tests/parcats_test.js b/test/jasmine/tests/parcats_test.js index ef9a0cc01fe..894f44c5067 100644 --- a/test/jasmine/tests/parcats_test.js +++ b/test/jasmine/tests/parcats_test.js @@ -5,6 +5,7 @@ var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var failTest = require('../assets/fail_test'); var mouseEvent = require('../assets/mouse_event'); +var click = require('../assets/click'); var delay = require('../assets/delay'); var CALLBACK_DELAY = 500; @@ -802,7 +803,7 @@ describe('Drag to reordered categories', function() { function getDragPositions(parcatsViewModel) { var dragDimStartX = parcatsViewModel.dimensions[1].x; - var mouseStartY = parcatsViewModel.y + parcatsViewModel.dimensions[1].categories[2].y, + var mouseStartY = parcatsViewModel.y + parcatsViewModel.dimensions[1].categories[2].y + 10, mouseStartX = parcatsViewModel.x + dragDimStartX + dimWidth / 2; // Pause mouse half-way between the original location of @@ -1092,7 +1093,7 @@ describe('Drag to reordered categories', function() { it('should NOT support dragging category to reorder categories or dimensions in fixed arrangement', function(done) { // Set arrangement - mock.data[0].arrangement = 'fixed'; + mock.data[0].arrangement = 'fixed'; Plotly.newPlot(gd, mock) .then(function() { @@ -1161,6 +1162,174 @@ describe('Drag to reordered categories', function() { }); }); + +describe('Click events', function() { + + // Variable declarations + // --------------------- + // ### Trace level ### + var gd, + mock; + + // Fixtures + // -------- + beforeEach(function() { + gd = createGraphDiv(); + mock = Lib.extendDeep({}, require('@mocks/parcats_basic_freeform.json')); + }); + + afterEach(destroyGraphDiv); + + it('should fire on category click', function(done) { + + var clickData; + Plotly.newPlot(gd, mock) + .then(function() { + /** @type {ParcatsViewModel} */ + var parcatsViewModel = d3.select('g.trace.parcats').datum(); + + gd.on('plotly_click', function(data) { + clickData = data; + }); + + // Click on the lowest category in the middle dimension (category "C") + var dimStartX = parcatsViewModel.dimensions[1].x; + var mouseY = parcatsViewModel.y + parcatsViewModel.dimensions[1].categories[2].y + 10, + mouseX = parcatsViewModel.x + dimStartX + dimWidth / 2; + + // Position mouse for start of drag + // -------------------------------- + click(mouseX, mouseY); + }) + .then(delay(CALLBACK_DELAY)) + .then(function() { + // Check that click callback was called + expect(clickData).toBeDefined(); + + // Check that the right points were reported + var pts = clickData.points.sort(function(a, b) { + return a.pointNumber - b.pointNumber; + }); + + // Check points + expect(pts).toEqual([ + {curveNumber: 0, pointNumber: 4}, + {curveNumber: 0, pointNumber: 5}, + {curveNumber: 0, pointNumber: 8}]); + }) + .catch(failTest) + .then(done); + }); + + it('should NOT fire on category click if hoverinfo is skip', function(done) { + + var clickData; + mock.data[0].hoverinfo = 'skip'; + + Plotly.newPlot(gd, mock) + .then(function() { + /** @type {ParcatsViewModel} */ + var parcatsViewModel = d3.select('g.trace.parcats').datum(); + + console.log(gd.data[0]); + console.log(parcatsViewModel.hoverinfoItems); + + gd.on('plotly_click', function(data) { + clickData = data; + }); + + // Click on the lowest category in the middle dimension (category "C") + var dimStartX = parcatsViewModel.dimensions[1].x; + var mouseY = parcatsViewModel.y + parcatsViewModel.dimensions[1].categories[2].y + 10, + mouseX = parcatsViewModel.x + dimStartX + dimWidth / 2; + + // Position mouse for start of drag + // -------------------------------- + click(mouseX, mouseY); + }) + .then(delay(CALLBACK_DELAY)) + .then(function() { + // Check that click callback was called + expect(clickData).toBeUndefined(); + }) + .catch(failTest) + .then(done); + }); + + it('should fire on path click', function(done) { + + var clickData; + + Plotly.newPlot(gd, mock) + .then(function() { + /** @type {ParcatsViewModel} */ + var parcatsViewModel = d3.select('g.trace.parcats').datum(); + + gd.on('plotly_click', function(data) { + clickData = data; + }); + + // Click on the top path to the right of the lowest category in the middle dimension (category "C") + var dimStartX = parcatsViewModel.dimensions[1].x; + var mouseY = parcatsViewModel.y + parcatsViewModel.dimensions[1].categories[2].y + 10, + mouseX = parcatsViewModel.x + dimStartX + dimWidth + 10; + + // Position mouse for start of drag + // -------------------------------- + click(mouseX, mouseY); + }) + .then(delay(CALLBACK_DELAY)) + .then(function() { + // Check that click callback was called + expect(clickData).toBeDefined(); + + // Check that the right points were reported + var pts = clickData.points.sort(function(a, b) { + return a.pointNumber - b.pointNumber; + }); + + // Check points + expect(pts).toEqual([ + {curveNumber: 0, pointNumber: 5}, + {curveNumber: 0, pointNumber: 8}]); + }) + .catch(failTest) + .then(done); + }); + + it('should NOT fire on path click if hoverinfo is skip', function(done) { + + var clickData; + mock.data[0].hoverinfo = 'skip'; + + Plotly.newPlot(gd, mock) + .then(function() { + /** @type {ParcatsViewModel} */ + var parcatsViewModel = d3.select('g.trace.parcats').datum(); + + gd.on('plotly_click', function(data) { + clickData = data; + }); + + // Click on the top path to the right of the lowest category in the middle dimension (category "C") + var dimStartX = parcatsViewModel.dimensions[1].x; + var mouseY = parcatsViewModel.y + parcatsViewModel.dimensions[1].categories[2].y + 10, + mouseX = parcatsViewModel.x + dimStartX + dimWidth + 10; + + // Position mouse for start of drag + // -------------------------------- + click(mouseX, mouseY); + }) + .then(delay(CALLBACK_DELAY)) + .then(function() { + // Check that click callback was called + expect(clickData).toBeUndefined(); + }) + .catch(failTest) + .then(done); + }); +}); + // To Test // ------- // ### Hovering @@ -1171,11 +1340,7 @@ describe('Drag to reordered categories', function() { // - [ ] Events emitted on category hover in 'category', 'color', 'dimension', and 'none' `hovermode` // - [ ] No events emitted on category or path in 'skip' `hovermode` // In each case, check hoverinfo text -// -// ### Clicking -// - [ ] Path click events fired unless `hovermode` is 'skip' -// - [ ] Category/color click events fired unless `hovermode` is 'skip' -// + // ### Test that properties have the desired effect on models // - [ ] visible // - [ ] counts