'use strict'; var Lib = require('../../lib'); var counterRegex = require('../../lib/regex').counter; var domainAttrs = require('../../plots/domain').attributes; var cartesianIdRegex = require('../../plots/cartesian/constants').idRegex; var Template = require('../../plot_api/plot_template'); var gridAttrs = { rows: { valType: 'integer', min: 1, editType: 'plot', description: [ 'The number of rows in the grid. If you provide a 2D `subplots`', 'array or a `yaxes` array, its length is used as the default.', 'But it\'s also possible to have a different length, if you', 'want to leave a row at the end for non-cartesian subplots.' ].join(' ') }, roworder: { valType: 'enumerated', values: ['top to bottom', 'bottom to top'], dflt: 'top to bottom', editType: 'plot', description: [ 'Is the first row the top or the bottom? Note that columns', 'are always enumerated from left to right.' ].join(' ') }, columns: { valType: 'integer', min: 1, editType: 'plot', description: [ 'The number of columns in the grid. If you provide a 2D `subplots`', 'array, the length of its longest row is used as the default.', 'If you give an `xaxes` array, its length is used as the default.', 'But it\'s also possible to have a different length, if you', 'want to leave a row at the end for non-cartesian subplots.' ].join(' ') }, subplots: { valType: 'info_array', freeLength: true, dimensions: 2, items: {valType: 'enumerated', values: [counterRegex('xy').toString(), ''], editType: 'plot'}, editType: 'plot', description: [ 'Used for freeform grids, where some axes may be shared across subplots', 'but others are not. Each entry should be a cartesian subplot id, like', '*xy* or *x3y2*, or ** to leave that cell empty. You may reuse x axes', 'within the same column, and y axes within the same row.', 'Non-cartesian subplots and traces that support `domain` can place themselves', 'in this grid separately using the `gridcell` attribute.' ].join(' ') }, xaxes: { valType: 'info_array', freeLength: true, items: {valType: 'enumerated', values: [cartesianIdRegex.x.toString(), ''], editType: 'plot'}, editType: 'plot', description: [ 'Used with `yaxes` when the x and y axes are shared across columns and rows.', 'Each entry should be an x axis id like *x*, *x2*, etc., or ** to', 'not put an x axis in that column. Entries other than ** must be unique.', 'Ignored if `subplots` is present. If missing but `yaxes` is present,', 'will generate consecutive IDs.' ].join(' ') }, yaxes: { valType: 'info_array', freeLength: true, items: {valType: 'enumerated', values: [cartesianIdRegex.y.toString(), ''], editType: 'plot'}, editType: 'plot', description: [ 'Used with `yaxes` when the x and y axes are shared across columns and rows.', 'Each entry should be an y axis id like *y*, *y2*, etc., or ** to', 'not put a y axis in that row. Entries other than ** must be unique.', 'Ignored if `subplots` is present. If missing but `xaxes` is present,', 'will generate consecutive IDs.' ].join(' ') }, pattern: { valType: 'enumerated', values: ['independent', 'coupled'], dflt: 'coupled', editType: 'plot', description: [ 'If no `subplots`, `xaxes`, or `yaxes` are given but we do have `rows` and `columns`,', 'we can generate defaults using consecutive axis IDs, in two ways:', '*coupled* gives one x axis per column and one y axis per row.', '*independent* uses a new xy pair for each cell, left-to-right across each row', 'then iterating rows according to `roworder`.' ].join(' ') }, xgap: { valType: 'number', min: 0, max: 1, editType: 'plot', description: [ 'Horizontal space between grid cells, expressed as a fraction', 'of the total width available to one cell. Defaults to 0.1', 'for coupled-axes grids and 0.2 for independent grids.' ].join(' ') }, ygap: { valType: 'number', min: 0, max: 1, editType: 'plot', description: [ 'Vertical space between grid cells, expressed as a fraction', 'of the total height available to one cell. Defaults to 0.1', 'for coupled-axes grids and 0.3 for independent grids.' ].join(' ') }, domain: domainAttrs({name: 'grid', editType: 'plot', noGridCell: true}, { description: [ 'The first and last cells end exactly at the domain', 'edges, with no grout around the edges.' ].join(' ') }), xside: { valType: 'enumerated', values: ['bottom', 'bottom plot', 'top plot', 'top'], dflt: 'bottom plot', editType: 'plot', description: [ 'Sets where the x axis labels and titles go. *bottom* means', 'the very bottom of the grid. *bottom plot* is the lowest plot', 'that each x axis is used in. *top* and *top plot* are similar.' ].join(' ') }, yside: { valType: 'enumerated', values: ['left', 'left plot', 'right plot', 'right'], dflt: 'left plot', editType: 'plot', description: [ 'Sets where the y axis labels and titles go. *left* means', 'the very left edge of the grid. *left plot* is the leftmost plot', 'that each y axis is used in. *right* and *right plot* are similar.' ].join(' ') }, editType: 'plot' }; function getAxes(layout, grid, axLetter) { var gridVal = grid[axLetter + 'axes']; var splomVal = Object.keys((layout._splomAxes || {})[axLetter] || {}); if(Array.isArray(gridVal)) return gridVal; if(splomVal.length) return splomVal; } // the shape of the grid - this needs to be done BEFORE supplyDataDefaults // so that non-subplot traces can place themselves in the grid function sizeDefaults(layoutIn, layoutOut) { var gridIn = layoutIn.grid || {}; var xAxes = getAxes(layoutOut, gridIn, 'x'); var yAxes = getAxes(layoutOut, gridIn, 'y'); if(!layoutIn.grid && !xAxes && !yAxes) return; var hasSubplotGrid = Array.isArray(gridIn.subplots) && Array.isArray(gridIn.subplots[0]); var hasXaxes = Array.isArray(xAxes); var hasYaxes = Array.isArray(yAxes); var isSplomGenerated = ( hasXaxes && xAxes !== gridIn.xaxes && hasYaxes && yAxes !== gridIn.yaxes ); var dfltRows, dfltColumns; if(hasSubplotGrid) { dfltRows = gridIn.subplots.length; dfltColumns = gridIn.subplots[0].length; } else { if(hasYaxes) dfltRows = yAxes.length; if(hasXaxes) dfltColumns = xAxes.length; } var gridOut = Template.newContainer(layoutOut, 'grid'); function coerce(attr, dflt) { return Lib.coerce(gridIn, gridOut, gridAttrs, attr, dflt); } var rows = coerce('rows', dfltRows); var columns = coerce('columns', dfltColumns); if(!(rows * columns > 1)) { delete layoutOut.grid; return; } if(!hasSubplotGrid && !hasXaxes && !hasYaxes) { var useDefaultSubplots = coerce('pattern') === 'independent'; if(useDefaultSubplots) hasSubplotGrid = true; } gridOut._hasSubplotGrid = hasSubplotGrid; var rowOrder = coerce('roworder'); var reversed = rowOrder === 'top to bottom'; var dfltGapX = hasSubplotGrid ? 0.2 : 0.1; var dfltGapY = hasSubplotGrid ? 0.3 : 0.1; var dfltSideX, dfltSideY; if(isSplomGenerated && layoutOut._splomGridDflt) { dfltSideX = layoutOut._splomGridDflt.xside; dfltSideY = layoutOut._splomGridDflt.yside; } gridOut._domains = { x: fillGridPositions('x', coerce, dfltGapX, dfltSideX, columns), y: fillGridPositions('y', coerce, dfltGapY, dfltSideY, rows, reversed) }; } // coerce x or y sizing attributes and return an array of domains for this direction function fillGridPositions(axLetter, coerce, dfltGap, dfltSide, len, reversed) { var dirGap = coerce(axLetter + 'gap', dfltGap); var domain = coerce('domain.' + axLetter); coerce(axLetter + 'side', dfltSide); var out = new Array(len); var start = domain[0]; var step = (domain[1] - start) / (len - dirGap); var cellDomain = step * (1 - dirGap); for(var i = 0; i < len; i++) { var cellStart = start + step * i; out[reversed ? (len - 1 - i) : i] = [cellStart, cellStart + cellDomain]; } return out; } // the (cartesian) contents of the grid - this needs to happen AFTER supplyDataDefaults // so that we know what cartesian subplots are available function contentDefaults(layoutIn, layoutOut) { var gridOut = layoutOut.grid; // make sure we got to the end of handleGridSizing if(!gridOut || !gridOut._domains) return; var gridIn = layoutIn.grid || {}; var subplots = layoutOut._subplots; var hasSubplotGrid = gridOut._hasSubplotGrid; var rows = gridOut.rows; var columns = gridOut.columns; var useDefaultSubplots = gridOut.pattern === 'independent'; var i, j, xId, yId, subplotId, subplotsOut, yPos; var axisMap = gridOut._axisMap = {}; if(hasSubplotGrid) { var subplotsIn = gridIn.subplots || []; subplotsOut = gridOut.subplots = new Array(rows); var index = 1; for(i = 0; i < rows; i++) { var rowOut = subplotsOut[i] = new Array(columns); var rowIn = subplotsIn[i] || []; for(j = 0; j < columns; j++) { if(useDefaultSubplots) { subplotId = (index === 1) ? 'xy' : ('x' + index + 'y' + index); index++; } else subplotId = rowIn[j]; rowOut[j] = ''; if(subplots.cartesian.indexOf(subplotId) !== -1) { yPos = subplotId.indexOf('y'); xId = subplotId.slice(0, yPos); yId = subplotId.slice(yPos); if((axisMap[xId] !== undefined && axisMap[xId] !== j) || (axisMap[yId] !== undefined && axisMap[yId] !== i) ) { continue; } rowOut[j] = subplotId; axisMap[xId] = j; axisMap[yId] = i; } } } } else { var xAxes = getAxes(layoutOut, gridIn, 'x'); var yAxes = getAxes(layoutOut, gridIn, 'y'); gridOut.xaxes = fillGridAxes(xAxes, subplots.xaxis, columns, axisMap, 'x'); gridOut.yaxes = fillGridAxes(yAxes, subplots.yaxis, rows, axisMap, 'y'); } var anchors = gridOut._anchors = {}; var reversed = gridOut.roworder === 'top to bottom'; for(var axisId in axisMap) { var axLetter = axisId.charAt(0); var side = gridOut[axLetter + 'side']; var i0, inc, iFinal; if(side.length < 8) { // grid edge - ie not "* plot" - make these as free axes // since we're not guaranteed to have a subplot there at all anchors[axisId] = 'free'; } else if(axLetter === 'x') { if((side.charAt(0) === 't') === reversed) { i0 = 0; inc = 1; iFinal = rows; } else { i0 = rows - 1; inc = -1; iFinal = -1; } if(hasSubplotGrid) { var column = axisMap[axisId]; for(i = i0; i !== iFinal; i += inc) { subplotId = subplotsOut[i][column]; if(!subplotId) continue; yPos = subplotId.indexOf('y'); if(subplotId.slice(0, yPos) === axisId) { anchors[axisId] = subplotId.slice(yPos); break; } } } else { for(i = i0; i !== iFinal; i += inc) { yId = gridOut.yaxes[i]; if(subplots.cartesian.indexOf(axisId + yId) !== -1) { anchors[axisId] = yId; break; } } } } else { if((side.charAt(0) === 'l')) { i0 = 0; inc = 1; iFinal = columns; } else { i0 = columns - 1; inc = -1; iFinal = -1; } if(hasSubplotGrid) { var row = axisMap[axisId]; for(i = i0; i !== iFinal; i += inc) { subplotId = subplotsOut[row][i]; if(!subplotId) continue; yPos = subplotId.indexOf('y'); if(subplotId.slice(yPos) === axisId) { anchors[axisId] = subplotId.slice(0, yPos); break; } } } else { for(i = i0; i !== iFinal; i += inc) { xId = gridOut.xaxes[i]; if(subplots.cartesian.indexOf(xId + axisId) !== -1) { anchors[axisId] = xId; break; } } } } } } function fillGridAxes(axesIn, axesAllowed, len, axisMap, axLetter) { var out = new Array(len); var i; function fillOneAxis(i, axisId) { if(axesAllowed.indexOf(axisId) !== -1 && axisMap[axisId] === undefined) { out[i] = axisId; axisMap[axisId] = i; } else out[i] = ''; } if(Array.isArray(axesIn)) { for(i = 0; i < len; i++) { fillOneAxis(i, axesIn[i]); } } else { // default axis list is the first `len` axis ids fillOneAxis(0, axLetter); for(i = 1; i < len; i++) { fillOneAxis(i, axLetter + (i + 1)); } } return out; } module.exports = { moduleType: 'component', name: 'grid', schema: { layout: {grid: gridAttrs} }, layoutAttributes: gridAttrs, sizeDefaults: sizeDefaults, contentDefaults: contentDefaults };