Skip to content

Commit 91308b3

Browse files
committed
Add new property for charts: legendPosition. GH-228
1 parent 2267120 commit 91308b3

27 files changed

+377
-79
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,3 +763,4 @@ See https://github.com/Daveiano/weewx-wdc/compare/v3.3.0...580071ca175a03fe4924b
763763
- Allow to add custom content to front page via custom templates GH-217
764764
- Updated windGust display in forecast table GH-240
765765
- Updated sorting of min/max graphs in the `temp_min_max_avg` graph GH-247
766+
- Added new property for charts: `legendPosition` - Change legend position GH-228

skins/weewx-wdc/skin.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ SKIN_VERSION = 3.5.0-alpha1
321321
areaOpacity = 0.07
322322
# @see https://github.com/Daveiano/weewx-wdc/wiki/Configuration#diagrams, at "curve".
323323
curve = "natural"
324+
legendPosition = "top right"
324325
[[[bar]]]
325326
enableLabel = False
326327
isInteractive = True

skins/weewx-wdc/src/js/diagrams/d3/combined.tsx

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { Maximize } from "../../assets/maximize";
2626
import { Tooltip } from "./components/tooltip";
2727
import { addLegend } from "./components/legend";
28+
import { OutsideLegend } from "./components/outside-legend";
2829
import { addMarkers } from "./components/marker";
2930

3031
type CombinedDiagramBaseProps = DiagramBaseProps & {
@@ -43,6 +44,7 @@ export const CombinedDiagram: FunctionComponent<CombinedDiagramBaseProps> = (
4344
const size: Size = useWindowSize();
4445
const isVisible = useIsVisible(svgRef);
4546
const [tooltip, setTooltip] = useState<Datum[]>([] as Datum[]);
47+
const [legendHeight, setLegendHeight] = useState(0);
4648

4749
// @todo This adds one MutationObserver per LineDiagram. Add this to one
4850
// general component which shares the state.
@@ -124,23 +126,37 @@ export const CombinedDiagram: FunctionComponent<CombinedDiagramBaseProps> = (
124126
};
125127
}, []);
126128

129+
// Determine the number of unique units to display.
130+
const unitCombinedDistinct: string[] = [...new Set(props.unit)];
131+
127132
useEffect(() => {
128133
if (isVisible) {
129134
// Clean up (otherwise on resize it gets rendered multiple times).
130135
d3.select(svgRef.current).selectChildren().remove();
131136

132-
// Determine the number of unique units to display.
133-
const unitCombinedDistinct: string[] = [...new Set(props.unit)];
134-
135137
// @see https://gist.github.com/mbostock/3019563
136138
const margin = {
137-
top: 20,
138-
// Second axis on the right?
139-
right: unitCombinedDistinct.length > 1 ? 50 : 10,
140-
bottom: props.context === "alltime" ? 50 : 40,
141-
left: getMargins(props.observation[0]).left - 2.5,
142-
},
143-
width = svgRef.current?.parentElement
139+
top: 20,
140+
// Second axis on the right?
141+
right: unitCombinedDistinct.length > 1 ? 50 : 10,
142+
bottom: props.context === "alltime" ? 50 : 40,
143+
left: getMargins(props.observation[0]).left - 2.5,
144+
};
145+
146+
if (
147+
(props.nivoProps.legendPosition === "top" ||
148+
props.nivoProps.legendPosition === "bottom") &&
149+
legendHeight > 0
150+
) {
151+
if (props.nivoProps.legendPosition === "top") {
152+
margin.top = 20 + legendHeight;
153+
}
154+
if (props.nivoProps.legendPosition === "bottom") {
155+
margin.bottom = 45 + legendHeight;
156+
}
157+
}
158+
159+
const width = svgRef.current?.parentElement
144160
? svgRef.current?.parentElement.clientWidth -
145161
margin.left -
146162
margin.right
@@ -561,7 +577,21 @@ export const CombinedDiagram: FunctionComponent<CombinedDiagramBaseProps> = (
561577
.style("stroke-width", "1");
562578

563579
// Legend.
564-
addLegend(svgElement, width, props.data, props.unit, colors, true);
580+
if (
581+
props.nivoProps.legendPosition !== "top" &&
582+
props.nivoProps.legendPosition !== "bottom"
583+
) {
584+
addLegend(
585+
svgElement,
586+
width,
587+
height,
588+
props.nivoProps.legendPosition,
589+
props.data,
590+
props.unit,
591+
colors,
592+
true
593+
);
594+
}
565595

566596
// Markers.
567597
if (Object.entries(scales).length === 1) {
@@ -707,7 +737,7 @@ export const CombinedDiagram: FunctionComponent<CombinedDiagramBaseProps> = (
707737
);
708738
});
709739
}
710-
}, [size, props.data, darkMode, isVisible]);
740+
}, [size, props.data, darkMode, isVisible, legendHeight]);
711741

712742
const handleFullScreen = () => {
713743
if (document.fullscreenElement) {
@@ -723,11 +753,36 @@ export const CombinedDiagram: FunctionComponent<CombinedDiagramBaseProps> = (
723753
<>
724754
<Maximize onClick={handleFullScreen} />
725755
<div style={{ height: "100%", position: "relative" }}>
756+
{props.nivoProps.legendPosition === "top" && isVisible ? (
757+
<OutsideLegend
758+
legendPosition={props.nivoProps.legendPosition}
759+
data={props.data}
760+
units={props.unit}
761+
colors={colors}
762+
setLegendHeight={setLegendHeight}
763+
marginLeft={getMargins(props.observation[0]).left - 2.5}
764+
showUnits={unitCombinedDistinct.length > 1}
765+
/>
766+
) : null}
767+
726768
<svg
727769
ref={svgRef}
728770
xmlns="http://www.w3.org/2000/svg"
729771
data-test="d3-diagram-svg"
730772
/>
773+
774+
{props.nivoProps.legendPosition === "bottom" && isVisible ? (
775+
<OutsideLegend
776+
legendPosition={props.nivoProps.legendPosition}
777+
data={props.data}
778+
units={props.unit}
779+
colors={colors}
780+
setLegendHeight={setLegendHeight}
781+
marginLeft={getMargins(props.observation[0]).left - 2.5}
782+
showUnits={unitCombinedDistinct.length > 1}
783+
/>
784+
) : null}
785+
731786
<div
732787
ref={tooltipRef}
733788
className="d3-diagram-tooltip"

skins/weewx-wdc/src/js/diagrams/d3/components/legend.ts

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import * as d3 from "d3";
2-
import { Serie } from "../../types";
2+
import { Serie, legendPosition } from "../../types";
33

44
export const addLegend = (
55
svgElement: d3.Selection<SVGGElement, unknown, null, undefined>,
66
width: number,
7+
height: number,
8+
legendPosition: legendPosition,
79
data: Serie[],
810
units: string[],
911
colors: string[],
@@ -15,11 +17,12 @@ export const addLegend = (
1517

1618
const size = 14,
1719
x = 144,
18-
y = 1.5;
20+
y = 1.5,
21+
positionsLeft = ["top left", "bottom left"];
1922

2023
const legend = svgElement.append("g").attr("class", "legend");
2124

22-
data.map((item, index) => {
25+
data.map((item, index): void => {
2326
const legendItem = legend
2427
.append("g")
2528
.attr("transform", `translate(0, ${index * 21})`);
@@ -37,7 +40,7 @@ export const addLegend = (
3740
.data([item.id])
3841
.enter()
3942
.append("rect")
40-
.attr("x", x)
43+
.attr("x", positionsLeft.includes(legendPosition) ? 0 : x)
4144
.attr("y", y)
4245
.attr("width", size)
4346
.attr("data-testid", `legend-rect-${item.observation}`)
@@ -56,28 +59,72 @@ export const addLegend = (
5659
)
5760
.enter()
5861
.append("text")
59-
.attr("x", x - 10)
62+
.attr("x", positionsLeft.includes(legendPosition) ? size + 5 : x - 10)
6063
.attr("y", y + 6)
6164
.style("fill", () => {
6265
return colors[index];
6366
})
6467
.text(function (d) {
6568
return d;
6669
})
67-
.attr("text-anchor", "end")
70+
.attr(
71+
"text-anchor",
72+
positionsLeft.includes(legendPosition) ? "start" : "end"
73+
)
6874
.style("dominant-baseline", "central")
6975
.style("pointer-events", "none")
7076
.style("font-size", "11px");
7177
});
7278

73-
legend.attr(
74-
"transform",
75-
`translate(${
76-
width -
77-
(legend.node()?.getBBox().width as number) -
78-
(legend.node()?.getBBox().x as number)
79-
}, 0)`
80-
);
79+
// Legend position.
80+
switch (legendPosition) {
81+
case "top right":
82+
legend.attr(
83+
"transform",
84+
`translate(${
85+
width -
86+
(legend.node()?.getBBox().width as number) -
87+
(legend.node()?.getBBox().x as number)
88+
}, 0)`
89+
);
90+
break;
91+
case "top left":
92+
legend.attr("transform", `translate(0, 0)`);
93+
break;
94+
case "bottom right":
95+
legend.attr(
96+
"transform",
97+
`translate(${
98+
width -
99+
(legend.node()?.getBBox().width as number) -
100+
(legend.node()?.getBBox().x as number)
101+
}, ${
102+
height -
103+
(legend.node()?.getBBox().height as number) -
104+
(legend.node()?.getBBox().y as number)
105+
})`
106+
);
107+
break;
108+
case "bottom left":
109+
legend.attr(
110+
"transform",
111+
`translate(0, ${
112+
height -
113+
(legend.node()?.getBBox().height as number) -
114+
(legend.node()?.getBBox().y as number)
115+
})`
116+
);
117+
break;
118+
default:
119+
legend.attr(
120+
"transform",
121+
`translate(${
122+
width -
123+
(legend.node()?.getBBox().width as number) -
124+
(legend.node()?.getBBox().x as number)
125+
}, 0)`
126+
);
127+
}
81128

82129
return legend;
83130
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React, { useRef, useEffect } from "react";
2+
import { Serie, legendPosition } from "../../types";
3+
4+
type OutsideLegendProps = {
5+
legendPosition: legendPosition;
6+
data: Serie[];
7+
units: string[];
8+
colors: string[];
9+
setLegendHeight: (height: number) => void;
10+
marginLeft: number;
11+
showUnits?: boolean;
12+
};
13+
14+
export const OutsideLegend = (
15+
props: OutsideLegendProps
16+
): React.ReactElement => {
17+
const size = 14;
18+
const legend = useRef<HTMLDivElement>(null);
19+
20+
useEffect(() => {
21+
if (legend.current) {
22+
props.setLegendHeight(legend.current.offsetHeight);
23+
}
24+
}, []);
25+
26+
return (
27+
<div
28+
className="diagram-legend"
29+
style={
30+
props.legendPosition === "bottom"
31+
? { bottom: 0, left: props.marginLeft }
32+
: { left: props.marginLeft }
33+
}
34+
ref={legend}
35+
>
36+
{props.data.map((item, index) => (
37+
<div className="legend-item" key={index}>
38+
<div
39+
className="legend-color"
40+
style={{
41+
width: `${size}px`,
42+
height: `${size}px`,
43+
backgroundColor: props.colors[index],
44+
}}
45+
></div>
46+
<div className="legend-label">
47+
{item.id}
48+
{props.showUnits && ` (${props.units[index]})`}
49+
</div>
50+
</div>
51+
))}
52+
</div>
53+
);
54+
};

0 commit comments

Comments
 (0)