@ -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
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
* text=auto
|
@ -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
|
@ -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}
|
@ -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.
|
@ -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
|
||||
```
|
@ -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>
|
After Width: | Height: | Size: 703 B |
@ -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>
|
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 9.4 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 34 KiB |
@ -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'));
|
||||
});
|
||||
};
|
||||
}
|
@ -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';
|
@ -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;
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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'
|
||||
}
|
||||
};
|
||||
|
@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
/*
|
||||
* App config overrides for staging.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
environment: 'staging'
|
||||
};
|
@ -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'));
|
@ -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
|
||||
});
|
@ -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);
|
||||
};
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -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
|
||||
========================================================================== */
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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.";
|
||||
}
|
||||
}
|
@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. Correct color not being inherited.
|
||||
* Known issue: affects color of disabled elements.
|
||||
* 2. Correct font properties not being inherited.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
button {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||
* Correct `select` style inheritance in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and border in Firefox 4+.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended that you don't attempt to style these elements.
|
||||
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||
*
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||
* 2. Remove excess padding in IE 8/9/10.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||
* `font-size` values of the `input`, it causes the cursor style of the
|
||||
* decrement button to change from `default` to `text`.
|
||||
*/
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
|
||||
* (include `-moz` to future-proof).
|
||||
*/
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
-moz-box-sizing: content-box;
|
||||
-webkit-box-sizing: content-box; /* 2 */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||
* padding (and `textfield` appearance).
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't inherit the `font-weight` (applied by a rule above).
|
||||
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||
*/
|
||||
|
||||
optgroup {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tables
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Reset ==============================================================
|
||||
* Based on http://meyerweb.com/eric/tools/css/reset
|
||||
*/
|
||||
|
||||
html,
|
||||
body,
|
||||
|
||||
/* Structures */
|
||||
div,
|
||||
span,
|
||||
applet,
|
||||
object,
|
||||
iframe,
|
||||
|
||||
/* Text */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
blockquote,
|
||||
pre,
|
||||
a,
|
||||
abbr,
|
||||
acronym,
|
||||
address,
|
||||
big,
|
||||
cite,
|
||||
code,
|
||||
del,
|
||||
dfn,
|
||||
em,
|
||||
font,
|
||||
img,
|
||||
ins,
|
||||
kbd,
|
||||
q,
|
||||
s,
|
||||
samp,
|
||||
small,
|
||||
strike,
|
||||
strong,
|
||||
sub,
|
||||
sup,
|
||||
tt,
|
||||
var,
|
||||
b,
|
||||
u,
|
||||
i,
|
||||
center,
|
||||
|
||||
/* Lists */
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ol,
|
||||
ul,
|
||||
li,
|
||||
|
||||
/* Forms */
|
||||
fieldset,
|
||||
form,
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
label,
|
||||
legend,
|
||||
|
||||
/* Tables */
|
||||
table,
|
||||
caption,
|
||||
tbody,
|
||||
tfoot,
|
||||
thead,
|
||||
tr,
|
||||
th,
|
||||
td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
background: transparent;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
.item-list ul,
|
||||
.item-list ul li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
blockquote,
|
||||
q { quotes: none; }
|
||||
|
||||
blockquote:before,
|
||||
blockquote:after,
|
||||
q:before, q:after {
|
||||
content:'';
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* remember to define focus styles! */
|
||||
:focus { outline: 0; }
|
||||
|
||||
/* remember to highlight inserts somehow! */
|
||||
ins { text-decoration: none; }
|
||||
del { text-decoration: line-through; }
|
||||
|
||||
button,
|
||||
html input[type=button],
|
||||
input[type=reset],
|
||||
input[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
height: auto;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
font: inherit;
|
||||
height: auto;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
legend,
|
||||
label {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/* ==========================================================================
|
||||
Utils
|
||||
========================================================================== */
|
||||
|
||||
/* Font smoothing
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Antialiased font smoothing works best for light text on a dark background.
|
||||
* Apply to single elements instead of globally to body.
|
||||
* Note this only applies to webkit-based desktop browsers and Firefox 25 (and later) on the Mac.
|
||||
*/
|
||||
|
||||
.antialiased {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Truncated text
|
||||
========================================================================== */
|
||||
|
||||
.truncated {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Hidden content
|
||||
========================================================================== */
|
||||
|
||||
/* Hide from both screenreaders and browsers */
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Hide only visually, but have it available for screenreaders */
|
||||
|
||||
.visually-hidden {
|
||||
border: 0 none;
|
||||
clip: rect(0px, 0px, 0px, 0px);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the .visually-hidden class to allow the element
|
||||
* to be focusable when navigated to via the keyboard
|
||||
*/
|
||||
|
||||
.visually-hidden.focusable:active,
|
||||
.visually-hidden.focusable:focus {
|
||||
clip: auto;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: static;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Undo visually-hidden */
|
||||
|
||||
.visually-hidden-undo {
|
||||
position: inherit;
|
||||
overflow: visible;
|
||||
height: auto;
|
||||
width: auto;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* Hide visually and from screenreaders, but maintain layout */
|
||||
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* Clearfix
|
||||
========================================================================== */
|
||||
|
||||
.clearfix {
|
||||
&:before,
|
||||
&:after {
|
||||
content: " ";
|
||||
display: table;
|
||||
}
|
||||
&:after {
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Disabled
|
||||
========================================================================== */
|
||||
|
||||
.disabled {
|
||||
opacity: 0.48;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/* ==========================================================================
|
||||
Colors
|
||||
========================================================================== */
|
||||
$base-color: #2c3e50; // Midnight blue
|
||||
$primary-color: #16a085; // Belize Hole
|
||||
$secondary-color: #3498db; // Purple
|
||||
// $tertiary-color: #ffffff; // Undefined
|
||||
|
||||
$danger-color: #ea4f54; // Red
|
||||
$success-color: $primary-color; // lime
|
||||
$warning-color: #f6d55f; // Yellow
|
||||
$info-color: #008CBA; // Blue
|
||||
|
||||
$link-color: $primary-color;
|
||||
$base-alpha-color: rgba($base-color, 0.12);
|
||||
|
||||
|
||||
/* ==========================================================================
|
||||
Typography
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* $base-* refers to the base styles when there's nothing else to override them.
|
||||
*/
|
||||
$base-font-color: $base-color;
|
||||
$base-font-family: "Exo 2", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
$base-font-style: normal;
|
||||
$base-font-light: 300;
|
||||
$base-font-regular: 400;
|
||||
$base-font-bold: 700;
|
||||
$base-font-weight: $base-font-regular;
|
||||
|
||||
$code-font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
|
||||
|
||||
/* ==========================================================================
|
||||
Decoration
|
||||
========================================================================== */
|
||||
|
||||
/* Border radius */
|
||||
|
||||
$global-radius: 0.25rem;
|
||||
$semi-radius: $global-radius/2;
|
||||
$global-rounded: 60rem;
|
||||
|
||||
|
||||
/* ==========================================================================
|
||||
Spacing and media queries
|
||||
========================================================================== */
|
||||
|
||||
/* Rows */
|
||||
|
||||
$row-min-width: 320px;
|
||||
$row-max-width: 1280px;
|
||||
$jeet-max-width: $row-max-width; // reset jeet.gs max width
|
||||
|
||||
/* Media queries */
|
||||
|
||||
$xsmall-range: (0, 543px);
|
||||
$small-range: (544px, 767px);
|
||||
$medium-range: (768px, 991px);
|
||||
$large-range: (992px, 1199px);
|
||||
$xlarge-range: (1200px);
|
||||
|
||||
$screen: "only screen";
|
||||
|
||||
/* Spacing */
|
||||
|
||||
$global-spacing: 1rem;
|
||||
$section-spacing: 4rem;
|
@ -0,0 +1,55 @@
|
||||
/* ==========================================================================
|
||||
Main
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Following Necolas' Principles of writing consistent, idiomatic CSS:
|
||||
* https://github.com/necolas/idiomatic-css
|
||||
*/
|
||||
|
||||
/* Charset */
|
||||
|
||||
@charset "UTF-8";
|
||||
|
||||
|
||||
/* Libraries
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Using Bourbon for extended SASS functionality:
|
||||
* http://bourbon.io/
|
||||
*/
|
||||
|
||||
@import "bourbon";
|
||||
|
||||
/**
|
||||
* Using Jeet, a grid system designed for humans:
|
||||
* http://jeet.gs/
|
||||
*/
|
||||
|
||||
@import "jeet/index";
|
||||
|
||||
|
||||
/* Reboot
|
||||
========================================================================== */
|
||||
|
||||
@import "normalize";
|
||||
@import "reset";
|
||||
|
||||
/* Settings
|
||||
========================================================================== */
|
||||
|
||||
@import "variables";
|
||||
@import "functions";
|
||||
@import "mixins";
|
||||
|
||||
|
||||
/* Main
|
||||
========================================================================== */
|
||||
|
||||
@import "collecticons";
|
||||
@import "charts";
|
||||
@import "base";
|
||||
/* Utilities */
|
||||
|
||||
@import "utils";
|
@ -0,0 +1,63 @@
|
||||
<!doctype html>
|
||||
<!--[if lt IE 10]> <html class="lt-ie10 no-js" lang=""> <![endif]-->
|
||||
<!--[if gt IE 9]><!--> <html class="no-js" lang=""> <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="description" content="Devseed sense dashboards" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
|
||||
<title>Devseed Sense - Lisbon</title>
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@developmentseed" />
|
||||
<meta name="twitter:title" content="Devseed Sense - Lisbon">
|
||||
<meta name="twitter:description" content="Devseed sense dashboards." />
|
||||
<meta name="twitter:image:src" content="assets/graphics/meta/default-meta-image.png" /
|
||||
<!--/ Twitter -->
|
||||
|
||||
<!-- OG -->
|
||||
<meta property="og:site_name" content="Devseed Sense - Lisbon" />
|
||||
<meta property="og:title" content="Devseed Sense - Lisbon" />
|
||||
<meta property="og:url" content="" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:description" content="Devseed sense dashboards." />
|
||||
<meta property="og:image" content="assets/graphics/meta/default-meta-image.png" />
|
||||
<!--/ OG -->
|
||||
|
||||
<link rel="shortcut icon" href="assets/graphics/meta/favicon.ico" type="image/x-icon" />
|
||||
<link rel="apple-touch-icon" href="assets/graphics/meta/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="assets/graphics/meta/apple-touch-icon-57x57.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="assets/graphics/meta/apple-touch-icon-72x72.png" />
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="assets/graphics/meta/apple-touch-icon-76x76.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="assets/graphics/meta/apple-touch-icon-114x114.png" />
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="assets/graphics/meta/apple-touch-icon-120x120.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="assets/graphics/meta/apple-touch-icon-144x144.png" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="assets/graphics/meta/apple-touch-icon-152x152.png" />
|
||||
|
||||
<!-- build:css assets/styles/main.css -->
|
||||
<link rel="stylesheet" href="assets/styles/main.css">
|
||||
<!-- endbuild -->
|
||||
<link rel="author" type="text/plain" href="humans.txt" />
|
||||
|
||||
<link href='https://fonts.googleapis.com/css?family=Exo+2:400,400italic,300italic,300,700,700italic' rel='stylesheet' type='text/css'>
|
||||
</head>
|
||||
<body>
|
||||
<!--[if lt IE 10]>
|
||||
<div id="nocando">
|
||||
<h1>No can do!</h1>
|
||||
<p>You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
</div>
|
||||
<![endif]-->
|
||||
|
||||
<div id="site-canvas"><!-- Content goes inside #site-canvas --></div>
|
||||
|
||||
<!-- build:js assets/scripts/vendor.js -->
|
||||
<script src="assets/scripts/vendor.js"></script>
|
||||
<!-- endbuild -->
|
||||
<!-- build:js assets/scripts/bundle.js -->
|
||||
<script src="assets/scripts/bundle.js"></script>
|
||||
<!-- endbuild -->
|
||||
</body>
|
||||
</html>
|
After Width: | Height: | Size: 95 KiB |
@ -0,0 +1,251 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var gulp = require('gulp');
|
||||
var $ = require('gulp-load-plugins')();
|
||||
var del = require('del');
|
||||
var browserSync = require('browser-sync');
|
||||
var reload = browserSync.reload;
|
||||
var watchify = require('watchify');
|
||||
var browserify = require('browserify');
|
||||
var source = require('vinyl-source-stream');
|
||||
var buffer = require('vinyl-buffer');
|
||||
var sourcemaps = require('gulp-sourcemaps');
|
||||
var gutil = require('gulp-util');
|
||||
var exit = require('gulp-exit');
|
||||
var rev = require('gulp-rev');
|
||||
var revReplace = require('gulp-rev-replace');
|
||||
var notifier = require('node-notifier');
|
||||
var cp = require('child_process');
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// --------------------------- Variables -------------------------------------//
|
||||
// ---------------------------------------------------------------------------//
|
||||
|
||||
// The package.json
|
||||
var pkg;
|
||||
|
||||
// Environment
|
||||
// Set the correct environment, which controls what happens in config.js
|
||||
if (!process.env.DS_ENV) {
|
||||
if (!process.env.TRAVIS_BRANCH || process.env.TRAVIS_BRANCH !== process.env.DEPLOY_BRANCH) {
|
||||
process.env.DS_ENV = 'staging';
|
||||
} else {
|
||||
process.env.DS_ENV = 'production';
|
||||
}
|
||||
}
|
||||
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// ------------------------- Helper functions --------------------------------//
|
||||
// ---------------------------------------------------------------------------//
|
||||
|
||||
function readPackage () {
|
||||
pkg = JSON.parse(fs.readFileSync('package.json'));
|
||||
}
|
||||
readPackage();
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// ------------------------- Callable tasks ----------------------------------//
|
||||
// ---------------------------------------------------------------------------//
|
||||
|
||||
gulp.task('default', ['clean'], function () {
|
||||
gulp.start('build');
|
||||
});
|
||||
|
||||
gulp.task('serve', ['vendorScripts', 'javascript', 'styles', 'fonts'], function () {
|
||||
browserSync({
|
||||
port: 1337,
|
||||
server: {
|
||||
baseDir: ['.tmp', 'app'],
|
||||
routes: {
|
||||
'/node_modules': './node_modules'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// watch for changes
|
||||
gulp.watch([
|
||||
'app/*.html',
|
||||
'app/assets/graphics/**/*',
|
||||
'.tmp/assets/fonts/**/*'
|
||||
]).on('change', reload);
|
||||
|
||||
gulp.watch('app/assets/styles/**/*.scss', ['styles']);
|
||||
gulp.watch('app/assets/fonts/**/*', ['fonts']);
|
||||
gulp.watch('package.json', ['vendorScripts']);
|
||||
});
|
||||
|
||||
gulp.task('clean', function () {
|
||||
return del(['.tmp', 'dist'])
|
||||
.then(function () {
|
||||
$.cache.clearAll();
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('build', ['vendorScripts', 'javascript', 'collecticons'], function () {
|
||||
gulp.start(['html', 'images', 'fonts', 'extras'], function () {
|
||||
return gulp.src('dist/**/*')
|
||||
.pipe($.size({title: 'build', gzip: true}))
|
||||
.pipe(exit());
|
||||
});
|
||||
});
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// ------------------------- Browserify tasks --------------------------------//
|
||||
// ------------------- (Not to be called directly) ---------------------------//
|
||||
// ---------------------------------------------------------------------------//
|
||||
|
||||
// Compiles the user's script files to bundle.js.
|
||||
// When including the file in the index.html we need to refer to bundle.js not
|
||||
// main.js
|
||||
gulp.task('javascript', function () {
|
||||
var watcher = watchify(browserify({
|
||||
entries: ['./app/assets/scripts/main.js'],
|
||||
debug: true,
|
||||
cache: {},
|
||||
packageCache: {},
|
||||
fullPaths: true
|
||||
}));
|
||||
|
||||
function bundler () {
|
||||
if (pkg.dependencies) {
|
||||
watcher.external(Object.keys(pkg.dependencies));
|
||||
}
|
||||
return watcher.bundle()
|
||||
.on('error', function (e) {
|
||||
notifier.notify({
|
||||
title: 'Oops! Browserify errored:',
|
||||
message: e.message
|
||||
});
|
||||
console.log('Javascript error:', e);
|
||||
// Allows the watch to continue.
|
||||
this.emit('end');
|
||||
})
|
||||
.pipe(source('bundle.js'))
|
||||
.pipe(buffer())
|
||||
// Source maps.
|
||||
.pipe(sourcemaps.init({loadMaps: true}))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest('.tmp/assets/scripts'))
|
||||
.pipe(reload({stream: true}));
|
||||
}
|
||||
|
||||
watcher
|
||||
.on('log', gutil.log)
|
||||
.on('update', bundler);
|
||||
|
||||
return bundler();
|
||||
});
|
||||
|
||||
// Vendor scripts. Basically all the dependencies in the package.js.
|
||||
// Therefore be careful and keep the dependencies clean.
|
||||
gulp.task('vendorScripts', function () {
|
||||
// Ensure package is updated.
|
||||
readPackage();
|
||||
var vb = browserify({
|
||||
debug: true,
|
||||
require: pkg.dependencies ? Object.keys(pkg.dependencies) : []
|
||||
});
|
||||
return vb.bundle()
|
||||
.on('error', gutil.log.bind(gutil, 'Browserify Error'))
|
||||
.pipe(source('vendor.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true}))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest('.tmp/assets/scripts/'))
|
||||
.pipe(reload({stream: true}));
|
||||
});
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// ------------------------- Collecticon tasks -------------------------------//
|
||||
// --------------------- (Font generation related) ---------------------------//
|
||||
// ---------------------------------------------------------------------------//
|
||||
gulp.task('collecticons', function (done) {
|
||||
var args = [
|
||||
'node_modules/collecticons-processor/bin/collecticons.js',
|
||||
'compile',
|
||||
'app/assets/graphics/collecticons/',
|
||||
'--font-embed',
|
||||
'--font-dest', 'app/assets/fonts',
|
||||
'--font-name', 'collecticons',
|
||||
'--font-types', 'woff',
|
||||
'--style-format', 'sass',
|
||||
'--style-dest', 'app/assets/styles/',
|
||||
'--style-name', 'collecticons',
|
||||
'--class-name', 'collecticons',
|
||||
'--no-standalone',
|
||||
'--no-preview'
|
||||
];
|
||||
|
||||
return cp.spawn('node', args, {stdio: 'inherit'})
|
||||
.on('close', done);
|
||||
});
|
||||
|
||||
// //////////////////////////////////////////////////////////////////////////////
|
||||
// --------------------------- Helper tasks -----------------------------------//
|
||||
// ----------------------------------------------------------------------------//
|
||||
|
||||
gulp.task('styles', function () {
|
||||
return gulp.src('app/assets/styles/main.scss')
|
||||
.pipe($.plumber(function (e) {
|
||||
notifier.notify({
|
||||
title: 'Oops! Sass errored:',
|
||||
message: e.message
|
||||
});
|
||||
console.log('Sass error:', e.toString());
|
||||
// Allows the watch to continue.
|
||||
this.emit('end');
|
||||
}))
|
||||
.pipe($.sourcemaps.init())
|
||||
.pipe($.sass({
|
||||
outputStyle: 'expanded',
|
||||
precision: 10,
|
||||
includePaths: ['.'].concat(require('node-bourbon').includePaths).concat(['node_modules/jeet/scss'])
|
||||
}))
|
||||
.pipe($.sourcemaps.write())
|
||||
.pipe(gulp.dest('.tmp/assets/styles'))
|
||||
.pipe(reload({stream: true}));
|
||||
});
|
||||
|
||||
gulp.task('html', ['styles'], function () {
|
||||
return gulp.src('app/*.html')
|
||||
.pipe($.useref({searchPath: ['.tmp', 'app', '.']}))
|
||||
.pipe($.if('*.js', $.uglify()))
|
||||
.pipe($.if('*.css', $.csso()))
|
||||
.pipe($.if(/\.(css|js)$/, rev()))
|
||||
.pipe(revReplace())
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('images', function () {
|
||||
return gulp.src('app/assets/graphics/**/*')
|
||||
.pipe($.cache($.imagemin({
|
||||
progressive: true,
|
||||
interlaced: true,
|
||||
// don't remove IDs from SVGs, they are often used
|
||||
// as hooks for embedding and styling
|
||||
svgoPlugins: [{cleanupIDs: false}]
|
||||
})))
|
||||
.pipe(gulp.dest('dist/assets/graphics'));
|
||||
});
|
||||
|
||||
gulp.task('fonts', function () {
|
||||
return gulp.src('app/assets/fonts/**/*')
|
||||
.pipe(gulp.dest('.tmp/assets/fonts'))
|
||||
.pipe(gulp.dest('dist/assets/fonts'));
|
||||
});
|
||||
|
||||
gulp.task('extras', function () {
|
||||
return gulp.src([
|
||||
'app/**/*',
|
||||
'!app/*.html',
|
||||
'!app/assets/graphics/**',
|
||||
'!app/assets/vendor/**',
|
||||
'!app/assets/styles/**',
|
||||
'!app/assets/scripts/**'
|
||||
], {
|
||||
dot: true
|
||||
}).pipe(gulp.dest('dist'));
|
||||
});
|
@ -0,0 +1,93 @@
|
||||
{
|
||||
"name": "devseed-sense",
|
||||
"version": "1.0.0",
|
||||
"description": "Devseed sense dashboards",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/developmentseed/sense"
|
||||
},
|
||||
"author": {
|
||||
"name": "Development Seed",
|
||||
"url": "https://developmentseed.org"
|
||||
},
|
||||
"license": "WTFPL",
|
||||
"bugs": {
|
||||
"url": "https://github.com/developmentseed/sense/issues"
|
||||
},
|
||||
"homepage": "http://sense.devseed.org",
|
||||
"scripts": {
|
||||
"postinstall": "[ -f app/assets/scripts/config/local.js ] || echo 'module.exports = {};' > app/assets/scripts/config/local.js",
|
||||
"serve": "gulp collecticons && gulp serve",
|
||||
"build": "NODE_ENV=production gulp",
|
||||
"lint": "eslint app/assets/scripts/ --ext .js",
|
||||
"test": "echo \"No tests\" && exit 0"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
[
|
||||
"babelify",
|
||||
{
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
],
|
||||
"require-globify",
|
||||
"envify"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel": "^6.5.2",
|
||||
"babel-preset-es2015": "^6.5.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babelify": "^7.2.0",
|
||||
"browser-sync": "^2.11.1",
|
||||
"browserify": "^13.0.0",
|
||||
"collecticons-processor": "^2.0.0",
|
||||
"del": "^2.2.0",
|
||||
"envify": "^3.4.0",
|
||||
"eslint": "^1.0.0",
|
||||
"eslint-config-semistandard": "^5.0.0",
|
||||
"eslint-config-standard": "^4.0.0",
|
||||
"eslint-plugin-promise": "^1.0.8",
|
||||
"eslint-plugin-react": "^3.16.1",
|
||||
"eslint-plugin-standard": "^1.3.2",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-cache": "^0.4.2",
|
||||
"gulp-csso": "^1.0.1",
|
||||
"gulp-exit": "0.0.2",
|
||||
"gulp-if": "^2.0.0",
|
||||
"gulp-imagemin": "^2.4.0",
|
||||
"gulp-load-plugins": "^1.2.0",
|
||||
"gulp-plumber": "^1.1.0",
|
||||
"gulp-rev": "^7.0.0",
|
||||
"gulp-rev-replace": "^0.4.3",
|
||||
"gulp-sass": "^2.2.0",
|
||||
"gulp-size": "^2.0.0",
|
||||
"gulp-sourcemaps": "^1.6.0",
|
||||
"gulp-uglify": "^1.5.2",
|
||||
"gulp-useref": "^3.0.5",
|
||||
"gulp-util": "^3.0.7",
|
||||
"jeet": "^6.1.2",
|
||||
"node-bourbon": "^4.2.3",
|
||||
"node-notifier": "^4.5.0",
|
||||
"require-globify": "^1.3.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"watchify": "^3.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-polyfill": "^6.3.14",
|
||||
"d3": "^3.5.16",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"lodash": "^4.10.0",
|
||||
"react": "^0.14.7",
|
||||
"react-dom": "^0.14.7",
|
||||
"react-redux": "^4.3.0",
|
||||
"react-router": "^2.0.0-rc5",
|
||||
"react-router-redux": "^2.0.4",
|
||||
"redux": "^3.2.1",
|
||||
"redux-thunk": "^1.0.3"
|
||||
}
|
||||
}
|