├── .gitignore ├── screenshot.png ├── README.md ├── docs ├── index.html └── styles.css ├── src ├── index.html ├── scss │ └── styles.scss └── js │ └── app.js ├── webpack.config.js ├── LICENSE └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awkweb/chernoff-fish/HEAD/screenshot.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chernoff Fish 2 | 3 | Data-driven fish with D3 & React because humans are really bad at understanding multi-dimensional data (and I wanted to learn React, Webpack, etc). Concept from [*Visualizing Financial Data*](http://www.wiley.com/WileyCDA/WileyTitle/productCd-111890785X.html) by Julie Rodriguez and Piotr Kaczmarek. 4 | 5 | ![Chernoff Fish](screenshot.png) 6 | 7 | ## Development 8 | 9 | ```shell 10 | > npm install 11 | > npm start 12 | ``` 13 | 14 | ## Build for GitHub pages 15 | ```shell 16 | > webpack 17 | ``` 18 | 19 | ## License 20 | 21 | Released under the MIT license. See LICENSE for details. -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chernoff Fish 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chernoff Fish 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | const extractCSS = new ExtractTextPlugin('styles.css'); 6 | const extractHTML = new ExtractTextPlugin('index.html'); 7 | 8 | module.exports = { 9 | watch: true, 10 | context: path.join(__dirname, "src"), 11 | entry: "./js/app.js", 12 | output: { 13 | path: __dirname + "/docs/", 14 | filename: "app.min.js" 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | loader: 'babel-loader', 22 | query: { 23 | presets: ['react'] 24 | } 25 | }, 26 | { 27 | test: /\.scss$/, 28 | loader: extractCSS.extract('css!sass') 29 | }, 30 | { 31 | test: /\.css$/, 32 | loader: "style-loader!css-loader" 33 | }, 34 | { 35 | test: /\.html$/, 36 | loader: extractHTML.extract('html') 37 | } 38 | ] 39 | }, 40 | plugins: [ 41 | extractCSS, 42 | extractHTML 43 | ] 44 | }; 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Thomas F. Meagher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chernoff-fish", 3 | "version": "1.0.0", 4 | "description": "data-driven with d3 and react", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/tmm/chernoff-fish" 8 | }, 9 | "main": "webpack.config.js", 10 | "scripts": { 11 | "start": "./node_modules/.bin/webpack-dev-server --content-base src --inline --hot --progress", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "Tom Meagher", 15 | "license": "MIT", 16 | "dependencies": { 17 | "d3": "^4.2.2", 18 | "html-loader": "^0.4.3", 19 | "normalize.css": "^4.2.0", 20 | "rc-slider": "^4.0.1", 21 | "rc-tooltip": "^3.4.2", 22 | "react": "^15.3.1", 23 | "react-dom": "^15.3.1", 24 | "react-faux-dom": "^3.0.0" 25 | }, 26 | "devDependencies": { 27 | "babel-core": "^6.14.0", 28 | "babel-loader": "^6.2.5", 29 | "babel-preset-react": "^6.11.1", 30 | "css-loader": "^0.24.0", 31 | "extract-text-webpack-plugin": "^1.0.1", 32 | "node-sass": "^3.8.0", 33 | "sass-loader": "^4.0.1", 34 | "style-loader": "^0.13.1", 35 | "webpack": "^1.13.2", 36 | "webpack-dev-server": "^1.15.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-size: 16px; 5 | font-family: Rubik, sans-serif; 6 | color: #333; } 7 | 8 | #main { 9 | display: flex; 10 | height: 100vh; } 11 | @media screen and (max-width: 768px) { 12 | #main { 13 | flex-direction: column; } } 14 | 15 | #form { 16 | flex: 5; 17 | padding: 1rem; 18 | overflow: auto; } 19 | @media screen and (max-width: 768px) { 20 | #form { 21 | flex: 4; } } 22 | 23 | #chart { 24 | display: flex; 25 | flex: 5; 26 | background-color: #F3F3F3; } 27 | @media screen and (max-width: 768px) { 28 | #chart { 29 | flex: 6; } } 30 | 31 | svg { 32 | flex: 10; } 33 | 34 | .long a:hover { 35 | color: #00BAE3; } 36 | 37 | .long .select:hover { 38 | border-color: #00BAE3; } 39 | 40 | .long .rc-slider-handle { 41 | border-color: #00BAE3 !important; } 42 | 43 | .long .rc-slider-track { 44 | background-color: #00BAE3 !important; } 45 | 46 | .long #back, .long .fin { 47 | fill: #00BAE3; } 48 | 49 | .long #belly, .long #eye-outline { 50 | fill: #A8E2F7; } 51 | 52 | .long #eye-pupil { 53 | fill: #005B91; } 54 | 55 | .long .em { 56 | fill: #00BAE3; } 57 | 58 | .long .dev { 59 | fill: #A8E2F7; } 60 | 61 | .short a:hover { 62 | color: #D66230; } 63 | 64 | .short .select:hover { 65 | border-color: #D66230; } 66 | 67 | .short .rc-slider-handle { 68 | border-color: #D66230 !important; } 69 | 70 | .short .rc-slider-track { 71 | background-color: #D66230 !important; } 72 | 73 | .short #back, .short .fin { 74 | fill: #D66230; } 75 | 76 | .short #belly, .short #eye-outline { 77 | fill: #FFAD41; } 78 | 79 | .short #eye-pupil { 80 | fill: #674111; } 81 | 82 | .short .em { 83 | fill: #D66230; } 84 | 85 | .short .dev { 86 | fill: #FFAD41; } 87 | 88 | button { 89 | position: absolute; 90 | left: 1rem; 91 | border: 2px solid #bbb; 92 | border-radius: 2px; 93 | padding: 10px 15px; 94 | color: #333; 95 | background-color: #bbb; 96 | font-size: 1rem; 97 | -webkit-appearance: none; 98 | cursor: pointer; } 99 | @media screen and (min-width: 768px) { 100 | button { 101 | bottom: 1rem; } } 102 | @media screen and (max-width: 768px) { 103 | button { 104 | top: 1rem; } } 105 | button:hover { 106 | transition: 0.5s; 107 | border-color: #7b7b7b; } 108 | button:active { 109 | transition: 0.5s; 110 | color: #fff; 111 | background-color: #7b7b7b; } 112 | 113 | form { 114 | display: flex; 115 | flex-direction: column; } 116 | 117 | h1, h3, h4 { 118 | font-weight: 300; } 119 | 120 | h3 { 121 | margin: .5rem 0 1rem; 122 | border-bottom: 2px solid #F3F3F3; 123 | padding: 0 0 .5rem .2rem; } 124 | 125 | h4 { 126 | margin: 0 0 .5rem; 127 | padding: 0 0 0 .2rem; } 128 | 129 | a { 130 | text-decoration: none; 131 | color: #7b7b7b; 132 | border-bottom: 2px solid #F3F3F3; } 133 | a:hover { 134 | transition: 0.5s; } 135 | 136 | .field-container { 137 | display: flex; 138 | margin-bottom: .5rem; } 139 | 140 | .field-50 { 141 | flex: 5; } 142 | 143 | .field { 144 | display: flex; 145 | flex-direction: column; 146 | margin-bottom: .85rem; } 147 | 148 | .label-container { 149 | display: flex; 150 | margin-bottom: .15rem; } 151 | 152 | label { 153 | flex: 1; 154 | font-size: .95rem; 155 | font-weight: 400; 156 | padding-left: .2rem; 157 | color: #7b7b7b; } 158 | 159 | .help { 160 | padding-right: .2rem; 161 | font-size: .95rem; 162 | font-weight: 300; 163 | color: #7b7b7b; } 164 | 165 | .select { 166 | display: flex; 167 | flex: 1; 168 | border: 2px solid #ddd; 169 | border-radius: 2px; 170 | padding: 5px 10px; 171 | padding-right: 25px; 172 | background-color: #fff; 173 | background: url("data:image/svg+xml;charset=utf-8,") right 10px center no-repeat; 174 | background-size: 8px 8px; 175 | color: #333; 176 | font-size: 1rem; 177 | -webkit-appearance: none; 178 | outline: none; 179 | cursor: pointer; } 180 | .select:hover { 181 | transition: 0.5s; } 182 | 183 | .slider { 184 | margin: .5rem .3rem; } 185 | 186 | .rc-slider { 187 | background-color: #eaeaea !important; } 188 | 189 | footer { 190 | margin: 1rem 0 3rem; 191 | color: #7b7b7b; 192 | font-size: .9rem; 193 | font-weight: 300; 194 | text-transform: uppercase; } 195 | -------------------------------------------------------------------------------- /src/scss/styles.scss: -------------------------------------------------------------------------------- 1 | $blue: #00BAE3; 2 | $blue-d: #005B91; 3 | $blue-l: #A8E2F7; 4 | $orange: #D66230; 5 | $orange-d: #674111; 6 | $orange-l: #FFAD41; 7 | $gray: #7b7b7b; 8 | $gray-d: #333; 9 | $gray-l: #F3F3F3; 10 | 11 | $md: 768px; 12 | $sans: Rubik, sans-serif; 13 | $transition: .5s; 14 | 15 | body { 16 | margin: 0; 17 | padding: 0; 18 | font-size: 16px; 19 | font-family: $sans; 20 | color: $gray-d; 21 | } 22 | 23 | #main { 24 | display: flex; 25 | height: 100vh; 26 | 27 | @media screen and (max-width: $md) { 28 | flex-direction: column; 29 | } 30 | } 31 | 32 | #form { 33 | flex: 5; 34 | padding: 1rem; 35 | overflow: auto; 36 | 37 | @media screen and (max-width: $md) { 38 | flex: 4; 39 | } 40 | } 41 | 42 | #chart { 43 | display: flex; 44 | flex: 5; 45 | background-color: $gray-l; 46 | 47 | @media screen and (max-width: $md) { 48 | flex: 6; 49 | } 50 | } 51 | 52 | svg { 53 | flex: 10; 54 | } 55 | 56 | .long { 57 | a { 58 | &:hover { 59 | color: $blue; 60 | } 61 | } 62 | 63 | .select { 64 | &:hover { 65 | border-color: $blue; 66 | } 67 | } 68 | 69 | .rc-slider-handle { 70 | border-color: $blue !important; 71 | } 72 | 73 | .rc-slider-track { 74 | background-color: $blue !important; 75 | } 76 | 77 | #back, .fin { 78 | fill: $blue; 79 | } 80 | 81 | #belly, #eye-outline { 82 | fill: $blue-l; 83 | } 84 | 85 | #eye-pupil { 86 | fill: $blue-d; 87 | } 88 | 89 | .em { 90 | fill: $blue; 91 | } 92 | 93 | .dev { 94 | fill: $blue-l; 95 | } 96 | } 97 | 98 | .short { 99 | a { 100 | &:hover { 101 | color: $orange; 102 | } 103 | } 104 | 105 | .select { 106 | &:hover { 107 | border-color: $orange; 108 | } 109 | } 110 | 111 | .rc-slider-handle { 112 | border-color: $orange !important; 113 | } 114 | 115 | .rc-slider-track { 116 | background-color: $orange !important; 117 | } 118 | 119 | #back, .fin { 120 | fill: $orange; 121 | } 122 | 123 | #belly, #eye-outline { 124 | fill: $orange-l; 125 | } 126 | 127 | #eye-pupil { 128 | fill: $orange-d; 129 | } 130 | 131 | .em { 132 | fill: $orange; 133 | } 134 | 135 | .dev { 136 | fill: $orange-l; 137 | } 138 | } 139 | 140 | button { 141 | position: absolute; 142 | left: 1rem; 143 | border: 2px solid #bbb; 144 | border-radius: 2px; 145 | padding: 10px 15px; 146 | color: $gray-d; 147 | background-color: #bbb; 148 | font-size: 1rem; 149 | -webkit-appearance: none; 150 | cursor: pointer; 151 | 152 | @media screen and (min-width: $md) { 153 | bottom: 1rem; 154 | } 155 | 156 | @media screen and (max-width: $md) { 157 | top: 1rem; 158 | } 159 | 160 | &:hover { 161 | transition: $transition; 162 | border-color: $gray; 163 | } 164 | 165 | &:active { 166 | transition: $transition; 167 | color: #fff; 168 | background-color: $gray; 169 | } 170 | } 171 | 172 | form { 173 | display: flex; 174 | flex-direction: column; 175 | } 176 | 177 | h1, h3, h4 { 178 | font-weight: 300; 179 | } 180 | 181 | h3 { 182 | margin: .5rem 0 1rem; 183 | border-bottom: 2px solid $gray-l; 184 | padding: 0 0 .5rem .2rem; 185 | } 186 | 187 | h4 { 188 | margin: 0 0 .5rem; 189 | padding: 0 0 0 .2rem; 190 | } 191 | 192 | a { 193 | text-decoration: none; 194 | color: $gray; 195 | border-bottom: 2px solid $gray-l; 196 | 197 | &:hover { 198 | transition: $transition; 199 | } 200 | } 201 | 202 | .field-container { 203 | display: flex; 204 | margin-bottom: .5rem; 205 | } 206 | 207 | .field-50 { 208 | flex: 5; 209 | } 210 | 211 | .field { 212 | display: flex; 213 | flex-direction: column; 214 | margin-bottom: .85rem; 215 | } 216 | 217 | .label-container { 218 | display: flex; 219 | margin-bottom: .15rem; 220 | } 221 | 222 | label { 223 | flex: 1; 224 | font-size: .95rem; 225 | font-weight: 400; 226 | padding-left: .2rem; 227 | color: $gray; 228 | } 229 | 230 | .help { 231 | padding-right: .2rem; 232 | font-size: .95rem; 233 | font-weight: 300; 234 | color: $gray; 235 | } 236 | 237 | .select { 238 | display: flex; 239 | flex: 1; 240 | border: 2px solid #ddd; 241 | border-radius: 2px; 242 | padding: 5px 10px; 243 | padding-right: 25px; 244 | background-color: #fff; 245 | background: url("data:image/svg+xml;charset=utf-8,") right 10px center no-repeat; 246 | background-size: 8px 8px; 247 | color: #333; 248 | font-size: 1rem; 249 | -webkit-appearance: none; 250 | outline: none; 251 | cursor: pointer; 252 | 253 | &:hover { 254 | transition: $transition; 255 | } 256 | } 257 | 258 | .slider { 259 | margin: .5rem .3rem; 260 | } 261 | 262 | .rc-slider { 263 | background-color: lighten(#ddd, 5) !important; 264 | } 265 | 266 | footer { 267 | margin: 1rem 0 3rem; 268 | color: $gray; 269 | font-size: .9rem; 270 | font-weight: 300; 271 | text-transform: uppercase; 272 | } 273 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | require('../scss/styles.scss'); 2 | require('../index.html'); 3 | require('rc-slider/assets/index.css'); 4 | 5 | var React = require('react'); 6 | var ReactDOM = require('react-dom'); 7 | var Slider = require('rc-slider'); 8 | var Tooltip = require('rc-tooltip'); 9 | var d3 = require('d3'); 10 | 11 | const WIDTH = 500, 12 | HEIGHT = 500, 13 | BREAKPOINT = 768, 14 | BODY_HEIGHT = 350, 15 | EYE_RADIUS = 15, 16 | SPINE_WIDTH = 20, 17 | SPINE_HEIGHT = 100, 18 | SPINE_SIZE = 50, 19 | DURATION = 1000; 20 | 21 | function getXPosition(width, breakpoint) { 22 | var screenWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); 23 | var x = screenWidth < breakpoint ? (screenWidth / 2) - (width / 4) : (screenWidth / 4) - (width / 4); 24 | return x; 25 | } 26 | 27 | function getYPosition(breakpoint) { 28 | var screenWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); 29 | var screenHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); 30 | var y = screenWidth < breakpoint ? (screenHeight / 4) + 55: (screenHeight / 2); 31 | return y; 32 | } 33 | 34 | function drawSpine(position, width, height) { 35 | var x1 = ((position - 1) * width), 36 | x2 = (x1 - width) + 1, 37 | width = -width, 38 | height = -height; 39 | return "M" + x1 +",0 Q" + x1 + "," + (height + -width) + " " + x2 + "," + height + " L" + x2 + ",0 Z"; 40 | } 41 | 42 | function drawBody(x, y, width, height) { 43 | var start = x + "," + y + " "; 44 | return "M" + start + "C" + start + (width / 2) + "," + height + " " + width + "," + y + " Z"; 45 | } 46 | 47 | function drawFin(x, y, width, direction) { 48 | var start = x + "," + y + " ", 49 | height = width * 2.25 * direction, 50 | width = -width; 51 | return "M" + start + "Q" + x + "," + height + " " + width + "," + height + " L" + width + "," + y + " Z"; 52 | } 53 | 54 | function updateWindow(){ 55 | d3.select("#chernoff") 56 | .attr("transform", "translate(" + getXPosition(WIDTH, BREAKPOINT) + "," + getYPosition(BREAKPOINT) + ")"); 57 | } 58 | window.onresize = updateWindow; 59 | 60 | function percentFormatter(v) { 61 | return `${v}%`; 62 | } 63 | 64 | const spineScale = d3.scaleLinear() 65 | .clamp(true) 66 | .domain([0, 100]) 67 | .range([0, SPINE_HEIGHT]); 68 | 69 | const bodyScale = d3.scaleLinear() 70 | .domain([0, 100]) 71 | .range([0, BODY_HEIGHT]); 72 | 73 | const eyeScale = d3.scaleLinear() 74 | .clamp(true) 75 | .domain([0, 100]) 76 | .range([0, EYE_RADIUS - 1]); 77 | 78 | const finScale = d3.scaleLinear() 79 | .clamp(true) 80 | .domain([0, 100]) 81 | .range([0, SPINE_SIZE]); 82 | 83 | var Spine = React.createClass({ 84 | render: function() { 85 | const spine = this.props.spine; 86 | const id = spine.name + "-" + spine.type; 87 | const text = `${Math.round(spine.percent)}% ${spine.name} ${spine.type}`; 88 | return ( 89 | 93 | 98 | 102 | 103 | 104 | 105 | ); 106 | } 107 | }); 108 | 109 | var Spines = React.createClass({ 110 | render: function() { 111 | const transform = "translate(" + (WIDTH / 4.6) + "," + bodyScale(-this.props.back / 2.5 ) + ")"; 112 | const spines = this.props.spines.map(function(spine) { 113 | return ; 114 | }); 115 | return ( 116 | 120 | {spines} 121 | 122 | ); 123 | } 124 | }); 125 | 126 | var Body = React.createClass({ 127 | render: function() { 128 | return ( 129 | 130 | 134 | 135 | 139 | 140 | 141 | ); 142 | } 143 | }); 144 | 145 | var Eye = React.createClass({ 146 | render: function() { 147 | const transform = "translate(" + ((WIDTH / 2) / 1.2) + "," + 0 + ")"; 148 | const text = `${Math.round(this.props.eye)}% return`; 149 | return ( 150 | 154 | 160 | 161 | 166 | 170 | 171 | 172 | 173 | ); 174 | } 175 | }); 176 | 177 | var Fin = React.createClass({ 178 | render: function() { 179 | const fin = this.props.fin; 180 | const transform = fin.name === "defensive" ? "translate(" + (WIDTH / 3.45) + "," + 10 + ")" : "translate(0,0)"; 181 | const text = `${Math.round(fin.percent)}% ${fin.name}`; 182 | return ( 183 | 188 | 193 | 196 | 197 | 198 | 199 | ); 200 | } 201 | }) 202 | 203 | var Tail = React.createClass({ 204 | render: function() { 205 | const fins = this.props.fins.map(function(fin) { 206 | return ; 207 | }); 208 | return ( 209 | 212 | {fins} 213 | 214 | ); 215 | } 216 | }); 217 | 218 | var Fish = React.createClass({ 219 | render: function() { 220 | const transform = "translate(" + getXPosition(WIDTH, BREAKPOINT) + "," + getYPosition(BREAKPOINT) + ")"; 221 | const fins = [this.props.sensitive, this.props.cyclical]; 222 | const spines = [ 223 | this.props.americas_dev, 224 | this.props.americas_em, 225 | this.props.asia_dev, 226 | this.props.asia_em, 227 | this.props.eu_dev, 228 | this.props.eu_em, 229 | this.props.ame_dev, 230 | this.props.ame_em 231 | ]; 232 | return ( 233 | 234 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | ); 247 | } 248 | }); 249 | 250 | var Form = React.createClass({ 251 | handleChange: function(event) { 252 | this.props.onUserInput( 253 | this.refs.strategy.value, 254 | this.refs.style.value, 255 | this.refs.market_cap.value 256 | ); 257 | }, 258 | 259 | handleReturnSliderChange: function(value) { 260 | this.props.returnSliderChange( 261 | value 262 | ); 263 | }, 264 | 265 | handleDefensiveSliderChange: function(value) { 266 | this.props.defensiveSliderChange( 267 | value 268 | ); 269 | }, 270 | 271 | handleCyclicalSliderChange: function(value) { 272 | this.props.cyclicalSliderChange( 273 | value 274 | ); 275 | }, 276 | 277 | handleSensitiveSliderChange: function(value) { 278 | this.props.sensitivesSliderChange( 279 | value 280 | ); 281 | }, 282 | 283 | handleAmericasDevSliderChange: function(value) { 284 | this.props.americasDevSliderChange( 285 | value 286 | ); 287 | }, 288 | 289 | handleAmericasEmSliderChange: function(value) { 290 | this.props.americasEmSliderChange( 291 | value 292 | ); 293 | }, 294 | 295 | handleAsiaDevSliderChange: function(value) { 296 | this.props.asiaDevSliderChange( 297 | value 298 | ); 299 | }, 300 | 301 | handleAsiaEmSliderChange: function(value) { 302 | this.props.asiaEmSliderChange( 303 | value 304 | ); 305 | }, 306 | 307 | handleEuropeDevSliderChange: function(value) { 308 | this.props.europeDevSliderChange( 309 | value 310 | ); 311 | }, 312 | 313 | handleEuropeEmSliderChange: function(value) { 314 | this.props.europeEmSliderChange( 315 | value 316 | ); 317 | }, 318 | 319 | handleAmeDevSliderChange: function(value) { 320 | this.props.ameDevSliderChange( 321 | value 322 | ); 323 | }, 324 | 325 | handleAmeEmSliderChange: function(value) { 326 | this.props.ameEmSliderChange( 327 | value 328 | ); 329 | }, 330 | 331 | render: function() { 332 | return ( 333 |
336 |

Chernoff Fish with D3 & React

337 |

1. General

338 | 339 |
340 |
341 | 342 | Fund type 343 |
344 | 353 |
354 | 355 |
356 |
357 | 358 | Overarching theory 359 |
360 | 370 |
371 | 372 |
373 |
374 | 375 | Total market value 376 |
377 | 387 |
388 | 389 |
390 |
391 | 392 | Return as % of category max 393 |
394 |
395 | 402 |
403 |
404 | 405 |

2. Sector Exposure

406 | 407 |
408 |
409 | 410 | Consumer staples, health care, utilities 411 |
412 |
413 | 420 |
421 |
422 | 423 |
424 |
425 | 426 | Materials, financial, consumer discr. 427 |
428 |
429 | 436 |
437 |
438 | 439 |
440 |
441 | 442 | Technology, energy, industrials 443 |
444 |
445 | 452 |
453 |
454 | 455 |

3. Regional Exposure

456 | 457 |

Americas

458 | 459 |
460 |
461 |
462 | 463 |
464 |
465 | 472 |
473 |
474 | 475 |
476 |
477 | 478 |
479 |
480 | 487 |
488 |
489 |
490 | 491 |

Asia

492 | 493 |
494 |
495 |
496 | 497 |
498 |
499 | 506 |
507 |
508 | 509 |
510 |
511 | 512 |
513 |
514 | 521 |
522 |
523 |
524 | 525 |

Europe

526 | 527 |
528 |
529 |
530 | 531 |
532 |
533 | 540 |
541 |
542 | 543 |
544 |
545 | 546 |
547 |
548 | 555 |
556 |
557 |
558 | 559 |

Africa & Middle East

560 | 561 |
562 |
563 |
564 | 565 |
566 |
567 | 574 |
575 |
576 | 577 |
578 |
579 | 580 |
581 |
582 | 589 |
590 |
591 |
592 | 593 |
594 | ); 595 | } 596 | }); 597 | 598 | var App = React.createClass({ 599 | getRandomNumber(max) { 600 | return Math.ceil(Math.random() * max); 601 | }, 602 | 603 | generateRandomValues: function() { 604 | const strategyValues = ["long", "short"]; 605 | 606 | const fReturn = Math.ceil(Math.random() * 100); 607 | const styleAndMarketCapValues = [23, 60, 98]; 608 | const style = styleAndMarketCapValues[this.getRandomNumber(3) - 1]; 609 | const marketCap = styleAndMarketCapValues[this.getRandomNumber(3) - 1]; 610 | 611 | const defensive = this.getRandomNumber(100); 612 | const cyclical = this.getRandomNumber(100); 613 | const sensitive = this.getRandomNumber(100); 614 | const sectorTotal = defensive + cyclical + sensitive; 615 | 616 | const americas = this.getRandomNumber(100); 617 | const asia = this.getRandomNumber(100); 618 | const eu = this.getRandomNumber(100); 619 | const ame = this.getRandomNumber(100); 620 | const regionTotal = americas + asia + eu + ame; 621 | 622 | const americasTotal = (americas / regionTotal) * 100; 623 | const asiaTotal = (asia / regionTotal) * 100; 624 | const euTotal = (eu / regionTotal) * 100; 625 | const ameTotal = (ame / regionTotal) * 100; 626 | 627 | const americasEm = this.getRandomNumber(americasTotal); 628 | const asiaEm = this.getRandomNumber(asiaTotal); 629 | const euEm = this.getRandomNumber(eu); 630 | const ameEm = this.getRandomNumber(ameTotal); 631 | 632 | const americasDev = this.getRandomNumber(americasTotal); 633 | const asiaDev = this.getRandomNumber(asiaTotal); 634 | const euDev = this.getRandomNumber(eu); 635 | const ameDev = this.getRandomNumber(ameTotal); 636 | 637 | return { 638 | strategy: strategyValues[this.getRandomNumber(2) - 1], 639 | style: style, 640 | market_cap: marketCap, 641 | f_return: fReturn, 642 | defensive: { 643 | "name": "defensive", 644 | "percent": (defensive / sectorTotal) * 100, 645 | "direction": 1 646 | }, 647 | cyclical: { 648 | "name": "cyclical", 649 | "percent": (cyclical / sectorTotal) * 100, 650 | "direction": -1 651 | }, 652 | sensitive: { 653 | "name": "sensitive", 654 | "percent": (sensitive / sectorTotal) * 100, 655 | "direction": 1 656 | }, 657 | americas_dev: { 658 | "name": "americas", 659 | "type": "dev", 660 | "percent": (americasDev / (americasEm + americasDev)) * 100, 661 | "position": 1 662 | }, 663 | americas_em:{ 664 | "name": "americas", 665 | "type": "em", 666 | "percent": (americasEm / (americasEm + americasDev)) * 100, 667 | "position": 1 668 | }, 669 | asia_dev: { 670 | "name": "asia", 671 | "type": "dev", 672 | "percent": (asiaDev / (asiaEm + asiaDev)) * 100, 673 | "position": 2 674 | }, 675 | asia_em: { 676 | "name": "asia", 677 | "type": "em", 678 | "percent": (asiaEm / (asiaEm + asiaDev)) * 100, 679 | "position": 2 680 | }, 681 | eu_dev: { 682 | "name": "eu", 683 | "type": "dev", 684 | "percent": (euDev / (euEm + euDev)) * 100, 685 | "position": 3 686 | }, 687 | eu_em: { 688 | "name": "eu", 689 | "type": "em", 690 | "percent": (euEm / (euEm + euDev)) * 100, 691 | "position": 3 692 | }, 693 | ame_dev: { 694 | "name": "ame", 695 | "type": "dev", 696 | "percent": (ameDev / (ameEm + ameDev)) * 100, 697 | "position": 4 698 | }, 699 | ame_em: { 700 | "name": "ame", 701 | "type": "em", 702 | "percent": (ameEm / (ameEm + ameDev)) * 100, 703 | "position": 4 704 | } 705 | }; 706 | }, 707 | 708 | randomFish: function() { 709 | var rands = this.generateRandomValues(); 710 | this.setState(rands); 711 | }, 712 | 713 | getInitialState: function() { 714 | var rands = this.generateRandomValues(); 715 | return rands; 716 | }, 717 | 718 | handleUserInput: function(strategy, style, market_cap) { 719 | this.setState({ 720 | strategy: strategy, 721 | style: style, 722 | market_cap: market_cap 723 | }); 724 | }, 725 | 726 | handleReturnInput: function(f_return) { 727 | this.setState({ 728 | f_return: f_return 729 | }); 730 | }, 731 | 732 | handleDefensiveInput: function(percent) { 733 | this.setState({ 734 | defensive: { 735 | "name": "defensive", 736 | "percent": percent, 737 | "direction": 1 738 | } 739 | }); 740 | }, 741 | 742 | handleCyclicalInput: function(percent) { 743 | this.setState({ 744 | cyclical: { 745 | "name": "cyclical", 746 | "percent": percent, 747 | "direction": -1 748 | } 749 | }); 750 | }, 751 | 752 | handleSensitivesInput: function(percent) { 753 | this.setState({ 754 | sensitive: { 755 | "name": "sensitive", 756 | "percent": percent, 757 | "direction": 1 758 | } 759 | }); 760 | }, 761 | 762 | handleAmericasDevSliderChange: function(percent) { 763 | this.setState({ 764 | americas_dev: { 765 | "name": "americas", 766 | "type": "dev", 767 | "percent": percent, 768 | "position": 1 769 | } 770 | }); 771 | }, 772 | 773 | handleAmericasEmSliderChange: function(percent) { 774 | this.setState({ 775 | americas_em: { 776 | "name": "americas", 777 | "type": "em", 778 | "percent": percent, 779 | "position": 1 780 | } 781 | }); 782 | }, 783 | 784 | handleAsiaDevSliderChange: function(percent) { 785 | this.setState({ 786 | asia_dev: { 787 | "name": "asia", 788 | "type": "dev", 789 | "percent": percent, 790 | "position": 2 791 | } 792 | }); 793 | }, 794 | 795 | handleAsiaEmSliderChange: function(percent) { 796 | this.setState({ 797 | asia_em: { 798 | "name": "asia", 799 | "type": "em", 800 | "percent": percent, 801 | "position": 2 802 | } 803 | }); 804 | }, 805 | 806 | handleEuropeDevSliderChange: function(percent) { 807 | this.setState({ 808 | eu_dev: { 809 | "name": "europe", 810 | "type": "dev", 811 | "percent": percent, 812 | "position": 3 813 | } 814 | }); 815 | }, 816 | 817 | handleEuropeEmSliderChange: function(percent) { 818 | this.setState({ 819 | eu_em: { 820 | "name": "europe", 821 | "type": "em", 822 | "percent": percent, 823 | "position": 3 824 | } 825 | }); 826 | }, 827 | 828 | handleAmeDevSliderChange: function(percent) { 829 | this.setState({ 830 | ame_dev: { 831 | "name": "ame", 832 | "type": "dev", 833 | "percent": percent, 834 | "position": 4 835 | } 836 | }); 837 | }, 838 | 839 | handleAmeEmSliderChange: function(percent) { 840 | this.setState({ 841 | ame_em: { 842 | "name": "ame", 843 | "type": "em", 844 | "percent": percent, 845 | "position": 4 846 | } 847 | }); 848 | }, 849 | 850 | render: function() { 851 | return ( 852 |
853 |
854 | 873 | 874 | 879 |
880 |
881 |
914 | 919 |
920 |
921 | ); 922 | } 923 | }); 924 | 925 | ReactDOM.render( 926 | , 927 | document.getElementById("app") 928 | ); 929 | --------------------------------------------------------------------------------