...
 
Commits (9)
//const path = require('path');
const { editWebpackPlugin, appendWebpackPlugin } = require('@rescripts/utilities')
const CopyWebpackPlugin = require('copy-webpack-plugin');
//const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
......@@ -10,15 +11,19 @@ module.exports = config => {
)
config.resolve.plugins.splice(scopePluginIndex, 1)
config = appendWebpackPlugin(
new CopyWebpackPlugin([{
from: '../public'
}]),
config,
new CopyWebpackPlugin({
patterns: [
{
from: '../public',
to: '../build'
}
]
}),
config
)
// config = appendWebpackPlugin(
// new BundleAnalyzerPlugin(),
// config,
......@@ -38,7 +43,7 @@ module.exports = config => {
config,
)
// set js filename
// set js filename
config.output.filename = 'bundle.js'
// disable sourcemap
......@@ -54,7 +59,6 @@ module.exports = config => {
}
return config
}
import React from 'react'
export const appSettingsDefault = {
language: "nl",
header: false,
panelBreakpoint: 600,
yAxisWidth: 30,
defaultGranularity: "hourly"
defaultGranularity: "hourly",
mapRoute: "/"
}
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -86,13 +86,13 @@ class StationLayer extends CompositeLayer {
let color = getColorArray("#2fb5bb")
color[3] = color[3] * 0.5
return color
} else if(d.mean === null || d.mean > range[1]*cuckooThreshold) { // no value or above cuckoo range
} else if(d.mean === null || d.mean > range[1]*cuckooThreshold || d.dataAge > 6) { // no value or above cuckoo range or too old
let color = getColorArray(mapItemSettings.colorOffline)
color[3] = color[3] * 0.5
return color
} else {
let color = getColorArray(getColor(Math.min(d.mean, range[1]), legend, (scale === 'log')))
color[3] = color[3] * (1 - 0.15 * d.dataAge)
color[3] = color[3] * (1 - 0.1 * d.dataAge)
return color
}
},
......
......@@ -17,13 +17,13 @@ const SensorTooltip = props => {
<h4>{d.name}</h4>
{texts.lastMean !== label && <em>{label}</em>}
<table className="data"><tbody>
{ d.mean !== null &&
{ d.status === null &&
<tr>
<td>{`${texts.lastMean}${dataAgeText}:`}</td>
<td><strong>{ roundBy(d.mean, 1) }{ unit }</strong></td>
</tr>
}
{ d.mean === null && d.status !== null &&
{ d.status !== null &&
<tr>
<td>{ texts.status[d.status] }</td>
</tr>
......
......@@ -6,7 +6,7 @@ class MapAttributions extends React.Component {
<div className="mapboxgl-ctrl-bottom-right">
<div className="mapboxgl-ctrl mapboxgl-ctrl-attrib">
<div className="mapboxgl-ctrl-attrib-inner">
<span><a href="https://gitlab.waag.org/code/data-on-a-map-app/" target="_blank" rel="noopener noreferrer">v. 09/03/2020</a></span>
<span><a href="https://gitlab.waag.org/code/data-on-a-map-app/" target="_blank" rel="noopener noreferrer">v. 31/03/2020</a></span>
<span><a href="https://waag.org" target="_blank" rel="noopener noreferrer">waag</a></span>
<span>© <a href="http://www.openstreetmap.org/about/" target="_blank" rel="noopener noreferrer">OpenStreetMap</a></span>
</div>
......
......@@ -31,11 +31,11 @@ class Modal extends React.Component {
return (
<div className="modal" onClick={event => {
// Prevent closing on double click / touch devices (it's registering a click event for some reason..)
if(+ new Date() - this.mountTime > 300) closeModal(event)
if(closeModal && + new Date() - this.mountTime > 300) closeModal(event)
}}>
<div className="content" onClick={event => { event.stopPropagation() }}>
<button className="button-close" onClick={event => closeModal(event)}>×</button>
{this.props.children}
{ closeModal && <button className="button-close" onClick={event => closeModal(event)}>×</button>}
{ this.props.children }
</div>
</div>
)
......
......@@ -257,7 +257,7 @@ class StationInfo extends React.Component {
</span> }
</header>
{ downloadJSX }
<div id="chart-body">
<div id="chart-body" className="station-chart">
<ResponsiveContainer width="100%" height={chartHeight}>
<ComposedChart data={data} margin={{ top: 0, right: 30, left: 0, bottom: 10 }}>
{ gradients }
......@@ -271,7 +271,7 @@ class StationInfo extends React.Component {
{ plots }
<Brush dataKey="timestamp" height={40} fill="#eee" stroke="none" travellerWidth={4}
<Brush dataKey="timestamp" height={40} fill="#eee" stroke="none" travellerWidth={5}
onChange={ indexes => { this.handleRangeChange(indexes) } }
startIndex={ (data.length - 7*24 > 0 ? data.length - 7*24 : 0) }
tickFormatter={this.xAxisTickFormatter} >
......
import React from 'react'
import { Link } from 'react-router-dom'
import { appSettings } from "../../../config/app"
const Header = (props) => {
return (
<header id="site-header">
<Link id="site-titles" to="/">
<h1>{ appSettings.header.title }</h1>
<h2>{ appSettings.header.subtitle }</h2>
</Link>
<ul id="site-navigation">
{ Object.keys(appSettings.header.navigation).map(i => {
const iValue = appSettings.header.navigation[i]
if(typeof iValue === 'string' || iValue instanceof String) {
return <li key={i}>
<Link to={iValue}>{i}</Link>
</li>
} else {
return <li key={i}>
<span>{i}</span>
<div className="sub">
<ul>
{ Object.keys(iValue).map(j => <li key={j}><Link to={iValue[j]}>{ j }</Link></li>) }
</ul>
</div>
</li>
}
})}
</ul>
</header>
)
}
export default Header
\ No newline at end of file
import React from 'react'
import ReScatterChart from './ReScatterChart'
import ReLineChart from './ReLineChart'
import ReAreaChart from './ReAreaChart'
import ReBarChart from './ReBarChart'
const Page = (props) => {
return (
<div className="page">
<h1>{props.title}</h1>
<div dangerouslySetInnerHTML={{__html: props.intro}} />
{ props.content.map(item => {
switch (item.type) {
case 'scatter-chart':
return <ReScatterChart key={item.title} title={item.title} data={item.data} settings={item.settings} />
case 'line-chart':
return <ReLineChart key={item.title} title={item.title} data={item.data} settings={item.settings} />
case 'area-chart':
return <ReAreaChart key={item.title} title={item.title} data={item.data} settings={item.settings} />
case 'bar-chart':
return <ReBarChart key={item.title} title={item.title} data={item.data} settings={item.settings} />
default:
return <div>...</div>
}
}
)}
</div>
)
}
export default Page
\ No newline at end of file
import React, { useState, useEffect } from 'react';
import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip, Legend } from "recharts"
import { appSettings } from "../../../config/app"
const ReLineChart = (props) => {
const settings = props.settings
const [data, setData] = useState(null)
useEffect(() => {
async function fetchData() {
const response = await fetch(props.settings.data)
const json = await response.json()
setData(json)
}
fetchData();
}, [props.settings.data]);
return (
<div>
<h2>{props.title}</h2>
<ResponsiveContainer width="100%" height={320}>
<AreaChart data={data} margin={{ top: 5, right: 15, bottom: 20, left: 0 }}>
<XAxis { ...settings.x } />
<YAxis label={{ value: settings.y.name, angle: -90, position: 'insideBottomLeft', offset:10, style: {textAnchor: 'start'}}} { ...settings.y } />
<Tooltip cursor={{ strokeDasharray: '3 3' }} animationDuration={0} { ...settings.tooltip } />
<Legend align="right" { ...settings.legend } />
{ settings.categories.map((category, i) => {
return <Area type="linear" stackId="1" key={category} dataKey={category} fill={appSettings.colors[i]} fillOpacity="1" stroke={appSettings.colors[i]} />
}) }
</AreaChart>
</ResponsiveContainer>
</div>
)
}
export default ReLineChart
\ No newline at end of file
import React, { useState, useEffect } from 'react';
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, Legend } from "recharts"
import { appSettings } from "../../../config/app"
const CustomizedAxisTick = (props) => {
const { x, y, payload } = props
const words = payload.value ? payload.value.replace('_answer','').split('_') : []
return (
<g transform={`translate(${x},${y})`}>
{ words.map((word, i) => <text key={i} x={0} y={0} dy={14*(i+1)} textAnchor="middle">{word}</text>) }
</g>
)
}
const ReLineChart = (props) => {
const settings = props.settings
const [data, setData] = useState(null)
useEffect(() => {
async function fetchData() {
const response = await fetch(props.settings.data)
const json = await response.json()
setData(json)
}
fetchData();
}, [props.settings.data]);
return (
<div>
<h2>{props.title}</h2>
<ResponsiveContainer width="100%" height={320}>
<BarChart data={data} margin={{ top: 5, right: 15, bottom: 20, left: 0 }}>
<XAxis { ...settings.x } tick={CustomizedAxisTick} minTickGap={0} interval={0} />
<YAxis label={{ value: settings.y.name, angle: -90, position: 'insideBottomLeft', offset:10, style: {textAnchor: 'start'}}} { ...settings.y } />
<Tooltip cursor={{ strokeDasharray: '3 3' }} animationDuration={0} />
<Legend align="right" wrapperStyle={{bottom: 0}} { ...settings.legend } />
{ settings.categories.map((category, i) => {
return <Bar type="linear" key={category} dataKey={category} fill={appSettings.colorsBinary[i]} />
}) }
</BarChart>
</ResponsiveContainer>
</div>
)
}
export default ReLineChart
\ No newline at end of file
import React, { useState, useEffect } from 'react';
import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip, Legend } from "recharts"
import { appSettings } from "../../../config/app"
const ReLineChart = (props) => {
const settings = props.settings
const [data, setData] = useState(null)
useEffect(() => {
async function fetchData() {
const response = await fetch(props.settings.data)
const json = await response.json()
setData(json)
}
fetchData();
}, [props.settings.data]);
return (
<div>
<h2>{props.title}</h2>
<ResponsiveContainer width="100%" height={320}>
<LineChart data={data} margin={{ top: 5, right: 15, bottom: 20, left: 0 }}>
<XAxis label={{ value: settings.x.name, position: 'insideBottomLeft', offset:-18, style: {textAnchor: 'start'}}} { ...settings.x } />
<YAxis label={{ value: settings.y.name, angle: -90, position: 'insideBottomLeft', offset:10, style: {textAnchor: 'start'}}} { ...settings.y } />
<Tooltip cursor={{ strokeDasharray: '3 3' }} animationDuration={0} />
<Legend align="right" { ...settings.legend } />
{ settings.categories.map((category, i) => {
return <Line type="linear" key={category} dataKey={category} stroke={appSettings.colors[i]} />
}) }
</LineChart>
</ResponsiveContainer>
</div>
)
}
export default ReLineChart
\ No newline at end of file
import React, { useState, useEffect } from 'react';
import { ResponsiveContainer, ScatterChart, Scatter, XAxis, YAxis, ZAxis, Tooltip, Legend } from "recharts"
import { appSettings } from "../../../config/app"
const ReScatterChart = (props) => {
const settings = props.settings
const [data, setData] = useState({});
useEffect(() => {
async function fetchData() {
const response = await fetch(props.settings.data);
const json = await response.json();
// Object.keys(json).forEach(c => {
// json[c] = getRandomSubarray(json[c], 1000)
// })
setData(json);
}
fetchData();
}, [props.settings.data]);
return (
<div>
<h2>{props.title}</h2>
<ResponsiveContainer width="100%" height={320}>
<ScatterChart margin={{ top: 5, right: 15, bottom: 20, left: 0 }}>
<XAxis label={{ value: settings.x.name, position: 'insideBottomLeft', offset:-18, style: {textAnchor: 'start'}}} { ...settings.x } />
<YAxis label={{ value: settings.y.name, angle: -90, position: 'insideBottomLeft', offset:10, style: {textAnchor: 'start'}}} { ...settings.y } />
<ZAxis dataKey="c" type="number" range={[5,150]} />
<Tooltip cursor={{ strokeDasharray: '3 3' }} animationDuration={0} />
<Legend align="right" />
{Object.keys(data).map((category, i) => <Scatter key={category} name={category} data={data[category]} fill={appSettings.colors[i]} opacity="0.7" />)}
</ScatterChart>
</ResponsiveContainer>
</div>
)
}
export default ReScatterChart
\ No newline at end of file
......@@ -105,7 +105,7 @@ class PanelLayers extends React.Component {
</span>
}
{ parameter.tooltip && <Tooltip parameter={parameter} /> }
{ unit && <Legend unit={unit} width={this.container.current.offsetWidth} /> }
{ unit && <Legend unit={unit} width={this.container.current ? this.container.current.offsetWidth : 0} /> }
</li>)
})
}
......
$header-height: 3rem;
#app.with-header {
position: absolute;
height: calc(100% - #{$header-height});
bottom: 0;
overflow: initial;
}
#site-header {
position: absolute;
bottom: 100%;
width: 100%;
height: $header-height;
padding: 0 0.6rem;
background-color: #FFF;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.15);
z-index: 1300;
display: flex;
justify-content: space-between;
}
#site-titles {
display: flex;
align-items: baseline;
text-decoration: none;
h1 {
line-height: $header-height;
margin: 0 0.5rem 0 0;
}
h2 {
line-height: $header-height;
margin: 0;
font-size: 1.2rem;
}
}
#site-navigation {
display: flex;
align-items: center;
align-items: flex-start;
li {
position: relative;
margin: 0 1rem;
a, span {
display: inline-block;
line-height: $header-height;
text-decoration: none;
}
span {
cursor: default;
}
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.sub {
display: none;
position: absolute;
right: 0;
padding-top: 0.5rem;
}
&:hover .sub {
display: block;
}
ul {
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.15);
li {
margin: 0;
background-color: #FFF;
a {
line-height: 2.5em;
padding: 0 0.75em;
}
}
&:before {
content: "";
position: absolute;
bottom: calc(100% - 0.5rem);
right: 0.5rem;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid #FFF;
}
}
}
}
\ No newline at end of file
......@@ -4,7 +4,7 @@
left: 0;
bottom: 0;
right: 0;
padding: 0.5rem;
padding: 0.75rem;
background: rgba(0, 0, 0, 0.15);
overflow: auto;
text-align: center;
......
@mixin line-on-area { stroke-width: 1px; stroke-dasharray: 2px; }
@mixin line-on-area-primary {
@include line-on-area;
stroke: #000;
}
@mixin line-on-area-secondary {
@include line-on-area;
stroke: #bbb;
}
@mixin line { stroke-width: 1.5px; }
@mixin line-primary {
@include line;
stroke: url(#yaxis);
}
@mixin line-secondary {
@include line;
stroke: #ddd;
}
#chart-header {
display: flex;
flex-wrap: wrap;
......@@ -110,8 +133,18 @@
right: 0;
}
}
path.line.primary { @include line-primary; stroke: url(#legend); }
path.line.secondary { @include line-secondary; }
path.line-on-area.primary { @include line-on-area-primary; }
path.line-on-area.secondary { @include line-on-area-secondary; }
path.area.primary { fill: url(#legend); fill-opacity: 0.65; }
path.area.secondary { fill: #ddd; }
}
.recharts-wrapper {
user-select: none;
}
......@@ -137,9 +170,6 @@
}
}
.yAxis .recharts-cartesian-axis-line {
transform: translateX(3px);
}
rect.recharts-brush-slide {
fill: #000;
......@@ -170,54 +200,30 @@ rect.recharts-brush-slide {
}
@mixin line-on-area { stroke-width: 1px; stroke-dasharray: 2px; }
@mixin line-on-area-primary {
@include line-on-area;
stroke: #000;
}
@mixin line-on-area-secondary {
@include line-on-area;
stroke: #bbb;
}
@mixin line { stroke-width: 1.5px; }
@mixin line-primary {
@include line;
stroke: url(#yaxis);
}
@mixin line-secondary {
@include line;
stroke: #ddd;
}
.recharts-line {
&.line {
&.primary path { @include line-primary; }
&.secondary path { @include line-secondary; }
}
&.line-on-area {
&.primary path { @include line-on-area-primary; }
&.secondary path { @include line-on-area-secondary; }
.station-chart {
.yAxis .recharts-cartesian-axis-line {
transform: translateX(3px);
}
}
.recharts-area {
path { stroke: none; fill-opacity: 0.5; }
.recharts-line {
&.line {
&.primary path { @include line-primary; }
&.secondary path { @include line-secondary; }
}
&.line-on-area {
&.primary path { @include line-on-area-primary; }
&.secondary path { @include line-on-area-secondary; }
}
}
&.primary path.recharts-area-area { fill: url(#yaxis); }
&.secondary path.recharts-area-area { fill: #ddd; }
.recharts-area {
path { stroke: none; fill-opacity: 0.5; }
&.primary path.recharts-area-area { fill: url(#yaxis); }
&.secondary path.recharts-area-area { fill: #ddd; }
}
}
#chart-data-selection {
path.line.primary { @include line-primary; stroke: url(#legend); }
path.line.secondary { @include line-secondary; }
path.line-on-area.primary { @include line-on-area-primary; }
path.line-on-area.secondary { @include line-on-area-secondary; }
path.area.primary { fill: url(#legend); fill-opacity: 0.65; }
path.area.secondary { fill: #ddd; }
}
.recharts-active-dot circle {
......
......@@ -171,7 +171,26 @@ button.button-text {
cursor: pointer;
&:hover {
opacity: 0.9;
opacity: 0.85;
}
}
a.btn {
display: inline-block;
border: none;
background-color: var(--color-button, #000000);
color: #FFF;
font-size: 0.9rem;
line-height: 1.8rem;
height: 1.8rem;
margin: 0 0.25rem 0.25rem 0;
padding: 0 0.5em;
cursor: pointer;
text-decoration: none;
&:hover {
opacity: 0.85;
}
}
......
......@@ -8,6 +8,7 @@ body {
@import "typography";
@import "ui";
@import "forms";
@import "header";
@import "panel";
@import "modal";
@import "tooltip";
......@@ -19,6 +20,7 @@ body {
height: 100%;
font-size: 16px;
background-color: green;
#app {
position: relative;
......@@ -27,6 +29,10 @@ body {
overflow: hidden;
}
.page .recharts-responsive-container {
margin-bottom: 4rem;
}
img {
max-width: 100%;
height: auto;
......
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import { createStore } from 'redux'
import { Provider, connect } from 'react-redux'
......@@ -19,8 +20,8 @@ import '../../config/style.css'
import "moment/locale/nl-be"
import { texts } from "../../config/texts"
import { appSettings } from "../../config/app"
import { appSettings } from '../../config/app.js'
import { initialLayers, dataGroups } from "../../config/data.js"
import { mapDefaults, mapLocations, mapControlSettings, mapStyle } from "../../config/map.js"
......@@ -43,6 +44,11 @@ import PanelTimeSelection from 'Panels/PanelTimeSelection.js'
import Modal from "./Modal/Modal.js"
import Header from 'Page/Header.js'
import Page from 'Page/Page.js'
const store = createStore(reducer)
......@@ -118,10 +124,6 @@ class App extends React.Component {
if(!deckLayers[layer]) {
deckLayers[layer] = {}
layerDefinition.sources.forEach(source => {
deckLayers[layer][source.name] = {}
})
}
layerDefinition.sources.forEach(source => {
......@@ -143,11 +145,11 @@ class App extends React.Component {
const uniquePois = {}
const timestamps = []
console.log(data)
Object.keys(deckLayers).forEach(parameter => {
Object.keys(deckLayers[parameter]).forEach(source => {
const sourceTimestamps = Object.keys(deckLayers[parameter][source].data)
// just take now() for this?
const lastTimestamp = sourceTimestamps[sourceTimestamps.length-1]
// populate pois object
......@@ -159,7 +161,6 @@ class App extends React.Component {
sourceTimestamps.forEach(timestamp => {
if(!inArray(timestamps, timestamp)) timestamps.push(timestamp)
})
})
})
......@@ -300,32 +301,44 @@ class App extends React.Component {
const { pitchMax, zoomMax, zoomMin } = mapControlSettings
return (
<div id="app">
<MapGL { ...viewport } width="100%" height="100%" maxPitch={pitchMax} maxZoom={zoomMax} minZoom={zoomMin} mapStyle={mapStyle}
onViewportChange={ this.onViewportChange }
onTransitionStart={() => { this.setTransitionState(true) } }
onTransitionEnd={() => { this.setTransitionState(false) } } >
<DeckLayers viewport={viewport} loading={loading} setModal={ this.setModal } />
</MapGL>
<MapControls mapControlSettings={mapControlSettings} viewport={viewport} changeViewport={this.changeViewport} />
<MapAttributions />
<div className='mapboxgl-ctrl-bottom-left'>
{ texts.about && <Panel id="about" content={ texts.about } openOnLoad={true} /> }
<PanelLayers layers={layers} forceUpdate={layers.join()} dataGroups={dataGroups} toggleLayer={this.toggleLayer} viewportHeight={viewport.height} loading={loading} reload={this.reload} />
<PanelTimeSelection />
</div>
<div className='mapboxgl-ctrl-top-left'>
{ mapLocations && <MapLocations mapLocations={mapLocations} changeViewportBounds={this.changeViewportBounds} selectedCenter={selectedCenter} /> }
<MapSearch changeViewportCenter={this.changeViewportCenter} />
<Router>
<div id="app" className={ appSettings.header ? "with-header" : "" }>
{ appSettings.header && <Header /> }
<Switch>
<Route exact path={appSettings.mapRoute} render={() => (
[
<MapGL key="1" { ...viewport } width="100%" height="100%" maxPitch={pitchMax} maxZoom={zoomMax} minZoom={zoomMin} onViewportChange={ this.onViewportChange } mapStyle={mapStyle} onTransitionStart={() => { this.setTransitionState(true) } } onTransitionEnd={() => { this.setTransitionState(false) } }>
<DeckLayers viewport={viewport} loading={loading} setModal={ this.setModal } />
</MapGL>,
<MapControls key="2" mapControlSettings={mapControlSettings} viewport={viewport} changeViewport={this.changeViewport} />,
<MapAttributions key="3" />,
<div key="4" className='mapboxgl-ctrl-bottom-left'>
{ texts.about && <Panel id="about" content={ texts.about } openOnLoad={true} /> }
<PanelLayers layers={layers} forceUpdate={layers.join()} dataGroups={dataGroups} toggleLayer={this.toggleLayer} viewportHeight={viewport.height} loading={loading} reload={this.reload} />
<PanelTimeSelection />
</div>,
<div key="5" className='mapboxgl-ctrl-top-left'>
{ mapLocations && <MapLocations mapLocations={mapLocations} changeViewportBounds={this.changeViewportBounds} selectedCenter={selectedCenter} /> }
<MapSearch changeViewportCenter={this.changeViewportCenter} />
</div>
]
)} />
{ appSettings.pages.map(page => {
return <Route key={page.route} path={page.route} render={() => (
<Modal>
<Page title={page.title} intro={page.intro} content={page.content} />
</Modal>
)} />
}) }
</Switch>
{ this.renderModal() }
</div>
{ this.renderModal() }
</div>
</Router>
)
}
}
......
import { getTimestampsRange } from "./time"
export const groupArrayOfObjectsBy = (array, key) => {
......@@ -53,7 +54,7 @@ export const addMissingDataPoints = (data, start, end, granularity) => {
if (granularity === "hourly") interval = 3600000
if (granularity === "daily") interval = 86400000
for (let i = start; i < end; i = i + interval) {
for (let i = start; i <= end; i = i + interval) {
if(existing.indexOf(i) < 0) {
data.push({
timestamp: i,
......@@ -66,6 +67,52 @@ export const addMissingDataPoints = (data, start, end, granularity) => {
}
export const addMissingDataPointsAll = (range, stations, start, end) => {
const timestampsRange = getTimestampsRange(start, end)
const last = {}
const rangeData = {}
timestampsRange.forEach(timestamp => {
rangeData[timestamp] = []
Object.keys(stations).forEach(station => {
let data = null
if(range[timestamp]) {
let stationData = range[timestamp].find(o => o.id === station)
if(stationData) {
data = {
timestamp: timestamp,
mean: stationData.value,
status: null
}
}
}
if(data === null && last[station]) {
const dataAgeInHours = ((new Date(timestamp) - new Date(last[station].timestamp)) / 3600000) - 1
data = {
...last[station],
dataAge: dataAgeInHours,
status: dataAgeInHours < 8 ? null : 1
}
}
rangeData[timestamp].push({
...stations[station],
...data
})
last[station] = data
})
})
return rangeData
}
export const downsampleData = (inputData, granularity, prefixes) => {
const groupedData = {}
......@@ -116,3 +163,21 @@ export const pad = (n, width, z = '0') => {
n = n + '';
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
}
export const getRandomSubarray = (arr, size) => {
var shuffled = arr.slice(0), i = arr.length, temp, index;
while (i--) {
index = Math.floor((i + 1) * Math.random());
temp = shuffled[index];
shuffled[index] = shuffled[i];
shuffled[i] = temp;
}
return shuffled.slice(0, size);
}
export const normalizeLetters = (string) => {
return string.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
}
......@@ -12,39 +12,47 @@ const pad = (number) => {
return r;
}
const getIsoStringHourly = (date) => {
return date.getUTCFullYear()
+ '-' + pad(date.getUTCMonth() + 1)
+ '-' + pad(date.getUTCDate())
+ 'T' + pad(date.getUTCHours())
+ ':00:00.000Z'
}
export const getNowISO = () => {
const now = new Date()
return now.getUTCFullYear()
+ '-' + pad(now.getUTCMonth() + 1)
+ '-' + pad(now.getUTCDate())
+ 'T' + pad(now.getUTCHours())
+ ':' + pad(now.getUTCMinutes() )
+ ':00.000Z'
const getIsoStringDaily = (date) => {
return date.getUTCFullYear()
+ '-' + pad(date.getUTCMonth() + 1)
+ '-' + pad(date.getUTCDate())
+ 'T00:00:00.000Z'
}
// export const getNowISO = () => {
// const now = new Date()
// return now.getUTCFullYear()
// + '-' + pad(now.getUTCMonth() + 1)
// + '-' + pad(now.getUTCDate())
// + 'T' + pad(now.getUTCHours())
// + ':' + pad(now.getUTCMinutes() )
// + ':00.000Z'
// }
export const getNowHourISO = () => {
const now = new Date()
return now.getUTCFullYear()
+ '-' + pad(now.getUTCMonth() + 1)
+ '-' + pad(now.getUTCDate())
+ 'T' + pad(now.getUTCHours())
+ ':00:00.000Z'
return getIsoStringHourly(now)
}
export const getLastHourISO = () => {
const now = new Date()
now.setHours(now.getHours() - 1);
now.setHours(now.getHours() - 1)
return now.getUTCFullYear()
+ '-' + pad(now.getUTCMonth() + 1)
+ '-' + pad(now.getUTCDate())
+ 'T' + pad(now.getUTCHours())
+ ':00:00.000Z'
return getIsoStringHourly(now)
}
......@@ -53,11 +61,7 @@ export const getHoursAgoISO = (hours) => {
now.setHours(now.getHours() - hours);
return now.getUTCFullYear()
+ '-' + pad(now.getUTCMonth() + 1)
+ '-' + pad(now.getUTCDate())
+ 'T' + pad(now.getUTCHours())
+ ':00:00.000Z'
return getIsoStringHourly(now)
}
......@@ -66,11 +70,7 @@ export const getDaysAgoHourISO = (days) => {
now.setDate(now.getDate() - days)
return now.getUTCFullYear()
+ '-' + pad(now.getUTCMonth() + 1)
+ '-' + pad(now.getUTCDate())
+ 'T' + pad(now.getUTCHours())
+ ':00:00.000Z'
return getIsoStringHourly(now)
}
export const getDaysAgoISO = (days) => {
......@@ -78,10 +78,7 @@ export const getDaysAgoISO = (days) => {
now.setDate(now.getDate() - days)
return now.getUTCFullYear()
+ '-' + pad(now.getUTCMonth() + 1)
+ '-' + pad(now.getUTCDate())
+ 'T00:00:00.000Z'
return getIsoStringDaily(now)
}
......@@ -90,11 +87,7 @@ export const subtractOneHourISO = (datestring) => {
date.setHours(date.getHours() - 1)
return date.getUTCFullYear()
+ '-' + pad(date.getUTCMonth() + 1)
+ '-' + pad(date.getUTCDate())
+ 'T' + pad(date.getUTCHours())
+ ':00:00.000Z'
return getIsoStringHourly(date)
}
......@@ -103,11 +96,7 @@ export const subtractOneHourISO = (datestring) => {
// now.setDate(now.getDate() - 30*months)
// return now.getUTCFullYear()
// + '-' + pad(now.getUTCMonth() + 1)
// + '-' + pad(now.getUTCDate())
// + 'T' + pad(now.getUTCHours())
// + ':00:00.000Z'
// return getIsoStringHourly(now)
// }
......@@ -132,4 +121,17 @@ export const getDataAge = (timestamp, granularity, timestamp_end = false) => {
if(timestamp_end) inUnit -= unit
return inUnit
}
\ No newline at end of file
}
export const getTimestampsRange = (start, end) => {
const startTime = new Date(start).getTime()
const endTime = new Date(end).getTime()
const timestampsRange = []
for(let loopTime = startTime; loopTime < endTime; loopTime += 3600000) {
timestampsRange.push(getIsoStringHourly(new Date(loopTime)))
}
return timestampsRange
}