diff --git a/spikes/graphs-matrix/epoch/.babelrc b/spikes/graphs-matrix/epoch/.babelrc
new file mode 100644
index 00000000..82cc857a
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/.babelrc
@@ -0,0 +1,15 @@
+{
+ "presets": [
+ "react",
+ "es2015"
+ ],
+ "plugins": [
+ ["transform-object-rest-spread", {
+ "useBuiltIns": true
+ }],
+ "add-module-exports",
+ "transform-es2015-modules-commonjs",
+ "react-hot-loader/babel"
+ ],
+ "sourceMaps": "both"
+}
diff --git a/spikes/graphs-matrix/epoch/.eslintignore b/spikes/graphs-matrix/epoch/.eslintignore
new file mode 100644
index 00000000..683e721c
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/.eslintignore
@@ -0,0 +1,3 @@
+/node_modules
+coverage
+.nyc_output
diff --git a/spikes/graphs-matrix/epoch/.eslintrc b/spikes/graphs-matrix/epoch/.eslintrc
new file mode 100644
index 00000000..19bd88dc
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/.eslintrc
@@ -0,0 +1,29 @@
+{
+ "extends": "semistandard",
+ "parser": "babel-eslint",
+ "parserOptions": {
+ "ecmaVersion": 7,
+ "ecmaFeatures": {
+ "jsx": true
+ },
+ "sourceType": "module"
+ },
+ "plugins": [
+ "babel",
+ "react"
+ ],
+ "rules": {
+ "generator-star-spacing": 0,
+ "babel/generator-star-spacing": 1,
+ "space-before-function-paren": [2, "never"],
+ "react/jsx-uses-react": 2,
+ "react/jsx-uses-vars": 2,
+ "react/react-in-jsx-scope": 2,
+ "object-curly-newline": ["error", {
+ "minProperties": 1
+ }],
+ "sort-vars": ["error", {
+ "ignoreCase": true
+ }]
+ }
+}
\ No newline at end of file
diff --git a/spikes/graphs-matrix/epoch/.gitignore b/spikes/graphs-matrix/epoch/.gitignore
new file mode 100644
index 00000000..c21b0ba0
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/.gitignore
@@ -0,0 +1,4 @@
+/node_modules
+coverage
+.nyc_output
+npm-debug.log
diff --git a/spikes/graphs-matrix/epoch/client/actions.js b/spikes/graphs-matrix/epoch/client/actions.js
new file mode 100644
index 00000000..4136fe1e
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/client/actions.js
@@ -0,0 +1,64 @@
+const takeRight = require('lodash.takeright');
+
+const actions = {
+ 'UPDATE_STATS': (state, action) => {
+ const data = (state[action.subscription] || []).concat([action.payload]);
+
+ return {
+ ...state,
+ [action.subscription]: takeRight(data, 50)
+ };
+ }
+};
+
+module.exports = (state, action) => {
+ return !actions[action.type] ? state : actions[action.type](state, action);
+};
+
+module.exports.subscribe = (id) => (dispatch, getState) => {
+ const {
+ ws
+ } = getState();
+
+ const p = new Promise((resolve, reject) => {
+ ws.subscribe(`/stats/${id}`, (update, flag) => {
+ dispatch({
+ type: 'UPDATE_STATS',
+ payload: update,
+ subscription: id
+ });
+ }, (err) => {
+ if (err) {
+ return reject(err);
+ }
+
+ resolve();
+ });
+ });
+
+ return dispatch({
+ action: 'SUBSCRIBE',
+ payload: p
+ });
+};
+
+module.exports.unsubscribe = (id) => (dispatch, getState) => {
+ const {
+ ws
+ } = getState();
+
+ const p = new Promise((resolve, reject) => {
+ ws.unsubscribe(`/stats/${id}`, null, (err) => {
+ if (err) {
+ return reject(err);
+ }
+
+ resolve();
+ });
+ });
+
+ return dispatch({
+ action: 'UNSUBSCRIBE',
+ payload: p
+ });
+};
\ No newline at end of file
diff --git a/spikes/graphs-matrix/epoch/client/epoch.js b/spikes/graphs-matrix/epoch/client/epoch.js
new file mode 100644
index 00000000..daac3937
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/client/epoch.js
@@ -0,0 +1,63 @@
+// injects into `window` (ikr)
+require('epoch-charting');
+
+const ReactRedux = require('react-redux');
+const React = require('react');
+
+const {
+ Time: {
+ Bar
+ }
+} = window.Epoch;
+
+const {
+ connect
+} = ReactRedux;
+
+const style = {
+ height: '220px'
+};
+
+module.exports = React.createClass({
+ ref: function(name) {
+ this._refs = this._refs || {};
+
+ return (el) => {
+ this._refs[name] = el;
+ };
+ },
+ fromData: function(data) {
+ return (data || []).map((d) => {
+ return {
+ y: d.cpu,
+ time: d.when
+ };
+ });
+ },
+ componentDidMount: function() {
+ this.chart = new Bar({
+ el: this._refs.component,
+ type: 'time.bar',
+ data: [{
+ label: 'A',
+ values: []
+ }]
+ });
+ },
+ componentWillReceiveProps: function(nextProps) {
+ this.fromData(this.props.data).forEach((r) => this.chart.push([r]));
+ },
+ render: function() {
+ const className = (this.props.median > 50)
+ ? 'red'
+ : 'blue';
+
+ return (
+
+ );
+ }
+});
\ No newline at end of file
diff --git a/spikes/graphs-matrix/epoch/client/index.js b/spikes/graphs-matrix/epoch/client/index.js
new file mode 100644
index 00000000..34d1748c
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/client/index.js
@@ -0,0 +1,37 @@
+const ReactDOM = require('react-dom');
+const React = require('react');
+const Store = require('./store');
+const nes = require('nes/dist/client');
+
+const {
+ Client
+} = nes;
+
+const client = new Client(`ws://${document.location.host}`);
+
+client.connect((err) => {
+ if (err) {
+ throw err;
+ }
+
+ console.log('connected');
+});
+
+const store = Store({
+ ws: client
+});
+
+const render = () => {
+ const Root = require('./root');
+
+ ReactDOM.render(
+ ,
+ document.getElementById('root')
+ );
+};
+
+render();
+
+if (module.hot) {
+ module.hot.accept('./root', render);
+}
diff --git a/spikes/graphs-matrix/epoch/client/matrix.js b/spikes/graphs-matrix/epoch/client/matrix.js
new file mode 100644
index 00000000..ae2ca2b8
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/client/matrix.js
@@ -0,0 +1,95 @@
+const React = require('react');
+const buildArray = require('build-array');
+const ReactRedux = require('react-redux');
+const Epoch = require('./epoch');
+const actions = require('./actions');
+
+const {
+ connect
+} = ReactRedux;
+
+const {
+ subscribe,
+ unsubscribe
+} = actions;
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ data: state[ownProps.id]
+ };
+};
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+ return {
+ subscribe: () => {
+ return dispatch(subscribe(ownProps.id));
+ },
+ unsubscribe: () => {
+ return unsubscribe(ownProps.id);
+ }
+ }
+};
+
+const Graph = connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(React.createClass({
+ componentWillMount: function() {
+ this.props.subscribe();
+ },
+ componentWillUnmount: function() {
+ this.props.unsubscribe();
+ },
+ render: function() {
+ const {
+ data = []
+ } = this.props;
+
+ const median = data.reduce((sum, v) => (sum + v.cpu), 0) / data.length;
+
+ const bg = median > 50
+ ? 'rgba(205, 54, 54, 0.3)'
+ : 'rgba(54, 74, 205, 0.3)';
+
+ const shadow = median > 50
+ ? 'inset 0 1px 0 0 rgba(248, 51, 51, 0.5)'
+ : 'inset 0 1px 0 0 rgba(54, 73, 205, 0.5)';
+
+ return (
+
+ );
+ }
+}));
+
+module.exports = ({
+ x,
+ y
+}) => {
+ const m = buildArray(y).map((v, i) => {
+ const m = buildArray(x).map((v, y) => {
+ const id = `${i}${y}`;
+ return (
+
+
+
+ );
+ });
+
+ return (
+
+ {m}
+
+ );
+ });
+
+ return (
+
+ {m}
+
+ );
+};
diff --git a/spikes/graphs-matrix/epoch/client/root.js b/spikes/graphs-matrix/epoch/client/root.js
new file mode 100644
index 00000000..58534619
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/client/root.js
@@ -0,0 +1,24 @@
+const React = require('react');
+const ReactHotLoader = require('react-hot-loader');
+const ReactRedux = require('react-redux');
+const Matrix = require('./matrix');
+
+const {
+ AppContainer
+} = ReactHotLoader;
+
+const {
+ Provider
+} = ReactRedux;
+
+module.exports = ({
+ store
+}) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/spikes/graphs-matrix/epoch/client/store.js b/spikes/graphs-matrix/epoch/client/store.js
new file mode 100644
index 00000000..452af806
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/client/store.js
@@ -0,0 +1,19 @@
+const createLogger = require('redux-logger');
+const promiseMiddleware = require('redux-promise-middleware').default;
+const thunk = require('redux-thunk').default;
+const redux = require('redux');
+const reducer = require('./actions');
+
+const {
+ createStore,
+ compose,
+ applyMiddleware
+} = redux;
+
+module.exports = (state = Object.freeze({})) => {
+ return createStore(reducer, state, applyMiddleware(
+ createLogger(),
+ promiseMiddleware(),
+ thunk
+ ));
+};
diff --git a/spikes/graphs-matrix/epoch/package.json b/spikes/graphs-matrix/epoch/package.json
new file mode 100644
index 00000000..f3c2dfbb
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/package.json
@@ -0,0 +1,62 @@
+{
+ "name": "epoch-graphing-spike",
+ "private": true,
+ "license": "private",
+ "main": "server/index.js",
+ "dependencies": {
+ "autoprefixer": "^6.5.1",
+ "babel-eslint": "^7.0.0",
+ "babel-loader": "^6.2.5",
+ "babel-plugin-add-module-exports": "^0.2.1",
+ "babel-plugin-transform-es2015-modules-commonjs": "^6.16.0",
+ "babel-plugin-transform-object-rest-spread": "^6.16.0",
+ "babel-plugin-transform-runtime": "^6.15.0",
+ "babel-preset-es2015": "^6.16.0",
+ "babel-preset-react": "^6.16.0",
+ "babel-preset-react-hmre": "^1.1.1",
+ "babel-runtime": "^6.11.6",
+ "build-array": "^1.0.0",
+ "classnames": "^2.2.5",
+ "component-emitter": "^1.2.1",
+ "css-loader": "^0.25.0",
+ "d3": "^4.3.0",
+ "epoch-charting": "^0.8.4",
+ "hapi": "^15.2.0",
+ "hapi-webpack-dev-plugin": "^1.1.4",
+ "inert": "^4.0.2",
+ "lodash.takeright": "^4.1.1",
+ "nes": "^6.3.1",
+ "postcss-loader": "^1.0.0",
+ "postcss-modules-values": "^1.2.2",
+ "postcss-nested": "^1.0.0",
+ "react": "^15.3.2",
+ "react-dom": "^15.3.2",
+ "react-hot-loader": "^3.0.0-beta.6",
+ "react-redux": "^4.4.5",
+ "redux": "^3.6.0",
+ "redux-logger": "^2.7.4",
+ "redux-promise-middleware": "^4.1.0",
+ "redux-thunk": "^2.1.0",
+ "require-dir": "^0.3.1",
+ "style-loader": "^0.13.1",
+ "webpack": "^1.13.2",
+ "webpack-dev-server": "^1.16.2"
+ },
+ "devDependencies": {
+ "babel-register": "^6.16.3",
+ "eslint": "^3.8.1",
+ "eslint-config-semistandard": "^7.0.0",
+ "eslint-config-standard": "^6.2.0",
+ "eslint-plugin-babel": "^3.3.0",
+ "eslint-plugin-promise": "^3.3.0",
+ "eslint-plugin-react": "^6.4.1",
+ "eslint-plugin-standard": "^2.0.1",
+ "json-loader": "^0.5.4"
+ },
+ "ava": {
+ "require": [
+ "babel-register"
+ ],
+ "babel": "inherit"
+ }
+}
diff --git a/spikes/graphs-matrix/epoch/readme.md b/spikes/graphs-matrix/epoch/readme.md
new file mode 100644
index 00000000..5172d589
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/readme.md
@@ -0,0 +1,15 @@
+# Epoch
+
+ - [x] Graphs should maintain aspect ration
+ - [ ] Graphs should match Antonas' first draft designs
+ - [x] Should have 3 Graphs on each row
+ - [x] Should be a 3 x 4 matrix of graphs, showing different data
+ - [x] Graphs should not jitter, ideally smoothly move across the x axis
+ - [x] All graphs should be a bar graph
+ - [ ] Animations when a graph comes into view
+
+## notes
+
+ - Epoch is not responsive. Even though they maintain aspect ratio, using a responsive grid they get cluttered between each other
+ - With short update intervals, the graphs start using to much cpu and can't handle it
+ - Even looking at the [documentation](https://epochjs.github.io/epoch/styles), it's not obvious how styling works and I wasn't able to make it work.
diff --git a/spikes/graphs-matrix/epoch/server/index.js b/spikes/graphs-matrix/epoch/server/index.js
new file mode 100644
index 00000000..b2a6a529
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/server/index.js
@@ -0,0 +1,29 @@
+const requireDir = require('require-dir');
+const plugins = require('./plugins');
+const routes = requireDir('./routes');
+const Hapi = require('hapi');
+const path = require('path');
+const fs = require('fs');
+
+const server = new Hapi.Server();
+
+server.connection({
+ host: 'localhost',
+ port: 8000
+});
+
+server.register(plugins, (err) => {
+ if (err) {
+ throw err;
+ }
+
+ Object.keys(routes).forEach((name) => {
+ routes[name](server);
+ });
+
+ server.start((err) => {
+ server.connections.forEach((conn) => {
+ console.log(`started at: ${conn.info.uri}`);
+ });
+ });
+});
diff --git a/spikes/graphs-matrix/epoch/server/metric.js b/spikes/graphs-matrix/epoch/server/metric.js
new file mode 100644
index 00000000..0d64fad2
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/server/metric.js
@@ -0,0 +1,34 @@
+const Emitter = require('component-emitter');
+
+const cdm = {};
+
+module.exports = (server) => ({
+ on: (id) => {
+ console.log('on', cdm[id]);
+ if (cdm[id] && (cdm[id].sockets > 0)) {
+ cdm[id].sockets +=1;
+ return;
+ }
+
+
+ let messageId = 0;
+ const interval = setInterval(() => {
+ console.log(`publishing /stats/${id}`);
+
+ server.publish(`/stats/${id}`, {
+ when: new Date().getTime(),
+ cpu: Math.random() * 100
+ });
+ }, 400);
+
+ cdm[id] = {
+ interval,
+ sockets: 1
+ };
+ },
+ off: (id) => {
+ if (!(cdm[id].sockets -= 1)) {
+ clearInterval(cdm[id].interval);
+ }
+ }
+});
diff --git a/spikes/graphs-matrix/epoch/server/plugins.js b/spikes/graphs-matrix/epoch/server/plugins.js
new file mode 100644
index 00000000..84944329
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/server/plugins.js
@@ -0,0 +1,15 @@
+const webpack = require('webpack');
+const path = require('path');
+
+const cfg = require('../webpack.config.js');
+
+module.exports = [
+ require('inert'),
+ require('nes'), {
+ register: require('hapi-webpack-dev-plugin'),
+ options: {
+ compiler: webpack(cfg),
+ devIndex: path.join(__dirname, '../static')
+ }
+ }
+];
diff --git a/spikes/graphs-matrix/epoch/server/routes/home.js b/spikes/graphs-matrix/epoch/server/routes/home.js
new file mode 100644
index 00000000..48ead969
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/server/routes/home.js
@@ -0,0 +1,11 @@
+const path = require('path');
+
+module.exports = (server) => {
+ server.route({
+ method: 'GET',
+ path: '/',
+ handler: (request, reply) => {
+ reply.file(path.join(__dirname, '../../static/index.html'));
+ }
+ });
+};
diff --git a/spikes/graphs-matrix/epoch/server/routes/metrics.js b/spikes/graphs-matrix/epoch/server/routes/metrics.js
new file mode 100644
index 00000000..e6e1c261
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/server/routes/metrics.js
@@ -0,0 +1,18 @@
+const Metric = require('../metric');
+
+module.exports = (server) => {
+ const metric = Metric(server);
+
+ server.subscription('/stats/{id}', {
+ onSubscribe: (socket, path, params, next) => {
+ console.log('onSubscribe');
+ metric.on(params.id);
+ next();
+ },
+ onUnsubscribe: (socket, path, params, next) => {
+ console.log('onUnsubscribe');
+ metric.off(params.id);
+ next();
+ }
+ });
+};
diff --git a/spikes/graphs-matrix/epoch/server/routes/static.js b/spikes/graphs-matrix/epoch/server/routes/static.js
new file mode 100644
index 00000000..5a41f602
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/server/routes/static.js
@@ -0,0 +1,15 @@
+const path = require('path');
+
+module.exports = (server) => {
+ // server.route({
+ // method: 'GET',
+ // path: '/{param*}',
+ // handler: {
+ // directory: {
+ // path: path.join(__dirname, '../../static'),
+ // redirectToSlash: true,
+ // index: true
+ // }
+ // }
+ // });
+};
diff --git a/spikes/graphs-matrix/epoch/server/routes/version.js b/spikes/graphs-matrix/epoch/server/routes/version.js
new file mode 100644
index 00000000..987747cb
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/server/routes/version.js
@@ -0,0 +1,18 @@
+const Pkg = require('../../package.json');
+
+const internals = {
+ response: {
+ version: Pkg.version
+ }
+};
+
+module.exports = (server) => {
+ server.route({
+ method: 'GET',
+ path: '/ops/version',
+ config: {
+ description: 'Returns the version of the server',
+ handler: (request, reply) => reply(internals.response)
+ }
+ });
+};
diff --git a/spikes/graphs-matrix/epoch/static/index.html b/spikes/graphs-matrix/epoch/static/index.html
new file mode 100644
index 00000000..fd29e69d
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/static/index.html
@@ -0,0 +1,983 @@
+
+
+
+ React Boilerplate
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spikes/graphs-matrix/epoch/webpack.config.js b/spikes/graphs-matrix/epoch/webpack.config.js
new file mode 100644
index 00000000..792e8f4d
--- /dev/null
+++ b/spikes/graphs-matrix/epoch/webpack.config.js
@@ -0,0 +1,65 @@
+const webpack = require('webpack');
+const path = require('path');
+
+const config = {
+ debug: true,
+ devtool: 'source-map',
+ context: path.join(__dirname, './client'),
+ app: path.join(__dirname, './client/index.js'),
+ entry: [
+ 'webpack-dev-server/client?http://localhost:8080',
+ 'webpack/hot/only-dev-server',
+ 'react-hot-loader/patch',
+ './index.js'
+ ],
+ output: {
+ path: path.join(__dirname, './static'),
+ publicPath: '/static/',
+ filename: 'bundle.js'
+ },
+ plugins: [
+ new webpack.HotModuleReplacementPlugin(),
+ new webpack.NoErrorsPlugin(),
+ new webpack.ProvidePlugin({
+ 'd3': 'd3'
+ })
+ ],
+ module: {
+ loaders: [{
+ test: /js?$/,
+ exclude: /node_modules/,
+ include: [
+ path.join(__dirname, './client')
+ ],
+ loaders: ['babel']
+ }, {
+ test: /\.json?$/,
+ exclude: /node_modules/,
+ include: [
+ path.join(__dirname, './client')
+ ],
+ loaders: ['json']
+ }, {
+ test: /\.css$/,
+ exclude: /node_modules/,
+ include: [
+ path.join(__dirname, './client')
+ ],
+ loader: 'style-loader!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader'
+ }]
+ }
+};
+
+const devServer = {
+ hot: true,
+ compress: true,
+ lazy: false,
+ publicPath: config.output.publicPath,
+ historyApiFallback: {
+ index: './static/index.html'
+ }
+};
+
+module.exports = Object.assign({
+ devServer
+}, config);