Skip to content

Commit

Permalink
Add option to explicitly set chart legend position
Browse files Browse the repository at this point in the history
  • Loading branch information
kravets-levko committed May 6, 2020
1 parent fc246aa commit 02c2531
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 86 deletions.
37 changes: 29 additions & 8 deletions viz-lib/src/visualizations/chart/Editor/GeneralSettings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,35 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
)}

{!includes(["custom", "heatmap"], options.globalSeriesType) && (
<Section>
<Checkbox
data-test="Chart.ShowLegend"
defaultChecked={options.legend.enabled}
onChange={event => onOptionsChange({ legend: { enabled: event.target.checked } })}>
Show Legend
</Checkbox>
</Section>
<React.Fragment>
<Section>
<Checkbox
data-test="Chart.ShowLegend"
defaultChecked={options.legend.enabled}
onChange={event => onOptionsChange({ legend: { enabled: event.target.checked } })}>
Show Legend
</Checkbox>
</Section>
{options.legend.enabled && (
<Section>
<Select
label="Legend Placement"
data-test="Chart.LegendPlacement"
defaultValue={options.legend.placement}
onChange={placement => onOptionsChange({ legend: { placement } })}>
<Select.Option value="auto" data-test="Chart.LegendPlacement.Auto">
Auto
</Select.Option>
<Select.Option value="right" data-test="Chart.LegendPlacement.Right">
Next to the Plot
</Select.Option>
<Select.Option value="below" data-test="Chart.LegendPlacement.Below">
Below the Plot
</Select.Option>
</Select>
</Section>
)}
</React.Fragment>
)}

{includes(["box"], options.globalSeriesType) && (
Expand Down
4 changes: 2 additions & 2 deletions viz-lib/src/visualizations/chart/Renderer/PlotlyChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);

Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion viz-lib/src/visualizations/chart/getOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
176 changes: 103 additions & 73 deletions viz-lib/src/visualizations/chart/plotly/applyLayoutFixes.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,90 +14,120 @@ 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 "right":
placeLegendNextToPlot(plotlyElement, layout, updatePlot);
break;
case "below":
placeLegendBelowPlot(plotlyElement, layout, updatePlot);
break;
// no default
}
}
}
4 changes: 2 additions & 2 deletions viz-lib/src/visualizations/chart/plotly/prepareLayout.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { filter, has, isNumber, isObject, isUndefined, map, max, min } from "lodash";
import { filter, isNumber, isObject, isUndefined, map, max, min } from "lodash";
import { getPieDimensions } from "./preparePieData";

function getAxisTitle(axis) {
Expand Down Expand Up @@ -122,7 +122,7 @@ export default function prepareLayout(element, options, data) {
width: Math.floor(element.offsetWidth),
height: Math.floor(element.offsetHeight),
autosize: false,
showlegend: has(options, "legend") ? options.legend.enabled : true,
showlegend: options.legend.enabled,
};

switch (options.globalSeriesType) {
Expand Down

0 comments on commit 02c2531

Please sign in to comment.