diff --git a/core/webapp/vis/src/internal/D3Renderer.js b/core/webapp/vis/src/internal/D3Renderer.js index b3df4053cd3..3a6ae5a06e3 100644 --- a/core/webapp/vis/src/internal/D3Renderer.js +++ b/core/webapp/vis/src/internal/D3Renderer.js @@ -1762,7 +1762,7 @@ LABKEY.vis.internal.D3Renderer = function(plot) { y = plot.grid.height / 2; } else if (name == 'x') { x = plot.grid.leftEdge + (plot.grid.rightEdge - plot.grid.leftEdge) / 2; - y = plot.grid.height - (plot.labels[name].position != undefined ? plot.labels[name].position : 10); + y = plot.grid.bottomEdge + (plot.labels[name].position != undefined ? plot.labels[name].position : 50); } else if (name == 'xTop') { x = plot.grid.leftEdge + (plot.grid.rightEdge - plot.grid.leftEdge) / 2; y = plot.grid.topEdge - (plot.labels[name].position != undefined ? plot.labels[name].position : 25); @@ -1907,10 +1907,20 @@ LABKEY.vis.internal.D3Renderer = function(plot) { var fontFamily = plot.fontFamily ? plot.fontFamily : 'Roboto, arial, helvetica, sans-serif'; selection.attr('font-family', fontFamily).attr('font-size', '11px'); - xPad = plot.scales.yRight && plot.scales.yRight.scale ? 50 : 0; - glyphX = plot.grid.rightEdge + 30 + xPad; - textX = glyphX + 15; - yAcc = function(d, i) {return plot.grid.topEdge + (i * 15);}; + if (plot.legendPos === 'bottom') { + // Render the legendTop relative to the xLabel position, which defaults to 50 below the grid.bottomEdge + const xLabelOffset = plot.labels.x.position ?? 50; + let legendTop = plot.grid.bottomEdge + xLabelOffset + 10; + glyphX = plot.grid.leftEdge + 5; + textX = glyphX + 15; + yAcc = function(d, i) {return legendTop + (i * 15);}; + } else { + xPad = plot.scales.yRight && plot.scales.yRight.scale ? 50 : 0; + glyphX = plot.grid.rightEdge + 30 + xPad; + textX = glyphX + 15; + yAcc = function(d, i) {return plot.grid.topEdge + (i * 15);}; + } + colorAcc = function(d) { return d.color ? d.color : (d.separator ? '#FFF' : '#000'); }; diff --git a/core/webapp/vis/src/plot.js b/core/webapp/vis/src/plot.js index b68226e7544..2599582bb42 100644 --- a/core/webapp/vis/src/plot.js +++ b/core/webapp/vis/src/plot.js @@ -314,39 +314,48 @@ boxPlot.render(); var margins = {}, top = 75, right = 75, bottom = 50, left = 75; // Defaults. var foundLegendScale = false, foundYRight = false; - for(var i = 0; i < allAes.length; i++){ + for (var i = 0; i < allAes.length; i++){ var aes = allAes[i]; - if(!foundLegendScale && (aes.shape || (aes.color && (!scales.color || (scales.color && scales.color.scaleType == 'discrete'))) || aes.outlierColor || aes.outlierShape || aes.pathColor) && legendPos != 'none'){ + + if (!foundLegendScale && (aes.shape || (aes.color && (!scales.color || (scales.color && scales.color.scaleType === 'discrete'))) || aes.outlierColor || aes.outlierShape || aes.pathColor) && legendPos !== 'none'){ foundLegendScale = true; - right = right + 150; } - if(!foundYRight && aes.yRight){ + if (!foundYRight && aes.yRight){ foundYRight = true; right = right + 25; } } + if (foundLegendScale) { + if (!legendPos || legendPos === 'right') { + right = right + 150; + } else if (legendPos === 'bottom') { + // The goal here is to net us space to render one item per color of our discrete color scale (8 items) + bottom = bottom += 170; + } + } + if(!userMargins){ userMargins = {}; } - if(typeof userMargins.top === 'undefined'){ + if (typeof userMargins.top === 'undefined'){ margins.top = top + (labels && labels.subtitle ? 20 : 0); } else { margins.top = userMargins.top; } - if(typeof userMargins.right === 'undefined'){ + if (typeof userMargins.right === 'undefined'){ margins.right = right; } else { margins.right = userMargins.right; } - if(typeof userMargins.bottom === 'undefined'){ + if (typeof userMargins.bottom === 'undefined'){ margins.bottom = bottom; } else { margins.bottom = userMargins.bottom; } - if(typeof userMargins.left === 'undefined'){ + if (typeof userMargins.left === 'undefined'){ margins.left = left; } else { margins.left = userMargins.left; @@ -918,7 +927,7 @@ boxPlot.render(); this.data = config.data ? config.data : null; // An array of rows, required. Each row could have several pieces of data. (e.g. {subjectId: '249534596', hemoglobin: '350', CD4:'1400', day:'120'}) this.layers = config.layers ? config.layers : []; // An array of layers, required. (e.g. a layer for a CD4 line chart over time, and a layer for a Hemoglobin line chart over time). this.clipRect = config.clipRect ? config.clipRect : false; - this.legendPos = config.legendPos; + this.legendPos = config.legendPos ?? 'right'; this.legendNoWrap = config.legendNoWrap; this.throwErrors = config.throwErrors || false; // Allows the configuration to specify whether chart errors should be thrown or logged (default). this.brushing = ('brushing' in config && config.brushing != null && config.brushing != undefined) ? config.brushing : null; @@ -1033,7 +1042,7 @@ boxPlot.render(); this.layers[i].render(this.renderer, this.grid, this.scales, this.data, this.aes, i); } - if(!this.legendPos || (this.legendPos && !(this.legendPos == "none"))){ + if (this.legendPos !== "none") { this.renderer.renderLegend(); } diff --git a/visualization/resources/web/vis/chartWizard/genericChartPanel.js b/visualization/resources/web/vis/chartWizard/genericChartPanel.js index 03e7a745539..9e035d70f98 100644 --- a/visualization/resources/web/vis/chartWizard/genericChartPanel.js +++ b/visualization/resources/web/vis/chartWizard/genericChartPanel.js @@ -1029,6 +1029,10 @@ Ext4.define('LABKEY.ext4.GenericChartPanel', { if (this.getCustomChartOptions) config.customOptions = this.getCustomChartOptions(); + // Apps can set the legendPos to "bottom", so we use the legendPos if it's set on the original config + if (this.savedReportInfo?.visualizationConfig?.chartConfig?.legendPos) + config.legendPos = this.savedReportInfo.visualizationConfig.chartConfig.legendPos; + return config; }, diff --git a/visualization/resources/web/vis/genericChart/genericChartHelper.js b/visualization/resources/web/vis/genericChart/genericChartHelper.js index cb2a4911b81..ec6d1600dc2 100644 --- a/visualization/resources/web/vis/genericChart/genericChartHelper.js +++ b/visualization/resources/web/vis/genericChart/genericChartHelper.js @@ -1053,6 +1053,7 @@ LABKEY.vis.GenericChartHelper = new function(){ layers = [], clipRect, emptyTextFn = function(){return '';}, plotConfig = { + legendPos: chartConfig.legendPos, renderTo: renderTo, rendererType: 'd3', width: chartConfig.width, @@ -1184,7 +1185,18 @@ LABKEY.vis.GenericChartHelper = new function(){ scales.x.tickLabelMax = Math.floor((plotConfig.width - 300) / 30); } - var margins = _getPlotMargins(renderType, scales, aes, data, plotConfig, chartConfig); + var wrapLines = _wrapXAxisTickTextLines(plotConfig, chartConfig, aes, scales, data); + var margins = _getPlotMargins(renderType, data, chartConfig, wrapLines); + + if (wrapLines > 1) { + labels = { + ...labels, + // x-axis position defaults to 50 (see D3Renderer.renderLabel) but if we're wrapping our tick labels + // then they will probably collide with the label, so we add 20 to hopefully not collide. + x: { value: labels.x.value, position: 70 } + } + } + if (LABKEY.Utils.isObject(margins)) { plotConfig.margins = margins; } @@ -1440,37 +1452,47 @@ LABKEY.vis.GenericChartHelper = new function(){ }); }; - var _wrapXAxisTickTextLines = function(scales, plotConfig, maxTickLength, data) { - if (scales.x && scales.x.scaleType === 'discrete') { - var tickCount = scales.x && scales.x.tickLabelMax ? Math.min(scales.x.tickLabelMax, data.length) : data.length; + var _wrapXAxisTickTextLines = function(plotConfig, chartConfig, aes, scales, data) { + if (LABKEY.Utils.isArray(data) && scales.x && scales.x.scaleType === 'discrete') { + let maxTickLength = 0; + $.each(data, function(idx, d) { + const val = LABKEY.Utils.isFunction(aes.x) ? aes.x(d) : d[aes.x]; + const subVal = LABKEY.Utils.isFunction(aes.xSub) ? aes.xSub(d) : d[aes.xSub]; + if (LABKEY.Utils.isString(subVal)) { + maxTickLength = Math.max(maxTickLength, subVal.length); + } else if (LABKEY.Utils.isString(val)) { + maxTickLength = Math.max(maxTickLength, val.length); + } + }); + + let tickCount = scales.x && scales.x.tickLabelMax ? Math.min(scales.x.tickLabelMax, data.length) : data.length; // after 10 tick labels, we switch to rotating the label, so use that as the max here tickCount = Math.min(tickCount, 10); - var approxTickLabelWidth = plotConfig.width / tickCount; - return Math.max(1, Math.floor((maxTickLength * 8) / approxTickLabelWidth)); + const approxTickLabelWidth = plotConfig.width / tickCount; + return Math.max(1, Math.floor((maxTickLength * 12) / approxTickLabelWidth)); } return 1; }; - var _getPlotMargins = function(renderType, scales, aes, data, plotConfig, chartConfig) { - var margins = {}; + const chartConfigHasLegend = (chartConfig) => { + const { color, series, shape, xSub } = chartConfig.measures; + return !!(color || series || shape || xSub); + } + + const _getPlotMargins = function(renderType, data, chartConfig, wrapLines) { + const margins = {}; + const hasLegend = chartConfigHasLegend(chartConfig); // issue 29690: for bar and box plots, set default bottom margin based on the number of labels and the max label length if (LABKEY.Utils.isArray(data)) { - var maxLen = 0; - $.each(data, function(idx, d) { - var val = LABKEY.Utils.isFunction(aes.x) ? aes.x(d) : d[aes.x]; - var subVal = LABKEY.Utils.isFunction(aes.xSub) ? aes.xSub(d) : d[aes.xSub]; - if (LABKEY.Utils.isString(subVal)) { - maxLen = Math.max(maxLen, subVal.length); - } else if (LABKEY.Utils.isString(val)) { - maxLen = Math.max(maxLen, val.length); - } - }); - - var wrapLines = _wrapXAxisTickTextLines(scales, plotConfig, maxLen, data); - // min bottom margin: 50, max bottom margin: 150 - margins.bottom = Math.min(150, 60 + ((wrapLines - 1) * 25)); + if (chartConfig.legendPos === 'bottom' && hasLegend) { + // min bottom margin: 170, max bottom margin: 360 + margins.bottom = Math.min(360, 170 + ((wrapLines - 1) * 25)); + } else { + // min bottom margin: 80, max bottom margin: 150 + margins.bottom = Math.min(150, 80 + ((wrapLines - 1) * 25)); + } } // issue 31857: allow custom margins to be set in Chart Layout dialog