commit
1507d29adf
@ -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); |
||||
} |
@ -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> |
||||
); |
||||
} |
||||
); |
@ -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); |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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; |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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} /> |
||||
</> |
||||
); |
||||
} |
||||
|
@ -0,0 +1,5 @@ |
||||
.page { |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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; |
||||
}; |
@ -0,0 +1,3 @@ |
||||
import { useWindowDimensions } from "./getWindowDimensions"; |
||||
|
||||
export const useIsMobile = () => useWindowDimensions().width <= 768; |
Loading…
Reference in new issue