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 React, { ChangeEvent, useEffect, useState } from 'react'
import { CircleMarker, LayerGroup, LayersControl, MapContainer, Popup, TileLayer, ZoomControl } from 'react-leaflet'
import { useHistory } from 'react-router-dom'
import chroma from 'chroma-js'
import { InputWithLabel } from '../../components/inputWithLabel/InputWithLabel'
import DatePicker from 'react-datepicker'
import Input from '../../components/input/Input'
import styled from 'styled-components'
import { distance } from '../../style/sizes'
import Select from '../../components/select/Select'
import Button from '../../components/button/Button'
import { ClipLoader } from 'react-spinners'
import { BikePointProperty } from '../../../../backend/src/entities/BikePoint'
interface IBikePoint {
id: string
commonName: string
lat: number
lon: number
additionalProperties: BikePointProperty[]
}
interface IBikePointActivity {
rentals: number
returns: number
rentalsReturnsImbalance: number
}
interface IBikePointsActivity {
[bikePointId: string]: IBikePointActivity
}
interface INumberRange {
min: number
max: number
}
interface IBikePointsActivityResponse {
bikePointsActivity: IBikePointsActivity
rentalsRange: INumberRange
returnsRange: INumberRange
rentalsReturnsImbalanceRange: INumberRange
}
interface IColorRange {
min: string
max: string
}
type ActivityType = 'rentals' | 'returns' | 'rentalsReturnsImbalance'
/**
* This page contains a map of London. The user can select from different interactive visualizations.
*/
export const BikeSharingMap = () => {
const history = useHistory()
const [startTimeStamp, setStartTimestamp] = useState(Date.UTC(2015, 0, 4, 0, 0))
const [endTimeStamp, setEndTimeStamp] = useState(Date.UTC(2015, 0, 19, 0, 0))
const [activityType, setActivityType] = useState<ActivityType>('rentals')
const [colorRange, setColorRange] = useState<IColorRange>({min: '#fcf2ae', max: '#a90e00'})
const [bikePoints, setBikePoints] = useState<IBikePoint[] | undefined>(undefined)
const [data, setData] = useState<IBikePointsActivityResponse | undefined>(undefined)
const [loadingState, setLoadingState] = useState(false)
// first, fetch bike-points with their coordinates to show on map fast
useEffect(() => {
(async () => {
const response = await fetch(`http://localhost:8081/api/bike-points/all`)
const jsonResponse = await response.json()
setBikePoints(jsonResponse.bikePoints)
})()
}, [])
const fetchData = async () => {
setLoadingState(true)
const response = await fetch(`http://localhost:8081/api/bike-points-activity?from=${startTimeStamp / 1000}&to=${endTimeStamp / 1000}`)
const jsonResponse = await response.json()
setData(jsonResponse)
setLoadingState(false)
}
// fetch data for bike stations from our server once at beginning, later only on button click
useEffect(() => {fetchData()}, [])
return (
<>
<StyledControl>
<div>
<StyledRow>
<Button onClick={() => history.push("/")}>Back home</Button>
</StyledRow>
<StyledRow>
<h2>Map view</h2>
</StyledRow>
<StyledRow>
Visualize the number of rented or returned bikes at bike points in the chosen timeframe. Or the imbalance between the two.
<br/><br/>
An intense color indicates a high number, relative to the other ones. The color range always adjusts to the current max and min value.
<br/><br/>
Black dots represent bike points, for which no data is available in the selected timeframe.
<br/><br/>
</StyledRow>
<StyledRow>
<Select onChange={(event: ChangeEvent<HTMLSelectElement>) => setActivityType(event.target.value as ActivityType)}>
<option value='rentals'>Number of rentals</option>
<option value='returns'>Number of returns</option>
<option value='rentalsReturnsImbalance'>Returns/Rentals imbalance</option>
</Select>
</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>
</div>
<div>
<StyledRow>
<StyledLegendText>
<div>{data ? getValuesRange(activityType, data).min : '-'}</div>
<div>{data ? getValuesRange(activityType, data).max : '-'}</div>
</StyledLegendText>
</StyledRow>
<StyledRow>
<StyledGradient {...colorRange}/>
</StyledRow>
</div>
</StyledControl>
<MapContainer center={[51.5, -0.17]} zoom={12} zoomControl={false} style={{zIndex: 1}}>
<LayersControl position='bottomright'>
<LayersControl.Overlay checked name='Bike points'>
<LayerGroup>
{
bikePoints?.map(bikePoint => {
const activities = data?.bikePointsActivity[bikePoint.id]
const numberToVisualize = activities && activities[activityType]
const color = numberToVisualize && data ? getColor(numberToVisualize, activityType, data, colorRange) : 'black'
return (
<CircleMarker center={[bikePoint.lat, bikePoint.lon]} pathOptions={{color: color, fillColor: color, fillOpacity: 1}} radius={6}>
<Popup>
<h3><Black>{bikePoint.commonName}</Black></h3>
<br/><b>Number of docks:</b> {Number(bikePoint.additionalProperties.find(additionalProperty => additionalProperty.key==='NbDocks')?.value ?? 'unknown')}
<br/>
<br/><b>Rentals:</b> {activities?.rentals ?? '-'}
<br/><b>Returns:</b> {activities?.returns ?? '-'}
<br/><b>Returns/Rentals imbalance:</b> {activities?.rentalsReturnsImbalance ?? '-'}
<br/><br/>
<Button onClick={() => history.push(`/bike-point-details/${bikePoint.id}`)}>To bike point details</Button>
</Popup>
</CircleMarker>
)
})
}
</LayerGroup>
</LayersControl.Overlay>
<LayersControl.BaseLayer checked name='Stadia.AlidadeSmoothDark'>
<TileLayer
url='https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png'
attribution='&copy; <a href="https://stadiamaps.com/">Stadia Maps</a>, &copy; <a href="https://openmaptiles.org/">OpenMapTiles</a> &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name='Stadia.AlidadeSmooth'>
<TileLayer
url='https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png'
attribution='&copy; <a href="https://stadiamaps.com/">Stadia Maps</a>, &copy; <a href="https://openmaptiles.org/">OpenMapTiles</a> &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="OpenStreetMap.BlackAndWhite">
<TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png"
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="OpenStreetMap.Mapnik">
<TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
</LayersControl.BaseLayer>
</LayersControl>
<ZoomControl position={'topright'}/>
</MapContainer>
</>
)
}
// returns interpolated color according to provided value, using current selected range
const getColor = (value: number, activityType: ActivityType, data: IBikePointsActivityResponse, colorRange: IColorRange) => {
const valuesRange = getValuesRange(activityType, data)
return chroma.scale([colorRange.min, colorRange.max]).domain([valuesRange.min, valuesRange.max])(value).hex()
}
// returns value range for selected data
const getValuesRange = (activityType: ActivityType, data: IBikePointsActivityResponse) => {
let rangeValues: INumberRange
switch (activityType) {
case 'rentals':
rangeValues = data.rentalsRange
break
case 'returns':
rangeValues = data.returnsRange
break
case 'rentalsReturnsImbalance':
rangeValues = data.rentalsReturnsImbalanceRange
break
}
return rangeValues
}
const StyledGradient = styled.div<IColorRange>`
height: 2rem;
width: 100%;
background: linear-gradient(to right, ${props => props.min} 0%, ${props => props.max} 100%);
`
const StyledLegendText = styled.div`
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
`
const StyledControl = styled.div`
padding: ${distance.large};
position: absolute;
height: 100vh;
max-width: 30vw;
min-width: 24vw;
z-index: 2;
background: ${({theme}) => theme.colors.backgroundSecondary};
display: flex;
flex-direction: column;
justify-content: space-between;
`
const Black = styled.div`
color: black;
`
const StyledRow = styled.div`
padding: ${distance.verySmall};
display: flex;
flex-direction: row;
`
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;
}
`
import styled from 'styled-components'
import { maxScreenWidthMobile, pageMarginLeftRight, pageMarginLeftRightMobile } from './sizes'
export const PageMarginLeftRight = styled.div`
@media screen and (max-width: ${maxScreenWidthMobile}px) {
margin-left: ${pageMarginLeftRightMobile};
margin-right: ${pageMarginLeftRightMobile};
}
@media screen and (min-width: ${maxScreenWidthMobile}px) {
margin-left: ${pageMarginLeftRight};
margin-right: ${pageMarginLeftRight};
}
`
\ No newline at end of file
/** Color palettes to use in charts */
export const chartColors = {
red: {
'50': '#ffebee',
'100': '#ffcdd2',
'200': '#ef9a9a',
'300': '#e57373',
'400': '#ef5350',
'500': '#f44336',
'600': '#e53935',
'700': '#d32f2f',
'800': '#c62828',
'900': '#b71c1c',
'a100': '#ff8a80',
'a200': '#ff5252',
'a400': '#ff1744',
'a700': '#d50000'
},
pink: {
'50': '#fce4ec',
'100': '#f8bbd0',
'200': '#f48fb1',
'300': '#f06292',
'400': '#ec407a',
'500': '#e91e63',
'600': '#d81b60',
'700': '#c2185b',
'800': '#ad1457',
'900': '#880e4f',
'a100': '#ff80ab',
'a200': '#ff4081',
'a400': '#f50057',
'a700': '#c51162'
},
purple: {
'50': '#f3e5f5',
'100': '#e1bee7',
'200': '#ce93d8',
'300': '#ba68c8',
'400': '#ab47bc',
'500': '#9c27b0',
'600': '#8e24aa',
'700': '#7b1fa2',
'800': '#6a1b9a',
'900': '#4a148c',
'a100': '#ea80fc',
'a200': '#e040fb',
'a400': '#d500f9',
'a700': '#aa00ff'
},
deeppurple: {
'50': '#ede7f6',
'100': '#d1c4e9',
'200': '#b39ddb',
'300': '#9575cd',
'400': '#7e57c2',
'500': '#673ab7',
'600': '#5e35b1',
'700': '#512da8',
'800': '#4527a0',
'900': '#311b92',
'a100': '#b388ff',
'a200': '#7c4dff',
'a400': '#651fff',
'a700': '#6200ea'
},
indigo: {
'50': '#e8eaf6',
'100': '#c5cae9',
'200': '#9fa8da',
'300': '#7986cb',
'400': '#5c6bc0',
'500': '#3f51b5',
'600': '#3949ab',
'700': '#303f9f',
'800': '#283593',
'900': '#1a237e',
'a100': '#8c9eff',
'a200': '#536dfe',
'a400': '#3d5afe',
'a700': '#304ffe'
},
blue: {
'50': '#e3f2fd',
'100': '#bbdefb',
'200': '#90caf9',
'300': '#64b5f6',
'400': '#42a5f5',
'500': '#2196f3',
'600': '#1e88e5',
'700': '#1976d2',
'800': '#1565c0',
'900': '#0d47a1',
'a100': '#82b1ff',
'a200': '#448aff',
'a400': '#2979ff',
'a700': '#2962ff'
},
lightblue: {
'50': '#e1f5fe',
'100': '#b3e5fc',
'200': '#81d4fa',
'300': '#4fc3f7',
'400': '#29b6f6',
'500': '#03a9f4',
'600': '#039be5',
'700': '#0288d1',
'800': '#0277bd',
'900': '#01579b',
'a100': '#80d8ff',
'a200': '#40c4ff',
'a400': '#00b0ff',
'a700': '#0091ea'
},
cyan: {
'50': '#e0f7fa',
'100': '#b2ebf2',
'200': '#80deea',
'300': '#4dd0e1',
'400': '#26c6da',
'500': '#00bcd4',
'600': '#00acc1',
'700': '#0097a7',
'800': '#00838f',
'900': '#006064',
'a100': '#84ffff',
'a200': '#18ffff',
'a400': '#00e5ff',
'a700': '#00b8d4'
},
teal: {
'50': '#e0f2f1',
'100': '#b2dfdb',
'200': '#80cbc4',
'300': '#4db6ac',
'400': '#26a69a',
'500': '#009688',
'600': '#00897b',
'700': '#00796b',
'800': '#00695c',
'900': '#004d40',
'a100': '#a7ffeb',
'a200': '#64ffda',
'a400': '#1de9b6',
'a700': '#00bfa5'
},
green: {
'50': '#e8f5e9',
'100': '#c8e6c9',
'200': '#a5d6a7',
'300': '#81c784',
'400': '#66bb6a',
'500': '#4caf50',
'600': '#43a047',
'700': '#388e3c',
'800': '#2e7d32',
'900': '#1b5e20',
'a100': '#b9f6ca',
'a200': '#69f0ae',
'a400': '#00e676',
'a700': '#00c853'
},
lightgreen: {
'50': '#f1f8e9',
'100': '#dcedc8',
'200': '#c5e1a5',
'300': '#aed581',
'400': '#9ccc65',
'500': '#8bc34a',
'600': '#7cb342',
'700': '#689f38',
'800': '#558b2f',
'900': '#33691e',
'a100': '#ccff90',
'a200': '#b2ff59',
'a400': '#76ff03',
'a700': '#64dd17'
},
lime: {
'50': '#f9fbe7',
'100': '#f0f4c3',
'200': '#e6ee9c',
'300': '#dce775',
'400': '#d4e157',
'500': '#cddc39',
'600': '#c0ca33',
'700': '#afb42b',
'800': '#9e9d24',
'900': '#827717',
'a100': '#f4ff81',
'a200': '#eeff41',
'a400': '#c6ff00',
'a700': '#aeea00'
},
yellow: {
'50': '#fffde7',
'100': '#fff9c4',
'200': '#fff59d',
'300': '#fff176',
'400': '#ffee58',
'500': '#ffeb3b',
'600': '#fdd835',
'700': '#fbc02d',
'800': '#f9a825',
'900': '#f57f17',
'a100': '#ffff8d',
'a200': '#ffff00',
'a400': '#ffea00',
'a700': '#ffd600'
},
amber: {
'50': '#fff8e1',
'100': '#ffecb3',
'200': '#ffe082',
'300': '#ffd54f',
'400': '#ffca28',
'500': '#ffc107',
'600': '#ffb300',
'700': '#ffa000',
'800': '#ff8f00',
'900': '#ff6f00',
'a100': '#ffe57f',
'a200': '#ffd740',
'a400': '#ffc400',
'a700': '#ffab00'
},
orange: {
'50': '#fff3e0',
'100': '#ffe0b2',
'200': '#ffcc80',
'300': '#ffb74d',
'400': '#ffa726',
'500': '#ff9800',
'600': '#fb8c00',
'700': '#f57c00',
'800': '#ef6c00',
'900': '#e65100',
'a100': '#ffd180',
'a200': '#ffab40',
'a400': '#ff9100',
'a700': '#ff6d00'
},
deeporange: {
'50': '#fbe9e7',
'100': '#ffccbc',
'200': '#ffab91',
'300': '#ff8a65',
'400': '#ff7043',
'500': '#ff5722',
'600': '#f4511e',
'700': '#e64a19',
'800': '#d84315',
'900': '#bf360c',
'a100': '#ff9e80',
'a200': '#ff6e40',
'a400': '#ff3d00',
'a700': '#dd2c00'
},
brown: {
'50': '#efebe9',
'100': '#d7ccc8',
'200': '#bcaaa4',
'300': '#a1887f',
'400': '#8d6e63',
'500': '#795548',
'600': '#6d4c41',
'700': '#5d4037',
'800': '#4e342e',
'900': '#3e2723'
},
grey: {
'50': '#fafafa',
'100': '#f5f5f5',
'200': '#eeeeee',
'300': '#e0e0e0',
'400': '#bdbdbd',
'500': '#9e9e9e',
'600': '#757575',
'700': '#616161',
'800': '#424242',
'900': '#212121'
},
bluegrey: {
'50': '#eceff1',
'100': '#cfd8dc',
'200': '#b0bec5',
'300': '#90a4ae',
'400': '#78909c',
'500': '#607d8b',
'600': '#546e7a',
'700': '#455a64',
'800': '#37474f',
'900': '#263238'
}
}
\ No newline at end of file
export const fonts = {
default: 'Poppins, sans-serif',
}
\ No newline at end of file
Supports Markdown
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