Compare commits

...

8 Commits

Author SHA1 Message Date
e26chiu 5136922e32 Merge branch 'main' of https://git.csclub.uwaterloo.ca/www/cs-2022-class-profile into stackedbar-component
continuous-integration/drone/push Build is passing Details
2022-09-03 14:07:32 -04:00
e26chiu b6d72f7d0b add horizontal stacked bar graph 2022-09-03 13:53:28 -04:00
Mark Chiu 9526f1b0f5 Add BoxPlot component (Closes #6) (#34)
continuous-integration/drone/push Build is passing Details
Done:

- [x] Display boxplot with left and bottom axis in `playground.tsx`
- [x] Add mock data for boxplot
- [x] Hovering over boxplot displays a Tool Tip box.

My version:
![image](/attachments/6c8c4499-a1bd-4434-9230-7117266691ea)

Note:

 - No percentage displayed next to value axis labels.
 - We can add outlier points although it could complicate the graph.
 - Still displays the following error:
 	- Error: Hydration failed because the initial UI does not match what was rendered on the server.
    - Error: Text content does not match server-rendered HTML.
    - Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

    Staging: https://boxplot-component-csc-class-profile-staging-snedadah.k8s.csclub.cloud/
Co-authored-by: Miniapple8888 <miniapple8888@gmail.com>
Co-authored-by: Emily Chiu <e26chiu@corn-syrup.csclub.uwaterloo.ca>
Co-authored-by: e26chiu <e26chiu@csc.uwaterloo.ca>
Reviewed-on: #34
Reviewed-by: Amy <a258wang@csclub.uwaterloo.ca>
2022-09-03 11:13:58 -04:00
e26chiu d53468a0eb fix visual nits + tooltip 2022-09-03 10:36:03 -04:00
e26chiu e346069421 fix visual nits 2022-09-03 08:25:03 -04:00
e26chiu 9e6f1e86d0 Merge branch 'main' of https://git.csclub.uwaterloo.ca/www/cs-2022-class-profile into stackedbar-component 2022-09-03 08:24:13 -04:00
Amy Wang a2dbcb90c6 Add Quotation Carousel (#36)
continuous-integration/drone/push Build is passing Details
Made without visx, because that was easier.

Closes #10.

Co-authored-by: Amy Wang <a258wang@csclub.uwaterloo.ca>
Reviewed-on: #36
Reviewed-by: j285he <j285he@localhost>
2022-09-02 21:53:05 -04:00
Shahan Nedadahandeh 9cd5c158e7 Sample page and graph wrappers (#32)
continuous-integration/drone/push Build is passing Details
2022-09-02 17:39:46 -04:00
21 changed files with 6023 additions and 259 deletions

View File

@ -2,12 +2,13 @@
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.format.enable": true,
"eslint.codeActionsOnSave.mode": "all",
"css.lint.validProperties": ["composes"],
"css.format.spaceAroundSelectorSeparator": true,
"[css]": {
"editor.suggest.insertMode": "replace",
"gitlens.codeLens.scopes": ["document"],
"editor.formatOnSave": true,
"editor.defaultFormatter": "vscode.css-language-features"
"editor.defaultFormatter": "vscode.css-language-features",
},
"[javascript]": {
"editor.formatOnSave": false,

View File

@ -0,0 +1,38 @@
.boxplot {
fill: var(--primary-accent-light);
}
.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);
}

361
components/Boxplot.tsx Normal file
View File

@ -0,0 +1,361 @@
import { AxisLeft, AxisBottom } from "@visx/axis";
import { GridRows, GridColumns } from "@visx/grid";
import { Group } from "@visx/group";
import { Stats } from "@visx/mock-data/lib/generators/genStats";
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 { WithTooltipProvidedProps } from "@visx/tooltip/lib/enhancers/withTooltip";
import React from "react";
import { Color } from "utils/Color";
import styles from "./Boxplot.module.css";
const DEFAULT_LABEL_SIZE = 16;
const TICK_LABEL_FONT_WEIGHT = 800;
interface BoxPlotData {
category: string;
min: number;
median: number;
max: number;
firstQuartile: number;
thirdQuartile: number;
outliers?: number[];
}
type TooltipData = Omit<BoxPlotData, "outliers">;
export type StatsPlotProps = {
data: BoxPlotData[];
/** Width of the entire graph, in pixels, greater than 10. */
width: number;
/** Height of the entire graph, in pixels. */
height: number;
/** Distance between the edge of the graph and the area where the bars are drawn, in pixels. */
margin: {
top: number;
left: number;
};
/** Width of the lines in the graph, in px. */
strokeWidth?: number;
/** Length of the dashes and the gaps in the graph, in px. */
strokeDashArray?: string;
/** Number of ticks for the value (y-)axis */
numTicksLeftAxis?: number;
/** Distance between the boxplot and the top of the grid, in px. */
plotTopOffset?: number;
/** Distance between the left axis labels and the start of the lines of the graph, in px. */
valueAxisLeftOffset?: number;
/** Distance between the top and the first label of the y axis, in px. */
valueAxisLabelTopOffset?: number;
/** Distance between the left and the labels of the y axis, in px. */
valueAxisLabelLeftOffset?: number;
/** Distance between the left and the start of the first label of the x axis, in px. */
categoryAxisLabelLeftOffset?: number;
/** Distance between the top and the column lines of the grid of the graph, in px. */
gridColumnTopOffset?: number;
/** Distance between the top of the point in the boxplot and the start of the tooltip box, in px. */
toolTipTopOffset?: number;
/** Distance between the left of the point in the boxplot and the start of the tooltip box, in px. */
toolTipLeftOffset?: number;
/** Font size of the category (x-)axis labels */
categoryAxisLabelSize?: number;
/** Font size of the value (y-)axis labels */
valueAxisLabelSize?: number;
/** Font size of the text in the tool tip box */
toolTipFontSize?: number;
/** Factor multiplied with the compressed width to determine the box width, in px. */
boxPlotWidthFactor?: number;
/** Factor multiplied with the compressed width to determine the distance between boxes, in px. */
boxPlotLeftOffset?: number;
};
export const BoxPlot = withTooltip<StatsPlotProps, TooltipData>(
({
width,
height,
data,
margin,
tooltipOpen,
tooltipLeft,
tooltipTop,
tooltipData,
showTooltip,
hideTooltip,
strokeWidth = 2.5,
strokeDashArray = "10,4",
numTicksLeftAxis = 6,
plotTopOffset = 10,
valueAxisLeftOffset = 40,
gridColumnTopOffset = -20,
valueAxisLabelTopOffset = 5,
valueAxisLabelLeftOffset = 10,
categoryAxisLabelLeftOffset = 30,
toolTipTopOffset = 20,
toolTipLeftOffset = 5,
categoryAxisLabelSize = DEFAULT_LABEL_SIZE,
valueAxisLabelSize = DEFAULT_LABEL_SIZE,
boxPlotWidthFactor = 0.4,
boxPlotLeftOffset = 0.3,
}: StatsPlotProps & WithTooltipProvidedProps<TooltipData>) => {
// bounds
const xMax = width;
const yMax = height - 120;
// formatting data
const plotData: Stats[] = data.map((d) => {
return {
boxPlot: {
...d,
x: d.category,
outliers: [],
},
binData: [],
};
});
// accessors
const getX = (d: Stats) => d.boxPlot.x;
const getMin = (d: Stats) => d.boxPlot.min;
const getMax = (d: Stats) => d.boxPlot.max;
const getMedian = (d: Stats) => d.boxPlot.median;
const getFirstQuartile = (d: Stats) => d.boxPlot.firstQuartile;
const getThirdQuartile = (d: Stats) => d.boxPlot.thirdQuartile;
// scales
const xScale = scaleBand<string>({
range: [18, xMax - 80], // scaling is needed due to the left offset
round: true,
domain: plotData.map(getX),
padding: 0.3,
});
const values = plotData.reduce((allValues, { boxPlot }) => {
allValues.push(boxPlot.min, boxPlot.max);
return allValues;
}, [] as number[]);
const minYValue = Math.min(...values);
const maxYValue = Math.max(...values);
const yScale = scaleLinear<number>({
range: [yMax, 0],
round: true,
domain: [minYValue, maxYValue],
});
const constrainedWidth = Math.min(200, xScale.bandwidth());
return width < 10 ? null : (
<div>
<svg width={width} height={height}>
<Group top={margin.top} left={margin.left}>
<GridRows
top={plotTopOffset}
left={valueAxisLeftOffset}
scale={yScale}
width={xMax}
numTicks={numTicksLeftAxis}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<GridColumns
scale={xScale}
height={yMax + plotTopOffset - gridColumnTopOffset}
top={gridColumnTopOffset}
left={valueAxisLeftOffset}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<Line
fill={Color.tertiaryBackground}
to={new Point({ x: valueAxisLeftOffset, y: gridColumnTopOffset })}
from={
new Point({ x: valueAxisLeftOffset, y: yMax + plotTopOffset })
}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<Line
fill={Color.tertiaryBackground}
to={
new Point({
x: xMax - margin.left - strokeWidth,
y: gridColumnTopOffset,
})
}
from={
new Point({
x: xMax - margin.left - strokeWidth,
y: yMax + plotTopOffset,
})
}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<AxisBottom
top={yMax + plotTopOffset - gridColumnTopOffset}
left={categoryAxisLabelLeftOffset}
scale={xScale}
hideAxisLine
hideTicks
labelProps={{
fontSize: `${categoryAxisLabelSize / 16}rem`,
}}
tickLabelProps={() => {
return {
fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT,
};
}}
/>
<AxisLeft
scale={yScale}
top={plotTopOffset + valueAxisLabelTopOffset}
left={valueAxisLabelLeftOffset}
numTicks={numTicksLeftAxis}
hideAxisLine
labelProps={{
fontSize: `${valueAxisLabelSize / 16}rem`,
}}
tickLabelProps={() => {
return {
fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT,
};
}}
/>
<Group top={plotTopOffset}>
{plotData.map((d: Stats, i) => (
<Group key={i}>
<VisxBoxPlot
className={styles.boxplot}
min={getMin(d)}
max={getMax(d)}
left={
xScale(getX(d))! +
boxPlotLeftOffset * constrainedWidth +
valueAxisLeftOffset
}
firstQuartile={getFirstQuartile(d)}
thirdQuartile={getThirdQuartile(d)}
median={getMedian(d)}
boxWidth={constrainedWidth * boxPlotWidthFactor}
rx={0}
ry={0}
stroke={Color.label}
strokeWidth={strokeWidth}
valueScale={yScale}
minProps={{
onMouseOver: () => {
showTooltip({
tooltipTop:
(yScale(getMin(d)) ?? 0) + toolTipTopOffset,
tooltipLeft:
xScale(getX(d))! +
constrainedWidth +
toolTipLeftOffset,
tooltipData: {
...d.boxPlot,
category: getX(d),
},
});
},
onMouseLeave: () => {
hideTooltip();
},
}}
maxProps={{
onMouseOver: () => {
showTooltip({
tooltipTop:
(yScale(getMax(d)) ?? 0) + toolTipTopOffset,
tooltipLeft:
xScale(getX(d))! +
constrainedWidth +
toolTipLeftOffset,
tooltipData: {
...d.boxPlot,
category: getX(d),
},
});
},
onMouseLeave: () => {
hideTooltip();
},
}}
boxProps={{
onMouseOver: () => {
showTooltip({
tooltipTop:
(yScale(getMedian(d)) ?? 0) + toolTipTopOffset,
tooltipLeft:
xScale(getX(d))! +
constrainedWidth +
toolTipLeftOffset,
tooltipData: {
...d.boxPlot,
category: getX(d),
},
});
},
strokeWidth: 0,
onMouseLeave: () => {
hideTooltip();
},
}}
medianProps={{
style: {
stroke: Color.label,
},
onMouseOver: () => {
showTooltip({
tooltipTop:
(yScale(getMedian(d)) ?? 0) + toolTipTopOffset,
tooltipLeft:
xScale(getX(d))! +
constrainedWidth +
toolTipLeftOffset,
tooltipData: {
...d.boxPlot,
category: getX(d),
},
});
},
onMouseLeave: () => {
hideTooltip();
},
}}
/>
</Group>
))}
</Group>
</Group>
</svg>
{tooltipOpen && tooltipData && (
<Tooltip
top={tooltipTop}
left={tooltipLeft}
className={styles.tooltip}
unstyled
>
<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>
)}
</div>
);
}
);

View File

@ -0,0 +1,59 @@
.sideWrapperCommon {
background-color: var(--secondary-background);
display: flex;
padding: calc(40rem / 16) calc(50rem / 16);
margin: calc(65rem / 16) 0;
width: 90%;
}
.wrapperRight {
composes: sideWrapperCommon;
align-self: end;
margin-right: 0;
padding-right: 0;
border-radius: calc(200rem / 16) 0 0 calc(200rem / 16);
flex-direction: row-reverse;
padding-right: calc(50rem / 16);
}
.wrapperLeft {
composes: sideWrapperCommon;
align-self: start;
margin-left: 0;
padding-left: 0;
border-radius: 0 calc(200rem / 16) calc(200rem / 16) 0;
flex-direction: row;
padding-left: calc(50rem / 16);
}
.noBackground {
background: none;
align-self: center;
}
.wrapperCenter {
flex-direction: column;
text-align: center;
gap: calc(25rem / 16);
/* to match the 65px margin with the left/right variant:
add 45px bottom margin, since internal wrapper contributes 20px for the center component
0px top margin, since h3 contributes 45px and internal wrapper contributes 20px for the center component
*/
margin: 0 0 calc(45rem / 16) 0;
padding: 0 15%;
}
@media screen and (max-width: 768px) {
.sideWrapperCommon {
margin: auto;
flex-direction: column;
text-align: center;
padding: 0;
border-radius: 0;
width: 100%;
}
}
.internalWrapper {
padding: calc(20rem / 16);
}

View File

@ -0,0 +1,42 @@
import React from "react";
import styles from "./ComponentWrapper.module.css";
type AlignOption = "left" | "center" | "right";
type ComponentWrapperProps = {
children: React.ReactNode;
heading: string;
bodyText: string;
align?: AlignOption;
noBackground?: boolean;
};
export function ComponentWrapper({
heading,
bodyText,
children,
align = "left",
noBackground = false,
}: ComponentWrapperProps) {
const alignClasses: { [key in AlignOption]: string } = {
left: styles.wrapperLeft,
center: styles.wrapperCenter,
right: styles.wrapperRight,
};
return (
<div
className={`
${alignClasses[align]}
${noBackground ? styles.noBackground : ""}
`}
>
<div className={styles.internalWrapper}>
<h3>{heading}</h3>
<p>{bodyText}</p>
</div>
<div className={styles.internalWrapper}>{children}</div>
</div>
);
}

View File

@ -0,0 +1,124 @@
.carousel {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
gap: calc(8rem / 16);
}
.circle {
position: absolute;
top: 30%;
right: 52%;
z-index: -1;
background-color: var(--tertiary-background);
clip-path: circle();
}
.right.circle {
top: unset;
right: unset;
bottom: 30%;
left: 52%;
}
.carouselButton {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: calc(16rem / 16);
height: min-content;
background: none;
border: none;
cursor: pointer;
}
.arrow {
position: relative;
width: calc(20rem / 16);
height: calc(40rem / 16);
transition: 0.2s;
}
.previous.arrow {
transform: rotate(180deg);
}
.carouselButton:hover > .arrow {
translate: calc(4rem / 16);
}
.carouselButton:hover > .previous.arrow {
translate: calc(-4rem / 16);
}
.card {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: stretch;
gap: calc(16rem / 16);
min-height: inherit;
height: 100%;
width: 100%;
padding: calc(30rem / 16);
background-color: var(--translucent-accent);
border: calc(2rem / 16) solid var(--primary-text);
border-radius: calc(12rem / 16);
box-shadow: 0 calc(1rem / 16) calc(10rem / 16) var(--primary-accent);
}
.card ul {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
width: 100%;
margin: 0;
padding: 0;
flex-grow: 1;
}
.card li {
position: absolute;
left: 0;
right: 0;
margin: 0;
padding: 0;
list-style: none;
visibility: visible;
opacity: 1;
transition: 0.1s;
}
.card li.hidden {
visibility: hidden;
opacity: 0;
}
.card p {
margin: 0 calc(16rem / 16);
font-weight: bold;
text-align: center;
}
.quotationMark {
width: calc(20rem / 16);
height: calc(20rem / 16);
}
.right.quotationMark {
transform: rotate(180deg);
align-self: end;
}

View File

@ -0,0 +1,130 @@
import React, { useState } from "react";
import { Color } from "utils/Color";
import styles from "./QuotationCarousel.module.css";
interface QuotationCarouselProps {
data: string[];
/** Width of the entire carousel including the buttons, in px. */
width?: number;
/** Minimum height of the carousel, in px. */
height?: number;
/** Diameter of the background circles, in px. Set to 0 for no circles. */
circleDiameter?: number;
className?: string;
}
interface CarouselButtonProps {
onClick: () => void;
isPrevious?: boolean;
}
export function QuotationCarousel(props: QuotationCarouselProps) {
const {
data,
width = 600,
height = 100,
circleDiameter = 120,
className,
} = props;
const [activeIdx, setActiveIdx] = useState(0);
function showNextCard() {
setActiveIdx((activeIdx + 1) % data.length);
}
function showPreviousCard() {
setActiveIdx((activeIdx - 1 + data.length) % data.length);
}
return (
<section
className={
className ? `${className} ${styles.carousel}` : styles.carousel
}
style={{ width: `${width / 16}rem`, minHeight: `${height / 16}rem` }}
>
<Circle className={styles.circle} diameter={circleDiameter} />
<Circle
className={`${styles.right} ${styles.circle}`}
diameter={circleDiameter}
/>
<CarouselButton onClick={showPreviousCard} isPrevious />
<div className={styles.card}>
<QuotationMark className={styles.quotationMark} />
<ul>
{data.map((quote, idx) => (
<li key={idx} className={idx !== activeIdx ? styles.hidden : ""}>
<p>{quote}</p>
</li>
))}
</ul>
<QuotationMark className={`${styles.right} ${styles.quotationMark}`} />
</div>
<CarouselButton onClick={showNextCard} />
</section>
);
}
function Circle({
className,
diameter,
}: {
className: string;
diameter: number;
}) {
return (
<div
className={className}
aria-hidden
style={{
width: `${diameter / 16}rem`,
height: `${diameter / 16}rem`,
}}
/>
);
}
function CarouselButton({ isPrevious, onClick }: CarouselButtonProps) {
return (
<button className={styles.carouselButton} onClick={onClick}>
<svg
className={
isPrevious ? `${styles.previous} ${styles.arrow}` : styles.arrow
}
width="39"
height="72"
viewBox="0 0 39 72"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 4L34.4206 35.804C35.2926 36.7157 35.2597 38.1619 34.3471 39.0329L4 68"
stroke={Color.primaryAccentLighter}
strokeWidth="4"
strokeLinecap="round"
/>
</svg>
</button>
);
}
function QuotationMark({ className }: { className: string }) {
return (
<svg
className={className}
aria-hidden
width="68"
height="56"
viewBox="0 0 68 56"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21.34 0.855375L31.1131 9.91768C28.9808 11.9315 27.0262 13.9454 25.2492 15.9592C23.5908 17.9731 22.2877 19.9869 21.34 22.0008C20.3923 24.0146 19.9185 25.9692 19.9185 27.8646C19.9185 29.4046 20.3331 30.7077 21.1623 31.7738C21.9915 32.84 22.9392 33.8469 24.0054 34.7946C25.0715 35.7423 26.0192 36.8677 26.8485 38.1708C27.6777 39.4738 28.0923 41.1323 28.0923 43.1461C28.0923 46.5815 26.8485 49.5431 24.3608 52.0308C21.9915 54.5185 18.8523 55.7623 14.9431 55.7623C11.1523 55.7623 7.71693 54.2223 4.63693 51.1423C1.67539 48.0623 0.194616 44.1531 0.194616 39.4146C0.194616 35.6238 0.964617 31.7146 2.50462 27.6869C4.16308 23.5408 6.53231 19.2169 9.61231 14.7154C12.6923 10.2138 16.6015 5.59383 21.34 0.855375ZM57.7669 0.855375L67.54 9.91768C65.4077 11.9315 63.4531 13.9454 61.6762 15.9592C60.0177 17.9731 58.7146 19.9869 57.7669 22.0008C56.8192 24.0146 56.3454 25.9692 56.3454 27.8646C56.3454 29.4046 56.76 30.7077 57.5892 31.7738C58.4185 32.84 59.3662 33.8469 60.4323 34.7946C61.4985 35.7423 62.4462 36.8677 63.2754 38.1708C64.1046 39.4738 64.5192 41.1323 64.5192 43.1461C64.5192 46.5815 63.2754 49.5431 60.7877 52.0308C58.4185 54.5185 55.2792 55.7623 51.37 55.7623C47.5792 55.7623 44.1439 54.2223 41.0639 51.1423C38.1023 48.0623 36.6215 44.1531 36.6215 39.4146C36.6215 35.6238 37.3915 31.7146 38.9315 27.6869C40.59 23.5408 42.9592 19.2169 46.0392 14.7154C49.1192 10.2138 53.0285 5.59383 57.7669 0.855375Z"
fill={Color.primaryText}
/>
</svg>
);
}

View File

@ -9,18 +9,27 @@
.legend {
position: absolute;
display: flex;
font-size: 16px;
font-size: calc(16rem / 16);
top: 0;
}
.toolTip {
padding: 10px 0;
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 5px;
margin: 0 calc(5rem / 16);
font-size: calc(16rem / 16);
}
.key {
font-weight: 700;
font-weight: bold;
}

View File

@ -5,9 +5,10 @@ import { Group } from "@visx/group";
import { LegendOrdinal } from "@visx/legend";
import { Point } from "@visx/point";
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
import { BarStack, Line } from "@visx/shape";
import { BarStack, BarStackHorizontal, Line } from "@visx/shape";
import { SeriesPoint } from "@visx/shape/lib/types";
import { useTooltip, useTooltipInPortal, defaultStyles } from "@visx/tooltip";
import { withTooltip, Tooltip } from "@visx/tooltip";
import { WithTooltipProvidedProps } from "@visx/tooltip/lib/enhancers/withTooltip";
import React from "react";
import { Color } from "utils/Color";
@ -41,10 +42,12 @@ export type StackedBarProps = {
colorRange: string[];
/** Distance between the edge of the graph and the area where the bars are drawn, in pixels. */
margin: { top: number; left: number };
/** Number of ticks for the value (y-)axis */
numTicksLeftAxis?: number;
/** Number of ticks for the value axis */
numTicksValueAxis?: number;
/** Distance between the left axis labels and the start of the lines of the graph, in px. */
valueAxisLeftOffset?: number;
axisLeftOffset?: number;
/** Distance between the bottom axis and the bottom of the container of the graph, in px. */
axisBottomOffset?: number;
/** Distance between the right side of the graph and the legend, in px. */
legendLeftOffset?: number;
/** Distance between the top of the graph and the legend, in px. */
@ -57,210 +60,396 @@ export type StackedBarProps = {
scalePadding?: number;
/** Margin for each item in the legend */
itemMargin?: string;
};
const tooltipStyles = {
...defaultStyles,
minWidth: 60,
backgroundColor: Color.primaryAccentLighter,
color: Color.primaryBackground,
/** Factor multiplied with an offset to center the labels of the category-axis depending on the width/height of the graph.
* >1 for width/height <600 and <1 for width/height >600 (vertical=width/horizontal=height) */
categoryAxisLeftFactor?: number;
};
let tooltipTimeout: number;
export function StackedBarGraph({
data,
width,
height,
keys,
colorRange,
margin,
scalePadding = 0.3,
numTicksLeftAxis = 6,
valueAxisLeftOffset = 40,
strokeWidth = 2.5,
strokeDashArray = "10,4",
legendLeftOffset = 40,
legendTopOffset = 40,
itemMargin = "15px 0 0 0",
}: StackedBarProps) {
const yTotals = data.reduce((allTotals, currCategory) => {
const yTotal = keys.reduce((categoryTotal, k) => {
categoryTotal += currCategory[k] as number;
return categoryTotal;
}, 0);
allTotals.push(yTotal);
return allTotals;
}, [] as number[]);
const TICK_LABEL_FONT_WEIGHT = 800;
// accessors
const getCategory = (d: StackedBarData) => d.category;
// scales
const xScale = scaleBand<string>({
domain: data.map(getCategory),
padding: scalePadding,
});
const yScale = scaleLinear<number>({
domain: [0, Math.max(...yTotals)],
nice: true,
});
const colorScale = scaleOrdinal<string, string>({
domain: keys,
range: colorRange,
});
const {
export const StackedBarGraphVertical = withTooltip<
StackedBarProps,
TooltipData
>(
({
data,
width,
height,
keys,
colorRange,
margin,
scalePadding = 0.3,
numTicksValueAxis = 6,
axisLeftOffset = 40,
axisBottomOffset = 40,
strokeWidth = 2.5,
strokeDashArray = "10,4",
legendLeftOffset = 40,
legendTopOffset = 40,
itemMargin = "15px 0 0 0",
categoryAxisLeftFactor = 1,
tooltipOpen,
tooltipLeft,
tooltipTop,
tooltipData,
hideTooltip,
showTooltip,
} = useTooltip<TooltipData>();
}: StackedBarProps & WithTooltipProvidedProps<TooltipData>) => {
const yTotals = data.reduce((allTotals, currCategory) => {
const yTotal = keys.reduce((categoryTotal, k) => {
categoryTotal += currCategory[k] as number;
return categoryTotal;
}, 0);
allTotals.push(yTotal);
return allTotals;
}, [] as number[]);
const { containerRef, TooltipInPortal } = useTooltipInPortal({
// TooltipInPortal is rendered in a separate child of <body /> and positioned
// with page coordinates which should be updated on scroll.
scroll: true,
});
const TICK_LABEL_FONT_WEIGHT = 800;
if (width < 10) return null;
// bounds
const xMax = width;
const yMax = height - margin.top - 50;
// accessors
const getCategory = (d: StackedBarData) => d.category;
xScale.rangeRound([0, xMax - valueAxisLeftOffset]);
yScale.range([yMax, 0]);
// scales
const categoryScale = scaleBand<string>({
domain: data.map(getCategory),
padding: scalePadding,
});
const valueScale = scaleLinear<number>({
domain: [0, Math.max(...yTotals)],
nice: true,
});
const colorScale = scaleOrdinal<string, string>({
domain: keys,
range: colorRange,
});
return width < 10 ? null : (
<div className={styles.container}>
<svg ref={containerRef} width={width} height={height}>
<Group top={margin.top} left={margin.left}>
<GridRows
scale={yScale}
width={xMax}
height={yMax}
left={valueAxisLeftOffset}
numTicks={numTicksLeftAxis}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<GridColumns
scale={xScale}
height={yMax}
left={valueAxisLeftOffset}
offset={xScale.bandwidth() / 2}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<Group left={valueAxisLeftOffset}>
<BarStack<StackedBarData, string>
data={data}
keys={keys}
x={getCategory}
xScale={xScale}
yScale={yScale}
color={colorScale}
>
{(barStacks) =>
barStacks.map((barStack) =>
barStack.bars.map((bar) => (
<rect
className={styles.barStack}
key={`bar-stack-${barStack.index}-${bar.index}`}
x={bar.x}
y={bar.y}
height={bar.height}
width={bar.width / 2}
fill={bar.color}
onMouseLeave={() => {
tooltipTimeout = window.setTimeout(() => {
hideTooltip();
}, 300);
}}
onMouseMove={(event) => {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
// TooltipInPortal expects coordinates to be relative to containerRef
// localPoint returns coordinates relative to the nearest SVG
const eventSvgCoords = localPoint(event);
const left = bar.x + bar.width / 2;
showTooltip({
tooltipData: bar,
tooltipTop: eventSvgCoords?.y,
tooltipLeft: left,
});
}}
/>
))
)
// bounds
const xMax = width;
const yMax = height - margin.top - axisBottomOffset;
categoryScale.rangeRound([0, xMax - axisLeftOffset]);
valueScale.range([yMax, 0]);
return width < 10 ? null : (
<div className={styles.container}>
<svg width={width} height={height}>
<Group top={margin.top} left={margin.left}>
<GridRows
scale={valueScale}
width={xMax}
height={yMax}
left={axisLeftOffset}
numTicks={numTicksValueAxis}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<GridColumns
scale={categoryScale}
height={yMax}
left={axisLeftOffset}
offset={categoryScale.bandwidth() / 2}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<Group left={axisLeftOffset}>
<BarStack<StackedBarData, string>
data={data}
keys={keys}
x={getCategory}
xScale={categoryScale}
yScale={valueScale}
color={colorScale}
>
{(barStacks) =>
barStacks.map((barStack) =>
barStack.bars.map((bar) => (
<rect
className={styles.barStack}
key={`bar-stack-${barStack.index}-${bar.index}`}
x={bar.x}
y={bar.y}
height={bar.height}
width={bar.width / 2}
fill={bar.color}
onMouseLeave={() => {
tooltipTimeout = window.setTimeout(() => {
hideTooltip();
}, 300);
}}
onMouseMove={(event) => {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
const eventSvgCoords = localPoint(event);
const left = bar.x + bar.width / 2;
showTooltip({
tooltipData: bar,
tooltipTop: eventSvgCoords?.y,
tooltipLeft: left,
});
}}
/>
))
)
}
</BarStack>
</Group>
<Line
fill={Color.tertiaryBackground}
to={new Point({ x: axisLeftOffset, y: 0 })}
from={new Point({ x: axisLeftOffset, y: yMax })}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<AxisBottom
top={yMax}
scale={categoryScale}
left={
((categoryScale.bandwidth() * 100) / width) *
categoryAxisLeftFactor
}
</BarStack>
</Group>
<Line
fill={Color.tertiaryBackground}
to={new Point({ x: valueAxisLeftOffset, y: 0 })}
from={new Point({ x: valueAxisLeftOffset, y: yMax })}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<AxisBottom
top={yMax}
scale={xScale}
left={valueAxisLeftOffset / 5}
hideTicks
hideAxisLine
labelProps={{
fontSize: `${10 / 16}rem`,
}}
tickLabelProps={() => ({
fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT,
})}
/>
<AxisLeft
scale={yScale}
numTicks={numTicksLeftAxis}
hideAxisLine
labelProps={{
fontSize: `${10 / 16}rem`,
}}
tickLabelProps={() => {
return {
hideTicks
hideAxisLine
labelProps={{
fontSize: `${10 / 16}rem`,
}}
tickLabelProps={() => ({
fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT,
};
}}
/>
</Group>
</svg>
<div
className={styles.legend}
style={{ left: width + legendLeftOffset, top: legendTopOffset }}
>
<LegendOrdinal
scale={colorScale}
direction="column"
itemMargin={itemMargin}
/>
</div>
{tooltipOpen && tooltipData ? (
<TooltipInPortal
className={styles.toolTip}
top={tooltipTop}
left={tooltipLeft}
style={tooltipStyles}
})}
/>
<AxisLeft
scale={valueScale}
top={5}
numTicks={numTicksValueAxis}
hideAxisLine
labelProps={{
fontSize: `${10 / 16}rem`,
}}
tickLabelProps={() => {
return {
fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT,
};
}}
/>
</Group>
</svg>
<div
className={styles.legend}
style={{ left: width + legendLeftOffset, top: legendTopOffset }}
>
<p className={styles.key}>{tooltipData.key}</p>
<p>{tooltipData.bar.data[tooltipData.key]}</p>
<p>{getCategory(tooltipData.bar.data)}</p>
</TooltipInPortal>
) : null}
</div>
);
}
<LegendOrdinal
scale={colorScale}
direction="column"
itemMargin={itemMargin}
/>
</div>
{tooltipOpen && tooltipData ? (
<Tooltip
className={styles.toolTip}
top={tooltipTop}
left={tooltipLeft}
unstyled
>
<p className={styles.key}>{tooltipData.key}</p>
<p>{tooltipData.bar.data[tooltipData.key]}</p>
<p>{getCategory(tooltipData.bar.data)}</p>
</Tooltip>
) : null}
</div>
);
}
);
export const StackedBarGraphHorizontal = withTooltip<
StackedBarProps,
TooltipData
>(
({
data,
width,
height,
keys,
colorRange,
margin,
scalePadding = 0.3,
numTicksValueAxis = 6,
axisLeftOffset = 40,
axisBottomOffset = 40,
strokeWidth = 2.5,
strokeDashArray = "10,4",
legendLeftOffset = 40,
legendTopOffset = 40,
itemMargin = "15px 0 0 0",
categoryAxisLeftFactor = 1,
tooltipOpen,
tooltipLeft,
tooltipTop,
tooltipData,
hideTooltip,
showTooltip,
}: StackedBarProps & WithTooltipProvidedProps<TooltipData>) => {
const yTotals = data.reduce((allTotals, currCategory) => {
const yTotal = keys.reduce((categoryTotal, k) => {
categoryTotal += currCategory[k] as number;
return categoryTotal;
}, 0);
allTotals.push(yTotal);
return allTotals;
}, [] as number[]);
const TICK_LABEL_FONT_WEIGHT = 800;
// accessors
const getCategory = (d: StackedBarData) => d.category;
// scales
const valueScale = scaleLinear<number>({
domain: [0, Math.max(...yTotals)],
nice: true,
});
const categoryScale = scaleBand<string>({
domain: data.map(getCategory),
padding: scalePadding,
});
const colorScale = scaleOrdinal<string, string>({
domain: keys,
range: colorRange,
});
// bounds
const xMax = width;
const yMax = height - margin.top - axisBottomOffset;
categoryScale.rangeRound([yMax, 0]);
valueScale.range([0, xMax - axisLeftOffset]);
return width < 10 ? null : (
<div className={styles.container}>
<svg width={width} height={height}>
<Group top={margin.top} left={margin.left}>
<GridRows
scale={categoryScale}
width={xMax}
height={yMax}
offset={categoryScale.bandwidth() / 2}
left={axisLeftOffset}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<GridColumns
scale={valueScale}
height={yMax}
numTicks={numTicksValueAxis}
left={axisLeftOffset}
stroke={Color.tertiaryBackground}
strokeWidth={strokeWidth}
strokeDasharray={strokeDashArray}
/>
<Group left={axisLeftOffset}>
<BarStackHorizontal<StackedBarData, string>
data={data}
keys={keys}
y={getCategory}
xScale={valueScale}
yScale={categoryScale}
color={colorScale}
>
{(barStacks) =>
barStacks.map((barStack) =>
barStack.bars.map((bar) => (
<rect
className={styles.barStack}
key={`bar-stack-${barStack.index}-${bar.index}`}
x={bar.x}
y={bar.y}
height={bar.height / 2}
width={bar.width}
fill={bar.color}
onMouseLeave={() => {
tooltipTimeout = window.setTimeout(() => {
hideTooltip();
}, 300);
}}
onMouseMove={(event) => {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
const eventSvgCoords = localPoint(event);
const left = bar.x + bar.width / 2;
showTooltip({
tooltipData: bar,
tooltipTop: eventSvgCoords?.y,
tooltipLeft: left,
});
}}
/>
))
)
}
</BarStackHorizontal>
</Group>
<AxisBottom
top={yMax}
scale={valueScale}
left={axisLeftOffset}
numTicks={numTicksValueAxis}
hideAxisLine
hideTicks
labelProps={{
fontSize: `${10 / 16}rem`,
}}
tickLabelProps={() => ({
fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT,
})}
/>
<AxisLeft
top={
-((categoryScale.bandwidth() * 100) / width) *
categoryAxisLeftFactor
}
scale={categoryScale}
hideAxisLine
hideTicks
labelProps={{
fontSize: `${10 / 16}rem`,
}}
tickLabelProps={() => {
return {
fill: Color.label,
fontWeight: TICK_LABEL_FONT_WEIGHT,
};
}}
/>
</Group>
</svg>
<div
className={styles.legend}
style={{ left: width + legendLeftOffset, top: legendTopOffset }}
>
<LegendOrdinal
scale={colorScale}
direction="column"
itemMargin={itemMargin}
/>
</div>
{tooltipOpen && tooltipData ? (
<Tooltip
className={styles.toolTip}
top={tooltipTop}
left={tooltipLeft}
unstyled
>
<p className={styles.key}>{tooltipData.key}</p>
<p>{tooltipData.bar.data[tooltipData.key]}</p>
<p>{getCategory(tooltipData.bar.data)}</p>
</Tooltip>
) : null}
</div>
);
}
);

View File

@ -197,10 +197,12 @@ const shouldNotRerender = (
nextProps: WordCloudWordsProps
) => {
if (
prevProps.tooltipLeft !== nextProps.tooltipLeft ||
prevProps.tooltipTop !== nextProps.tooltipTop ||
nextProps.tooltipLeft === undefined ||
nextProps.tooltipTop === undefined
// if width changes, rerender, else don't rerender for a tooltip change
prevProps.width === nextProps.width &&
(prevProps.tooltipLeft !== nextProps.tooltipLeft ||
prevProps.tooltipTop !== nextProps.tooltipTop ||
nextProps.tooltipLeft === undefined ||
nextProps.tooltipTop === undefined)
) {
return true; // do not re-render
}

View File

@ -65,36 +65,6 @@ export const moreMockCategoricalData = [
{ key: "Dart", value: 2.21 },
];
export const mockBoxPlotData = [
{
x: "1A",
min: 20,
firstQuartile: 25,
median: 30,
thirdQuartile: 80,
max: 100,
outliers: [],
},
{
x: "1B",
min: 0,
firstQuartile: 20,
median: 30,
thirdQuartile: 50,
max: 100,
outliers: [],
},
{
x: "2A",
min: 25,
firstQuartile: 35,
median: 50,
thirdQuartile: 90,
max: 100,
outliers: [],
},
];
export const mockStackedBarGraphData = [
{
category: "1A",
@ -133,3 +103,45 @@ export const mockStackedBarKeys = [
"geese breeders",
"geese catchers",
];
export const mockBoxPlotData = [
{
category: "1A",
min: 20,
firstQuartile: 25,
median: 30,
thirdQuartile: 80,
max: 100,
outliers: [],
},
{
category: "1B",
min: 0,
firstQuartile: 20,
median: 30,
thirdQuartile: 50,
max: 100,
outliers: [],
},
{
category: "2A",
min: 25,
firstQuartile: 35,
median: 50,
thirdQuartile: 90,
max: 100,
outliers: [],
},
];
export const mockQuoteData = [
"The quick brown fox jumps over the lazy dog.",
"Sphinx of black quartz, judge my vow!",
"Pack my box with five dozen liquor jugs.",
];
export const mockQuoteDataLong = [
"Here, have some quotes of varying lengths, and see how they look.",
"Hello, world!",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla in enim neque. Sed sit amet convallis tellus. Integer condimentum a felis id gravida. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam metus libero, sagittis in consectetur in, scelerisque sed sapien. Nullam ut feugiat sapien. Praesent dictum ac ipsum ac lacinia.",
];

4574
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@ body {
--light--primary-accent-light: var(--orange);
--light--primary-accent-lighter: #FFBC9F;
--light--secondary-accent: #E55F98;
--light--translucent-accent: rgba(255, 231, 231, 0.75);
--light--secondary-accent-light: #FEA0C8;
--light--primary-heading: #D02B53;
--light--primary-text: #483B35;
@ -37,6 +38,7 @@ body {
--dark--primary-accent-lighter: var(--lighter-pink);
--dark--secondary-accent: var(--orange);
--dark--secondary-accent-light: var(--light-orange);
--dark--translucent-accent: rgba(239, 131, 157, 0.75);
--dark--primary-heading: #FFC48D;
--dark--secondary-heading: var(--pink);
--dark--link: var(--pink);
@ -54,6 +56,7 @@ body {
--primary-accent-lighter: var(--dark--primary-accent-lighter);
--secondary-accent: var(--dark--secondary-accent);
--secondary-accent-light: var(--dark--secondary-accent-light);
--translucent-accent: var(--dark--translucent-accent);
--primary-heading: var(--dark--primary-heading);
--secondary-heading: var(--dark--secondary-heading);
--link: var(--dark--link);
@ -73,7 +76,11 @@ h2 {
color: var(--primary-heading);
}
h3,
h3 {
color: var(--secondary-heading);
font-size: calc(45rem / 16);
}
h4,
h5,
h6 {
@ -89,6 +96,10 @@ a:hover {
color: var(--link-hover);
}
p {
font-size: calc(28rem / 16);
}
@media only screen and (prefers-color-scheme: dark) {
body {
--primary-background: var(--dark--primary-background);

View File

@ -1,9 +1,20 @@
import type { AppProps } from "next/app";
import Head from "next/head";
import React from "react";
import "./_app.css";
import "./font.css";
export default function App({ Component, pageProps }: AppProps): JSX.Element {
return <Component {...pageProps} />;
return (
<>
<Head>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0,user-scalable=0"
/>
</Head>
<Component {...pageProps} />
</>
);
}

View File

@ -5,6 +5,8 @@ export default function Home() {
return (
<p>
Click <Link href="/playground">here</Link> to visit the playground
<br />
Click <Link href="/samplePage">here</Link> to visit a sample page
</p>
);
}

View File

@ -5,3 +5,11 @@
.barGraphDemo {
border: calc(1rem / 16) solid black;
}
.quotationCarouselDemo {
display: flex;
flex-direction: column;
align-items: center;
gap: calc(48rem / 16);
margin: calc(32rem / 16);
}

View File

@ -1,14 +1,22 @@
import { BarGraphHorizontal, BarGraphVertical } from "components/BarGraph";
import { BoxPlot } from "components/Boxplot";
import {
mockCategoricalData,
moreMockCategoricalData,
mockStackedBarGraphData,
mockStackedBarKeys,
mockStackedBarGraphData,
mockBoxPlotData,
mockQuoteData,
mockQuoteDataLong,
} from "data/mocks";
import React from "react";
import { Color } from "utils/Color";
import { StackedBarGraph } from "@/components/StackedBarGraph";
import { QuotationCarousel } from "@/components/QuotationCarousel";
import {
StackedBarGraphVertical,
StackedBarGraphHorizontal,
} from "@/components/StackedBarGraph";
import { ColorPalette } from "../components/ColorPalette";
import { WordCloud } from "../components/WordCloud";
@ -69,9 +77,9 @@ export default function Home() {
/>
<h2>
<code>{"<StackedBarGraph />"}</code>
<code>{"<StackedBarGraphVertical />"}</code>
</h2>
<StackedBarGraph
<StackedBarGraphVertical
width={600}
height={400}
keys={mockStackedBarKeys}
@ -86,6 +94,55 @@ export default function Home() {
left: 20,
}}
/>
<h2>
<code>{"<StackedBarGraphHorizontal />"}</code>
</h2>
<p>
<code>{"<StackedBarGraphHorizontal />"}</code> takes the same props as{" "}
<code>{"<StackedBarGraphVertical />"}</code>.
</p>
<StackedBarGraphHorizontal
width={600}
height={400}
keys={mockStackedBarKeys}
colorRange={[
Color.primaryAccent,
Color.secondaryAccentLight,
Color.primaryAccentLighter,
]}
data={mockStackedBarGraphData}
margin={{
top: 20,
left: 20,
}}
/>
<h2>
<code>{"<BoxPlot />"}</code>
</h2>
<BoxPlot
width={600}
height={400}
data={mockBoxPlotData}
margin={{
top: 20,
left: 20,
}}
/>
<h2>
<code>{"<QuotationCarousel />"}</code>
</h2>
<div className={styles.quotationCarouselDemo}>
<QuotationCarousel data={mockQuoteData} circleDiameter={0} />
<QuotationCarousel
data={mockQuoteDataLong}
width={800}
height={160}
circleDiameter={180}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,5 @@
.page {
display: flex;
flex-direction: column;
justify-content: center;
}

127
pages/samplePage.tsx Normal file
View File

@ -0,0 +1,127 @@
import { BarGraphHorizontal, BarGraphVertical } from "components/BarGraph";
import { mockCategoricalData, moreMockCategoricalData } from "data/mocks";
import React from "react";
import { useWindowDimensions } from "utils/getWindowDimensions";
import { useIsMobile } from "utils/isMobile";
import { ComponentWrapper } from "@/components/ComponentWrapper";
import { WordCloud } from "../components/WordCloud";
import styles from "./samplePage.module.css";
export default function SamplePage() {
const { width } = useWindowDimensions();
const isMobile = useIsMobile();
return (
<div className={styles.page}>
<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."
>
<BarGraphVertical
data={mockCategoricalData}
// For components that are in the side wrappers, it looks better if they fill a certain amount of width, so we can make the width dynamic like this
width={isMobile ? width / 1.25 : width / 2}
height={500}
margin={{
top: 20,
bottom: 80,
left: 60,
right: 20,
}}
/>
</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="center"
>
<WordCloud
data={moreMockCategoricalData.map((word) => ({
text: word.key,
value: word.value,
}))}
// For components that we don't want to match the width necessarily we can provide direct values
width={isMobile ? width / 1.5 : 800}
height={500}
/>
</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"
>
<BarGraphHorizontal
className={styles.barGraphDemo}
data={mockCategoricalData}
width={isMobile ? width / 1.45 : width / 2}
height={500}
margin={{
top: 20,
bottom: 40,
left: 150,
right: 20,
}}
/>
</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="left"
noBackground
>
<BarGraphHorizontal
className={styles.barGrapDemo}
data={mockCategoricalData}
width={isMobile ? width / 1.45 : width / 2}
height={500}
margin={{
top: 20,
bottom: 40,
left: 150,
right: 20,
}}
/>
</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."
>
<BarGraphHorizontal
className={styles.barGraphDemo}
data={mockCategoricalData}
width={isMobile ? width / 1.45 : width / 2}
height={500}
margin={{
top: 20,
bottom: 40,
left: 150,
right: 20,
}}
/>
</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="left"
noBackground
>
<WordCloud
data={moreMockCategoricalData.map((word) => ({
text: word.key,
value: word.value,
}))}
width={isMobile ? width / 1.5 : width / 2}
height={500}
/>
</ComponentWrapper>
</div>
);
}

View File

@ -0,0 +1,35 @@
// Attribution: https://stackoverflow.com/questions/36862334/get-viewport-window-height-in-reactjs
import { useState, useEffect } from "react";
type WindowDimensions = {
width: number;
height: number;
};
const getWindowDimensions = (): WindowDimensions => {
const { innerWidth, innerHeight } = window;
return {
width: innerWidth,
height: innerHeight,
};
};
export const useWindowDimensions = (): WindowDimensions => {
const [windowDimensions, setWindowDimensions] = useState<WindowDimensions>({
width: 0,
height: 0,
});
const handleResize = () => {
setWindowDimensions(getWindowDimensions());
};
useEffect(() => {
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return windowDimensions;
};

3
utils/isMobile.ts Normal file
View File

@ -0,0 +1,3 @@
import { useWindowDimensions } from "./getWindowDimensions";
export const useIsMobile = () => useWindowDimensions().width <= 768;