Browse Source

Start the measurements :high_brightness::hotsprings::chart_with_upwards_trend:

:watermelon:
master
Daniel da Silva 3 years ago
commit
3cf0e60008
50 changed files with 3036 additions and 0 deletions
  1. 11
    0
      .build_scripts/deploy.sh
  2. 8
    0
      .build_scripts/lint.sh
  3. 29
    0
      .eslintrc
  4. 1
    0
      .gitattributes
  5. 118
    0
      .gitignore
  6. 1
    0
      .nvmrc
  7. 39
    0
      .travis.yml
  8. 13
    0
      LICENSE
  9. 36
    0
      README.md
  10. 1
    0
      app/CNAME
  11. 7
    0
      app/assets/graphics/collecticons/clock.svg
  12. 1
    0
      app/assets/graphics/layout/devseed-logo-symbol.svg
  13. 1
    0
      app/assets/graphics/layout/ds-logo-neg.svg
  14. 1
    0
      app/assets/graphics/layout/ds-logo-pos.svg
  15. BIN
      app/assets/graphics/meta/apple-touch-icon-114x114.png
  16. BIN
      app/assets/graphics/meta/apple-touch-icon-120x120.png
  17. BIN
      app/assets/graphics/meta/apple-touch-icon-144x144.png
  18. BIN
      app/assets/graphics/meta/apple-touch-icon-152x152.png
  19. BIN
      app/assets/graphics/meta/apple-touch-icon-57x57.png
  20. BIN
      app/assets/graphics/meta/apple-touch-icon-72x72.png
  21. BIN
      app/assets/graphics/meta/apple-touch-icon-76x76.png
  22. BIN
      app/assets/graphics/meta/apple-touch-icon.png
  23. BIN
      app/assets/graphics/meta/default-meta-image.png
  24. BIN
      app/assets/graphics/meta/favicon.ico
  25. 44
    0
      app/assets/scripts/actions/action-creators.js
  26. 15
    0
      app/assets/scripts/actions/action-types.js
  27. 444
    0
      app/assets/scripts/components/charts/chart-line.js
  28. 77
    0
      app/assets/scripts/components/sensor-widget.js
  29. 30
    0
      app/assets/scripts/config.js
  30. 17
    0
      app/assets/scripts/config/production.js
  31. 8
    0
      app/assets/scripts/config/staging.js
  32. 48
    0
      app/assets/scripts/main.js
  33. 40
    0
      app/assets/scripts/reducers/reducer.js
  34. 22
    0
      app/assets/scripts/utils/format.js
  35. 33
    0
      app/assets/scripts/views/app.js
  36. 263
    0
      app/assets/scripts/views/home.js
  37. 29
    0
      app/assets/scripts/views/uhoh.js
  38. 282
    0
      app/assets/styles/_base.scss
  39. 45
    0
      app/assets/styles/_charts.scss
  40. 24
    0
      app/assets/styles/_functions.scss
  41. 143
    0
      app/assets/styles/_mixins.scss
  42. 425
    0
      app/assets/styles/_normalize.scss
  43. 143
    0
      app/assets/styles/_reset.scss
  44. 105
    0
      app/assets/styles/_utils.scss
  45. 70
    0
      app/assets/styles/_variables.scss
  46. 55
    0
      app/assets/styles/main.scss
  47. 63
    0
      app/index.html
  48. BIN
      devseed-sense-dashboard.png
  49. 251
    0
      gulpfile.js
  50. 93
    0
      package.json

+ 11
- 0
.build_scripts/deploy.sh View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e # halt script on error

echo "Get ready, we're pushing to gh-pages!"
cd dist
git init
git config user.name "Travis-CI"
git config user.email "travis-ci@danielfdsilva.com"
git add .
git commit -m "CI deploy to gh-pages"
git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages

+ 8
- 0
.build_scripts/lint.sh View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e # halt script on error
# If deploying, don't balk on lint errors
if [ $TRAVIS_PULL_REQUEST = "false" ] && [ $TRAVIS_BRANCH = ${DEPLOY_BRANCH} ]; then
npm run lint || true
else
npm run lint
fi

+ 29
- 0
.eslintrc View File

@@ -0,0 +1,29 @@
{
"extends": ["semistandard"],
"env": {
"es6": true,
"browser": true
},
"plugins": [
"react"
],
"ecmaFeatures": {
"jsx": true
},
rules: {
'react/display-name': 1 ,
'react/jsx-no-duplicate-props': 2,
'react/jsx-no-undef': 2,
'react/jsx-uses-react': 2,
'react/jsx-uses-vars': 2,
'react/no-danger': 0,
'react/no-deprecated': 2,
'react/no-did-mount-set-state': [2, 'allow-in-func'],
'react/no-did-update-set-state': [2, 'allow-in-func'],
'react/no-direct-mutation-state': 2,
'react/no-is-mounted': 2,
'react/no-unknown-property': 2,
'react/prop-types': 2,
'react/react-in-jsx-scope': 2
}
}

+ 1
- 0
.gitattributes View File

@@ -0,0 +1 @@
* text=auto

+ 118
- 0
.gitignore View File

@@ -0,0 +1,118 @@
################################################
############### .gitignore ##################
################################################
#
# This file is only relevant if you are using git.
#
# Files which match the splat patterns below will
# be ignored by git. This keeps random crap and
# and sensitive credentials from being uploaded to
# your repository. It allows you to configure your
# app for your machine without accidentally
# committing settings which will smash the local
# settings of other developers on your team.
#
# Some reasonable defaults are included below,
# but, of course, you should modify/extend/prune
# to fit your needs!
################################################




################################################
# Local Configuration
#
# Explicitly ignore files which contain:
#
# 1. Sensitive information you'd rather not push to
# your git repository.
# e.g., your personal API keys or passwords.
#
# 2. Environment-specific configuration
# Basically, anything that would be annoying
# to have to change every time you do a
# `git pull`
# e.g., your local development database, or
# the S3 bucket you're using for file uploads
# development.
#
################################################

app/assets/scripts/config/local.js





################################################
# Dependencies
#
# When releasing a production app, you may
# consider including your node_modules and
# bower_components directory in your git repo,
# but during development, its best to exclude it,
# since different developers may be working on
# different kernels, where dependencies would
# need to be recompiled anyway.
#
# More on that here about node_modules dir:
# http://www.futurealoof.com/posts/nodemodules-in-git.html
# (credit Mikeal Rogers, @mikeal)
#
# About bower_components dir, you can see this:
# http://addyosmani.com/blog/checking-in-front-end-dependencies/
# (credit Addy Osmani, @addyosmani)
#
################################################

node_modules
bower_components
.sass-cache
test/bower_components
app/assets/styles/_collecticons.scss

################################################
# Node.js / NPM
#
# Common files generated by Node, NPM, and the
# related ecosystem.
################################################

lib-cov
*.seed
*.log
*.out
*.pid
npm-debug.log


################################################
# Apidocs
#
# Common files generated by apidocs
################################################



################################################
# Miscellaneous
#
# Common files generated by text editors,
# operating systems, file systems, etc.
################################################

*~
*#
.DS_STORE
.netbeans
nbproject
.idea
.resources
.node_history
temp
tmp
.tmp
dist

_README.md

+ 1
- 0
.nvmrc View File

@@ -0,0 +1 @@
v4.2.2

+ 39
- 0
.travis.yml View File

@@ -0,0 +1,39 @@
language: node_js
node_js:
- "4.2"

env:
global:
- CXX=g++-4.8
- GH_REF=github.com/developmentseed/sense.git
- DEPLOY_BRANCH=master
- secure: "sxDPVseehEH9G/JMuM1E7hsSnGeG4Ju3poWDbp/3Q/Y2Phx/U5IYOQ9e1kggafBi61JOsi8/cySnmiC7c0wP/EHoMnh9gcyAGPdTU4N1UVfZ7xM0eIqm4LQrHBHM0A9ghr3ZhevRIX07LqcNjC0kUR9RX/mlQUvTy/725SzKmQIE/nCWxJwLG5GYRytE/SyX8pzOhuCgIsuBeebpC1Dl91AZmofcym7Lawezvr0ZE1ZcP3wMiZHTCQ8mBIMzd7mSD8rdiZH/QggVPtis5aCSDomOdiKIxxpDM3Mj1J42xtSiEIhRlU9nk3smb9Auioql+BzV5bE8a4oW1D66c320Yb6SAnkSM90a+g0NlS1Z6blNjJ4N8L4QCmVHH4S+AyiXgqccrZiRl/2tsoKE+ZoBFvzIxmOzOQUqGVKX47hQq7QLHFKLCaJIx2XKhHPrfUhi752+5rYFG01teIkOP0usac34qM96dCxOodmC6xUY2YpBWboIwSr3nhQtLBs2fUw23PlgaDdgGpOyv3Pporwnp1Js99UZXtSBDWHcaFxjWnp3gEUdWwMwR69DMOzu6Udm9RMj3WtPm7G5fz9E5pPlVjiBFgsLaUXV3WVNwH/G1zrstl5vFAGyUzI1iJeHTGvncD/HT21uMHNkl6u5VA2+n97W8+J8pvxefQ+Plja7+gM="
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8

cache:
apt: true
directories:
- node_modules

before_install:
- chmod +x ./.build_scripts/deploy.sh
- chmod +x ./.build_scripts/lint.sh

before_script:
- ./.build_scripts/lint.sh

script:
- npm run build

deploy:
provider: script
skip_cleanup: true
script: .build_scripts/deploy.sh
on:
branch: ${DEPLOY_BRANCH}

+ 13
- 0
LICENSE View File

@@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004

Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>

Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.

DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

0. You just DO WHAT THE FUCK YOU WANT TO.

+ 36
- 0
README.md View File

@@ -0,0 +1,36 @@
# Devseed Sense

Simple dashboard that taps into the [opensensemap](http://opensensemap.org/) api to show the measurements for a specific [sensebox](www.sensebox.de/en/).

![devseed-sense-dashboard.png](devseed-sense-dashboard.png)

## Development environment
To set up the development environment for this website, you'll need to install the following on your system:

- Node (v4.2.x) & Npm ([nvm](https://github.com/creationix/nvm) usage is advised)

> The versions mentioned are the ones used during development. It could work with newer ones.
Run `nvm use` to activate the correct version.

After these basic requirements are met, run the following commands in the website's folder:
```
$ npm install
```

### Getting started

```
$ npm run serve
```
Compiles the sass files, javascript, and launches the server making the site available at `http://localhost:1337/`
The system will watch files and execute tasks whenever one of them changes.
The site will automatically refresh since it is bundled with livereload.
The current code will show the values for the [DS Lisbon sensebox](http://opensensemap.org/#/explore/570629b945fd40c8197462fb).
This can be changed by setting the correct ids in `config/production.js`

### Other commands
Compile the files to the `dist` folder ready for production.
```
$ npm run build
```

+ 1
- 0
app/CNAME View File

@@ -0,0 +1 @@
sense.devseed.com

+ 7
- 0
app/assets/graphics/collecticons/clock.svg View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">

<path d="M288,128h-64v141.3l105.4,105.3l45.2-45.3L288,242.7V128z M256,0C114.6,0,0,114.6,0,256s114.6,256,256,256 s256-114.6,256-256S397.4,0,256,0z M256,448c-106,0-192-86-192-192S150,64,256,64s192,86,192,192S362,448,256,448z"/>
</svg>

+ 1
- 0
app/assets/graphics/layout/devseed-logo-symbol.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><path fill="#D04003" d="M512 32c0-17.7-14.3-32-32-32h-448c-17.7 0-32 14.3-32 32v448c0 17.7 14.3 32 32 32h448c17.7 0 32-14.3 32-32v-448z"/><path fill="#fff" d="M289.5 434.7c-.1-10.3 2.6-19.2 7-28.5-9.2 8.1-12.8 17.3-16.2 29.8.9-14.6 8-110.4 8-115.2 24.5-70 13.6-279 44.8-305.1-36 6.5-48.2 43.3-59.7 72.5-11.4-34.3-2.4-56.1-40.2-68.1 30.2 25.6 24.2 47.6 30.4 83-18.1-23.5-12.5-52.8-33.3-76.8-5.9-6.1-25.5-21-25.5-21 0 .9 15.7 32.3 18.6 48.9 2.9 16.6 9.8 49.8 8.8 62 2.6 67.8 19.6 133.5 31.3 200 2 11.4 7.8 44.5 7.8 44.5l1.3 13.8c-2.1-9.1-4.1-17.3-5.2-20.8-3.4-10-11.8-35.4-13.7-45.8-28.5-58.6-80.6-173.5-150.2-190.4 39.5 35 82 81.2 91.5 125.8-21.2-29.8-104.6-93.4-138.5-116.6 63.9 66.3 115.2 135 170.8 208.7 16.2 21 36.3 63.3 37.7 72.9 1.1 7.1 2.4 33.6 3.2 49.9-31.1-75.1-48.1-113.2-105.5-146.8-9.8-5.2-40.2-15.7-40.2-15.7 13.8 15.2 30.7 16.1 43.3 37.3-26.1-17.1-53.2-20.1-83-21.1 95.7 49.5 147.5 63.3 179.7 158.4 2.6 15.6-1.4 27.8-1 41.8h21.2c-.1-10 .1-19.6 0-21.6 1.1-6.6 1.3-26.6 4.2-32.7 10-25.4 26.2-37.4 48.8-52.2-20.3 6.6-30.6 4-46.2 29.1zm94.5-224.2c-7.8 7.4-31.3 37.2-31.3 37.2 1.3-42 11.9-87 38.7-118.4-37.3 24.4-58.3 84.6-68 121.4-3.8 14.8-47.9 100.7-27.4 146.7 4-16.8 13.5-33.1 24-47.6 4.9-6.6 25.5-39.3 46-86 18.1-47 47.8-74.5 89.6-97.8-31.6 4.8-49.6 23-71.6 44.5zm7.3-81.2z"/></svg>

+ 1
- 0
app/assets/graphics/layout/ds-logo-neg.svg
File diff suppressed because it is too large
View File


+ 1
- 0
app/assets/graphics/layout/ds-logo-pos.svg
File diff suppressed because it is too large
View File


BIN
app/assets/graphics/meta/apple-touch-icon-114x114.png View File


BIN
app/assets/graphics/meta/apple-touch-icon-120x120.png View File


BIN
app/assets/graphics/meta/apple-touch-icon-144x144.png View File


BIN
app/assets/graphics/meta/apple-touch-icon-152x152.png View File


BIN
app/assets/graphics/meta/apple-touch-icon-57x57.png View File


BIN
app/assets/graphics/meta/apple-touch-icon-72x72.png View File


BIN
app/assets/graphics/meta/apple-touch-icon-76x76.png View File


BIN
app/assets/graphics/meta/apple-touch-icon.png View File


BIN
app/assets/graphics/meta/default-meta-image.png View File


BIN
app/assets/graphics/meta/favicon.ico View File


+ 44
- 0
app/assets/scripts/actions/action-creators.js View File

@@ -0,0 +1,44 @@
import fetch from 'isomorphic-fetch';
import * as actions from './action-types';
import config from '../config';

// //////////////////////////////////////////////////////////////////////////
// // Fetch Section Access Thunk

function requestSensorData (sensor) {
return {
type: actions[`REQUEST_SENSOR_DATA_${sensor.toUpperCase()}`]
};
}

function receiveSensorData (sensor, json) {
return {
type: actions[`RECEIVE_SENSOR_DATA_${sensor.toUpperCase()}`],
data: json,
receivedAt: Date.now()
};
}

export function fetchSensorData (sensor, toDate) {
return dispatch => {
dispatch(requestSensorData(sensor));

let sensorId = config.senseBox[`sensorId--${sensor}`];
return fetch(`${config.api}/boxes/${config.senseBox.id}/data/${sensorId}?from-date=${toDate}`)
.then(response => {
if (response.status >= 400) {
throw new Error('Bad response');
}
return response.json();
})
.then(json => {
dispatch(receiveSensorData(sensor, json));
// setTimeout(() => {
// dispatch(receiveSensorData(sensor, json));
// }, Math.ceil(Math.random() * 5000));
}, e => {
console.log('e', e);
return dispatch(receiveSensorData(null, null, 'Data not available'));
});
};
}

+ 15
- 0
app/assets/scripts/actions/action-types.js View File

@@ -0,0 +1,15 @@
'use strict';
export const REQUEST_SENSOR_DATA_TEMPERATURE = 'REQUEST_SENSOR_DATA_TEMPERATURE';
export const RECEIVE_SENSOR_DATA_TEMPERATURE = 'RECEIVE_SENSOR_DATA_TEMPERATURE';

export const REQUEST_SENSOR_DATA_PRESSURE = 'REQUEST_SENSOR_DATA_PRESSURE';
export const RECEIVE_SENSOR_DATA_PRESSURE = 'RECEIVE_SENSOR_DATA_PRESSURE';

export const REQUEST_SENSOR_DATA_LUMINOSITY = 'REQUEST_SENSOR_DATA_LUMINOSITY';
export const RECEIVE_SENSOR_DATA_LUMINOSITY = 'RECEIVE_SENSOR_DATA_LUMINOSITY';

export const REQUEST_SENSOR_DATA_UV = 'REQUEST_SENSOR_DATA_UV';
export const RECEIVE_SENSOR_DATA_UV = 'RECEIVE_SENSOR_DATA_UV';

export const REQUEST_SENSOR_DATA_HUMIDITY = 'REQUEST_SENSOR_DATA_HUMIDITY';
export const RECEIVE_SENSOR_DATA_HUMIDITY = 'RECEIVE_SENSOR_DATA_HUMIDITY';

+ 444
- 0
app/assets/scripts/components/charts/chart-line.js View File

@@ -0,0 +1,444 @@
'use strict';
import React from 'react';
import d3 from 'd3';
import _ from 'lodash';
// import Popover from '../../utils/popover';

var LineChart = React.createClass({
displayName: 'LineChart',

propTypes: {
className: React.PropTypes.string,
data: React.PropTypes.array,
axisLineVal: React.PropTypes.number,
axisLineMax: React.PropTypes.number,
axisLineMin: React.PropTypes.number,
dataUnitSuffix: React.PropTypes.string
},

chart: null,

onWindowResize: function () {
this.chart.checkSize();
},

componentDidMount: function () {
// console.log('LineChart componentDidMount');
// Debounce event.
this.onWindowResize = _.debounce(this.onWindowResize, 200);

window.addEventListener('resize', this.onWindowResize);
this.chart = Chart();
d3.select(this.refs.container).call(this.chart
.data(this.props.data)
.axisLineVal(this.props.axisLineVal)
.axisValueMax(this.props.axisLineMax)
.axisValueMin(this.props.axisLineMin)
.dataUnitSuffix(this.props.dataUnitSuffix));
},

componentWillUnmount: function () {
// console.log('LineChart componentWillUnmount');
window.removeEventListener('resize', this.onWindowResize);
this.chart.destroy();
},

componentDidUpdate: function (prevProps/* prevState */) {
console.log('LineChart componentDidUpdate');
this.chart.pauseUpdate();
if (prevProps.data !== this.props.data) {
this.chart.data(this.props.data);
}
if (prevProps.axisLineVal !== this.props.axisLineVal) {
this.chart.axisLineVal(this.props.axisLineVal);
}
if (prevProps.axisLineMax !== this.props.axisLineMax) {
this.chart.axisValueMax(this.props.axisLineMax);
}
if (prevProps.axisLineMin !== this.props.axisLineMin) {
this.chart.axisValueMin(this.props.axisLineMin);
}
if (prevProps.dataUnitSuffix !== this.props.dataUnitSuffix) {
this.chart.dataUnitSuffix(this.props.dataUnitSuffix);
}
this.chart.continueUpdate();
},

render: function () {
return (
<div className={this.props.className} ref='container'></div>
);
}
});

module.exports = LineChart;

var Chart = function (options) {
// Data related variables for which we have getters and setters.
var _data = null;
var _axisLineVal, _axisValueMin, _axisValueMax, _dataUnitSuffix;

// Pause
var _pauseUpdate = false;

// Containters
var $el, $svg;
// Var declaration.
var margin = {top: 16, right: 32, bottom: 32, left: 24};
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// width and height refer to the data canvas. To know the svg size the margins
// must be added.
var _width, _height;
// Draw functions.
var line;

// Update functions.
var updateData, upateSize;

// X scale. Range updated in function.
var x = d3.time.scale();

// Y scale. Range updated in function.
var y = d3.scale.linear();

// Used for the zoom translate bounds.
var minX;

// Zoom
var zoom = d3.behavior
.zoom()
.scaleExtent([1, 1]);

// Line function for the delimit the area.
line = d3.svg.line()
.x(d => x(d.timestep))
.y(d => y(d.value));

// Define xAxis function.
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom')
.tickSize(0)
.tickFormat(d3.time.format('%H:%M'));
// .ticks(3);

function _calcSize () {
_width = parseInt($el.style('width'), 10) - margin.left - margin.right;
_height = parseInt($el.style('height'), 10) - margin.top - margin.bottom;
}

function chartFn (selection) {
$el = selection;

var layers = {
line: function () {
// lines.
let the_line = $dataCanvas.selectAll('.data-line')
.data([_data]);

// Handle new.
the_line.enter()
.append('path')
.attr('clip-path', 'url(#clip)');

// Update current.
the_line
.attr('d', d => line(d))
.attr('class', d => `data-line`);

// Remove old.
the_line.exit()
.remove();
},

minMax: function () {
let [sDate, eDate] = x.domain();

let f = (o) => {
let timestamp = o.timestep.getTime();
return timestamp >= sDate.getTime() && timestamp <= eDate.getTime();
};

// Min Max.
let sorted = _(_data).filter(f).sortBy('value').value();
if (!sorted.length) {
return;
}

let min = sorted[0];
let max = _.last(sorted);

let edgeG = $dataCanvas.selectAll('.edges')
.data([0])
.enter().append('g')
.attr('class', 'edges');

edgeG.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '-0.25em')
.attr('class', 'edge edge-max');

edgeG.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '1em')
.attr('class', 'edge edge-min');

$dataCanvas.select('.edge.edge-max')
.datum(max)
.attr('x', d => x(d.timestep))
.attr('y', d => y(d.value))
.text(d => d.value + _dataUnitSuffix);

$dataCanvas.select('.edge.edge-min')
.datum(min)
.attr('x', d => x(d.timestep))
.attr('y', d => y(d.value))
.text(d => d.value + _dataUnitSuffix);
},

xAxis: function () {
// Append Axis.
// X axis.
let xAx = $svg.selectAll('.x.axis')
.data([0]);

xAx.enter().append('g')
.attr('class', 'x axis')
.append('text')
.attr('class', 'label')
.attr('text-anchor', 'start');

xAx
.attr('transform', `translate(${margin.left},${_height + margin.top + 16})`)
.call(xAxis);
},

yAxis: function () {
// Append Axis.
// Y axis
let yAx = $svg.selectAll('.y.axis')
.data([0]);

let yAxEnter = yAx.enter().append('g')
.attr('class', 'y axis');

yAxEnter.append('text')
.attr('class', 'label')
.attr('text-anchor', 'end');

yAxEnter.append('line')
.attr('class', 'line');

yAx.select('.label')
.attr('y', y(_axisLineVal) + margin.top)
.attr('x', _width + margin.left + margin.right)
.attr('dy', '1em')
.text(_axisLineVal + _dataUnitSuffix);

yAx.select('.line')
.attr('x1', 0)
.attr('y1', y(_axisLineVal) + margin.top)
.attr('x2', _width + margin.left + margin.right)
.attr('y2', y(_axisLineVal) + margin.top);
},

days: function () {
// Compute days for the days steps.
let dateCopyDay = d => {
let n = new Date(d.getTime());
n.setHours(0);
n.setMinutes(0);
n.setSeconds(0);
n.setMilliseconds(0);
return n;
};
let eDay = dateCopyDay(_.last(_data).timestep);
let dt = dateCopyDay(_data[0].timestep);
let daySteps = [dt];
while (true) {
dt = d3.time.day.offset(dateCopyDay(dt), 1);
daySteps.push(dt);
if (dt.getTime() >= eDay.getTime()) {
break;
}
}

let selection = $dataCanvas.selectAll('.days')
.data([0]);

selection.enter().append('g')
.attr('class', 'days');

let $days = selection.selectAll('.day-tick')
.data(daySteps);

$days.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('class', 'day-tick');

$days
.attr('x', d => x(d))
.attr('y', _height + margin.top)
.text(d => `${d.getDate()} ${months[d.getMonth()]}`);
}
};

upateSize = function () {
$svg
.attr('width', _width + margin.left + margin.right)
.attr('height', _height + margin.top + margin.bottom);

$dataCanvas
.attr('width', _width)
.attr('height', _height);

$svg.select('#clip rect')
.attr('width', _width + margin.left)
.attr('height', _height);

// Update scale ranges.
x.range([0, _width]);
y.range([_height, 0]);

// Recalculate the minX and zoom since scale changed.
minX = x(_data[0].timestep);
zoom.x(x);

// Redraw.
layers.line();
layers.minMax();
layers.days();
layers.xAxis();
layers.yAxis();
};

updateData = function () {
if (!_data || _pauseUpdate) {
return;
}

// Update scale domains.
let eDate = _.last(_data).timestep;
let sDate = d3.time.day.offset(eDate, -1);
x.domain([sDate, eDate]);

// Since the data is stacked the last element will contain the
// highest values)
y.domain([_axisValueMin, _axisValueMax]);

// Recalculate the minX and zoom since scale changed.
minX = x(_data[0].timestep);
zoom.x(x);

// Redraw.
layers.line();
layers.minMax();
layers.days();
layers.xAxis();
layers.yAxis();
};

// -----------------------------------------------------------------
// INIT.
$svg = $el.append('svg')
.attr('class', 'chart')
.style('display', 'block');

// Datacanvas
var $dataCanvas = $svg.append('g')
.attr('class', 'data-canvas')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

$svg.append('defs')
.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('x', -margin.left) // Compensate for the dataCanvas translate.
.attr('y', 0);

$svg
.attr('cursor', 'move')
.call(zoom)
.on('mousewheel.zoom', null)
.on('DOMMouseScroll.zoom', null);

zoom.on('zoom', function () {
// Bound translate.
let [tx, ty] = zoom.translate();
tx = Math.max(tx, 0);
tx = Math.min(tx, Math.abs(minX) - margin.right);
tx = Math.round(tx);
zoom.translate([tx, ty]);

layers.line();
layers.minMax();
layers.days();
layers.xAxis();
});

_calcSize();
upateSize();
updateData();
}

chartFn.checkSize = function () {
_calcSize();
upateSize();
return chartFn;
};

chartFn.destroy = function () {
// Cleanup.
};

// --------------------------------------------
// Getters and setters.
chartFn.data = function (d) {
if (!arguments.length) return _data;
_data = _.cloneDeep(d);
if (typeof updateData === 'function') updateData();
return chartFn;
};

chartFn.axisLineVal = function (d) {
if (!arguments.length) return _axisLineVal;
_axisLineVal = d;
if (typeof updateData === 'function') updateData();
return chartFn;
};

chartFn.axisValueMin = function (d) {
if (!arguments.length) return _axisValueMin;
_axisValueMin = d;
if (typeof updateData === 'function') updateData();
return chartFn;
};

chartFn.axisValueMax = function (d) {
if (!arguments.length) return _axisValueMax;
_axisValueMax = d;
if (typeof updateData === 'function') updateData();
return chartFn;
};

chartFn.dataUnitSuffix = function (d) {
if (!arguments.length) return _dataUnitSuffix;
_dataUnitSuffix = d;
if (typeof updateData === 'function') updateData();
return chartFn;
};

chartFn.pauseUpdate = function () {
_pauseUpdate = true;
return chartFn;
};

chartFn.continueUpdate = function () {
_pauseUpdate = false;
if (typeof updateData === 'function') updateData();
return chartFn;
};

return chartFn;
};

+ 77
- 0
app/assets/scripts/components/sensor-widget.js View File

@@ -0,0 +1,77 @@
'use strict';
import React from 'react';
import ChartLine from './charts/chart-line';
import { numDisplay, formatDate } from '../utils/format';

var SensorWidget = React.createClass({
displayName: 'SensorWidget',

propTypes: {
fetching: React.PropTypes.bool,
fetched: React.PropTypes.bool,
className: React.PropTypes.string,
title: React.PropTypes.string,
lastReading: React.PropTypes.object,
avgs: React.PropTypes.object,
plotData: React.PropTypes.array,
axisLineVal: React.PropTypes.number,
axisLineMax: React.PropTypes.number,
axisLineMin: React.PropTypes.number,
unit: React.PropTypes.string
},

render: function () {
let {
className,
fetching, fetched,
title,
lastReading, avgs,
plotData,
axisLineVal, axisLineMax, axisLineMin,
unit } = this.props;

if (!fetched && !fetching) {
return null;
}

return (
<article className={'card ' + className}>
<header className='card__header'>
<div className='card__headline'>
<h1 className='card__title'>{title} {fetching ? '...' : null}</h1>
<dl className='stats'>
<dd className='stats__label'>Last update</dd>
<dt className='stats__date'>{lastReading !== null ? formatDate(lastReading.timestep) : '--'}</dt>
<dd className='stats__label'>Current temperature</dd>
<dt className='stats__value'>{lastReading !== null ? numDisplay(lastReading.value, 1) : '--'}{unit}</dt>
</dl>
</div>
</header>
<div className='card__body'>
<div className='infographic'>
{plotData.length ? (
<div className='line-chart-wrapper'>
<ChartLine
className='line-chart'
axisLineVal={axisLineVal}
axisLineMax={axisLineMax}
axisLineMin={axisLineMin}
dataUnitSuffix={unit}
data={plotData} />
</div>
) : null}
{!plotData.length && fetching ? <p className='card__loading'>Loading Data...</p> : null}
</div>
<div className='metrics'>
<ul className='metrics__list'>
<li><strong>{avgs !== null ? numDisplay(avgs.today, 1, unit) : '--'}</strong> avg today</li>
<li><strong>{avgs !== null ? numDisplay(avgs.yesterday, 1, unit) : '--'}</strong> avg yesterday</li>
</ul>
</div>
</div>
</article>
);
}
});

module.exports = SensorWidget;

+ 30
- 0
app/assets/scripts/config.js View File

@@ -0,0 +1,30 @@
'use strict';
var _ = require('lodash');
/*
* App configuration.
*
* Uses settings in config/production.js, with any properties set by
* config/staging.js or config/local.js overriding them depending upon the
* environment.
*
* This file should not be modified. Instead, modify one of:
*
* - config/production.js
* Production settings (base).
* - config/staging.js
* Overrides to production if ENV is staging.
* - config/local.js
* Overrides if local.js exists.
* This last file is gitignored, so you can safely change it without
* polluting the repo.
*/

var configurations = require('./config/*.js', {mode: 'hash'});
var config = configurations.local || {};

if (process.env.DS_ENV === 'staging') {
_.defaultsDeep(config, configurations.staging);
}
_.defaultsDeep(config, configurations.production);

module.exports = config;

+ 17
- 0
app/assets/scripts/config/production.js View File

@@ -0,0 +1,17 @@
'use strict';
/*
* App config for production.
*/
module.exports = {
environment: 'production',
api: 'http://opensensemap.org:8000',
senseBox: {
id: '570629b945fd40c8197462fb',
'sensorId--uv': '570629b945fd40c8197462fd',
'sensorId--luminosity': '570629b945fd40c8197462fe',
'sensorId--pressure': '570629b945fd40c8197462ff',
'sensorId--humidity': '570629b945fd40c819746300',
'sensorId--temperature': '570629b945fd40c819746301'
}
};


+ 8
- 0
app/assets/scripts/config/staging.js View File

@@ -0,0 +1,8 @@
'use strict';
/*
* App config overrides for staging.
*/

module.exports = {
environment: 'staging'
};

+ 48
- 0
app/assets/scripts/main.js View File

@@ -0,0 +1,48 @@
'use strict';
import 'babel-polyfill';
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { Router, Route, IndexRoute, useRouterHistory } from 'react-router';
import { createHashHistory } from 'history';
import { createStore, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { syncHistory } from 'react-router-redux';
import reducer from './reducers/reducer';

// import UhOh from './views/uhoh';
import App from './views/app';
import Home from './views/home';

const appHistory = useRouterHistory(createHashHistory)({ queryKey: false });

// Sync dispatched route actions to the history
const reduxRouterMiddleware = syncHistory(appHistory);
const finalCreateStore = compose(
applyMiddleware(reduxRouterMiddleware, thunkMiddleware)
)(createStore);

const store = finalCreateStore(reducer);

render((
<Provider store={store}>
<Router history={appHistory}>
<Route path='*' component={App}>
<IndexRoute component={Home}/>
</Route>
</Router>
</Provider>
), document.querySelector('#site-canvas'));

// render((
// <Provider store={store}>
// <Router history={appHistory}>
// <Route path='/' component={App}>
// <IndexRoute component={Home}/>
// </Route>
// <Route path='*' component={App}>
// <IndexRoute component={UhOh}/>
// </Route>
// </Router>
// </Provider>
// ), document.querySelector('#site-canvas'));

+ 40
- 0
app/assets/scripts/reducers/reducer.js View File

@@ -0,0 +1,40 @@
import _ from 'lodash';
import { combineReducers } from 'redux';
import { routeReducer } from 'react-router-redux';
import * as actions from '../actions/action-types';

const sensorReducerFactory = function (sensor) {
return function (state = {fetching: false, fetched: false, data: null}, action) {
let s = sensor.toUpperCase();
switch (action.type) {
case actions[`REQUEST_SENSOR_DATA_${s}`]:
console.log(`REQUEST_SENSOR_DATA_${s}`);
state = _.cloneDeep(state);
state.fetching = true;
break;
case actions[`RECEIVE_SENSOR_DATA_${s}`]:
console.log(`RECEIVE_SENSOR_DATA_${s}`);
state = _.cloneDeep(state);
state.data = action.data;
state.fetching = false;
state.fetched = true;
break;
}
return state;
};
};

const sensorUv = sensorReducerFactory('uv');
const sensorLuminosity = sensorReducerFactory('luminosity');
const sensorPressure = sensorReducerFactory('pressure');
const sensorHumidity = sensorReducerFactory('humidity');
const sensorTemperature = sensorReducerFactory('temperature');

export default combineReducers({
routing: routeReducer,
sensorUv,
sensorLuminosity,
sensorPressure,
sensorHumidity,
sensorTemperature
});

+ 22
- 0
app/assets/scripts/utils/format.js View File

@@ -0,0 +1,22 @@
'use strict';
module.exports.numDisplay = function (n, dec = 2, suffix = '', nan = '--') {
if (isNaN(n)) {
return nan;
}
let s = n.toString();
s = (s.indexOf('.') === -1) ? s : s.substr(0, s.indexOf('.') + dec + 1);
return s + suffix;
};

module.exports.formatDate = function (date) {
let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
let hour = date.getHours();
hour = hour < 10 ? `0${hour}` : hour;
let minute = date.getMinutes();
minute = minute < 10 ? `0${minute}` : minute;
return `${months[date.getMonth()]} ${date.getDate()}, ${hour}:${minute}`;
};

module.exports.round = function (n, dec = 2) {
return +module.exports.numDisplay(n, dec);
};

+ 33
- 0
app/assets/scripts/views/app.js View File

@@ -0,0 +1,33 @@
'use strict';
import React from 'react';

var App = React.createClass({
displayName: 'App',

propTypes: {
dispatch: React.PropTypes.func,
children: React.PropTypes.object
},

render: function () {
return (
<div>
<header className='site-header' role='banner'>
<div className='inner'>
<div className='site-headline'>
<h1 className='site-title'>Devseed Sense Lisbon
{/* <a href='/' title='Visit homepage'>Glacial Inferno</a> */}
</h1>
</div>
</div>
</header>

<main className='site-body' role='main'>
{this.props.children}
</main>
</div>
);
}
});

module.exports = App;

+ 263
- 0
app/assets/scripts/views/home.js View File

@@ -0,0 +1,263 @@
'use strict';
import React from 'react';
import { connect } from 'react-redux';
import _ from 'lodash';
import SensorWidget from '../components/sensor-widget';
import { fetchSensorData } from '../actions/action-creators';
import { round } from '../utils/format';

const getTime = function (str) {
let date = new Date(str);
return Math.floor(date.getTime() / 1000);
};

const sensorProps = React.PropTypes.shape({
fetched: React.PropTypes.bool,
fetching: React.PropTypes.bool,
data: React.PropTypes.array
});

var Home = React.createClass({
displayName: 'Home',

propTypes: {
_requestSensorData: React.PropTypes.func,
sensorUv: sensorProps,
sensorLuminosity: sensorProps,
sensorPressure: sensorProps,
sensorHumidity: sensorProps,
sensorTemperature: sensorProps
},

// Having measurements every minute is too much. Group them.
// Max seconds between reading for them to be considered part
// of the same group.
_mTimeThreshold: 120,
_mGroupSize: 6, // Equal to 10 measurements per hour.

_fetchInterval: null,
// In seconds.
_fetchRate: 300, // 5 min

prepareData: function (rawData) {
var data = null;

if (rawData) {
data = [];
rawData[0].value = +rawData[0].value;
let bucket = [rawData[0]];
for (var i = 1; i < rawData.length; i++) {
rawData[i].value = +rawData[i].value;
let prevTime = getTime(rawData[i - 1].createdAt);
let currTime = getTime(rawData[i].createdAt);
// Having measurements every minute is too much. Group them.
// To make sure that the grouped measurements are all around the same
// time there can't be more than X seconds difference between them.
if (currTime - prevTime > this._mTimeThreshold || bucket.length === this._mGroupSize) {
let f = {
createdAt: _.last(bucket).createdAt,
value: +round(_.meanBy(bucket, 'value'))
};
data.push(f);
bucket = [];
}
bucket.push(rawData[i]);
}
// After the loop finished there may still be data to process.
// if bucket.length < this._mGroupSize for example.
let f = {
createdAt: _.last(bucket).createdAt,
value: +round(_.meanBy(bucket, 'value'))
};
data.push(f);
}

let startToday = new Date();
startToday.setHours(0);
startToday.setMinutes(0);
startToday.setSeconds(0);

startToday = Math.floor(startToday.getTime() / 1000);
let startYesterday = startToday - (60 * 60 * 24);

let dataAll = [];
let dataToday = [];
let dataYesterday = [];

_.forEach(data, o => {
let date = new Date(o.createdAt);
let time = Math.floor(date.getTime() / 1000);
dataAll.push({
timestep: date,
value: +o.value
});
if (time >= startToday) {
dataToday.push({
timestep: date,
value: +o.value
});
}
if (time < startToday && time >= startYesterday) {
dataYesterday.push({
timestep: date,
value: +o.value
});
}
});

let avgs = {
today: _.meanBy(dataToday, 'value'),
yesterday: _.meanBy(dataYesterday, 'value')
};

let last = _.last(dataAll) || null;

return {
data: dataAll,
last,
avgs
};
},

fetchData: function () {
let daysAgo3 = (new Date()).getTime() - (60 * 60 * 24 * 3 * 1000);
daysAgo3 = new Date(daysAgo3).toISOString();
this.props._requestSensorData('temperature', daysAgo3);
this.props._requestSensorData('humidity', daysAgo3);
this.props._requestSensorData('uv', daysAgo3);
this.props._requestSensorData('luminosity', daysAgo3);
this.props._requestSensorData('pressure', daysAgo3);
},

componentDidMount: function () {
this.fetchData();
this._fetchInterval = setInterval(() => {
this.fetchData();
}, this._fetchRate * 1000);
},

componentWillUnmount: function () {
if (this._fetchInterval) {
clearInterval(this._fetchInterval);
}
},

render: function () {
let sensorTemperatureData = this.prepareData(this.props.sensorTemperature.data);
let sensorHumidityData = this.prepareData(this.props.sensorHumidity.data);
let sensorUvData = this.prepareData(this.props.sensorUv.data);
let sensorLuminosityData = this.prepareData(this.props.sensorLuminosity.data);
let sensorPressureData = this.prepareData(this.props.sensorPressure.data);

return (
<section className='page'>
<header className='page__header'>
<div className='inner'>
<div className='page__headline'>
<h1 className='page__title'>Sense Dashboard</h1>
</div>
</div>
</header>
<div className='page__body'>

<section className='page__content'>
<div className='inner'>

<SensorWidget
className='card--temp'
fetching={this.props.sensorTemperature.fetching}
fetched={this.props.sensorTemperature.fetched}
title='Temperature'
lastReading={sensorTemperatureData.last}
avgs={sensorTemperatureData.avgs}
plotData={sensorTemperatureData.data}
axisLineMax={35}
axisLineVal={20}
axisLineMin={4}
unit=' ºC'
/>

<SensorWidget
className='card--hum'
fetching={this.props.sensorHumidity.fetching}
fetched={this.props.sensorHumidity.fetched}
title='Humidity'
lastReading={sensorHumidityData.last}
avgs={sensorHumidityData.avgs}
plotData={sensorHumidityData.data}
axisLineMax={100}
axisLineVal={50}
axisLineMin={10}
unit=' %'
/>

<SensorWidget
className='card--uv'
fetching={this.props.sensorUv.fetching}
fetched={this.props.sensorUv.fetched}
title='Uv light'
lastReading={sensorUvData.last}
avgs={sensorUvData.avgs}
plotData={sensorUvData.data}
axisLineMax={5000}
axisLineVal={250}
axisLineMin={0}
unit=' μW/cm²'
/>

<SensorWidget
className='card--lux'
fetching={this.props.sensorLuminosity.fetching}
fetched={this.props.sensorLuminosity.fetched}
title='Luminosity'
lastReading={sensorLuminosityData.last}
avgs={sensorLuminosityData.avgs}
plotData={sensorLuminosityData.data}
axisLineMax={135000}
axisLineVal={50000}
axisLineMin={0}
unit=' lx'
/>

<SensorWidget
className='card--press'
fetching={this.props.sensorPressure.fetching}
fetched={this.props.sensorPressure.fetched}
title='Air Pressure'
lastReading={sensorPressureData.last}
avgs={sensorPressureData.avgs}
plotData={sensorPressureData.data}
axisLineMax={1020}
axisLineVal={1010}
axisLineMin={1000}
unit=' hPa'
/>

</div>
</section>
</div>
</section>
);
}
});

// /////////////////////////////////////////////////////////////////// //
// Connect functions

function selector (state) {
return {
sensorUv: state.sensorUv,
sensorLuminosity: state.sensorLuminosity,
sensorPressure: state.sensorPressure,
sensorHumidity: state.sensorHumidity,
sensorTemperature: state.sensorTemperature
};
}

function dispatcher (dispatch) {
return {
_requestSensorData: (sensor, toDate) => dispatch(fetchSensorData(sensor, toDate))
};
}

module.exports = connect(selector, dispatcher)(Home);

+ 29
- 0
app/assets/scripts/views/uhoh.js View File

@@ -0,0 +1,29 @@
'use strict';
import React from 'react';

var UhOh = React.createClass({
displayName: 'UhOh',

render: function () {
return (
<section className='page'>
<header className='page__header'>
<div className='inner'>
<div className='page__headline'>
<h1 className='page-title'>404 Not found</h1>
</div>
</div>
</header>
<div className='page__body'>
<div className='inner'>
<div className='page__content'>
<p>UhOh that is a bummer.</p>
</div>
</div>
</div>
</section>
);
}
});

module.exports = UhOh;

+ 282
- 0
app/assets/styles/_base.scss View File

@@ -0,0 +1,282 @@
/* ==========================================================================
Base
========================================================================== */

html {
box-sizing: border-box;
font-size: 16px;
}

*, *:before, *:after, input[type="search"] {
box-sizing: inherit;
}

html, body {
height: 100%;
}

body {
background: #fff;
color: $base-font-color;
font-size: 1rem;
line-height: 1.5;
font-family: $base-font-family;
font-weight: $base-font-weight;
font-style: $base-font-style;
min-width: $row-min-width;
}


/* Links
========================================================================== */

a {
cursor: pointer;
color: $link-color;
text-decoration: none;
transition: opacity 0.24s ease 0s;
}

a:visited {
color: $link-color;
}

a:hover {
opacity: 0.64;
outline: none;
}

a:active {
outline: none;
transform: translate(0, 1px);
}


/* Rows
========================================================================== */

.row {
@extend .clearfix;
padding-left: $global-spacing;
padding-right: $global-spacing;
@include media(small-up) {
padding-left: $global-spacing * 2;
padding-right: $global-spacing * 2;
}
@include media(xlarge-up) {
padding-left: $global-spacing * 4;
padding-right: $global-spacing * 4;
}
}

.row--centered {
max-width: $row-max-width;
margin-left: auto;
margin-right: auto;
}


/* ==========================================================================
Structure
========================================================================== */


/* Header
========================================================================== */

.site-header {
position: absolute;
width: 100%;
z-index: 1000;
background-color: #fff;
color: $base-color;
padding: $global-spacing 0;
box-shadow: inset 0 -1px 0 0 rgba($base-color, 0.12);
> .inner {
// @extend .row, .row--centered;
padding: 0 2rem;
}
@include media(medium-up) {
padding: ($global-spacing * 2) 0;
}
}

/* Headline */
.page__header {
padding-top: 5rem;
@include media(medium-up) {
padding-top: 8rem;
}
}

.page__title {
@extend .visually-hidden;
}

.site-headline {
@include col(12/12);
@include media(medium-up) {
@include col(6/12);
}
}

.site-title {
float: left;
margin: 0;
line-height: 1;
font-size: 1.25rem;
text-transform: uppercase;

@include media(medium-up) {
font-size: 1.75rem;
}

a {
display: block;
}
* {
vertical-align: top;
display: inline-block;
}
img {
width: auto;
height: 1rem;
}
span {
@extend .visually-hidden;
}
}

/* Body
========================================================================== */

.page__content {
> .inner {
// @extend .row, .row--centered;
padding: 0 2rem;
}
}

.card {
background: #fff;
box-shadow: 0 4px 0 0 rgba($base-color, 0.08), 0 0 0 1px $base-alpha-color;
border-radius: $global-radius;
position: relative;
overflow: hidden;
margin-bottom: 2rem;

@include media(large-up) {
@include col(6/12, $cycle: 2);
}
@include media(xlarge-up) {
@include col(4/12, $cycle: 3, $uncycle: 2);
}

&__header {
@extend .antialiased;
position: absolute;
padding: $global-spacing;
color: #fff;
top: 0;
right: 0;
left: 0;
}

&__title {
padding-right: 3rem;
font-size: 1.375rem;
margin-bottom: 0.5rem;
text-transform: uppercase;
}

&__loading {
padding: 3rem 0;
text-align: center;
color: #fff;
text-transform: uppercase;
}

.infographic {
padding: 5rem 1rem 1rem 1rem;
}

.stats {
&__label {
@extend .visually-hidden;
}

&__value {
position: absolute;
top: 1rem;
right: 1rem;
font-size: 1.375rem;
font-weight: $base-font-bold;
}

&__date {
font-size: 0.875rem;
line-height: 1rem;
color: rgba(#fff, 0.80);
&:before {
font-size: 1rem;
vertical-align: top;
margin-right: 0.5rem;
@extend %collecticons-clock;
}
}
}

.metrics {
padding: 1rem;

&__list {
@extend .clearfix;
li {
@include col(6/12);
text-align: center;

&:not(:last-child) {
box-shadow: 1px 0 0 0 rgba($base-color, 0.16);
}
}

strong {
font-size: 1.625rem;
display: block;
}
}
}

&.card--temp {
.infographic {
background: linear-gradient(#FF5722, #E64A19);
}
}

&.card--hum {
.infographic {
background: linear-gradient(#009688, #0097A7);
}
}

&.card--uv {
.infographic {
background: linear-gradient(#7B1FA2, #9C27B0);
}
}

&.card--lux {
.infographic {
background: linear-gradient(#FFA000, #FFC107);
}
}

&.card--press {
.infographic {
background: linear-gradient(#616161, #607D8B);
}
}
}

/* Footer
========================================================================== */

+ 45
- 0
app/assets/styles/_charts.scss View File

@@ -0,0 +1,45 @@
/* Footer
========================================================================== */
svg.chart {
// Avoids that the chart grows because of the gap left by inline elements.
display: block;
}

.line-chart-wrapper {
min-height: 10rem;
}

.line-chart {
.data-line {
stroke: #fff;
stroke-width: 2px;
fill: none;
}

.edge {
font-size: 0.75rem;
fill: rgba(#fff, 0.8);
}

.days {
font-size: 0.75rem;
fill: rgba(#fff, 0.8);
}

.x.axis text {
font-size: 0.75rem;
fill: rgba(#fff, 0.8);
}

.y.axis {
.line {
stroke: rgba(#fff, 0.8);
stroke-dasharray: 4 4;
}
.label {
@extend .antialiased;
fill: rgba(#fff, 0.8);
font-size: 0.75rem;
}
}
}

+ 24
- 0
app/assets/styles/_functions.scss View File

@@ -0,0 +1,24 @@
/* ==========================================================================
Functions
========================================================================== */

/* Range function
========================================================================== */

/**
* Define ranges for various things, like media queries.
*/

@function lower-bound($range){
@if length($range) <= 0 {
@return 0;
}
@return nth($range,1);
}

@function upper-bound($range) {
@if length($range) < 2 {
@return 999999999999;
}
@return nth($range, 2);
}

+ 143
- 0
app/assets/styles/_mixins.scss View File

@@ -0,0 +1,143 @@
/* ==========================================================================
Media queries
========================================================================== */

@mixin media($arg) {
@if $arg == screen {
@media #{$screen} { @content; }
}
@if $arg == landscape {
@media #{$screen} and (orientation: landscape) { @content; }
}
@if $arg == portrait {
@media #{$screen} and (orientation: portrait) { @content; }
}
@if $arg == xsmall-up {
@media #{$screen} and (min-width: lower-bound($xsmall-range)) { @content; }
}
@if $arg == xsmall-only {
@media #{$screen} and (max-width: upper-bound($xsmall-range)) { @content; }
}
@if $arg == small-up {
@media #{$screen} and (min-width: lower-bound($small-range)) { @content; }
}
@if $arg == small-only {
@media #{$screen} and (min-width: lower-bound($small-range)) and (max-width: upper-bound($small-range)) { @content; }
}
@if $arg == medium-up {
@media #{$screen} and (min-width: lower-bound($medium-range)) { @content; }
}
@if $arg == medium-down {
@media #{$screen} and (max-width: upper-bound($medium-range)) { @content; }
}
@if $arg == medium-only {
@media #{$screen} and (min-width: lower-bound($medium-range)) and (max-width: upper-bound($medium-range)) { @content; }
}
@if $arg == large-up {
@media #{$screen} and (min-width: lower-bound($large-range)) { @content; }
}
@if $arg == large-only {
@media #{$screen} and (min-width: lower-bound($large-range)) and (max-width: upper-bound($large-range)) { @content; }
}
@if $arg == xlarge-up {
@media #{$screen} and (min-width: lower-bound($xlarge-range)) { @content; }
}
@if $arg == xlarge-only {
@media #{$screen} and (min-width: lower-bound($xlarge-range)) and (max-width: upper-bound($xlarge-range)) { @content; }
}
@if $arg == xxlarge-up {
@media #{$screen} and (min-width: lower-bound($xxlarge-range)) { @content; }
}
}


/* ==========================================================================
Typography
========================================================================== */

@mixin heading($font-size, $max-media: small-up) {
font-size: $font-size;
line-height: $font-size + 0.5;

@if $max-media == medium-up or $max-media == large-up or $max-media == xlarge-up {
@include media(medium-up) {
$font-size: $font-size + 0.25;
font-size: $font-size;
line-height: $font-size + 0.5;
}
}

@if $max-media == large-up or $max-media == xlarge-up {
@include media(large-up) {
$font-size: $font-size + 0.25;
font-size: $font-size;
line-height: $font-size + 0.5;
}
}

@if $max-media == xlarge-up {
@include media(xlarge-up) {
$font-size: $font-size + 0.25;
font-size: $font-size;
line-height: $font-size + 0.5;
}
}
}


/* ==========================================================================
Buttons
========================================================================== */

@mixin button-variation($style, $color) {
@if $style == "filled" {
background-color: $color;
box-shadow: inset 0 0 0 1px rgba($base-color, 0.16);
&, &:visited {
color: #fff;
}
&:hover {
background-color: shade($color, 8%);
}
.drop--open > &,
&.button--active,
&.button--active:hover,
&:active {
background-color: shade($color, 16%);
}
}
@else if $style == "outline" {
background-color: rgba($color, 0);
box-shadow: inset 0 0 0 1px $color;
&, &:visited {
color: $color;
}
&:hover {
background-color: rgba($color, 0.08);
}
.open > &,
&.active,
&.active:hover,
&:active {
background-color: rgba($color, 0.16);
}
}
@else if $style == "unbounded" {
background-color: rgba($color, 0);
&, &:visited {
color: $color;
}
&:hover {
background-color: rgba($color, 0.08);
}
.open > &,
&.active,
&.active:hover,
&:active {
color: shade($color, 48%);
}
}
@else {
@error "Invalid style property for button variation.";
}
}

+ 425
- 0
app/assets/styles/_normalize.scss View File

@@ -0,0 +1,425 @@
/*! normalize.css v3.0.1 | MIT License | git.io/normalize */

/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/

html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}

/**
* Remove default margin.
*/

body {
margin: 0;
}

/* HTML5 display definitions
========================================================================== */

/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/

article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
nav,
section,
summary {
display: block;
}

/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/

audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}

/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/

audio:not([controls]) {
display: none;
height: 0;
}

/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/

[hidden],
template {
display: none;
}

/* Links
========================================================================== */

/**
* Remove the gray background color from active links in IE 10.
*/

a {
background: transparent;
}

/**
* Improve readability when focused and also mouse hovered in all browsers.
*/

a:active,
a:hover {
outline: 0;
}

/* Text-level semantics
========================================================================== */

/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/

abbr[title] {
border-bottom: 1px dotted;
}

/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/

b,
strong {
font-weight: bold;
}

/**
* Address styling not present in Safari and Chrome.
*/

dfn {
font-style: italic;
}

/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/

// h1 {
// font-size: 2em;
// margin: 0.67em 0;
// }

/**
* Address styling not present in IE 8/9.
*/

mark {
background: #ff0;
color: #000;
}

/**
* Address inconsistent and variable font size in all browsers.
*/

small {
font-size: 80%;
}

/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/

sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}

sup {
top: -0.5em;
}

sub {
bottom: -0.25em;
}

/* Embedded content
========================================================================== */

/**
* Remove border when inside `a` element in IE 8/9/10.
*/

img {
border: 0;
}

/**
* Correct overflow not hidden in IE 9/10/11.
*/

svg:not(:root) {
overflow: hidden;
}

/* Grouping content
========================================================================== */

/**
* Address margin not present in IE 8/9 and Safari.
*/

figure {
margin: 1em 40px;
}

/**
* Address differences between Firefox and other browsers.
*/

hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}

/**
* Contain overflow in all browsers.
*/

pre {
overflow: auto;
}

/**
* Address odd `em`-unit font size rendering in all browsers.
*/

code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}

/* Forms
========================================================================== */

/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/