feat: add legend

pull/47/head
Rebecca-Chou 3 months ago
parent 1642f5651c
commit 6df4dfdb05
  1. 11
      components/LineGraph.module.css
  2. 66
      components/LineGraph.tsx
  3. 29
      package-lock.json
  4. 1
      package.json

@ -21,4 +21,15 @@
padding: calc(10rem / 16);
font-size: calc(18rem / 16);
border-radius: calc(10rem / 16);
}
.wrapper {
display: flex;
align-items: center;
width: min-content;
}
.legend {
display: flex;
margin: calc(16rem / 8);
}

@ -4,8 +4,9 @@ import { leftTickLabelProps } from "@visx/axis/lib/axis/AxisLeft";
import { localPoint } from "@visx/event";
import { GridColumns, GridRows } from "@visx/grid";
import { Group } from "@visx/group";
import { LegendOrdinal } from "@visx/legend";
import { Point } from "@visx/point";
import { scaleBand, scaleLinear } from "@visx/scale";
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
import { LinePath } from "@visx/shape";
import { useTooltip, useTooltipInPortal } from "@visx/tooltip";
import React from "react";
@ -28,6 +29,22 @@ interface LineGraphData {
lines: LineData[];
}
interface LegendProps {
/** Position of the legend, relative to the graph. */
position?: "top" | "right";
/** Font size of the labels in the legend, in pixels. Default is 16px. */
itemLabelSize?: number;
/** Gap between items in the legend, in pixels. */
itemGap?: number;
/** Distance between the legend and other adjacent elements, in pixels. */
margin?: {
top?: number;
bottom?: number;
left?: number;
right?: number;
};
}
interface LineGraphProps {
data: LineGraphData;
/** Width of the entire graph, in pixels. */
@ -60,9 +77,11 @@ interface LineGraphProps {
yAxisLabelSize?: number;
/** Controls the distance between the value axis label and the value axis. */
yAxisLabelOffset?: number;
legendProps?: LegendProps;
}
const DEFAULT_LABEL_SIZE = 16;
const DEFAULT_LEGEND_GAP = 16;
export function LineGraph(props: LineGraphProps) {
const {
@ -80,8 +99,16 @@ export function LineGraph(props: LineGraphProps) {
yAxisLabel,
yAxisLabelSize = DEFAULT_LABEL_SIZE,
yAxisLabelOffset = 0,
legendProps,
} = props;
const {
position: legendPosition = "right",
itemLabelSize: legendLabelSize = DEFAULT_LABEL_SIZE,
itemGap: legendItemGap = DEFAULT_LEGEND_GAP,
margin: legendMargin = {},
} = legendProps ?? {};
const xLength = data.xValues.length;
data.lines.forEach((line) => {
@ -137,8 +164,20 @@ export function LineGraph(props: LineGraphProps) {
domain: [yMaxValue, 0],
});
const keys = data.lines.map((line) => line.label);
const legendScale = scaleOrdinal<string, string>({
domain: keys,
range: [Color.primaryAccent, Color.secondaryAccent],
});
return (
<>
<div
className={className ? `${className} ${styles.wrapper}` : styles.wrapper}
style={{
flexDirection: legendPosition === "right" ? "row" : "column-reverse",
}}
>
<svg ref={containerRef} width={width} height={height}>
<Group top={margin.top} left={margin.left}>
<GridColumns
@ -232,6 +271,27 @@ export function LineGraph(props: LineGraphProps) {
</Group>
</Group>
</svg>
<LegendOrdinal
className={styles.legend}
style={{
marginTop: legendMargin.top,
marginRight: legendMargin.right,
marginBottom: legendMargin.bottom,
marginLeft: legendMargin.left,
fontSize: legendLabelSize,
}}
scale={legendScale}
direction={legendPosition === "right" ? "column" : "row"}
itemMargin={
legendPosition === "right"
? `calc(${legendItemGap / 2}rem / 16) 0 calc(${
legendItemGap / 2
}rem / 16) 0`
: `0 calc(${legendItemGap / 2}rem / 16) 0 calc(${
legendItemGap / 2
}rem / 16)`
}
/>
{tooltipOpen && (
<TooltipInPortal
@ -244,6 +304,6 @@ export function LineGraph(props: LineGraphProps) {
<>{tooltipData}</>
</TooltipInPortal>
)}
</>
</div>
);
}

29
package-lock.json generated

@ -5,12 +5,14 @@
"requires": true,
"packages": {
"": {
"name": "cs-2022-class-profile",
"version": "0.1.0",
"dependencies": {
"@visx/axis": "^2.10.0",
"@visx/event": "^2.6.0",
"@visx/grid": "^2.10.0",
"@visx/group": "^2.10.0",
"@visx/legend": "^2.10.0",
"@visx/mock-data": "^2.1.2",
"@visx/scale": "^2.2.2",
"@visx/shape": "^2.10.0",
@ -973,6 +975,21 @@
"react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0"
}
},
"node_modules/@visx/legend": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@visx/legend/-/legend-2.10.0.tgz",
"integrity": "sha512-OI8BYE6QQI9eXAng/C7UzuVw7d0fwlzrth6RmrdhlyT1K+BA3WpExapV+pDfwxu/tkEik8Ps5cZRV6HjX1/Mww==",
"dependencies": {
"@types/react": "*",
"@visx/group": "2.10.0",
"@visx/scale": "2.2.2",
"classnames": "^2.3.1",
"prop-types": "^15.5.10"
},
"peerDependencies": {
"react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0"
}
},
"node_modules/@visx/mock-data": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@visx/mock-data/-/mock-data-2.1.2.tgz",
@ -5093,6 +5110,18 @@
"prop-types": "^15.6.2"
}
},
"@visx/legend": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@visx/legend/-/legend-2.10.0.tgz",
"integrity": "sha512-OI8BYE6QQI9eXAng/C7UzuVw7d0fwlzrth6RmrdhlyT1K+BA3WpExapV+pDfwxu/tkEik8Ps5cZRV6HjX1/Mww==",
"requires": {
"@types/react": "*",
"@visx/group": "2.10.0",
"@visx/scale": "2.2.2",
"classnames": "^2.3.1",
"prop-types": "^15.5.10"
}
},
"@visx/mock-data": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@visx/mock-data/-/mock-data-2.1.2.tgz",

@ -19,6 +19,7 @@
"@visx/event": "^2.6.0",
"@visx/grid": "^2.10.0",
"@visx/group": "^2.10.0",
"@visx/legend": "^2.10.0",
"@visx/mock-data": "^2.1.2",
"@visx/scale": "^2.2.2",
"@visx/shape": "^2.10.0",

Loading…
Cancel
Save