Merge branch 'main' of https://git.csclub.uwaterloo.ca/www/cs-2022-class-profile
This commit is contained in:
commit
02eafe44d3
|
@ -43,6 +43,16 @@ interface BarGraphProps {
|
|||
valueAxisLabelSize?: number;
|
||||
/** Controls the distance between the value axis label and the value axis. */
|
||||
valueAxisLabelOffset?: number;
|
||||
/** Minimum width of the graph. */
|
||||
minWidth?: number;
|
||||
/** Breakpoint width of graph where alernating labels are displayed. Only for Vertical graphs */
|
||||
widthAlternatingLabel?: number;
|
||||
/** Space added to the bottom of the graph to show overflowing labels. Only for Vertical graphs */
|
||||
alternatingLabelSpace?: number;
|
||||
/** Default position of labels in x-axis, in px. */
|
||||
defaultLabelDy?: string;
|
||||
/** Position of lower labels in x-axis, in px. Only for Vertical graphs */
|
||||
lowerLabelDy?: string;
|
||||
}
|
||||
|
||||
interface BarGraphData {
|
||||
|
@ -54,11 +64,11 @@ const DEFAULT_LABEL_SIZE = 16;
|
|||
|
||||
export function BarGraphHorizontal(props: BarGraphProps) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
margin,
|
||||
data,
|
||||
className,
|
||||
minWidth = 500,
|
||||
categoryTickLabelSize = DEFAULT_LABEL_SIZE,
|
||||
valueTickLabelSize = DEFAULT_LABEL_SIZE,
|
||||
hoverLabelSize,
|
||||
|
@ -68,8 +78,9 @@ export function BarGraphHorizontal(props: BarGraphProps) {
|
|||
valueAxisLabel,
|
||||
valueAxisLabelSize = DEFAULT_LABEL_SIZE,
|
||||
valueAxisLabelOffset = 0,
|
||||
defaultLabelDy = "0",
|
||||
} = props;
|
||||
|
||||
const width = props.width < minWidth ? minWidth : props.width; // Ensuring graph's width >= minWidth
|
||||
const barPadding = 0.4;
|
||||
|
||||
const categoryMax = height - margin.top - margin.bottom;
|
||||
|
@ -163,8 +174,6 @@ export function BarGraphHorizontal(props: BarGraphProps) {
|
|||
return {
|
||||
...leftTickLabelProps(),
|
||||
className: styles.tickLabel,
|
||||
dx: "-0.5rem",
|
||||
dy: "0.25rem",
|
||||
fontSize: `${categoryTickLabelSize / 16}rem`,
|
||||
};
|
||||
}}
|
||||
|
@ -186,7 +195,7 @@ export function BarGraphHorizontal(props: BarGraphProps) {
|
|||
return {
|
||||
...bottomTickLabelProps(),
|
||||
className: styles.tickLabel,
|
||||
dy: "0.25rem",
|
||||
dy: defaultLabelDy,
|
||||
fontSize: `${valueTickLabelSize / 16}rem`,
|
||||
};
|
||||
}}
|
||||
|
@ -203,11 +212,11 @@ export function BarGraphHorizontal(props: BarGraphProps) {
|
|||
|
||||
export function BarGraphVertical(props: BarGraphProps) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
margin,
|
||||
data,
|
||||
className,
|
||||
minWidth = 500,
|
||||
categoryTickLabelSize = DEFAULT_LABEL_SIZE,
|
||||
valueTickLabelSize = DEFAULT_LABEL_SIZE,
|
||||
hoverLabelSize,
|
||||
|
@ -217,12 +226,20 @@ export function BarGraphVertical(props: BarGraphProps) {
|
|||
valueAxisLabel,
|
||||
valueAxisLabelSize = DEFAULT_LABEL_SIZE,
|
||||
valueAxisLabelOffset = 0,
|
||||
widthAlternatingLabel = 600,
|
||||
alternatingLabelSpace = 80,
|
||||
defaultLabelDy = `0px`,
|
||||
lowerLabelDy = `30px`,
|
||||
} = props;
|
||||
|
||||
const width = props.width < minWidth ? minWidth : props.width; // Ensuring graph's width >= minWidth
|
||||
const barPadding = 0.4;
|
||||
const alternatingLabel = width <= widthAlternatingLabel;
|
||||
const final_margin_bottom = alternatingLabel
|
||||
? margin.bottom + alternatingLabelSpace
|
||||
: margin.bottom;
|
||||
|
||||
const categoryMax = width - margin.left - margin.right;
|
||||
const valueMax = height - margin.top - margin.bottom;
|
||||
const valueMax = height - margin.top - final_margin_bottom;
|
||||
|
||||
const getCategory = (d: BarGraphData) => d.category;
|
||||
const getValue = (d: BarGraphData) => d.value;
|
||||
|
@ -308,11 +325,12 @@ export function BarGraphVertical(props: BarGraphProps) {
|
|||
left={margin.left}
|
||||
hideAxisLine
|
||||
hideTicks
|
||||
tickLabelProps={() => {
|
||||
tickLabelProps={(value, index) => {
|
||||
const alternatingDy = index % 2 == 0 ? defaultLabelDy : lowerLabelDy;
|
||||
return {
|
||||
...bottomTickLabelProps(),
|
||||
className: styles.tickLabel,
|
||||
dy: "-0.25rem",
|
||||
dy: alternatingLabel ? alternatingDy : defaultLabelDy,
|
||||
fontSize: `${categoryTickLabelSize / 16}rem`,
|
||||
width: categoryScale.bandwidth(),
|
||||
verticalAnchor: "start",
|
||||
|
|
|
@ -5,34 +5,4 @@
|
|||
.boxplot:hover {
|
||||
fill: var(--primary-accent);
|
||||
filter: drop-shadow(0 0 calc(4rem / 16) var(--primary-accent));
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
font-family: "Inconsolata", monospace;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
background-color: var(--label);
|
||||
color: var(--primary-background);
|
||||
pointer-events: none;
|
||||
padding: calc(10rem / 16);
|
||||
border-radius: calc(10rem / 16);
|
||||
}
|
||||
|
||||
.tooltip .category {
|
||||
margin: calc(10rem / 16) 0 0 0;
|
||||
font-size: calc(16rem / 16);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tooltip .toolTipData {
|
||||
margin-top: calc(5rem / 16);
|
||||
margin-bottom: calc(10rem / 16);
|
||||
font-size: calc(16rem / 16);
|
||||
}
|
||||
|
||||
.tooltip .toolTipData p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: calc(16rem / 16);
|
||||
}
|
||||
}
|
|
@ -6,11 +6,13 @@ import { Point } from "@visx/point";
|
|||
import { scaleBand, scaleLinear } from "@visx/scale";
|
||||
import { Line } from "@visx/shape";
|
||||
import { BoxPlot as VisxBoxPlot } from "@visx/stats";
|
||||
import { withTooltip, Tooltip } from "@visx/tooltip";
|
||||
import { withTooltip } from "@visx/tooltip";
|
||||
import { WithTooltipProvidedProps } from "@visx/tooltip/lib/enhancers/withTooltip";
|
||||
import React from "react";
|
||||
import { Color } from "utils/Color";
|
||||
|
||||
import { TooltipWrapper } from "./TooltipWrapper";
|
||||
|
||||
import styles from "./Boxplot.module.css";
|
||||
|
||||
const DEFAULT_LABEL_SIZE = 16;
|
||||
|
@ -339,21 +341,17 @@ export const BoxPlot = withTooltip<StatsPlotProps, TooltipData>(
|
|||
</svg>
|
||||
|
||||
{tooltipOpen && tooltipData && (
|
||||
<Tooltip
|
||||
top={tooltipTop}
|
||||
<TooltipWrapper
|
||||
left={tooltipLeft}
|
||||
className={styles.tooltip}
|
||||
unstyled
|
||||
top={tooltipTop}
|
||||
header={tooltipData.category}
|
||||
>
|
||||
<p className={styles.category}>{tooltipData.category}</p>
|
||||
<div className={styles.toolTipData}>
|
||||
<p>max: {tooltipData.max}</p>
|
||||
<p>third quartile: {tooltipData.thirdQuartile}</p>
|
||||
<p>median: {tooltipData.median}</p>
|
||||
<p>first quartile: {tooltipData.firstQuartile}</p>
|
||||
<p>min: {tooltipData.min}</p>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<p>max: {tooltipData.max}</p>
|
||||
<p>third quartile: {tooltipData.thirdQuartile}</p>
|
||||
<p>median: {tooltipData.median}</p>
|
||||
<p>first quartile: {tooltipData.firstQuartile}</p>
|
||||
<p>min: {tooltipData.min}</p>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -45,7 +45,10 @@
|
|||
|
||||
.wrapperNoBodyText {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wrapperNoBodyText .internalWrapper {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
|
@ -61,8 +64,18 @@
|
|||
.wrapperCenter {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.horizontalScrollOnMobile {
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.internalWrapper {
|
||||
padding: calc(20rem / 16);
|
||||
}
|
||||
|
||||
.internalWrapper p {
|
||||
font-size: calc(24rem / 16);
|
||||
opacity: .85;
|
||||
line-height: 1.25;
|
||||
}
|
|
@ -37,7 +37,11 @@ export function ComponentWrapper({
|
|||
<h3>{heading}</h3>
|
||||
{bodyText ? <p>{bodyText}</p> : null}
|
||||
</div>
|
||||
<div className={styles.internalWrapper}>{children}</div>
|
||||
<div
|
||||
className={`${styles.internalWrapper} ${styles.horizontalScrollOnMobile}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,20 +8,6 @@
|
|||
filter: drop-shadow(0 0 calc(4rem / 16) var(--primary-accent));
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
font-family: "Inconsolata", monospace;
|
||||
font-weight: bold;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
background-color: var(--label);
|
||||
color: var(--primary-background);
|
||||
box-shadow: 0px calc(1rem / 16) calc(2rem / 16) var(--card-background);
|
||||
pointer-events: none;
|
||||
padding: calc(10rem / 16);
|
||||
font-size: calc(18rem / 16);
|
||||
border-radius: calc(10rem / 16);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
|
|
|
@ -8,10 +8,12 @@ import { LegendOrdinal } from "@visx/legend";
|
|||
import { Point } from "@visx/point";
|
||||
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
|
||||
import { LinePath } from "@visx/shape";
|
||||
import { useTooltip, useTooltipInPortal } from "@visx/tooltip";
|
||||
import { withTooltip } from "@visx/tooltip";
|
||||
import React from "react";
|
||||
import { Color } from "utils/Color";
|
||||
|
||||
import { TooltipWrapper } from "./TooltipWrapper";
|
||||
|
||||
import styles from "./LineGraph.module.css";
|
||||
|
||||
interface LineData {
|
||||
|
@ -85,8 +87,11 @@ const DEFAULT_LEGEND_GAP = 16;
|
|||
|
||||
// TODO: Address unused props in this file
|
||||
/* eslint-disable unused-imports/no-unused-vars*/
|
||||
export function LineGraph(props: LineGraphProps) {
|
||||
const {
|
||||
|
||||
type TooltipData = string;
|
||||
|
||||
export const LineGraph = withTooltip<LineGraphProps, TooltipData>(
|
||||
({
|
||||
width,
|
||||
height,
|
||||
margin,
|
||||
|
@ -101,211 +106,199 @@ 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) => {
|
||||
if (line.yValues.length != xLength) {
|
||||
throw new Error("Invalid data with wrong length.");
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
tooltipData,
|
||||
tooltipOpen,
|
||||
tooltipLeft,
|
||||
tooltipTop,
|
||||
tooltipOpen,
|
||||
showTooltip,
|
||||
tooltipData,
|
||||
hideTooltip,
|
||||
} = useTooltip();
|
||||
showTooltip,
|
||||
legendProps,
|
||||
}) => {
|
||||
const {
|
||||
position: legendPosition = "right",
|
||||
itemLabelSize: legendLabelSize = DEFAULT_LABEL_SIZE,
|
||||
itemGap: legendItemGap = DEFAULT_LEGEND_GAP,
|
||||
margin: legendMargin = {},
|
||||
} = legendProps ?? {};
|
||||
|
||||
const { containerRef, TooltipInPortal } = useTooltipInPortal({
|
||||
// use TooltipWithBounds
|
||||
detectBounds: true,
|
||||
// when tooltip containers are scrolled, this will correctly update the Tooltip position
|
||||
scroll: true,
|
||||
});
|
||||
const xLength = data.xValues.length;
|
||||
|
||||
const yMax = height - margin.top - margin.bottom;
|
||||
const xMax = width - margin.left - margin.right;
|
||||
|
||||
const actualData = data.lines.map((line) => {
|
||||
return line.yValues.map((val, idx) => {
|
||||
return { x: data.xValues[idx], y: val };
|
||||
data.lines.forEach((line) => {
|
||||
if (line.yValues.length != xLength) {
|
||||
throw new Error("Invalid data with wrong length.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const yMaxValue = Math.max(
|
||||
...data.lines.map((line) => {
|
||||
return Math.max(...line.yValues);
|
||||
})
|
||||
);
|
||||
const yMax = height - margin.top - margin.bottom;
|
||||
const xMax = width - margin.left - margin.right;
|
||||
|
||||
// data accessors
|
||||
const getX = (d: PointData) => d.x;
|
||||
const getY = (d: PointData) => d.y;
|
||||
const actualData = data.lines.map((line) => {
|
||||
return line.yValues.map((val, idx) => {
|
||||
return { x: data.xValues[idx], y: val };
|
||||
});
|
||||
});
|
||||
|
||||
// scales
|
||||
const xScale = scaleBand({
|
||||
range: [0, xMax],
|
||||
domain: data.xValues,
|
||||
});
|
||||
const yMaxValue = Math.max(
|
||||
...data.lines.map((line) => {
|
||||
return Math.max(...line.yValues);
|
||||
})
|
||||
);
|
||||
|
||||
const yScale = scaleLinear<number>({
|
||||
range: [0, yMax],
|
||||
nice: true,
|
||||
domain: [yMaxValue, 0],
|
||||
});
|
||||
// data accessors
|
||||
const getX = (d: PointData) => d.x;
|
||||
const getY = (d: PointData) => d.y;
|
||||
|
||||
const keys = data.lines.map((line) => line.label);
|
||||
// scales
|
||||
const xScale = scaleBand({
|
||||
range: [0, xMax],
|
||||
domain: data.xValues,
|
||||
});
|
||||
|
||||
const legendScale = scaleOrdinal<string, string>({
|
||||
domain: keys,
|
||||
range: [Color.primaryAccent, Color.secondaryAccent],
|
||||
});
|
||||
const yScale = scaleLinear<number>({
|
||||
range: [0, yMax],
|
||||
nice: true,
|
||||
domain: [yMaxValue, 0],
|
||||
});
|
||||
|
||||
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
|
||||
scale={xScale}
|
||||
height={yMax}
|
||||
left={margin.left}
|
||||
numTicks={5}
|
||||
stroke={Color.tertiaryBackground}
|
||||
strokeWidth={4}
|
||||
strokeDasharray="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<GridRows
|
||||
scale={yScale}
|
||||
width={xMax}
|
||||
left={margin.left * 2.3}
|
||||
numTicks={data.xValues.length}
|
||||
stroke={Color.tertiaryBackground}
|
||||
strokeWidth={4}
|
||||
strokeDasharray="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<AxisBottom
|
||||
scale={xScale}
|
||||
top={margin.top + yMax}
|
||||
left={margin.left}
|
||||
hideAxisLine
|
||||
hideTicks
|
||||
tickLabelProps={() => {
|
||||
return {
|
||||
...bottomTickLabelProps(),
|
||||
className: styles.tickLabel,
|
||||
dy: "-0.25rem",
|
||||
fontSize: `${xTickLabelSize / 16}rem`,
|
||||
width: xScale.bandwidth(),
|
||||
};
|
||||
}}
|
||||
/>
|
||||
<AxisLeft
|
||||
scale={yScale}
|
||||
left={margin.left}
|
||||
hideAxisLine
|
||||
hideTicks
|
||||
numTicks={5}
|
||||
tickLabelProps={() => {
|
||||
return {
|
||||
...leftTickLabelProps(),
|
||||
className: styles.tickLabel,
|
||||
dx: "1.25rem",
|
||||
dy: "0.25rem",
|
||||
fontSize: `${yTickLabelSize / 16}rem`,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
<Group left={margin.left + xMax / (data.xValues.length * 2)}>
|
||||
{actualData.map((lineData, i) => {
|
||||
const isEven = i % 2 === 0;
|
||||
return (
|
||||
<Group key={`line-${i}`}>
|
||||
<LinePath
|
||||
onMouseMove={(e) => {
|
||||
const eventSvgCoords = localPoint(
|
||||
// ownerSVGElement is given by visx docs but not recognized by typescript
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
e.target.ownerSVGElement as Element,
|
||||
e
|
||||
) as Point;
|
||||
showTooltip({
|
||||
tooltipData: data.lines[i].label,
|
||||
tooltipTop: eventSvgCoords.y,
|
||||
tooltipLeft: eventSvgCoords.x,
|
||||
});
|
||||
}}
|
||||
onMouseOut={hideTooltip}
|
||||
data={lineData}
|
||||
className={styles.line}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
x={(d) => xScale(getX(d))!}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
y={(d) => yScale(getY(d))!}
|
||||
stroke={
|
||||
isEven ? Color.primaryAccent : Color.secondaryAccent
|
||||
}
|
||||
strokeWidth={4}
|
||||
strokeOpacity={2}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
</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)`
|
||||
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 width={width} height={height}>
|
||||
<Group top={margin.top} left={margin.left}>
|
||||
<GridColumns
|
||||
scale={xScale}
|
||||
height={yMax}
|
||||
left={margin.left}
|
||||
numTicks={5}
|
||||
stroke={Color.tertiaryBackground}
|
||||
strokeWidth={4}
|
||||
strokeDasharray="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<GridRows
|
||||
scale={yScale}
|
||||
width={xMax}
|
||||
left={margin.left * 2.3}
|
||||
numTicks={data.xValues.length}
|
||||
stroke={Color.tertiaryBackground}
|
||||
strokeWidth={4}
|
||||
strokeDasharray="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<AxisBottom
|
||||
scale={xScale}
|
||||
top={margin.top + yMax}
|
||||
left={margin.left}
|
||||
hideAxisLine
|
||||
hideTicks
|
||||
tickLabelProps={() => {
|
||||
return {
|
||||
...bottomTickLabelProps(),
|
||||
className: styles.tickLabel,
|
||||
dy: "-0.25rem",
|
||||
fontSize: `${xTickLabelSize / 16}rem`,
|
||||
width: xScale.bandwidth(),
|
||||
};
|
||||
}}
|
||||
/>
|
||||
<AxisLeft
|
||||
scale={yScale}
|
||||
left={margin.left}
|
||||
hideAxisLine
|
||||
hideTicks
|
||||
numTicks={5}
|
||||
tickLabelProps={() => {
|
||||
return {
|
||||
...leftTickLabelProps(),
|
||||
className: styles.tickLabel,
|
||||
dx: "1.25rem",
|
||||
dy: "0.25rem",
|
||||
fontSize: `${yTickLabelSize / 16}rem`,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
<Group left={margin.left + xMax / (data.xValues.length * 2)}>
|
||||
{actualData.map((lineData, i) => {
|
||||
const isEven = i % 2 === 0;
|
||||
return (
|
||||
<Group key={`line-${i}`}>
|
||||
<LinePath
|
||||
onMouseMove={(e) => {
|
||||
const eventSvgCoords = localPoint(
|
||||
// ownerSVGElement is given by visx docs but not recognized by typescript
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
e.target.ownerSVGElement as Element,
|
||||
e
|
||||
) as Point;
|
||||
showTooltip({
|
||||
tooltipData: data.lines[i].label,
|
||||
tooltipTop: eventSvgCoords.y,
|
||||
tooltipLeft: eventSvgCoords.x,
|
||||
});
|
||||
}}
|
||||
onMouseOut={hideTooltip}
|
||||
data={lineData}
|
||||
className={styles.line}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
x={(d) => xScale(getX(d))!}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
y={(d) => yScale(getY(d))!}
|
||||
stroke={
|
||||
isEven ? Color.primaryAccent : Color.secondaryAccent
|
||||
}
|
||||
strokeWidth={4}
|
||||
strokeOpacity={2}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
</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
|
||||
top={tooltipTop}
|
||||
left={tooltipLeft}
|
||||
className={styles.tooltip}
|
||||
unstyled
|
||||
applyPositionStyle
|
||||
>
|
||||
<>{tooltipData}</>
|
||||
</TooltipInPortal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
{tooltipOpen && (
|
||||
<TooltipWrapper
|
||||
top={tooltipTop}
|
||||
left={tooltipLeft}
|
||||
header={tooltipData as string}
|
||||
></TooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -10,10 +10,23 @@
|
|||
color: var(--primary-accent-light);
|
||||
font-size: calc(70rem / 16);
|
||||
margin: calc(40rem / 16) auto;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
color: var(--primary-accent-lighter);
|
||||
font-size: calc(26rem / 16);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.title {
|
||||
font-size: calc(50rem / 16);
|
||||
margin: calc(20rem / 16) auto;
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
font-size: calc(30rem / 16);
|
||||
margin: auto calc(15rem / 16);
|
||||
}
|
||||
}
|
|
@ -13,23 +13,6 @@
|
|||
top: 0;
|
||||
}
|
||||
|
||||
.toolTip {
|
||||
font-family: "Inconsolata", monospace;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
background-color: var(--label);
|
||||
color: var(--primary-background);
|
||||
pointer-events: none;
|
||||
border-radius: calc(10rem / 16);
|
||||
padding: calc(10rem / 16);
|
||||
}
|
||||
|
||||
.toolTip p {
|
||||
margin: 0 calc(5rem / 16);
|
||||
font-size: calc(16rem / 16);
|
||||
}
|
||||
|
||||
.key {
|
||||
font-weight: bold;
|
||||
}
|
|
@ -7,11 +7,13 @@ import { Point } from "@visx/point";
|
|||
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
|
||||
import { BarStack, BarStackHorizontal, Line } from "@visx/shape";
|
||||
import { SeriesPoint } from "@visx/shape/lib/types";
|
||||
import { withTooltip, Tooltip } from "@visx/tooltip";
|
||||
import { withTooltip } from "@visx/tooltip";
|
||||
import { WithTooltipProvidedProps } from "@visx/tooltip/lib/enhancers/withTooltip";
|
||||
import React from "react";
|
||||
import { Color } from "utils/Color";
|
||||
|
||||
import { TooltipWrapper } from "./TooltipWrapper";
|
||||
|
||||
import styles from "./StackedBarGraph.module.css";
|
||||
|
||||
interface StackedBarData {
|
||||
|
@ -248,16 +250,14 @@ export const StackedBarGraphVertical = withTooltip<
|
|||
</div>
|
||||
|
||||
{tooltipOpen && tooltipData ? (
|
||||
<Tooltip
|
||||
className={styles.toolTip}
|
||||
<TooltipWrapper
|
||||
top={tooltipTop}
|
||||
left={tooltipLeft}
|
||||
unstyled
|
||||
header={tooltipData.key}
|
||||
>
|
||||
<p className={styles.key}>{tooltipData.key}</p>
|
||||
<p>{tooltipData.bar.data[tooltipData.key]}</p>
|
||||
<p>{getCategory(tooltipData.bar.data)}</p>
|
||||
</Tooltip>
|
||||
</TooltipWrapper>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
@ -438,16 +438,14 @@ export const StackedBarGraphHorizontal = withTooltip<
|
|||
</div>
|
||||
|
||||
{tooltipOpen && tooltipData ? (
|
||||
<Tooltip
|
||||
className={styles.toolTip}
|
||||
<TooltipWrapper
|
||||
top={tooltipTop}
|
||||
left={tooltipLeft}
|
||||
unstyled
|
||||
header={tooltipData.key}
|
||||
>
|
||||
<p className={styles.key}>{tooltipData.key}</p>
|
||||
<p>{tooltipData.bar.data[tooltipData.key]}</p>
|
||||
<p>{getCategory(tooltipData.bar.data)}</p>
|
||||
</Tooltip>
|
||||
</TooltipWrapper>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
.tooltip {
|
||||
font-family: "Inconsolata", monospace;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
background-color: var(--label);
|
||||
pointer-events: none;
|
||||
padding: calc(10rem / 16);
|
||||
border-radius: calc(10rem / 16);
|
||||
font-size: calc(18rem / 16);
|
||||
}
|
||||
|
||||
.header {
|
||||
color: var(--primary-background);
|
||||
margin: 0;
|
||||
font-size: calc(16rem / 16);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.body {
|
||||
color: var(--primary-background);
|
||||
margin-top: calc(5rem / 16);
|
||||
font-size: calc(16rem / 16);
|
||||
}
|
||||
|
||||
.body p {
|
||||
color: var(--primary-background);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: calc(16rem / 16) !important;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { Tooltip } from "@visx/tooltip";
|
||||
import React from "react";
|
||||
|
||||
import styles from "./TooltipWrapper.module.css";
|
||||
|
||||
type TooltipWrapperProps = {
|
||||
top?: number;
|
||||
left?: number;
|
||||
className?: string;
|
||||
header?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const TooltipWrapper = ({
|
||||
top,
|
||||
left,
|
||||
className,
|
||||
header,
|
||||
children,
|
||||
}: TooltipWrapperProps) => {
|
||||
return (
|
||||
<Tooltip
|
||||
top={top}
|
||||
left={left}
|
||||
className={`${styles.tooltip} ${className ?? ""}`}
|
||||
unstyled
|
||||
applyPositionStyle
|
||||
>
|
||||
{header ? <span className={styles.header}>{header}</span> : null}
|
||||
{children ? <div className={styles.body}>{children}</div> : null}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export { TooltipWrapper };
|
|
@ -2,19 +2,4 @@
|
|||
text-shadow: var(--primary-accent) 0 0 calc(20rem / 16);
|
||||
text-anchor: "middle";
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
font-family: "Inconsolata", monospace;
|
||||
font-weight: bold;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
background-color: var(--label);
|
||||
color: var(--primary-background);
|
||||
box-shadow: 0px calc(1rem / 16) calc(2rem / 16) var(--card-background);
|
||||
pointer-events: none;
|
||||
padding: calc(10rem / 16);
|
||||
font-size: calc(18rem / 16);
|
||||
border-radius: calc(10rem / 16);
|
||||
}
|
|
@ -2,10 +2,14 @@ import { localPoint } from "@visx/event";
|
|||
import { Point } from "@visx/point";
|
||||
import { scaleLog } from "@visx/scale";
|
||||
import { Text } from "@visx/text";
|
||||
import { TooltipWithBounds, useTooltip, withTooltip } from "@visx/tooltip";
|
||||
import { useTooltip, withTooltip } from "@visx/tooltip";
|
||||
import { Wordcloud as VisxWordcloud } from "@visx/wordcloud";
|
||||
import React from "react";
|
||||
import { Color } from "utils/Color";
|
||||
import { inDevEnvironment } from "utils/inDevEnviroment";
|
||||
import { useIsMobile } from "utils/isMobile";
|
||||
|
||||
import { TooltipWrapper } from "./TooltipWrapper";
|
||||
|
||||
import styles from "./WordCloud.module.css";
|
||||
|
||||
|
@ -13,16 +17,22 @@ interface WordCloudProps {
|
|||
data: Array<WordData>;
|
||||
/** Width of the graph, in px */
|
||||
width?: number;
|
||||
/** The minimum width of the graph */
|
||||
minWidth?: number;
|
||||
/** Height of the graph, in px */
|
||||
height?: number;
|
||||
/** Minimum padding between words, in px */
|
||||
wordPadding?: number;
|
||||
/** Weight of the font of the words */
|
||||
fontWeight?: number;
|
||||
/** The desired font size of the smallest word, in px.*/
|
||||
minFontSize?: number;
|
||||
/** The desired font size of the largest word, in px. */
|
||||
maxFontSize?: number;
|
||||
/** The desired font size of the smallest word on desktop, in px.*/
|
||||
desktopMinFontSize?: number;
|
||||
/** The desired font size of the smallest word on mobile, in px.*/
|
||||
mobileMinFontSize?: number;
|
||||
/** The desired font size of the largest word on desktop, in px. */
|
||||
desktopMaxFontSize?: number;
|
||||
/** The desired font size of the largest word on mobile, in px. */
|
||||
mobileMaxFontSize?: number;
|
||||
/** A random seed in the range [0, 1) used for placing the words, change this value to get an alternate placement of words */
|
||||
randomSeed?: number;
|
||||
/** Type of spiral used for rendering the words, either rectangular or archimedean */
|
||||
|
@ -46,11 +56,14 @@ export const WordCloud = withTooltip(
|
|||
height,
|
||||
wordPadding,
|
||||
fontWeight,
|
||||
minFontSize,
|
||||
maxFontSize,
|
||||
desktopMinFontSize,
|
||||
mobileMinFontSize,
|
||||
desktopMaxFontSize,
|
||||
mobileMaxFontSize,
|
||||
randomSeed,
|
||||
spiral,
|
||||
className,
|
||||
minWidth,
|
||||
}: WordCloudProps) => {
|
||||
const {
|
||||
tooltipData,
|
||||
|
@ -69,8 +82,10 @@ export const WordCloud = withTooltip(
|
|||
data={data}
|
||||
wordPadding={wordPadding}
|
||||
fontWeight={fontWeight}
|
||||
minFontSize={minFontSize}
|
||||
maxFontSize={maxFontSize}
|
||||
desktopMinFontSize={desktopMinFontSize}
|
||||
mobileMinFontSize={mobileMinFontSize}
|
||||
desktopMaxFontSize={desktopMaxFontSize}
|
||||
mobileMaxFontSize={mobileMaxFontSize}
|
||||
showTooltip={(data, left, top) => {
|
||||
showTooltip({
|
||||
tooltipData: data,
|
||||
|
@ -83,20 +98,18 @@ export const WordCloud = withTooltip(
|
|||
tooltipTop={tooltipTop}
|
||||
randomSeed={randomSeed}
|
||||
spiral={spiral}
|
||||
isMobile={useIsMobile()}
|
||||
minWidth={minWidth}
|
||||
/>
|
||||
|
||||
{tooltipOpen && tooltipData ? (
|
||||
<TooltipWithBounds
|
||||
<TooltipWrapper
|
||||
// set this to random so it correctly updates with parent bounds
|
||||
key={Math.random()}
|
||||
top={tooltipTop}
|
||||
left={tooltipLeft}
|
||||
className={styles.tooltip}
|
||||
unstyled
|
||||
applyPositionStyle
|
||||
>
|
||||
{tooltipData.text} ({tooltipData.value})
|
||||
</TooltipWithBounds>
|
||||
header={`${tooltipData.text} (${tooltipData.value})`}
|
||||
></TooltipWrapper>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
@ -114,27 +127,38 @@ type WordCloudWordsProps = Omit<WordCloudProps, "className"> & {
|
|||
// tooltipLeft and tooltipTop are used for preventing unnessary renders
|
||||
tooltipLeft?: number;
|
||||
tooltipTop?: number;
|
||||
isMobile: boolean; // passing in isMobile as a prop so we can rerender if this changes
|
||||
};
|
||||
const WordCloudWords: React.FC<WordCloudWordsProps> = ({
|
||||
data,
|
||||
width = 1000,
|
||||
minWidth = 500,
|
||||
height = 500,
|
||||
wordPadding = 30,
|
||||
fontWeight = 500,
|
||||
minFontSize = 20,
|
||||
maxFontSize = 150,
|
||||
wordPadding = 20,
|
||||
fontWeight = 400,
|
||||
desktopMinFontSize = 15,
|
||||
desktopMaxFontSize = 100,
|
||||
mobileMinFontSize = 15,
|
||||
mobileMaxFontSize = 60,
|
||||
randomSeed = 0.5,
|
||||
spiral = "rectangular",
|
||||
showTooltip,
|
||||
hideTooltip,
|
||||
isMobile,
|
||||
}) => {
|
||||
width = width < minWidth ? minWidth : width;
|
||||
|
||||
const minFontSize = isMobile ? mobileMinFontSize : desktopMinFontSize;
|
||||
const maxFontSize = isMobile ? mobileMaxFontSize : desktopMaxFontSize;
|
||||
|
||||
const maxVal = Math.max(...data.map((w) => w.value));
|
||||
const minVal = Math.min(...data.map((w) => w.value));
|
||||
|
||||
const fontScale = scaleLog({
|
||||
domain: [
|
||||
Math.min(...data.map((w) => w.value)),
|
||||
Math.max(...data.map((w) => w.value)),
|
||||
],
|
||||
domain: [minVal, maxVal],
|
||||
range: [minFontSize, maxFontSize],
|
||||
});
|
||||
|
||||
const fontSizeSetter = (datum: WordData) => fontScale(datum.value);
|
||||
const fixedValueGenerator = () => randomSeed;
|
||||
return (
|
||||
|
@ -149,8 +173,19 @@ const WordCloudWords: React.FC<WordCloudWordsProps> = ({
|
|||
rotate={0}
|
||||
random={fixedValueGenerator}
|
||||
>
|
||||
{(cloudWords) =>
|
||||
cloudWords.map((word, index) => {
|
||||
{(cloudWords) => {
|
||||
if (
|
||||
inDevEnvironment &&
|
||||
cloudWords.length != 0 && // since on initial load the length is 0, but thats not an error
|
||||
cloudWords.length != data.length
|
||||
) {
|
||||
console.error(
|
||||
`Not all words rendered for wordcloud! (${
|
||||
data.length - cloudWords.length
|
||||
} words missing) Please try adjusting the min/max font size, the random seed, and the wordPadding`
|
||||
);
|
||||
}
|
||||
return cloudWords.map((word, index) => {
|
||||
return (
|
||||
<Text
|
||||
key={`wordcloud-word-${word.text ?? ""}-${index}`}
|
||||
|
@ -163,19 +198,28 @@ const WordCloudWords: React.FC<WordCloudWordsProps> = ({
|
|||
textAnchor="middle"
|
||||
onMouseMove={
|
||||
((e: React.MouseEvent<SVGTextElement, MouseEvent>) => {
|
||||
const eventSvgCoords = localPoint(
|
||||
// ownerSVGElement is given by visx docs but not recognized by typescript
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
e.target.ownerSVGElement as Element,
|
||||
e
|
||||
) as Point;
|
||||
// ownerSVGElement is given by visx docs but not recognized by typescript
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const eventElement = e.target.ownerSVGElement as Element;
|
||||
const eventSvgCoords = localPoint(eventElement, e) as Point;
|
||||
const rootSVGLeft =
|
||||
eventElement.parentElement?.parentElement?.getBoundingClientRect()
|
||||
.left ?? 0;
|
||||
const parentDivLeft =
|
||||
eventElement.parentElement?.parentElement?.parentElement?.getBoundingClientRect()
|
||||
.left ?? 0;
|
||||
|
||||
// visx localPoint does not account for the horizontal shift due to centering of the parent element,
|
||||
// so manually add any shift from that
|
||||
const alignmentOffset = rootSVGLeft - parentDivLeft;
|
||||
|
||||
if (word.text) {
|
||||
showTooltip(
|
||||
{ text: word.text, value: data[index].value },
|
||||
eventSvgCoords.x -
|
||||
word.text.length * TOOLTIP_HORIZONTAL_SHIFT_SCALER,
|
||||
word.text.length * TOOLTIP_HORIZONTAL_SHIFT_SCALER +
|
||||
alignmentOffset,
|
||||
eventSvgCoords.y
|
||||
);
|
||||
}
|
||||
|
@ -186,8 +230,8 @@ const WordCloudWords: React.FC<WordCloudWordsProps> = ({
|
|||
{word.text}
|
||||
</Text>
|
||||
);
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
</VisxWordcloud>
|
||||
);
|
||||
};
|
||||
|
@ -199,6 +243,7 @@ const shouldNotRerender = (
|
|||
if (
|
||||
// if width changes, rerender, else don't rerender for a tooltip change
|
||||
prevProps.width === nextProps.width &&
|
||||
prevProps.isMobile === nextProps.isMobile &&
|
||||
(prevProps.tooltipLeft !== nextProps.tooltipLeft ||
|
||||
prevProps.tooltipTop !== nextProps.tooltipTop ||
|
||||
nextProps.tooltipLeft === undefined ||
|
||||
|
|
|
@ -0,0 +1,348 @@
|
|||
export const D1 = [
|
||||
{
|
||||
category: "CS",
|
||||
value: 88,
|
||||
},
|
||||
{
|
||||
category: "CS/BBA",
|
||||
value: 12,
|
||||
},
|
||||
{
|
||||
category: "CFM",
|
||||
value: 5,
|
||||
},
|
||||
];
|
||||
|
||||
export const D2 = [
|
||||
{
|
||||
category: "Man",
|
||||
value: 72,
|
||||
},
|
||||
{
|
||||
category: "Woman",
|
||||
value: 29,
|
||||
},
|
||||
{
|
||||
category: "Gender non-conforming",
|
||||
value: 4,
|
||||
},
|
||||
];
|
||||
|
||||
export const D3 = [
|
||||
{
|
||||
category: "She/Her/Her",
|
||||
value: 31,
|
||||
},
|
||||
{
|
||||
category: "He/Him/His",
|
||||
value: 73,
|
||||
},
|
||||
{
|
||||
category: "They/Them/Their",
|
||||
value: 3,
|
||||
},
|
||||
];
|
||||
|
||||
export const D4 = [
|
||||
{
|
||||
category: "Black",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
category: "Latin",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
category: "East Asian",
|
||||
value: 68,
|
||||
},
|
||||
{
|
||||
category: "Middle Eastern",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
category: "South Asian",
|
||||
value: 13,
|
||||
},
|
||||
{
|
||||
category: "Southeast Asian",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
category: "White",
|
||||
value: 18,
|
||||
},
|
||||
{
|
||||
category: "Prefer not to say",
|
||||
value: 1,
|
||||
},
|
||||
];
|
||||
|
||||
export const D5 = [
|
||||
{
|
||||
category: "89",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
category: "90",
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
category: "91",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
category: "92",
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
category: "93",
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
category: "94",
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
category: "95",
|
||||
value: 15,
|
||||
},
|
||||
{
|
||||
category: "96",
|
||||
value: 22,
|
||||
},
|
||||
{
|
||||
category: "97",
|
||||
value: 21,
|
||||
},
|
||||
{
|
||||
category: "98",
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
category: "99",
|
||||
value: 8,
|
||||
},
|
||||
];
|
||||
|
||||
export const D6 = [
|
||||
{
|
||||
category: "Asexual",
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
category: "Bisexual",
|
||||
value: 12,
|
||||
},
|
||||
{
|
||||
category: "Gay",
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
category: "Heterosexual",
|
||||
value: 81,
|
||||
},
|
||||
{
|
||||
category: "Queer",
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
category: "Pansexual",
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
category: "Questioning",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
category: "Prefer not to say",
|
||||
value: 2,
|
||||
},
|
||||
];
|
||||
|
||||
export const D7 = [
|
||||
{
|
||||
text: "Ontario (Other)",
|
||||
value: 15,
|
||||
},
|
||||
{
|
||||
text: "British Columbia",
|
||||
value: 11,
|
||||
},
|
||||
{
|
||||
text: "Kitchener / Waterloo",
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
text: "Alberta",
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
text: "Quebec",
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
text: "USA",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
text: "Hong Kong",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
text: "Manitoba",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
text: "India",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
text: "GTA / Toronto",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
text: "United Arab Emirates",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
text: "Indonesia",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
text: "Saskatchewan",
|
||||
value: 1,
|
||||
},
|
||||
];
|
||||
|
||||
export const D8 = [
|
||||
{
|
||||
category: "High School Diploma",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
category: "College Diploma",
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
category: "Bachelor's Degree",
|
||||
value: 43,
|
||||
},
|
||||
{
|
||||
category: "Master's Degree",
|
||||
value: 33,
|
||||
},
|
||||
{
|
||||
category: "Doctoral Degree",
|
||||
value: 17,
|
||||
},
|
||||
{
|
||||
category: "Not Applicable",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
category: "Prefer not to say",
|
||||
value: 2,
|
||||
},
|
||||
];
|
||||
|
||||
export const D9 = [
|
||||
{
|
||||
category: "0-50",
|
||||
value: 9,
|
||||
},
|
||||
{
|
||||
category: "51-100",
|
||||
value: 28,
|
||||
},
|
||||
{
|
||||
category: "101-150",
|
||||
value: 22,
|
||||
},
|
||||
{
|
||||
category: "151-200",
|
||||
value: 23,
|
||||
},
|
||||
{
|
||||
category: "201-250",
|
||||
value: 7,
|
||||
},
|
||||
{
|
||||
category: "251-300",
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
category: "301+",
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
category: "Prefer not to say",
|
||||
value: 6,
|
||||
},
|
||||
];
|
||||
|
||||
export const D10 = [
|
||||
{
|
||||
category: "0",
|
||||
value: 74,
|
||||
},
|
||||
{
|
||||
category: "1",
|
||||
value: 17,
|
||||
},
|
||||
{
|
||||
category: "2",
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
category: "3",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
category: "4",
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
category: "5",
|
||||
value: 2,
|
||||
},
|
||||
];
|
||||
|
||||
export const D11 = [
|
||||
{
|
||||
category: "No religious affiliation",
|
||||
value: 79,
|
||||
},
|
||||
{
|
||||
category: "Christianity",
|
||||
value: 16,
|
||||
},
|
||||
{
|
||||
category: "Atheist",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
category: "Judaism",
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
category: "Buddhism",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
category: "Islam",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
category: "Hinduism",
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
category: "Jainism",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
category: "Prefer not to say",
|
||||
value: 2,
|
||||
},
|
||||
];
|
|
@ -2,6 +2,10 @@ html {
|
|||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html, body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
/* Theme colours */
|
||||
--pink: #EF839D;
|
||||
|
@ -68,6 +72,7 @@ body {
|
|||
color: var(--primary-text);
|
||||
font-family: "Inconsolata", monospace;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
|
||||
/* Font styling for body */
|
||||
font-size: calc(18rem / 16);
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
import {
|
||||
D1,
|
||||
D2,
|
||||
D3,
|
||||
D4,
|
||||
D5,
|
||||
D6,
|
||||
D7,
|
||||
D8,
|
||||
D9,
|
||||
D10,
|
||||
D11,
|
||||
} from "data/demographics";
|
||||
import { pageRoutes } from "data/routes";
|
||||
import React from "react";
|
||||
import {
|
||||
barGraphProps,
|
||||
DefaultProp,
|
||||
pieChartProps,
|
||||
barGraphMargin,
|
||||
barGraphWidth,
|
||||
} from "utils/defaultProps";
|
||||
import { useWindowDimensions } from "utils/getWindowDimensions";
|
||||
import { useIsMobile } from "utils/isMobile";
|
||||
|
||||
import { BarGraphVertical, BarGraphHorizontal } from "@/components/BarGraph";
|
||||
import { BottomNav } from "@/components/BottomNav";
|
||||
import { ComponentWrapper } from "@/components/ComponentWrapper";
|
||||
import { Header } from "@/components/Header";
|
||||
import { PieChart } from "@/components/PieChart";
|
||||
import { SectionHeader } from "@/components/SectionHeader";
|
||||
import { WordCloud } from "@/components/WordCloud";
|
||||
|
||||
import styles from "./samplePage.module.css";
|
||||
|
||||
export default function Demographics() {
|
||||
const pageWidth = useWindowDimensions().width;
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<Header />
|
||||
<SectionHeader
|
||||
title="Demographics"
|
||||
subtitle="An insight into the demographics of UW’s CS programs"
|
||||
/>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="What program are you in?"
|
||||
bodyText="There are a total of 106 respondents of the CS Class Profile. Interestingly, there are a huge number of students that are just in CS, partially due to the overwhelming number of people in CS as seen in the total demographics."
|
||||
>
|
||||
<div className={styles.graphContainer}>
|
||||
<PieChart data={D1} {...pieChartProps(isMobile, pageWidth)} />
|
||||
</div>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="Please select the gender identity option(s) with which you identify."
|
||||
bodyText="The ratio between different genders and pronouns is pretty drastic, but it’s important to note that this is not representative of everyone. There is also an obvious correlation between the gender identities and used pronouns. Note that certain respondents have chosen two or more of the listed categories. We have counted each of them as a separate entry rather than a category itself."
|
||||
align="right"
|
||||
noBackground
|
||||
>
|
||||
<div className={styles.graphContainer}>
|
||||
<PieChart data={D2} {...pieChartProps(isMobile, pageWidth)} />
|
||||
</div>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper heading="Please indicate the pronouns that you use.">
|
||||
<div className={styles.graphContainer}>
|
||||
<PieChart data={D3} {...pieChartProps(isMobile, pageWidth)} />
|
||||
</div>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="Please select the racial category or categories with which you primarily identify."
|
||||
bodyText="We have quite a bit diversity in here! It’s great to see that, and extrapolating this to the rest of the population would probably yield similar results. Note that certain respondents have chosen two or more of the listed categories. We have counted each of them as a separate entry rather than a category itself."
|
||||
align="left"
|
||||
noBackground
|
||||
>
|
||||
<BarGraphVertical data={D4} {...barGraphProps(isMobile, pageWidth)} />
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="What was your high school admissions average?"
|
||||
bodyText="With a mean average of roughly 95.5%, getting into any of these programs is no easy feat. That makes everyone special from the day they were admitted into the school! :)"
|
||||
>
|
||||
<BarGraphVertical data={D5} {...barGraphProps(isMobile, pageWidth)} />
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="Please select the sexual identity option(s) you identify with."
|
||||
bodyText="There are a total of 106 respondents of the CS Class Profile. Interestingly, there are a huge number of students that are just in CS, partially due to the overwhelming number of people in CS as seen in the total demographics. Note that certain respondents have chosen two or more of the listed categories. We have counted each of them as a separate entry rather than a category itself."
|
||||
align="right"
|
||||
noBackground
|
||||
>
|
||||
<BarGraphVertical
|
||||
data={D6}
|
||||
{...barGraphProps(isMobile, pageWidth)}
|
||||
widthAlternatingLabel={700}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="Where did you live before coming to UW?"
|
||||
bodyText="UW CS gets quite a bit from the GTA as most people may expect. But it’s always great to meet people that come from all over the place."
|
||||
align="right"
|
||||
>
|
||||
<WordCloud
|
||||
data={D7}
|
||||
width={isMobile ? pageWidth / 1.5 : 800}
|
||||
height={DefaultProp.graphHeight}
|
||||
wordPadding={7}
|
||||
desktopMaxFontSize={75}
|
||||
mobileMaxFontSize={48}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="What is the highest education level of your parents?"
|
||||
bodyText="There’s quite a bit of spread in this chart! The real question is, how many of us are matching our parent’s levels of education? Find out later in the Class Profile…"
|
||||
noBackground
|
||||
>
|
||||
<BarGraphVertical data={D8} {...barGraphProps(isMobile, pageWidth)} />
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="What was your family income before entering your current UW program?"
|
||||
bodyText="Most families made more than the average income in Canada today, which has been an increasing number over the last several years (apart from 2019 to 2020). So, it’s safe to say the average income of these individuals was also higher than the average income of Canada at the time they started university."
|
||||
align="right"
|
||||
>
|
||||
<BarGraphVertical
|
||||
// TODO: change when histogram component is ready
|
||||
data={D9}
|
||||
{...barGraphProps(isMobile, pageWidth)}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="How many close relatives have attended UW (i.e. siblings, cousins, aunts & uncles, parents, etc.)?"
|
||||
bodyText="Wow! There’s a lot of people that are coming to UW as the first in the family, but it’s great to see that we have people who have had older siblings or other family members come here as well."
|
||||
align="left"
|
||||
noBackground
|
||||
>
|
||||
<BarGraphVertical data={D10} {...barGraphProps(isMobile, pageWidth)} />
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="Please indicate your religion and/or spiritual affiliation."
|
||||
bodyText="There are a total of 106 respondents of the CS Class Profile. Interestingly, there are a huge number of students that are just in CS, partially due to the overwhelming number of people in CS as seen in the total demographics. Note that certain respondents have chosen two or more of the listed categories. We have counted each of them as a separate entry rather than a category itself."
|
||||
align="right"
|
||||
>
|
||||
<BarGraphHorizontal
|
||||
data={D11}
|
||||
width={barGraphWidth(isMobile, pageWidth)}
|
||||
height={DefaultProp.graphHeight}
|
||||
margin={{ ...barGraphMargin, ...{ left: 220 } }}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<BottomNav rightPage={pageRoutes.academics}></BottomNav>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -2,4 +2,26 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.graphContainer {
|
||||
padding: 0 calc(70rem / 16);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.graphContainer {
|
||||
padding: 0 calc(40rem / 16);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1100px) {
|
||||
.graphContainer {
|
||||
padding: 0 calc(20rem / 16);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.graphContainer {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,9 @@
|
|||
import { mockCategoricalData, moreMockCategoricalData } from "data/mocks";
|
||||
import {
|
||||
mockBoxPlotData,
|
||||
mockCategoricalData,
|
||||
mockLineData,
|
||||
moreMockCategoricalData,
|
||||
} from "data/mocks";
|
||||
import { pageRoutes } from "data/routes";
|
||||
import React from "react";
|
||||
import { useWindowDimensions } from "utils/getWindowDimensions";
|
||||
|
@ -6,8 +11,10 @@ import { useIsMobile } from "utils/isMobile";
|
|||
|
||||
import { BarGraphVertical, BarGraphHorizontal } from "@/components/BarGraph";
|
||||
import { BottomNav } from "@/components/BottomNav";
|
||||
import { BoxPlot } from "@/components/Boxplot";
|
||||
import { ComponentWrapper } from "@/components/ComponentWrapper";
|
||||
import { Header } from "@/components/Header";
|
||||
import { LineGraph } from "@/components/LineGraph";
|
||||
import { SectionHeader } from "@/components/SectionHeader";
|
||||
import { WordCloud } from "@/components/WordCloud";
|
||||
|
||||
|
@ -22,7 +29,18 @@ export default function SamplePage() {
|
|||
const defaultGraphHeight = 500;
|
||||
|
||||
// Make vars for common configs such as common margins
|
||||
const defaultBarGraphMargin = { top: 20, bottom: 40, left: 150, right: 20 };
|
||||
const defaultVerticalBarGraphMargin = {
|
||||
top: 20,
|
||||
bottom: 80,
|
||||
left: 60,
|
||||
right: 20,
|
||||
};
|
||||
const defaultHorizontalBarGraphMargin = {
|
||||
top: 20,
|
||||
bottom: 80,
|
||||
left: 120,
|
||||
right: 20,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
|
@ -39,10 +57,9 @@ export default function SamplePage() {
|
|||
data={mockCategoricalData}
|
||||
width={defaultGraphWidth}
|
||||
height={defaultGraphHeight}
|
||||
margin={defaultBarGraphMargin}
|
||||
margin={defaultVerticalBarGraphMargin}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="What program are you in?"
|
||||
bodyText="There are a total of 106 respondents of the CS Class Profile. Interestingly, there are a huge number of students that are just in CS, partially due to the overwhelming number of people in CS as seen in the total demographics."
|
||||
|
@ -58,21 +75,16 @@ export default function SamplePage() {
|
|||
height={defaultGraphHeight}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="What program are you in?"
|
||||
bodyText="There are a total of 106 respondents of the CS Class Profile. Interestingly, there are a huge number of students that are just in CS, partially due to the overwhelming number of people in CS as seen in the total demographics."
|
||||
align="right"
|
||||
>
|
||||
<ComponentWrapper heading="What program are you in?" align="right">
|
||||
<BarGraphHorizontal
|
||||
className={styles.barGraphDemo}
|
||||
data={mockCategoricalData}
|
||||
width={defaultGraphWidth}
|
||||
height={defaultGraphHeight}
|
||||
margin={defaultBarGraphMargin}
|
||||
// You can override specific margins if needed
|
||||
margin={defaultHorizontalBarGraphMargin}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="What program are you in?"
|
||||
bodyText="There are a total of 106 respondents of the CS Class Profile. Interestingly, there are a huge number of students that are just in CS, partially due to the overwhelming number of people in CS as seen in the total demographics."
|
||||
|
@ -84,20 +96,18 @@ export default function SamplePage() {
|
|||
data={mockCategoricalData}
|
||||
width={defaultGraphWidth}
|
||||
height={defaultGraphHeight}
|
||||
margin={defaultBarGraphMargin}
|
||||
margin={defaultHorizontalBarGraphMargin}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper heading="What program are you in?" align="left">
|
||||
<BarGraphHorizontal
|
||||
className={styles.barGraphDemo}
|
||||
data={mockCategoricalData}
|
||||
width={defaultGraphWidth}
|
||||
height={defaultGraphHeight}
|
||||
margin={defaultBarGraphMargin}
|
||||
margin={defaultHorizontalBarGraphMargin}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="What program are you in?"
|
||||
bodyText="There are a total of 106 respondents of the CS Class Profile. Interestingly, there are a huge number of students that are just in CS, partially due to the overwhelming number of people in CS as seen in the total demographics."
|
||||
|
@ -113,7 +123,6 @@ export default function SamplePage() {
|
|||
height={defaultGraphHeight}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper heading="What program are you in? " align="right">
|
||||
<WordCloud
|
||||
data={moreMockCategoricalData.map((word) => ({
|
||||
|
@ -134,7 +143,6 @@ export default function SamplePage() {
|
|||
height={defaultGraphHeight}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper heading="What program are you in?" align="left">
|
||||
<WordCloud
|
||||
data={moreMockCategoricalData.map((word) => ({
|
||||
|
@ -145,7 +153,6 @@ export default function SamplePage() {
|
|||
height={defaultGraphHeight}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="What program are you in?"
|
||||
bodyText="There are a total of 106 respondents of the CS Class Profile. Interestingly, there are a huge number of students that are just in CS, partially due to the overwhelming number of people in CS as seen in the total demographics."
|
||||
|
@ -157,10 +164,9 @@ export default function SamplePage() {
|
|||
data={mockCategoricalData}
|
||||
width={defaultGraphWidth}
|
||||
height={defaultGraphHeight}
|
||||
margin={defaultBarGraphMargin}
|
||||
margin={defaultHorizontalBarGraphMargin}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper
|
||||
heading="What program are you in?"
|
||||
bodyText="There are a total of 106 respondents of the CS Class Profile. Interestingly, there are a huge number of students that are just in CS, partially due to the overwhelming number of people in CS as seen in the total demographics."
|
||||
|
@ -171,21 +177,37 @@ export default function SamplePage() {
|
|||
data={mockCategoricalData}
|
||||
width={defaultGraphWidth}
|
||||
height={defaultGraphHeight}
|
||||
margin={defaultBarGraphMargin}
|
||||
margin={defaultHorizontalBarGraphMargin}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<ComponentWrapper heading="What program are you in?" align="center">
|
||||
<WordCloud
|
||||
data={moreMockCategoricalData.map((word) => ({
|
||||
text: word.key,
|
||||
value: word.value,
|
||||
}))}
|
||||
width={defaultGraphWidth}
|
||||
height={defaultGraphHeight}
|
||||
<BoxPlot
|
||||
width={600}
|
||||
height={400}
|
||||
data={mockBoxPlotData}
|
||||
margin={{
|
||||
top: 20,
|
||||
left: 20,
|
||||
}}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
<ComponentWrapper
|
||||
heading="What program are you in?"
|
||||
align="right"
|
||||
bodyText="Pariatur deserunt aute laborum ea adipisicing. Labore labore ipsum duis nisi ea incididunt ipsum occaecat. Ut occaecat et exercitation incididunt sit sit duis deserunt velit culpa nisi et dolore."
|
||||
>
|
||||
<LineGraph
|
||||
data={mockLineData}
|
||||
width={600}
|
||||
height={400}
|
||||
margin={{
|
||||
top: 20,
|
||||
bottom: 80,
|
||||
left: 30,
|
||||
right: 20,
|
||||
}}
|
||||
/>
|
||||
</ComponentWrapper>
|
||||
|
||||
<BottomNav
|
||||
leftPage={pageRoutes.demographics}
|
||||
rightPage={pageRoutes.contributors}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
// Only static number props are included for this list
|
||||
const propNames = ["graphHeight", "labelSize", "labelWidth"] as const;
|
||||
|
||||
// This type is needed for smart autocomplete
|
||||
type PropName = typeof propNames[number];
|
||||
|
||||
const mobileBarGraphFactor = 1.25;
|
||||
const desktopBarGraphFactor = 2;
|
||||
|
||||
const mobilePieChartFactor = 1.25;
|
||||
const desktopPieChartFactor = 3;
|
||||
|
||||
const graphWidth = (
|
||||
isMobile: boolean,
|
||||
pageWidth: number,
|
||||
isPieChart: boolean
|
||||
): number => {
|
||||
const mobileFactor = isPieChart ? mobilePieChartFactor : mobileBarGraphFactor;
|
||||
const desktopFactor = isPieChart
|
||||
? desktopPieChartFactor
|
||||
: desktopBarGraphFactor;
|
||||
return isMobile ? pageWidth / mobileFactor : pageWidth / desktopFactor;
|
||||
};
|
||||
|
||||
export const barGraphWidth = (isMobile: boolean, width: number) =>
|
||||
graphWidth(isMobile, width, false);
|
||||
|
||||
export const pieChartWidth = (isMobile: boolean, width: number) =>
|
||||
graphWidth(isMobile, width, true);
|
||||
|
||||
export const barGraphMargin = {
|
||||
top: 20,
|
||||
bottom: 80,
|
||||
left: 60,
|
||||
right: 20,
|
||||
};
|
||||
|
||||
export const DefaultProp: { [key in PropName]: number } = {
|
||||
graphHeight: 500,
|
||||
labelSize: 24,
|
||||
labelWidth: 100,
|
||||
};
|
||||
|
||||
export const barGraphProps = (isMobile: boolean, pageWidth: number) => {
|
||||
return {
|
||||
width: barGraphWidth(isMobile, pageWidth),
|
||||
height: DefaultProp.graphHeight,
|
||||
margin: barGraphMargin,
|
||||
};
|
||||
};
|
||||
|
||||
export const pieChartProps = (isMobile: boolean, pageWidth: number) => {
|
||||
return {
|
||||
width: pieChartWidth(isMobile, pageWidth),
|
||||
labelWidth: DefaultProp.labelWidth,
|
||||
labelTextSize: DefaultProp.labelSize,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
const inDevEnvironment = process && process.env.NODE_ENV === "development";
|
||||
|
||||
export { inDevEnvironment };
|
|
@ -1,3 +1,3 @@
|
|||
import { useWindowDimensions } from "./getWindowDimensions";
|
||||
|
||||
export const useIsMobile = () => useWindowDimensions().width <= 768;
|
||||
export const useIsMobile = () => useWindowDimensions().width <= 900;
|
||||
|
|
Loading…
Reference in New Issue