Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to explicitly set chart legend position #4865

Merged
merged 4 commits into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 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,32 @@ 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
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
</Select.Option>
<Select.Option value="below" data-test="Chart.LegendPlacement.Below">
Below the Plot
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
</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
173 changes: 100 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,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
}
}
}