diff --git a/viz-lib/src/visualizations/chart/Editor/GeneralSettings.jsx b/viz-lib/src/visualizations/chart/Editor/GeneralSettings.jsx index 1fef28572c..487459e851 100644 --- a/viz-lib/src/visualizations/chart/Editor/GeneralSettings.jsx +++ b/viz-lib/src/visualizations/chart/Editor/GeneralSettings.jsx @@ -98,6 +98,14 @@ export default function GeneralSettings({ options, data, onOptionsChange }) { onOptionsChange({ columnMapping }, UpdateOptionsStrategy.shallowMerge); } + function handleLegendPlacementChange(value) { + if (value === "hidden") { + onOptionsChange({ legend: { enabled: false } }); + } else { + onOptionsChange({ legend: { enabled: true, placement: value } }); + } + } + return (
@@ -166,12 +174,21 @@ export default function GeneralSettings({ options, data, onOptionsChange }) { {!includes(["custom", "heatmap"], options.globalSeriesType) && (
- onOptionsChange({ legend: { enabled: event.target.checked } })}> - Show Legend - +
)} diff --git a/viz-lib/src/visualizations/chart/Editor/GeneralSettings.test.js b/viz-lib/src/visualizations/chart/Editor/GeneralSettings.test.js index 35c4069c2c..8b5725e553 100644 --- a/viz-lib/src/visualizations/chart/Editor/GeneralSettings.test.js +++ b/viz-lib/src/visualizations/chart/Editor/GeneralSettings.test.js @@ -75,10 +75,12 @@ describe("Visualizations -> Chart -> Editor -> General Settings", () => { done ); - findByTestID(el, "Chart.ShowLegend") + findByTestID(el, "Chart.LegendPlacement") .last() - .find("input") - .simulate("change", { target: { checked: false } }); + .simulate("click"); + findByTestID(el, "Chart.LegendPlacement.HideLegend") + .last() + .simulate("click"); }); test("Box: toggles show points", done => { diff --git a/viz-lib/src/visualizations/chart/Renderer/PlotlyChart.jsx b/viz-lib/src/visualizations/chart/Renderer/PlotlyChart.jsx index 6d1cc9e7ef..c2968cef94 100644 --- a/viz-lib/src/visualizations/chart/Renderer/PlotlyChart.jsx +++ b/viz-lib/src/visualizations/chart/Renderer/PlotlyChart.jsx @@ -39,7 +39,7 @@ export default function PlotlyChart({ options, data }) { // It will auto-purge previous graph Plotly.newPlot(container, plotlyData, plotlyLayout, plotlyOptions).then( catchErrors(() => { - applyLayoutFixes(container, plotlyLayout, (e, u) => Plotly.relayout(e, u)); + applyLayoutFixes(container, plotlyLayout, options, (e, u) => Plotly.relayout(e, u)); }, errorHandler) ); @@ -58,7 +58,7 @@ export default function PlotlyChart({ options, data }) { const unwatch = resizeObserver( container, catchErrors(() => { - applyLayoutFixes(container, plotlyLayout, (e, u) => Plotly.relayout(e, u)); + applyLayoutFixes(container, plotlyLayout, options, (e, u) => Plotly.relayout(e, u)); }, errorHandler) ); return unwatch; diff --git a/viz-lib/src/visualizations/chart/getOptions.js b/viz-lib/src/visualizations/chart/getOptions.js index cbd5cee406..b36dd82586 100644 --- a/viz-lib/src/visualizations/chart/getOptions.js +++ b/viz-lib/src/visualizations/chart/getOptions.js @@ -4,7 +4,7 @@ import { visualizationsSettings } from "@/visualizations/visualizationsSettings" const DEFAULT_OPTIONS = { globalSeriesType: "column", sortX: true, - legend: { enabled: true }, + legend: { enabled: true, placement: "auto" }, yAxis: [{ type: "linear" }, { type: "linear", opposite: true }], xAxis: { type: "-", labels: { enabled: true } }, error_y: { type: "data", visible: true }, diff --git a/viz-lib/src/visualizations/chart/plotly/applyLayoutFixes.js b/viz-lib/src/visualizations/chart/plotly/applyLayoutFixes.js index 0cdcecec2d..0410e310d1 100644 --- a/viz-lib/src/visualizations/chart/plotly/applyLayoutFixes.js +++ b/viz-lib/src/visualizations/chart/plotly/applyLayoutFixes.js @@ -14,90 +14,117 @@ function fixLegendContainer(plotlyElement) { } } -export default function applyLayoutFixes(plotlyElement, layout, updatePlot) { - // update layout size to plot container - layout.width = Math.floor(plotlyElement.offsetWidth); - layout.height = Math.floor(plotlyElement.offsetHeight); - +function placeLegendNextToPlot(plotlyElement, layout, updatePlot) { const transformName = find( ["transform", "WebkitTransform", "MozTransform", "MsTransform", "OTransform"], prop => prop in plotlyElement.style ); - if (layout.width <= 600) { - // Save current `layout.height` value because `updatePlot().then(...)` handler may be called multiple - // times within single update, and since the handler mutates `layout` object - it may lead to bugs - const layoutHeight = layout.height; + layout.legend = { + orientation: "v", + // vertical legend will be rendered properly, so just place it to the right + // side of plot + y: 1, + x: 1, + xanchor: "left", + yanchor: "top", + }; + + const legend = plotlyElement.querySelector(".legend"); + if (legend) { + legend.style[transformName] = null; + } - // change legend orientation to horizontal; plotly has a bug with this - // legend alignment - it does not preserve enough space under the plot; - // so we'll hack this: update plot (it will re-render legend), compute - // legend height, reduce plot size by legend height (but not less than - // half of plot container's height - legend will have max height equal to - // plot height), re-render plot again and offset legend to the space under - // the plot. - // Related issue: https://github.com/plotly/plotly.js/issues/1199 - layout.legend = { - orientation: "h", - // locate legend inside of plot area - otherwise plotly will preserve - // some amount of space under the plot; also this will limit legend height - // to plot's height - y: 0, - x: 0, - xanchor: "left", - yanchor: "bottom", - }; + updatePlot(plotlyElement, pick(layout, ["width", "height", "legend"])); +} - // set `overflow: visible` to svg containing legend because later we will - // position legend outside of it - fixLegendContainer(plotlyElement); +function placeLegendBelowPlot(plotlyElement, layout, updatePlot) { + const transformName = find( + ["transform", "WebkitTransform", "MozTransform", "MsTransform", "OTransform"], + prop => prop in plotlyElement.style + ); - updatePlot(plotlyElement, pick(layout, ["width", "height", "legend"])).then(() => { - const legend = plotlyElement.querySelector(".legend"); // eslint-disable-line no-shadow - if (legend) { - // compute real height of legend - items may be split into few columnns, - // also scrollbar may be shown - const bounds = reduce( - legend.querySelectorAll(".traces"), - (result, node) => { - const b = node.getBoundingClientRect(); - result = result || b; - return { - top: Math.min(result.top, b.top), - bottom: Math.max(result.bottom, b.bottom), - }; - }, - null - ); - // here we have two values: - // 1. height of plot container excluding height of legend items; - // it may be any value between 0 and plot container's height; - // 2. half of plot containers height. Legend cannot be larger than - // plot; if legend is too large, plotly will reduce it's height and - // show a scrollbar; in this case, height of plot === height of legend, - // so we can split container's height half by half between them. - layout.height = Math.floor(Math.max(layoutHeight / 2, layoutHeight - (bounds.bottom - bounds.top))); - // offset the legend - legend.style[transformName] = "translate(0, " + layout.height + "px)"; - updatePlot(plotlyElement, pick(layout, ["height"])); - } - }); - } else { - layout.legend = { - orientation: "v", - // vertical legend will be rendered properly, so just place it to the right - // side of plot - y: 1, - x: 1, - xanchor: "left", - yanchor: "top", - }; + // Save current `layout.height` value because `updatePlot().then(...)` handler may be called multiple + // times within single update, and since the handler mutates `layout` object - it may lead to bugs + const layoutHeight = layout.height; - const legend = plotlyElement.querySelector(".legend"); + // change legend orientation to horizontal; plotly has a bug with this + // legend alignment - it does not preserve enough space under the plot; + // so we'll hack this: update plot (it will re-render legend), compute + // legend height, reduce plot size by legend height (but not less than + // half of plot container's height - legend will have max height equal to + // plot height), re-render plot again and offset legend to the space under + // the plot. + // Related issue: https://github.com/plotly/plotly.js/issues/1199 + layout.legend = { + orientation: "h", + // locate legend inside of plot area - otherwise plotly will preserve + // some amount of space under the plot; also this will limit legend height + // to plot's height + y: 0, + x: 0, + xanchor: "left", + yanchor: "bottom", + }; + + // set `overflow: visible` to svg containing legend because later we will + // position legend outside of it + fixLegendContainer(plotlyElement); + + updatePlot(plotlyElement, pick(layout, ["width", "height", "legend"])).then(() => { + const legend = plotlyElement.querySelector(".legend"); // eslint-disable-line no-shadow if (legend) { - legend.style[transformName] = null; + // compute real height of legend - items may be split into few columnns, + // also scrollbar may be shown + const bounds = reduce( + legend.querySelectorAll(".traces"), + (result, node) => { + const b = node.getBoundingClientRect(); + result = result || b; + return { + top: Math.min(result.top, b.top), + bottom: Math.max(result.bottom, b.bottom), + }; + }, + null + ); + // here we have two values: + // 1. height of plot container excluding height of legend items; + // it may be any value between 0 and plot container's height; + // 2. half of plot containers height. Legend cannot be larger than + // plot; if legend is too large, plotly will reduce it's height and + // show a scrollbar; in this case, height of plot === height of legend, + // so we can split container's height half by half between them. + layout.height = Math.floor(Math.max(layoutHeight / 2, layoutHeight - (bounds.bottom - bounds.top))); + // offset the legend + legend.style[transformName] = "translate(0, " + layout.height + "px)"; + updatePlot(plotlyElement, pick(layout, ["height"])); } + }); +} + +function placeLegendAuto(plotlyElement, layout, updatePlot) { + if (layout.width <= 600) { + placeLegendBelowPlot(plotlyElement, layout, updatePlot); + } else { + placeLegendNextToPlot(plotlyElement, layout, updatePlot); + } +} - updatePlot(plotlyElement, pick(layout, ["width", "height", "legend"])); +export default function applyLayoutFixes(plotlyElement, layout, options, updatePlot) { + // update layout size to plot container + layout.width = Math.floor(plotlyElement.offsetWidth); + layout.height = Math.floor(plotlyElement.offsetHeight); + + if (options.legend.enabled) { + switch (options.legend.placement) { + case "auto": + placeLegendAuto(plotlyElement, layout, updatePlot); + break; + case "below": + placeLegendBelowPlot(plotlyElement, layout, updatePlot); + break; + // no default + } } }