Merge main
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Jared He 2022-09-07 22:03:22 -04:00
commit 1507d29adf
20 changed files with 1117 additions and 12 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

@ -197,10 +197,12 @@ const shouldNotRerender = (
nextProps: WordCloudWordsProps
) => {
if (
prevProps.tooltipLeft !== nextProps.tooltipLeft ||
// 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
nextProps.tooltipTop === undefined)
) {
return true; // do not re-render
}

View File

@ -79,3 +79,45 @@ export const moreMockCategoricalData = [
{ key: "Ada", value: 2.21 },
{ key: "Dart", value: 2.21 },
];
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.",
];

72
package-lock.json generated
View File

@ -11,8 +11,10 @@
"@visx/event": "^2.6.0",
"@visx/grid": "^2.10.0",
"@visx/group": "^2.10.0",
"@visx/mock-data": "^2.1.2",
"@visx/scale": "^2.2.2",
"@visx/shape": "^2.10.0",
"@visx/stats": "^2.10.0",
"@visx/text": "^2.10.0",
"@visx/tooltip": "^2.10.0",
"@visx/wordcloud": "^2.10.0",
@ -607,6 +609,11 @@
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ=="
},
"node_modules/@types/d3-random": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-2.2.1.tgz",
"integrity": "sha512-5vvxn6//poNeOxt1ZwC7QU//dG9QqABjy1T7fP/xmFHY95GnaOw3yABf29hiu5SR1Oo34XcpyHFbzod+vemQjA=="
},
"node_modules/@types/d3-scale": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.2.tgz",
@ -966,6 +973,15 @@
"react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0"
}
},
"node_modules/@visx/mock-data": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@visx/mock-data/-/mock-data-2.1.2.tgz",
"integrity": "sha512-6xUVP56tiPwVi3BxvoXPQzDYWG6iX2nnOlsHEYsHgK8gHq1r7AhjQtdbQUX7QF0QkmkJM0cW8TBjZ2e+dItB8Q==",
"dependencies": {
"@types/d3-random": "^2.2.0",
"d3-random": "^2.2.2"
}
},
"node_modules/@visx/point": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@visx/point/-/point-2.6.0.tgz",
@ -1006,6 +1022,23 @@
"react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0"
}
},
"node_modules/@visx/stats": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@visx/stats/-/stats-2.10.0.tgz",
"integrity": "sha512-4p8rQamOc1IC3IkqTHgfMHbSXvRl9DMWFCglJy+DmbH6Wx1TaWt2nj/N0Ttp350UTRzBy4o5ou/D4Gts8LZHuA==",
"dependencies": {
"@types/d3-shape": "^1.3.2",
"@types/react": "*",
"@visx/group": "2.10.0",
"@visx/scale": "2.2.2",
"classnames": "^2.3.1",
"d3-shape": "^1.2.0",
"prop-types": "^15.5.10"
},
"peerDependencies": {
"react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0"
}
},
"node_modules/@visx/text": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@visx/text/-/text-2.10.0.tgz",
@ -1391,6 +1424,7 @@
"version": "3.22.7",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.22.7.tgz",
"integrity": "sha512-wTriFxiZI+C8msGeh7fJcbC/a0V8fdInN1oS2eK79DMBGs8iIJiXhtFJCiT3rBa8w6zroHWW3p8ArlujZ/Mz+w==",
"deprecated": "core-js-pure@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js-pure.",
"dev": true,
"hasInstallScript": true,
"funding": {
@ -1534,6 +1568,11 @@
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
},
"node_modules/d3-random": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-2.2.2.tgz",
"integrity": "sha512-0D9P8TRj6qDAtHhRQn6EfdOtHMfsUWanl3yb/84C4DqpZ+VsgfI5iTVRNRbELCfNvRfpMr8OrqqUTQ6ANGCijw=="
},
"node_modules/d3-scale": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz",
@ -4789,6 +4828,11 @@
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ=="
},
"@types/d3-random": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-2.2.1.tgz",
"integrity": "sha512-5vvxn6//poNeOxt1ZwC7QU//dG9QqABjy1T7fP/xmFHY95GnaOw3yABf29hiu5SR1Oo34XcpyHFbzod+vemQjA=="
},
"@types/d3-scale": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.2.tgz",
@ -5042,6 +5086,15 @@
"prop-types": "^15.6.2"
}
},
"@visx/mock-data": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@visx/mock-data/-/mock-data-2.1.2.tgz",
"integrity": "sha512-6xUVP56tiPwVi3BxvoXPQzDYWG6iX2nnOlsHEYsHgK8gHq1r7AhjQtdbQUX7QF0QkmkJM0cW8TBjZ2e+dItB8Q==",
"requires": {
"@types/d3-random": "^2.2.0",
"d3-random": "^2.2.2"
}
},
"@visx/point": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@visx/point/-/point-2.6.0.tgz",
@ -5079,6 +5132,20 @@
"prop-types": "^15.5.10"
}
},
"@visx/stats": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@visx/stats/-/stats-2.10.0.tgz",
"integrity": "sha512-4p8rQamOc1IC3IkqTHgfMHbSXvRl9DMWFCglJy+DmbH6Wx1TaWt2nj/N0Ttp350UTRzBy4o5ou/D4Gts8LZHuA==",
"requires": {
"@types/d3-shape": "^1.3.2",
"@types/react": "*",
"@visx/group": "2.10.0",
"@visx/scale": "2.2.2",
"classnames": "^2.3.1",
"d3-shape": "^1.2.0",
"prop-types": "^15.5.10"
}
},
"@visx/text": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@visx/text/-/text-2.10.0.tgz",
@ -5442,6 +5509,11 @@
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
},
"d3-random": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-2.2.2.tgz",
"integrity": "sha512-0D9P8TRj6qDAtHhRQn6EfdOtHMfsUWanl3yb/84C4DqpZ+VsgfI5iTVRNRbELCfNvRfpMr8OrqqUTQ6ANGCijw=="
},
"d3-scale": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz",

View File

@ -19,11 +19,13 @@
"@visx/event": "^2.6.0",
"@visx/grid": "^2.10.0",
"@visx/group": "^2.10.0",
"@visx/mock-data": "^2.1.2",
"@visx/scale": "^2.2.2",
"@visx/shape": "^2.10.0",
"@visx/stats": "^2.10.0",
"@visx/text": "^2.10.0",
"@visx/tooltip": "^2.10.0",
"@visx/wordcloud": "^2.10.0",
"@visx/text": "^2.10.0",
"next": "12.1.6",
"react": "18.1.0",
"react-dom": "18.1.0"

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,12 +1,16 @@
import { BarGraphHorizontal, BarGraphVertical } from "components/BarGraph";
import { BoxPlot } from "components/Boxplot";
import {
mockPieData,
mockCategoricalData,
moreMockCategoricalData,
mockBoxPlotData,
mockQuoteData,
mockQuoteDataLong,
mockPieData,
} from "data/mocks";
import React from "react";
import { PieChart } from "@/components/PieChart";
import { QuotationCarousel } from "@/components/QuotationCarousel";
import { ColorPalette } from "../components/ColorPalette";
import { WordCloud } from "../components/WordCloud";
@ -34,7 +38,7 @@ export default function Home() {
width={800}
height={500}
margin={{
top: 20,
top: 25,
bottom: 40,
left: 150,
right: 20,
@ -68,6 +72,32 @@ export default function Home() {
value: word.value,
}))}
/>
<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;