From 9d3903a1dbc82948ad4f5b4eed15aabb6438a4d5 Mon Sep 17 00:00:00 2001 From: Tom Gallacher Date: Fri, 2 Dec 2016 11:02:58 +0000 Subject: [PATCH 1/6] Implement d3 avg rendering in react This is an initial implement that at the moment constructs the required SVG layout to be used by D3. --- spikes/architecture/react-d3/.babelrc | 15 ++++ spikes/architecture/react-d3/.eslintignore | 3 + spikes/architecture/react-d3/.eslintrc | 30 ++++++++ spikes/architecture/react-d3/.gitignore | 4 + spikes/architecture/react-d3/client/graph.js | 26 +++++++ spikes/architecture/react-d3/client/index.js | 18 +++++ spikes/architecture/react-d3/client/links.js | 10 +++ spikes/architecture/react-d3/client/nodes.js | 75 +++++++++++++++++++ spikes/architecture/react-d3/client/root.js | 21 ++++++ .../react-d3/client/services.json | 15 ++++ spikes/architecture/react-d3/client/store.js | 17 +++++ spikes/architecture/react-d3/package.json | 56 ++++++++++++++ spikes/architecture/react-d3/readme.md | 9 +++ spikes/architecture/react-d3/server/index.js | 29 +++++++ .../architecture/react-d3/server/plugins.js | 15 ++++ .../react-d3/server/routes/home.js | 11 +++ .../react-d3/server/routes/static.js | 15 ++++ .../react-d3/server/routes/version.js | 18 +++++ .../architecture/react-d3/static/index.html | 11 +++ .../architecture/react-d3/webpack.config.js | 69 +++++++++++++++++ 20 files changed, 467 insertions(+) create mode 100644 spikes/architecture/react-d3/.babelrc create mode 100644 spikes/architecture/react-d3/.eslintignore create mode 100644 spikes/architecture/react-d3/.eslintrc create mode 100644 spikes/architecture/react-d3/.gitignore create mode 100644 spikes/architecture/react-d3/client/graph.js create mode 100644 spikes/architecture/react-d3/client/index.js create mode 100644 spikes/architecture/react-d3/client/links.js create mode 100644 spikes/architecture/react-d3/client/nodes.js create mode 100644 spikes/architecture/react-d3/client/root.js create mode 100644 spikes/architecture/react-d3/client/services.json create mode 100644 spikes/architecture/react-d3/client/store.js create mode 100644 spikes/architecture/react-d3/package.json create mode 100644 spikes/architecture/react-d3/readme.md create mode 100644 spikes/architecture/react-d3/server/index.js create mode 100644 spikes/architecture/react-d3/server/plugins.js create mode 100644 spikes/architecture/react-d3/server/routes/home.js create mode 100644 spikes/architecture/react-d3/server/routes/static.js create mode 100644 spikes/architecture/react-d3/server/routes/version.js create mode 100644 spikes/architecture/react-d3/static/index.html create mode 100644 spikes/architecture/react-d3/webpack.config.js diff --git a/spikes/architecture/react-d3/.babelrc b/spikes/architecture/react-d3/.babelrc new file mode 100644 index 00000000..82cc857a --- /dev/null +++ b/spikes/architecture/react-d3/.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/architecture/react-d3/.eslintignore b/spikes/architecture/react-d3/.eslintignore new file mode 100644 index 00000000..683e721c --- /dev/null +++ b/spikes/architecture/react-d3/.eslintignore @@ -0,0 +1,3 @@ +/node_modules +coverage +.nyc_output diff --git a/spikes/architecture/react-d3/.eslintrc b/spikes/architecture/react-d3/.eslintrc new file mode 100644 index 00000000..a1e892dc --- /dev/null +++ b/spikes/architecture/react-d3/.eslintrc @@ -0,0 +1,30 @@ +{ + "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 + }], + "operator-linebreak": 0 + } +} diff --git a/spikes/architecture/react-d3/.gitignore b/spikes/architecture/react-d3/.gitignore new file mode 100644 index 00000000..c21b0ba0 --- /dev/null +++ b/spikes/architecture/react-d3/.gitignore @@ -0,0 +1,4 @@ +/node_modules +coverage +.nyc_output +npm-debug.log diff --git a/spikes/architecture/react-d3/client/graph.js b/spikes/architecture/react-d3/client/graph.js new file mode 100644 index 00000000..ec911f23 --- /dev/null +++ b/spikes/architecture/react-d3/client/graph.js @@ -0,0 +1,26 @@ +const ReactRedux = require('react-redux'); +const React = require('react'); +const Links = require('./links'); +const Nodes = require('./nodes'); + +const { + connect +} = ReactRedux; + +const Component = (props) => + + + + ; + +const mapStateToProps = ({ + data +}) => { + return { + data + }; +}; + +module.exports = connect( + mapStateToProps +)(Component); diff --git a/spikes/architecture/react-d3/client/index.js b/spikes/architecture/react-d3/client/index.js new file mode 100644 index 00000000..317b0fd5 --- /dev/null +++ b/spikes/architecture/react-d3/client/index.js @@ -0,0 +1,18 @@ +const ReactDOM = require('react-dom'); +const React = require('react'); +const store = require('./store')(); + +const render = () => { + const Root = require('./root'); + + ReactDOM.render( + , + document.getElementById('root') + ); +}; + +render(); + +if (module.hot) { + module.hot.accept('./root', render); +} diff --git a/spikes/architecture/react-d3/client/links.js b/spikes/architecture/react-d3/client/links.js new file mode 100644 index 00000000..c629c3ab --- /dev/null +++ b/spikes/architecture/react-d3/client/links.js @@ -0,0 +1,10 @@ +const React = require('react'); + +const renderLines = (props) => { + return () => ; +}; + +module.exports = (props) => + + { props.data.links.map(renderLines()) } + ; diff --git a/spikes/architecture/react-d3/client/nodes.js b/spikes/architecture/react-d3/client/nodes.js new file mode 100644 index 00000000..1260025c --- /dev/null +++ b/spikes/architecture/react-d3/client/nodes.js @@ -0,0 +1,75 @@ +const React = require('react'); + +function rightRoundedRect(x, y, width, height, radius) { + return 'M' + x + ',' + y // Move to (absolute) + + 'h ' + (width - radius) // Horizontal line to (relative) + + 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + radius // Relative arc + + 'v ' + (height - 2 * radius) // Vertical line to (relative) + + 'a ' + radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + radius // Relative arch + + 'h ' + (radius - width) // Horizontal lint to (relative) + + 'z '; // path back to start +} + +function leftRoundedRect(x, y, width, height, radius) { + return 'M' + (x + width) + ',' + y // Move to (absolute) start at top-right + + 'v ' + height // Vertical line to (relative) + + 'h ' + (radius - width) // Horizontal line to (relative) + + 'a ' + radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + -radius // Relative arc + + 'v ' + -(height - 2 * radius) // Vertical line to (relative) + + 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + -radius // Relative arch + + 'z '; // path back to start +} + +const InfoBoxContainer = () => + + + + ; + +const InfoBoxAlert = () => + + + {'!'} + ; + +const InfoBoxText = (props) => + {props.id}; + +module.exports = (props) => ( + + { props.data.nodes.map(node => + + + + + + ) + } + +); diff --git a/spikes/architecture/react-d3/client/root.js b/spikes/architecture/react-d3/client/root.js new file mode 100644 index 00000000..2af05235 --- /dev/null +++ b/spikes/architecture/react-d3/client/root.js @@ -0,0 +1,21 @@ +const React = require('react'); +const ReactHotLoader = require('react-hot-loader'); +const ReactRedux = require('react-redux'); +const Graph = require('./graph'); + +const { + AppContainer +} = ReactHotLoader; + +const { + Provider +} = ReactRedux; + +module.exports = ({ + store +}) => + + + + + ; diff --git a/spikes/architecture/react-d3/client/services.json b/spikes/architecture/react-d3/client/services.json new file mode 100644 index 00000000..cc8be654 --- /dev/null +++ b/spikes/architecture/react-d3/client/services.json @@ -0,0 +1,15 @@ +{ + "nodes": [ + {"id": "Nginx", "group": 1}, + {"id": "Wordpress", "group": 1}, + {"id": "Memcached", "group": 1}, + {"id": "Percona", "group": 1}, + {"id": "NFS", "group": 1} + ], + "links": [ + {"source": "Nginx", "target": "Wordpress", "value": 1}, + {"source": "Wordpress", "target": "Memcached", "value": 8}, + {"source": "Wordpress", "target": "NFS", "value": 8}, + {"source": "Wordpress", "target": "Percona", "value": 8} + ] +} diff --git a/spikes/architecture/react-d3/client/store.js b/spikes/architecture/react-d3/client/store.js new file mode 100644 index 00000000..8e5612d5 --- /dev/null +++ b/spikes/architecture/react-d3/client/store.js @@ -0,0 +1,17 @@ +const redux = require('redux'); +const services = require('./services'); + +const { + createStore +} = redux; + +const reducer = (state, action) => { + return { + ...state, + data: services + }; +}; + +module.exports = (state = Object.freeze({})) => { + return createStore(reducer, state); +}; diff --git a/spikes/architecture/react-d3/package.json b/spikes/architecture/react-d3/package.json new file mode 100644 index 00000000..6e83ff22 --- /dev/null +++ b/spikes/architecture/react-d3/package.json @@ -0,0 +1,56 @@ +{ + "name": "chartjs-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", + "component-emitter": "^1.2.1", + "css-loader": "^0.25.0", + "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", + "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/architecture/react-d3/readme.md b/spikes/architecture/react-d3/readme.md new file mode 100644 index 00000000..fe0718a4 --- /dev/null +++ b/spikes/architecture/react-d3/readme.md @@ -0,0 +1,9 @@ +# ChartJS + +![](http://g.recordit.co/N8vdP8DBk4.gif) + +## summary + + - [x] customisable via js + - [x] fast (handles 100ms updates well) + - [x] easy to update data diff --git a/spikes/architecture/react-d3/server/index.js b/spikes/architecture/react-d3/server/index.js new file mode 100644 index 00000000..b2a6a529 --- /dev/null +++ b/spikes/architecture/react-d3/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/architecture/react-d3/server/plugins.js b/spikes/architecture/react-d3/server/plugins.js new file mode 100644 index 00000000..84944329 --- /dev/null +++ b/spikes/architecture/react-d3/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/architecture/react-d3/server/routes/home.js b/spikes/architecture/react-d3/server/routes/home.js new file mode 100644 index 00000000..48ead969 --- /dev/null +++ b/spikes/architecture/react-d3/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/architecture/react-d3/server/routes/static.js b/spikes/architecture/react-d3/server/routes/static.js new file mode 100644 index 00000000..5a41f602 --- /dev/null +++ b/spikes/architecture/react-d3/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/architecture/react-d3/server/routes/version.js b/spikes/architecture/react-d3/server/routes/version.js new file mode 100644 index 00000000..987747cb --- /dev/null +++ b/spikes/architecture/react-d3/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/architecture/react-d3/static/index.html b/spikes/architecture/react-d3/static/index.html new file mode 100644 index 00000000..422fc652 --- /dev/null +++ b/spikes/architecture/react-d3/static/index.html @@ -0,0 +1,11 @@ + + + + D3 React Boilerplate + + + +
+ + + diff --git a/spikes/architecture/react-d3/webpack.config.js b/spikes/architecture/react-d3/webpack.config.js new file mode 100644 index 00000000..7295526a --- /dev/null +++ b/spikes/architecture/react-d3/webpack.config.js @@ -0,0 +1,69 @@ +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() + ], + postcss: () => { + return [ + require('postcss-modules-values'), + require('postcss-nested'), + require('autoprefixer') + ]; + }, + 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); From 1f3c6c2e41383a8571039753b5d73503fc8dc4ed Mon Sep 17 00:00:00 2001 From: Tom Gallacher Date: Mon, 5 Dec 2016 17:42:38 +0000 Subject: [PATCH 2/6] New pass as d3 --- spikes/architecture/d3-revamp/README.md | 14 ++ spikes/architecture/d3-revamp/index.html | 245 ++++++++++++++++++++ spikes/architecture/d3-revamp/services.json | 15 ++ 3 files changed, 274 insertions(+) create mode 100644 spikes/architecture/d3-revamp/README.md create mode 100644 spikes/architecture/d3-revamp/index.html create mode 100644 spikes/architecture/d3-revamp/services.json diff --git a/spikes/architecture/d3-revamp/README.md b/spikes/architecture/d3-revamp/README.md new file mode 100644 index 00000000..9304b03b --- /dev/null +++ b/spikes/architecture/d3-revamp/README.md @@ -0,0 +1,14 @@ +# D3 graph layouts + +D3 v4, has a nice force directed graph set of features, that allows you to implement features in the graph with ease. By just manipulating the nodes using svg attributes and css. +This with the addition that D3 is a very well respected and widely used toolkit, makes finding documentation and developer speed a breeze. + +It does seem to slow down on larger nodes, but this is mainly due to the built in force directed animation, disabling this, or computing this in a web-worker can speed up things considerably. + +As a developer I found my self more productive with the API that d3 exposes, and was able to accomplish more in the same time as other frameworks. + +## Research + +- React http://formidable.com/blog/2015/05/21/react-d3-layouts/ + +![screenshot from 2016-11-04 16-48-29](https://cloud.githubusercontent.com/assets/524382/20059671/5ab66080-a4ef-11e6-94d6-d09bbaa9a76a.png) diff --git a/spikes/architecture/d3-revamp/index.html b/spikes/architecture/d3-revamp/index.html new file mode 100644 index 00000000..208ca463 --- /dev/null +++ b/spikes/architecture/d3-revamp/index.html @@ -0,0 +1,245 @@ + + + + + + diff --git a/spikes/architecture/d3-revamp/services.json b/spikes/architecture/d3-revamp/services.json new file mode 100644 index 00000000..cc8be654 --- /dev/null +++ b/spikes/architecture/d3-revamp/services.json @@ -0,0 +1,15 @@ +{ + "nodes": [ + {"id": "Nginx", "group": 1}, + {"id": "Wordpress", "group": 1}, + {"id": "Memcached", "group": 1}, + {"id": "Percona", "group": 1}, + {"id": "NFS", "group": 1} + ], + "links": [ + {"source": "Nginx", "target": "Wordpress", "value": 1}, + {"source": "Wordpress", "target": "Memcached", "value": 8}, + {"source": "Wordpress", "target": "NFS", "value": 8}, + {"source": "Wordpress", "target": "Percona", "value": 8} + ] +} From 62aef3eec6907873734d7e16fcb8b992faa1db95 Mon Sep 17 00:00:00 2001 From: Tom Gallacher Date: Tue, 6 Dec 2016 12:38:17 +0000 Subject: [PATCH 3/6] Implement multi-tier info boxes These use HTML inside of SVG, this allows us to style the SVG's much easier, it also enables us to reference the dom nodes from within react. --- spikes/architecture/d3-revamp/index.html | 330 +++++++++++++++++------ 1 file changed, 245 insertions(+), 85 deletions(-) diff --git a/spikes/architecture/d3-revamp/index.html b/spikes/architecture/d3-revamp/index.html index 208ca463..8e1d0307 100644 --- a/spikes/architecture/d3-revamp/index.html +++ b/spikes/architecture/d3-revamp/index.html @@ -7,7 +7,7 @@ stroke-opacity: 1; } -.health { +.health, .health_warn { font-family: LibreFranklin; font-size: 12px; font-weight: bold; @@ -16,6 +16,10 @@ text-align: center; } +.health_warn { + font-size: 15px; +} + .stat { font-family: LibreFranklin; font-size: 12px; @@ -25,6 +29,29 @@ line-height: 1.5; } +.node_statistics { + font-family: LibreFranklin; + font-size: 12px; + font-weight: normal; + font-style: normal; + font-stretch: normal; + line-height: 1.5; +} + +.node_statistics p { + margin: 0 0 0 0; + color: rgba(255, 255, 255, 0.8); +} + +.primary, .secondary { + font-family: LibreFranklin; + font-size: 12px; + font-weight: normal; + font-style: normal; + font-stretch: normal; + line-height: 1.5; +} + @@ -97,6 +124,211 @@ function rect(x, y, width, height, radius) { d3.json('services.json', function(error, graph) { if (error) throw error; + function createNode(elm) { + // Box where label will live + elm.append('path') + .attr('class', 'node') + .attr('d', topRoundedRect('0', '0', 170, 47, 4)) + .attr('stroke', '#343434') + .attr('stroke-width', '1px') + .attr('fill', '#464646') + + // Hover-over text for a node's label. + var text = elm.append('g') + + text.append('text') + .attr('class', 'info_text') + .attr('x', '12') + .attr('y', '30') + .attr('text-anchor', 'start') + .attr('fill', '#fff') + .text(d => d.id) + + text.append('circle') + .attr('class', 'alert') + .attr('cx', function () { return d3.select(this.parentNode).select('.info_text').node().getBBox().width + 30 }) + .attr('cy', '24') + .attr('stroke-width', '0px') + .attr('fill', (d) => d.id == 'Memcached' ? 'rgb(217, 77, 68)' : 'rgb(0,175,102)') + .attr('r', '9px') + + // An icon or label that exists within the circle, inside the infobox + text.append('text') + .attr('class', 'health') + .attr('x', function () { return d3.select(this.parentNode).select('.info_text').node().getBBox().width + 30 }) + .attr('y', '29') + .attr('text-anchor', 'middle') + .attr('fill', '#fff') + .text((d) => d.id == 'Memcached' ? '!' : '❤') + + // Box where stats will live + var stats = elm.append('g'); + + stats.append('path') + .attr('class', 'node') + .attr('d', (d) => d.id == 'Percona' ? rect('0', '-39', 170, 78, 2) : bottomRoundedRect('0', '-39', 170, 78, 4)) + .attr('stroke', '#343434') + .attr('stroke-width', '1px') + .attr('fill', '#464646') + + // An icon or label that exists within the circle, inside the infobox + stats.append('text') + .attr('class', 'cpu') + .attr('class', 'stat') + .attr('x', '12') + .attr('y', '65') + .attr('text-anchor', 'start') + .attr('fill', 'rgba(255, 255, 255, 0.8)') + .text('CPU: 63%') + + // An icon or label that exists within the circle, inside the infobox + stats.append('text') + .attr('class', 'memory') + .attr('class', 'stat') + .attr('x', '12') + .attr('y', '85') + .attr('text-anchor', 'start') + .attr('fill', 'rgba(255, 255, 255, 0.8)') + .text('Memory: 50%') + + // An icon or label that exists within the circle, inside the infobox + stats.append('text') + .attr('class', 'network') + .attr('class', 'stat') + .attr('x', '12') + .attr('y', '105') + .attr('text-anchor', 'start') + .attr('fill', 'rgba(255, 255, 255, 0.8)') + .text('Network: 1.23kb/sec') + + + elm.call(d3.drag() + .on('start', dragstarted) + .on('drag', dragged) + .on('end', dragended)); + + elm.append('title') + .text(function(d) { return d.id; }); + + } + + function createExtendedNode(elm) { + elm.append('path') + .attr('class', 'node') + .attr('d', topRoundedRect('0', '0', 170, 47, 4)) + .attr('stroke', '#343434') + .attr('stroke-width', '1px') + .attr('fill', '#464646') + + // Hover-over text for a node's label. + var text = elm.append('g') + + text.append('text') + .attr('class', 'info_text') + .attr('x', '12') + .attr('y', '30') + .attr('text-anchor', 'start') + .attr('fill', '#fff') + .text(d => d.id) + + // Box where stats will live + var stats = elm.append('g'); + var primary = stats.append('g') + + primary.append('path') + .attr('class', 'node') + .attr('d', rect('0', '-39', 170, 78, 2)) + .attr('stroke', '#343434') + .attr('stroke-width', '1px') + .attr('fill', '#464646') + + primary.append('text') + .attr('class', 'primary') + .attr('x', '12') + .attr('y', '70') + .attr('text-anchor', 'start') + .attr('fill', '#fff') + .text('Primary') + + primary.append('circle') + .attr('class', 'alert') + .attr('cx', function () { return d3.select(this.parentNode).select('.primary').node().getBBox().width + 30 }) + .attr('cy', '64') + .attr('stroke-width', '0px') + .attr('fill', 'rgb(229, 163, 57)') + .attr('r', '9px') + + // An icon or label that exists within the circle, inside the infobox + primary.append('text') + .attr('class', 'health_warn') + .attr('x', function () { return d3.select(this.parentNode).select('.primary').node().getBBox().width + 30 }) + .attr('y', '69') + .attr('text-anchor', 'middle') + .attr('fill', '#fff') + .text('☇') + + var secondary = stats.append('g'); + + secondary.append('path') + .attr('class', 'node') + .attr('d', bottomRoundedRect('0', '-113', 170, 114, 4)) + .attr('stroke', '#343434') + .attr('stroke-width', '1px') + .attr('fill', '#464646') + + secondary.append('text') + .attr('class', 'secondary') + .attr('x', '12') + .attr('y', '150') + .attr('text-anchor', 'start') + .attr('fill', '#fff') + .text('Secondary') + + secondary.append('circle') + .attr('class', 'alert') + .attr('cx', function () { return d3.select(this.parentNode).select('.secondary').node().getBBox().width + 30 }) + .attr('cy', '144') + .attr('stroke-width', '0px') + .attr('fill', 'rgb(0,175,102)') + .attr('r', '9px') + + // An icon or label that exists within the circle, inside the infobox + secondary.append('text') + .attr('class', 'health') + .attr('x', function () { return d3.select(this.parentNode).select('.secondary').node().getBBox().width + 30 }) + .attr('y', '149') + .attr('text-anchor', 'middle') + .attr('fill', '#fff') + .text('❤') + + var html = secondary + .append('switch') + .append('foreignObject') + .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility') + .attr('x', 12) + .attr('y', 160) + .attr('width', 160) + .attr('height', 100) + // From here everything will be rendered with react using a ref. + // However for now these values are hard-coded. + .append('xhtml:div') + .attr('class', 'node_statistics') + + // Remove with react + dyanmic data. + html.append('p') + .text('CPU: 48%') + html.append('p') + .text('Memory: 54%') + html.append('p') + .text('Network: 1.75kb/sec') + + elm.call(d3.drag() + .on('start', dragstarted) + .on('drag', dragged) + .on('end', dragended)); + + } + // Drawing the links between nodes var link = svg.append('g') .attr('class', 'links') @@ -112,90 +344,16 @@ d3.json('services.json', function(error, graph) { .append('g') .attr('class', 'node_group'); - // Box where label will live - node.append('path') - .attr('class', 'node') - .attr('d', topRoundedRect('0', '0', 170, 47, 4)) - .attr('stroke', '#343434') - .attr('stroke-width', '1px') - .attr('fill', '#464646') - - // Hover-over text for a node's label. - var text = node.append('g') - - text.append('text') - .attr('class', 'info_text') - .attr('x', '12') - .attr('y', '30') - .attr('text-anchor', 'start') - .attr('fill', '#fff') - .text(d => d.id) - - text.append('circle') - .attr('class', 'alert') - .attr('cx', function () { return d3.select(this.parentNode).select('.info_text').node().getBBox().width + 30 }) - .attr('cy', '24') - .attr('stroke-width', '0px') - .attr('fill', (d) => d.id == 'Memcached' ? 'rgb(217, 77, 68)' : 'rgb(0,175,102)') - .attr('r', '9px') - - // An icon or label that exists within the circle, inside the infobox - text.append('text') - .attr('class', 'health') - .attr('x', function () { return d3.select(this.parentNode).select('.info_text').node().getBBox().width + 30 }) - .attr('y', '29') - .attr('text-anchor', 'middle') - .attr('fill', '#fff') - .text((d) => d.id == 'Memcached' ? '!' : '❤') - - // Box where stats will live - var stats = node.append('g'); - - stats.append('path') - .attr('class', 'node') - .attr('d', (d) => d.id == 'Percona' ? rect('0', '-39', 170, 78, 2) : bottomRoundedRect('0', '-39', 170, 78, 4)) - .attr('stroke', '#343434') - .attr('stroke-width', '1px') - .attr('fill', '#464646') - - // An icon or label that exists within the circle, inside the infobox - stats.append('text') - .attr('class', 'cpu') - .attr('class', 'stat') - .attr('x', '12') - .attr('y', '65') - .attr('text-anchor', 'start') - .attr('fill', 'rgba(255, 255, 255, 0.8)') - .text('CPU: 63%') - - // An icon or label that exists within the circle, inside the infobox - stats.append('text') - .attr('class', 'memory') - .attr('class', 'stat') - .attr('x', '12') - .attr('y', '85') - .attr('text-anchor', 'start') - .attr('fill', 'rgba(255, 255, 255, 0.8)') - .text('Memory: 50%') - - // An icon or label that exists within the circle, inside the infobox - stats.append('text') - .attr('class', 'network') - .attr('class', 'stat') - .attr('x', '12') - .attr('y', '105') - .attr('text-anchor', 'start') - .attr('fill', 'rgba(255, 255, 255, 0.8)') - .text('Network: 1.23kb/sec') - - - node.call(d3.drag() - .on('start', dragstarted) - .on('drag', dragged) - .on('end', dragended)); - - node.append('title') - .text(function(d) { return d.id; }); + svg.selectAll('.node_group').each(function (d) { + // Create different type of node for services with Primaries + Secondaries + // We could extend this further to allow us to have as many nested services + // as wanted. + if (d.id === 'Percona') { + createExtendedNode(d3.select(this)); + } else { + createNode(d3.select(this)); + } + }); simulation .nodes(graph.nodes) @@ -209,6 +367,8 @@ d3.json('services.json', function(error, graph) { } function ticked() { + // TODO: Remove these values and pull them out of the height of the boxes + // that the constraints belong to. r=180 r2=130 From d82a43d1e36690cab8c8af3e907c4f45f47695aa Mon Sep 17 00:00:00 2001 From: Tom Gallacher Date: Tue, 6 Dec 2016 16:29:45 +0000 Subject: [PATCH 4/6] Implemented collisions on nodes Also included in this is the implementation of stats as html foreign objects in svgs. --- spikes/architecture/d3-revamp/index.html | 129 +++++++++++++++-------- 1 file changed, 86 insertions(+), 43 deletions(-) diff --git a/spikes/architecture/d3-revamp/index.html b/spikes/architecture/d3-revamp/index.html index 8e1d0307..85dc2f2c 100644 --- a/spikes/architecture/d3-revamp/index.html +++ b/spikes/architecture/d3-revamp/index.html @@ -66,7 +66,7 @@ var color = d3.scaleOrdinal(d3.schemeCategory20); var simulation = d3.forceSimulation() .force('charge', d3.forceManyBody().strength(() => -50).distanceMin(() => 30)) .force('link', d3.forceLink().distance(() => 200).id(function(d) { return d.id; })) - .force('collide', d3.forceCollide().radius(function(d) { return 128 + 0.5; }).iterations(2)) + .force('collide', d3.forceCollide().radius(function(d) { return 200 + 0.5; }).iterations(2)) .force('center', d3.forceCenter(width * 1/3, height * 1/3)) function rightRoundedRect(x, y, width, height, radius) { @@ -171,35 +171,26 @@ d3.json('services.json', function(error, graph) { .attr('stroke-width', '1px') .attr('fill', '#464646') - // An icon or label that exists within the circle, inside the infobox - stats.append('text') - .attr('class', 'cpu') - .attr('class', 'stat') - .attr('x', '12') - .attr('y', '65') - .attr('text-anchor', 'start') - .attr('fill', 'rgba(255, 255, 255, 0.8)') - .text('CPU: 63%') + var html = stats + .append('switch') + .append('foreignObject') + .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility') + .attr('x', 12) + .attr('y', 57) + .attr('width', 160) + .attr('height', 70) + // From here everything will be rendered with react using a ref. + // However for now these values are hard-coded. + .append('xhtml:div') + .attr('class', 'node_statistics') - // An icon or label that exists within the circle, inside the infobox - stats.append('text') - .attr('class', 'memory') - .attr('class', 'stat') - .attr('x', '12') - .attr('y', '85') - .attr('text-anchor', 'start') - .attr('fill', 'rgba(255, 255, 255, 0.8)') - .text('Memory: 50%') - - // An icon or label that exists within the circle, inside the infobox - stats.append('text') - .attr('class', 'network') - .attr('class', 'stat') - .attr('x', '12') - .attr('y', '105') - .attr('text-anchor', 'start') - .attr('fill', 'rgba(255, 255, 255, 0.8)') - .text('Network: 1.23kb/sec') + // Remove with react + dyanmic data. + html.append('p') + .text('CPU: 48%') + html.append('p') + .text('Memory: 54%') + html.append('p') + .text('Network: 1.75kb/sec') elm.call(d3.drag() @@ -237,7 +228,7 @@ d3.json('services.json', function(error, graph) { primary.append('path') .attr('class', 'node') - .attr('d', rect('0', '-39', 170, 78, 2)) + .attr('d', rect('0', '-39', 170, 114, 2)) .attr('stroke', '#343434') .attr('stroke-width', '1px') .attr('fill', '#464646') @@ -267,11 +258,32 @@ d3.json('services.json', function(error, graph) { .attr('fill', '#fff') .text('☇') + var html = primary + .append('switch') + .append('foreignObject') + .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility') + .attr('x', 12) + .attr('y', 87) + .attr('width', 160) + .attr('height', 70) + // From here everything will be rendered with react using a ref. + // However for now these values are hard-coded. + .append('xhtml:div') + .attr('class', 'node_statistics') + + // Remove with react + dyanmic data. + html.append('p') + .text('CPU: 48%') + html.append('p') + .text('Memory: 54%') + html.append('p') + .text('Network: 1.75kb/sec') + var secondary = stats.append('g'); secondary.append('path') .attr('class', 'node') - .attr('d', bottomRoundedRect('0', '-113', 170, 114, 4)) + .attr('d', bottomRoundedRect('0', '-149', 170, 114, 4)) .attr('stroke', '#343434') .attr('stroke-width', '1px') .attr('fill', '#464646') @@ -279,7 +291,7 @@ d3.json('services.json', function(error, graph) { secondary.append('text') .attr('class', 'secondary') .attr('x', '12') - .attr('y', '150') + .attr('y', '183') .attr('text-anchor', 'start') .attr('fill', '#fff') .text('Secondary') @@ -287,7 +299,7 @@ d3.json('services.json', function(error, graph) { secondary.append('circle') .attr('class', 'alert') .attr('cx', function () { return d3.select(this.parentNode).select('.secondary').node().getBBox().width + 30 }) - .attr('cy', '144') + .attr('cy', '177') .attr('stroke-width', '0px') .attr('fill', 'rgb(0,175,102)') .attr('r', '9px') @@ -296,7 +308,7 @@ d3.json('services.json', function(error, graph) { secondary.append('text') .attr('class', 'health') .attr('x', function () { return d3.select(this.parentNode).select('.secondary').node().getBBox().width + 30 }) - .attr('y', '149') + .attr('y', '182') .attr('text-anchor', 'middle') .attr('fill', '#fff') .text('❤') @@ -306,9 +318,9 @@ d3.json('services.json', function(error, graph) { .append('foreignObject') .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility') .attr('x', 12) - .attr('y', 160) + .attr('y', 200) .attr('width', 160) - .attr('height', 100) + .attr('height', 70) // From here everything will be rendered with react using a ref. // However for now these values are hard-coded. .append('xhtml:div') @@ -369,18 +381,49 @@ d3.json('services.json', function(error, graph) { function ticked() { // TODO: Remove these values and pull them out of the height of the boxes // that the constraints belong to. - r=180 - r2=130 + r=174 + r2=270 link - .attr('x1', function(d) { return contrain(width, r, d.source.x) + 80; }) - .attr('y1', function(d) { return contrain(height, r2, d.source.y) + 24; }) - .attr('x2', function(d) { return contrain(width, r, d.target.x) + 80; }) - .attr('y2', function(d) { return contrain(height, r2, d.target.y) + 24; }); + .attr('x1', function(d) { + let x; + svg.selectAll('.node_group').each(function (_, i) { + if (i !== d.source.index) return; + x = d3.select(this).node().getBBox().width; + }); + + return contrain(width, x, d.source.x) + 80; + }) + .attr('y1', function(d) { + let y; + svg.selectAll('.node_group').each(function (_, i) { + if (i !== d.source.index) return; + y = d3.select(this).node().getBBox().height; + }); + return contrain(height, y, d.source.y) + 24; + }) + .attr('x2', function(d) { + let x; + svg.selectAll('.node_group').each(function (_, i) { + if (i !== d.target.index) return; + x = d3.select(this).node().getBBox().width; + }); + return contrain(width, x, d.target.x) + 80; + }) + .attr('y2', function(d) { + let y; + svg.selectAll('.node_group').each(function (_, i) { + if (i !== d.target.index) return; + y = d3.select(this).node().getBBox().height; + }); + return contrain(height, y, d.target.y) + 24; + }); svg.selectAll('.node_group') .attr('transform', function(d) { - return 'translate(' + contrain(width, r, d.x) + ',' + contrain(height, r2, d.y) + ')'; + let x = d3.select(this).node().getBBox().width; + let y = d3.select(this).node().getBBox().height; + return 'translate(' + contrain(width, x, d.x) + ',' + contrain(height, y, d.y) + ')'; }); } }); From fb98af153aa0583fb38f4e8b4d4e2bf4f0cbf93d Mon Sep 17 00:00:00 2001 From: Tom Gallacher Date: Tue, 6 Dec 2016 17:13:21 +0000 Subject: [PATCH 5/6] Update colour of warning --- spikes/architecture/d3-revamp/README.md | 13 +------------ spikes/architecture/d3-revamp/index.html | 2 +- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/spikes/architecture/d3-revamp/README.md b/spikes/architecture/d3-revamp/README.md index 9304b03b..08f0e276 100644 --- a/spikes/architecture/d3-revamp/README.md +++ b/spikes/architecture/d3-revamp/README.md @@ -1,14 +1,3 @@ -# D3 graph layouts +# Example -D3 v4, has a nice force directed graph set of features, that allows you to implement features in the graph with ease. By just manipulating the nodes using svg attributes and css. -This with the addition that D3 is a very well respected and widely used toolkit, makes finding documentation and developer speed a breeze. -It does seem to slow down on larger nodes, but this is mainly due to the built in force directed animation, disabling this, or computing this in a web-worker can speed up things considerably. - -As a developer I found my self more productive with the API that d3 exposes, and was able to accomplish more in the same time as other frameworks. - -## Research - -- React http://formidable.com/blog/2015/05/21/react-d3-layouts/ - -![screenshot from 2016-11-04 16-48-29](https://cloud.githubusercontent.com/assets/524382/20059671/5ab66080-a4ef-11e6-94d6-d09bbaa9a76a.png) diff --git a/spikes/architecture/d3-revamp/index.html b/spikes/architecture/d3-revamp/index.html index 85dc2f2c..742535ea 100644 --- a/spikes/architecture/d3-revamp/index.html +++ b/spikes/architecture/d3-revamp/index.html @@ -246,7 +246,7 @@ d3.json('services.json', function(error, graph) { .attr('cx', function () { return d3.select(this.parentNode).select('.primary').node().getBBox().width + 30 }) .attr('cy', '64') .attr('stroke-width', '0px') - .attr('fill', 'rgb(229, 163, 57)') + .attr('fill', 'rgb(227, 130, 0)') .attr('r', '9px') // An icon or label that exists within the circle, inside the infobox From d1a1362d2ad452ae7a37892929e614863f8c3317 Mon Sep 17 00:00:00 2001 From: Tom Gallacher Date: Tue, 6 Dec 2016 17:27:46 +0000 Subject: [PATCH 6/6] Update README.md --- spikes/architecture/d3-revamp/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/spikes/architecture/d3-revamp/README.md b/spikes/architecture/d3-revamp/README.md index 08f0e276..3818a1a0 100644 --- a/spikes/architecture/d3-revamp/README.md +++ b/spikes/architecture/d3-revamp/README.md @@ -1,3 +1,4 @@ # Example +![screenshot from 2016-12-06 17-03-28](https://cloud.githubusercontent.com/assets/524382/20936109/4508ebda-bbd9-11e6-9561-baa1f7fcc80c.png)