Commit e570f4b3 authored by Samuel Mergenthaler's avatar Samuel Mergenthaler
Browse files

add entry for bike sharing dashboard

parent 99cc083c
Pipeline #5868 passed with stages
in 15 seconds
import React, { FunctionComponent } from 'react'
import styled from 'styled-components'
import { InputWithLabel } from '../inputWithLabel/InputWithLabel'
import { distance } from '../../style/sizes'
interface ChartInfoBoxProps {
description: string
bold?: boolean
}
export const ChartInfoBox: FunctionComponent<ChartInfoBoxProps> = props => {
return <StyledChartInfoBox>
<StyledRow>
<InputWithLabel bold={props.bold} label={'Description:'}>
{props.description}
</InputWithLabel>
</StyledRow>
<StyledRow>
<InputWithLabel bold={props.bold} label={'Options:'}>
{props.children}
</InputWithLabel>
</StyledRow>
</StyledChartInfoBox>
}
const StyledChartInfoBox = styled.div`
display: flex;
flex-direction: column;
background: ${({ theme }) => theme.colors.backgroundSecondary};
`
const StyledRow = styled.div`
padding: ${distance.large};
display: flex;
flex-direction: row;
`
\ No newline at end of file
import styled from 'styled-components'
import { fonts } from 'style/fonts'
import { distance, fontSizes } from 'style/sizes'
const Input = styled.input.attrs({type: 'text'})`
font-family: ${fonts.default};
background-color: ${({theme}) => theme.colors.backgroundInput};
color: ${({theme}) => theme.colors.primary};
display: block;
width: 100%;
border-radius: 3px;
border: 0px solid ${({theme}) => theme.colors.secondary};
padding: ${distance.small};
margin-bottom: ${distance.medium};
font-size: ${fontSizes.text};
&:disabled {
opacity: 0.4;
}
/* to prevent white background when using autocomplete */
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus,
&:-webkit-autofill:active {
-webkit-text-fill-color: ${({theme}) => theme.colors.primary};
caret-color: ${({theme}) => theme.colors.primary};
transition: background-color 500000s ease-in-out 0s;
}
`
export default Input
\ No newline at end of file
import React, { FunctionComponent } from "react"
import Label from "../label/Label"
import styled from "styled-components"
import { distance } from "../../style/sizes"
interface InputWithLabelProps {
label: string
bold?: boolean
}
export const InputWithLabel: FunctionComponent<InputWithLabelProps> = props =>
<StyledInputWithLabel>
{props.bold ? <Label><b>{props.label}</b></Label> : <Label>{props.label}</Label>}
{props.children}
</StyledInputWithLabel>
const StyledInputWithLabel = styled.div`
display: flex;
flex-direction: column;
margin-right: ${distance.medium};
`
\ No newline at end of file
import styled from 'styled-components'
import { distance } from 'style/sizes'
const Label = styled.label`
display: block;
margin-bottom: ${distance.small};
`
export default Label
\ No newline at end of file
import styled from 'styled-components'
import { fonts } from 'style/fonts'
import { distance, fontSizes } from 'style/sizes'
const Select = styled.select.attrs({type: 'text'})`
font-family: ${fonts.default};
background-color: ${({theme}) => theme.colors.backgroundInput};
color: ${({theme}) => theme.colors.primary};
display: block;
width: 100%;
border-radius: 3px;
border: 0px solid ${({theme}) => theme.colors.secondary};
padding: ${distance.small};
margin-bottom: ${distance.medium};
font-size: ${fontSizes.text};
&:disabled {
opacity: 0.4;
}
/* to prevent white background when using autocomplete */
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus,
&:-webkit-autofill:active {
-webkit-text-fill-color: ${({theme}) => theme.colors.primary};
caret-color: ${({theme}) => theme.colors.primary};
transition: background-color 500000s ease-in-out 0s;
}
`
export default Select
\ No newline at end of file
declare module '*.jpg'
declare module '*.png'
\ No newline at end of file
// import original module declarations
import 'styled-components'
// extend the module declarations using custom theme type (declaration merging)
declare module 'styled-components' {
export interface DefaultTheme {
colors: {
primary: string
secondary: string
backgroundPrimary: string
backgroundSecondary: string
backgroundInput: string
barChart: string
}
}
}
\ No newline at end of file
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import reportWebVitals from './reportWebVitals'
ReactDOM.render(
<React.StrictMode>
<App/>
</React.StrictMode>,
document.getElementById('root')
)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()
import React, { ChangeEvent, useContext, useState } from 'react'
import DatePicker from 'react-datepicker'
import { PageMarginLeftRight } from '../../style/PageMarginLeftRight'
import styled, { ThemeContext } from 'styled-components'
import { distance } from '../../style/sizes'
import 'react-datepicker/dist/react-datepicker.css'
import { InputWithLabel } from 'components/inputWithLabel/InputWithLabel'
import Select from 'components/select/Select'
import { ChartInfoBox } from 'components/chartInfoBox/ChartInfoBox'
import Button from 'components/button/Button'
import Input from 'components/input/Input'
import { useHistory, useParams } from 'react-router'
import { useEffect } from 'react'
import { Bar, BarChart, CartesianGrid, Label, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
import { chartColors } from 'style/chartColors'
import { ClipLoader } from 'react-spinners'
interface IBikePointDetails {
id: string,
commonName: string,
diagrammData: IBikePointActivityMap,
installDate: number,
nbDocks: number
}
type IBikePointActivityMap = {[hourOfDay: number]: IBikePointActivityAtHourOfDay}
interface IBikePointActivityAtHourOfDay {
avgNbRentals: number,
avgNbReturns: number,
avgNbTotal: number
}
/**
* This page contains details about a specific bike point.
*/
export const BikePointDetails = () => {
const history = useHistory()
const { bikePointId } = useParams<{bikePointId: string}>()
const [startTimeStamp, setStartTimestamp] = useState(Date.UTC(2015, 0, 4))
const [endTimeStamp, setEndTimeStamp] = useState(Date.UTC(2015, 0, 19))
const [dayOfWeek, setDayOfWeek] = useState(0)
const [averageActivity, setAverageActivity] = useState('avgNbRentals')
const [maxValueYAxis, setValueYAxis] = useState(0)
// to be adjusted
const [bikePointDetails, setBikePointDetails] = useState<IBikePointDetails | undefined>(undefined)
const [loadingState, setLoadingState] = useState(false)
useEffect(() => {fetchData()}, [])
const fetchData = async () => {
setLoadingState(true)
const response = await fetch(`http://localhost:8081/api/bike-point-details/${bikePointId}?&from=${startTimeStamp / 1000}&to=${endTimeStamp / 1000}&day=${dayOfWeek}`)
const jsonResponse = await response.json()
//to be adjusted
setBikePointDetails(jsonResponse.bikePointDetails)
setLoadingState(false)
}
return (
<PageMarginLeftRight>
<StyledRow>
<StyledButton onClick={() => history.push('/')}>Back home</StyledButton>
<StyledButton onClick={() => history.push('/map')}>Back to map</StyledButton>
</StyledRow>
<StyledRow>
<h2>{bikePointDetails?.commonName ?? '...'}</h2>
</StyledRow>
<StyledTextRow><StyledMinWidth>ID:</StyledMinWidth>{bikePointDetails?.id ?? '...'}</StyledTextRow>
<StyledTextRow><StyledMinWidth>Install Date:</StyledMinWidth>{(bikePointDetails && new Date(bikePointDetails.installDate*1000).toDateString()) ?? '...'}</StyledTextRow>
<StyledTextRow><StyledMinWidth>Number of Docks:</StyledMinWidth>{bikePointDetails?.nbDocks ?? '...'}</StyledTextRow>
<StyledRow>
<div>
<ChartInfoBox bold={true} description={'This bar chart shows the average count of chosen activity sorted by hour of day (0-23) for the chosen day of week in the selected time range.'}>
<StyledRow>
<InputWithLabel label={'Day of week'}>
<Select onChange={(event: ChangeEvent<HTMLSelectElement>) => setDayOfWeek(Number(event.target.value))}>
<option value='0'>Sunday</option>
<option value='1'>Monday</option>
<option value='2'>Tuesday</option>
<option value='3'>Wednesday</option>
<option value='4'>Thursday</option>
<option value='5'>Friday</option>
<option value='6'>Saturday</option>
</Select>
</InputWithLabel>
<InputWithLabel label={'Average Activity'}>
<Select onChange={(event: ChangeEvent<HTMLSelectElement>) => setAverageActivity(event.target.value)}>
<option value='avgNbRentals'>Rentals</option>
<option value='avgNbReturns'>Returns</option>
<option value='avgNbTotal'>Total</option>
</Select>
</InputWithLabel>
</StyledRow>
<StyledRow>
<InputWithLabel label={'From'}>
<DatePicker
selected={new Date(startTimeStamp)}
onChange={(date: Date) => setStartTimestamp(date.getTime())}
dateFormat='yyyy/MM/dd'
timeIntervals={5}
customInput={<Input />}
/>
</InputWithLabel>
<InputWithLabel label={'To'}>
<DatePicker
selected={new Date(endTimeStamp)}
onChange={(date: Date) => setEndTimeStamp(date.getTime())}
dateFormat='yyyy/MM/dd'
timeIntervals={5}
customInput={<Input />}
/>
</InputWithLabel>
</StyledRow>
<StyledRow>
<Button onClick={fetchData}>Apply Options</Button>&nbsp;&nbsp;&nbsp;{loadingState && <ClipLoader color={'white'} loading={loadingState}/>}
</StyledRow>
</ChartInfoBox>
</div>
<ResponsiveContainer width='100%' height={600}>
<BarChart
data={Object.entries(bikePointDetails?.diagrammData ?? {}).map(entry => {
let value
switch(averageActivity){
case 'avgNbRentals':
value = entry[1].avgNbRentals
if (value > maxValueYAxis) {setValueYAxis(value)}
break
case 'avgNbReturns':
value = entry[1].avgNbReturns
if (value > maxValueYAxis) {setValueYAxis(value)}
break
case 'avgNbTotal':
value = entry[1].avgNbTotal
if (value > maxValueYAxis) {setValueYAxis(value)}
break
}
return { hourOfDay: entry[0], value: value?.toFixed(2) }
})}
margin={{top: 5, right: 0, left: 0, bottom: 20,}}
barCategoryGap={15}
>
<CartesianGrid strokeDasharray='3 3'/>
<XAxis dataKey='hourOfDay' textAnchor='start' height={50}>
<Label value='Hour of Day' offset={5} position='bottom' style={{fill: 'rgba(255, 255, 255, 1)'}}/>
</XAxis>
<YAxis allowDecimals={false} type='number' domain={[0, Math.round(maxValueYAxis+1)]}/>
<Tooltip contentStyle={{ backgroundColor: useContext(ThemeContext).colors.backgroundPrimary }}/>
<Legend layout='vertical' verticalAlign='top' align='center'/>
<Bar dataKey='value' fill={chartColors.cyan['50']} legendType='rect' name='Average Count' barSize={20} />
</BarChart>
</ResponsiveContainer>
</StyledRow>
<StyledRow>
</StyledRow>
</PageMarginLeftRight>
)
}
const StyledRow = styled.div`
padding: ${distance.large};
display: flex;
flex-direction: row;
`
const StyledMinWidth = styled.div`
min-width: 10rem;
`
const StyledTextRow = styled.div`
padding-left: ${distance.large};
display: flex;
flex-direction: row;
`
const StyledButton = styled(Button)`
margin-right: ${distance.large};
`
import React, { FC, FunctionComponent } from 'react'
import styled from 'styled-components'
import { PageMarginLeftRight } from '../../style/PageMarginLeftRight'
import { distance } from '../../style/sizes'
import { Link } from 'react-router-dom'
export const ErrorPage: FunctionComponent = () => {
return <ErrorPageTemplate message={'Sorry, the requested page was not found or could not be accessed :|'}/>
}
const ErrorPageTemplate: FC<{ message: string }> = ({message}) => {
return (
<PageMarginLeftRight>
<StyledRow>
<h2>{message}</h2>
</StyledRow>
<StyledRow>
<Link to="/">Return to Landing Page</Link>
</StyledRow>
</PageMarginLeftRight>
)
}
const StyledRow = styled.div`
padding: ${distance.large};
`
\ No newline at end of file
import React, { ChangeEvent, useContext, useEffect, useState } from 'react'
import { useHistory } from 'react-router-dom'
import DatePicker from 'react-datepicker'
import { PageMarginLeftRight } from '../../style/PageMarginLeftRight'
import styled, { ThemeContext } from 'styled-components'
import { distance } from '../../style/sizes'
import { Bar, BarChart, CartesianGrid, Label, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
import { chartColors } from '../../style/chartColors'
import 'react-datepicker/dist/react-datepicker.css'
import Input from '../../components/input/Input'
import { InputWithLabel } from '../../components/inputWithLabel/InputWithLabel'
import { ChartInfoBox } from '../../components/chartInfoBox/ChartInfoBox'
import Button from '../../components/button/Button'
import Select from 'components/select/Select'
import { ClipLoader } from 'react-spinners'
import logo from './../../pictures/SantanderCyclesLogo.png';
interface IBikeTripDurationCount {
classLabel: string,
count: number
}
/**
* Landing page of our dashboard.
* Contains non-map visualizations.
* Also contains link to access bike sharing map of London.
*/
export const Home = () => {
const history = useHistory()
const [startTimeStamp, setStartTimestamp] = useState(Date.UTC(2015, 0, 4, 0, 5))
const [endTimeStamp, setEndTimeStamp] = useState(Date.UTC(2015, 0, 19, 0, 10))
const [classSize, setClassSize] = useState(300)
const [tripDurationData, setTripDurationData] = useState<IBikeTripDurationCount[] | undefined>(undefined)
const [loadingState, setLoadingState] = useState(false)
// fetch data from our server once at beginning, later only on button click
useEffect(() => {fetchData()}, [])
const fetchData = async () => {
setLoadingState(true)
const response = await fetch(`http://localhost:8081/api/bike-trip-durations?classSize=${classSize}&from=${startTimeStamp / 1000}&to=${endTimeStamp / 1000}`)
const jsonResponse = await response.json()
setTripDurationData(jsonResponse.bikeTripDurationCounts)
setLoadingState(false)
}
return (
<PageMarginLeftRight>
<StyledTopRow>
<div><Button onClick={() => history.push('/map')}>To map</Button></div>
<img src={logo} alt='Logo' height='75' />
</StyledTopRow>
<StyledRow>
<h2>Bike-sharing in London</h2>
</StyledRow>
<StyledRow>
<div>
<ChartInfoBox bold={true} description={'This bar chart shows the count of trips with similar durations within the selected timerange. The bars represent the number of trips.'}>
<StyledRow>
<InputWithLabel label={'Class Size'}>
<Select defaultValue='300' onChange={(event: ChangeEvent<HTMLSelectElement>) => setClassSize(Number(event.target.value))}>
<option value='60'>1 minute</option>
<option value='300'>5 minutes</option>
<option value='600'>10 minutes</option>
<option value='1200'>20 minutes</option>
<option value='1800'>30 minutes</option>
</Select>
</InputWithLabel>
</StyledRow>
<StyledRow>
<InputWithLabel label={'From'}>
<DatePicker
selected={new Date(startTimeStamp)}
onChange={(date: Date) => setStartTimestamp(date.getTime())}
dateFormat='yyyy/MM/dd, HH:mm'
showTimeSelect
timeFormat='HH:mm'
timeIntervals={5}
customInput={<Input />}
/>
</InputWithLabel>
<InputWithLabel label={'To'}>
<DatePicker
selected={new Date(endTimeStamp)}
onChange={(date: Date) => setEndTimeStamp(date.getTime())}
dateFormat='yyyy/MM/dd, HH:mm'
showTimeSelect
timeFormat='HH:mm'
timeIntervals={5}
customInput={<Input />}
/>
</InputWithLabel>
</StyledRow>
<StyledRow>
<Button onClick={fetchData}>Apply Options</Button>&nbsp;&nbsp;&nbsp;{loadingState && <ClipLoader color={'white'} loading={loadingState}/>}
</StyledRow>
</ChartInfoBox>
</div>
<ResponsiveContainer width='100%' height={600}>
<BarChart
data={tripDurationData}
margin={{top: 5, right: 0, left: 30, bottom: 50,}}
barCategoryGap={15}
>
<CartesianGrid strokeDasharray='3 3'/>
<XAxis dataKey='classLabel' angle={45} textAnchor='start' height={50}>
<Label value='duration of trip in minutes' offset={10} position='bottom' style={{fill: 'rgba(255, 255, 255, 1)'}} />
</XAxis>
<YAxis allowDecimals={false} />
<Tooltip contentStyle={{ backgroundColor: useContext(ThemeContext).colors.backgroundPrimary }}/>
<Legend layout='vertical' verticalAlign='top' align='center'/>
<Bar dataKey='count' fill={chartColors.cyan['50']} legendType='rect' name='count of trips' />
</BarChart>
</ResponsiveContainer>
</StyledRow>
</PageMarginLeftRight>
)
}
const StyledRow = styled.div`
padding: ${distance.large};
display: flex;
flex-direction: row;
`
const StyledTopRow = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
padding: ${distance.large};
`
\ No newline at end of file
import { ReportHandler } from 'web-vitals'
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({getCLS, getFID, getFCP, getLCP, getTTFB}) => {
getCLS(onPerfEntry)
getFID(onPerfEntry)
getFCP(onPerfEntry)
getLCP(onPerfEntry)
getTTFB(onPerfEntry)
})
}
}
export default reportWebVitals
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom'
import { createGlobalStyle } from 'styled-components'
import { fonts } from './fonts'
import { fontSizes } from './sizes'
export const GlobalStyle = createGlobalStyle`
.leaflet-container {
width: 100%;
height: 100vh;
}
*,
*::after,
*::before {
box-sizing: border-box;
}
body {
background: ${({ theme }) => theme.colors.backgroundPrimary};
color: ${({ theme }) => theme.colors.primary};
padding: 0;
margin: 0;
max-width: 100%;
/* to make 'overflow-x: hidden;' work on mobile as well */
position: relative;
/* to make light text on dark backgrounds look lighter */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: ${fonts.default};
font-size: ${fontSizes.text};
}
html, body, #root, #root>div {
overflow-x: hidden;
}
h1, h2, h3, h4 {
color: ${({theme}) => theme.colors.primary};
margin-block-start: 0;
margin-block-end: 0;
}
h1, h2 {
color: ${({theme}) => theme.colors.secondary};
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
a {
color: ${({ theme }) => theme.colors.secondary};
text-decoration: none;
}
`
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment