├── .gitignore ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── coffeelint.json ├── demo ├── app-bar-horizontal.js ├── app-bar.js ├── app-line.js ├── app-ohlc.js ├── app-pie.js ├── app-scatter.js ├── app-stacked-area.js ├── app.js ├── bar-horizontal.html ├── bar.html ├── index.html ├── line.html ├── ohlc.html ├── pie.html ├── scatter.html └── stacked-area.html ├── dist ├── forest-d3-dark.css ├── forest-d3.css └── forest-d3.js ├── documentation └── README.md ├── examples ├── bar-horizontal.jade ├── bar.jade ├── index.jade ├── line.jade ├── ohlc.jade ├── pie.jade ├── scatter.jade ├── src │ ├── app-bar-horizontal.coffee │ ├── app-bar.coffee │ ├── app-line.coffee │ ├── app-ohlc.coffee │ ├── app-pie.coffee │ ├── app-scatter.coffee │ ├── app-stacked-area.coffee │ └── app.coffee ├── stacked-area.jade └── template.jade ├── package.json ├── src ├── bar-chart.coffee ├── base.coffee ├── chart.coffee ├── data.coffee ├── features │ ├── crosshairs.coffee │ ├── guideline.coffee │ ├── tooltip-content.coffee │ └── tooltip.coffee ├── main.coffee ├── pie-chart.coffee ├── plugins │ └── legend.coffee ├── stacked-chart.coffee ├── utils.coffee └── visualizations │ ├── bar.coffee │ ├── line-area.coffee │ ├── marker-line.coffee │ ├── ohlc.coffee │ ├── region.coffee │ └── scatter.coffee ├── style ├── bar.styl ├── chart.styl ├── legend.styl ├── line.styl ├── marker-line.styl ├── ohlc.styl ├── region.styl ├── tooltip.styl └── variables │ ├── dark.styl │ └── light.styl └── test ├── bar-chart.coffee ├── chart.coffee ├── data.coffee ├── guideline.coffee ├── legend.coffee ├── ohlc.coffee ├── smoke-tests.coffee ├── stacked-chart.coffee ├── state.coffee ├── tooltip.coffee └── utils.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | demo/forest-d3* 3 | karma.xml 4 | coverage/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | before_install: 5 | - "npm install -g grunt-cli" 6 | - "export DISPLAY=:99.0" 7 | - "sh -e /etc/init.d/xvfb start" 8 | install: 9 | - "npm install" 10 | script: 11 | - "npm test" 12 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt)-> 2 | grunt.initConfig 3 | jade: 4 | examples: 5 | options: 6 | pretty: true 7 | files: [ 8 | cwd: 'examples/' 9 | src: ['*.jade', '!template.jade'] 10 | dest: 'demo/' 11 | ext: '.html' 12 | expand: true 13 | flatten: true 14 | ] 15 | copy: 16 | demo: 17 | cwd: 'dist/' 18 | src: '*' 19 | dest: 'demo/' 20 | expand: true 21 | flatten: true 22 | 23 | stylus: 24 | light: 25 | files: 26 | 'dist/forest-d3.css': ['style/*.styl'] 27 | options: 28 | compress: false 29 | import: ['variables/light'] 30 | 31 | dark: 32 | files: 33 | 'dist/forest-d3-dark.css': ['style/*.styl'] 34 | options: 35 | compress: false 36 | import: ['variables/dark'] 37 | 38 | 39 | coffeelint: 40 | client: 41 | files: 42 | src: ['src/*.coffee'] 43 | options: 44 | configFile: 'coffeelint.json' 45 | 46 | coffee: 47 | options: 48 | bare: false 49 | client: 50 | files: 51 | 'dist/forest-d3.js': [ 52 | 'src/main.coffee' 53 | 'src/base.coffee' 54 | 'src/visualizations/*.coffee' 55 | 'src/utils.coffee' 56 | 'src/data.coffee' 57 | 'src/plugins/*.coffee' 58 | 'src/features/*.coffee' 59 | 'src/chart.coffee' 60 | 'src/bar-chart.coffee' 61 | 'src/stacked-chart.coffee' 62 | 'src/pie-chart.coffee' 63 | ] 64 | examples: 65 | expand: true 66 | flatten: true 67 | src: 'examples/src/*.coffee' 68 | dest: 'demo/' 69 | ext: '.js' 70 | 71 | karma: 72 | client: 73 | options: 74 | browsers: ['Firefox'] 75 | frameworks: ['mocha', 'sinon-chai'] 76 | reporters: ['spec', 'coverage'] 77 | junitReporter: 78 | outputFile: 'karma.xml' 79 | singleRun: true 80 | preprocessors: 81 | 'dist/*.js': ['coverage'] 82 | 'test/*.coffee': 'coffee' 83 | files: [ 84 | 'node_modules/d3/d3.js' 85 | 'node_modules/jquery/dist/jquery.js' 86 | 'dist/*.js' 87 | 'dist/*.css' 88 | 'test/*.coffee' 89 | ] 90 | 91 | grunt.loadNpmTasks 'grunt-contrib-coffee' 92 | grunt.loadNpmTasks 'grunt-coffeelint' 93 | grunt.loadNpmTasks 'grunt-contrib-jade' 94 | grunt.loadNpmTasks 'grunt-contrib-stylus' 95 | grunt.loadNpmTasks 'grunt-contrib-copy' 96 | grunt.loadNpmTasks 'grunt-karma' 97 | 98 | grunt.registerTask 'examples', ['coffee', 'stylus', 'jade', 'copy'] 99 | grunt.registerTask 'test', ['coffee', 'karma'] 100 | grunt.registerTask 'default', ['coffeelint','examples', 'karma'] 101 | 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Robin Hu 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. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Forest D3 2 | ### A javascript charting library 3 | 4 | [![Build Status](https://travis-ci.org/robinfhu/forest-d3.svg?branch=master)](https://travis-ci.org/robinfhu/forest-d3) 5 | 6 | My attempt at implementing a better time series charting library, based on d3.js 7 | 8 | ## Motivations and Design Ideas 9 | I learned a lot from my experience working on NVD3. I wanted to take the lessons 10 | learned from that project to build a better charting library. 11 | 12 | Here are some guidelines I'd like to apply to this library: 13 | 14 | * Better data cleanup. Having the library take care of filling in missing points. 15 | * For large datasets, charts should reduce resolution of the lines for performance gain. 16 | * Better margin auto adjusting. 17 | * Easier ability to integrate different chart types into the same plot (line, area, scatter, bars) 18 | * Easier ability to add horizontal and vertical line markers. 19 | * Adding and removing data points in real time should be seamless. 20 | * Ability to use data generators (so you can write functions like y=x^2). 21 | * Removal of the chart legend, with hooks to enable series'. Allows developer to create their own legend. 22 | * Provide an AngularJS and React.js companion library. 23 | * No need to create your own SVG tag. Library creates it for you and sizes it to the container. 24 | * Code is tested and linted properly. Written in CoffeeScript. 25 | 26 | ## Development 27 | To build the project, run the following 28 | 29 | npm install 30 | npm test 31 | 32 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrow_spacing": { 3 | "level": "ignore" 4 | }, 5 | "braces_spacing": { 6 | "level": "ignore", 7 | "spaces": 0, 8 | "empty_object_spaces": 0 9 | }, 10 | "camel_case_classes": { 11 | "level": "error" 12 | }, 13 | "coffeescript_error": { 14 | "level": "error" 15 | }, 16 | "colon_assignment_spacing": { 17 | "level": "ignore", 18 | "spacing": { 19 | "left": 0, 20 | "right": 0 21 | } 22 | }, 23 | "cyclomatic_complexity": { 24 | "value": 10, 25 | "level": "ignore" 26 | }, 27 | "duplicate_key": { 28 | "level": "error" 29 | }, 30 | "empty_constructor_needs_parens": { 31 | "level": "ignore" 32 | }, 33 | "ensure_comprehensions": { 34 | "level": "warn" 35 | }, 36 | "eol_last": { 37 | "level": "ignore" 38 | }, 39 | "indentation": { 40 | "value": 4, 41 | "level": "error" 42 | }, 43 | "line_endings": { 44 | "level": "ignore", 45 | "value": "unix" 46 | }, 47 | "max_line_length": { 48 | "value": 80, 49 | "level": "error", 50 | "limitComments": true 51 | }, 52 | "missing_fat_arrows": { 53 | "level": "ignore", 54 | "is_strict": false 55 | }, 56 | "newlines_after_classes": { 57 | "value": 3, 58 | "level": "ignore" 59 | }, 60 | "no_backticks": { 61 | "level": "error" 62 | }, 63 | "no_debugger": { 64 | "level": "warn", 65 | "console": false 66 | }, 67 | "no_empty_functions": { 68 | "level": "ignore" 69 | }, 70 | "no_empty_param_list": { 71 | "level": "ignore" 72 | }, 73 | "no_implicit_braces": { 74 | "level": "ignore", 75 | "strict": true 76 | }, 77 | "no_implicit_parens": { 78 | "strict": true, 79 | "level": "ignore" 80 | }, 81 | "no_interpolation_in_single_quotes": { 82 | "level": "ignore" 83 | }, 84 | "no_plusplus": { 85 | "level": "ignore" 86 | }, 87 | "no_stand_alone_at": { 88 | "level": "ignore" 89 | }, 90 | "no_tabs": { 91 | "level": "error" 92 | }, 93 | "no_this": { 94 | "level": "ignore" 95 | }, 96 | "no_throwing_strings": { 97 | "level": "error" 98 | }, 99 | "no_trailing_semicolons": { 100 | "level": "error" 101 | }, 102 | "no_trailing_whitespace": { 103 | "level": "error", 104 | "allowed_in_comments": false, 105 | "allowed_in_empty_lines": true 106 | }, 107 | "no_unnecessary_double_quotes": { 108 | "level": "ignore" 109 | }, 110 | "no_unnecessary_fat_arrows": { 111 | "level": "warn" 112 | }, 113 | "non_empty_constructor_needs_parens": { 114 | "level": "ignore" 115 | }, 116 | "prefer_english_operator": { 117 | "level": "ignore", 118 | "doubleNotLevel": "ignore" 119 | }, 120 | "space_operators": { 121 | "level": "ignore" 122 | }, 123 | "spacing_after_comma": { 124 | "level": "ignore" 125 | }, 126 | "transform_messes_up_line_numbers": { 127 | "level": "warn" 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /demo/app-bar-horizontal.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var chart, data, sortDir; 3 | 4 | chart = new ForestD3.BarChart('#example'); 5 | 6 | data = [ 7 | { 8 | key: 'series1', 9 | label: 'Long', 10 | color: '#555', 11 | values: [['Toyota', 100], ['Honda', 80], ['Mazda', 70], ['Prius', 10], ['Ford F150', 87], ['Hyundai', 23.4], ['Chrysler', 1], ['Lincoln', 102], ['Accord', -60], ['Hummer', -5.6], ['Dodge', -11]] 12 | } 13 | ]; 14 | 15 | chart.data(data).render(); 16 | 17 | chart.sortBy(function(d) { 18 | return d[1]; 19 | }); 20 | 21 | sortDir = 0; 22 | 23 | document.getElementById('sort-button').addEventListener('click', function() { 24 | chart.sortDirection(sortDir === 0 ? 'asc' : 'desc').render(); 25 | return sortDir = 1 - sortDir; 26 | }); 27 | 28 | }).call(this); 29 | -------------------------------------------------------------------------------- /demo/app-bar.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var chart, chartOneGroup, chartSingle, chartSingleSeries, chartStackedBar, chartTwoGroups, data, dataOneGroup, dataSingle, dataSingleSeries, dataStacked, dataTwoGroups, getStocks, getVals, legend, legendStacked, months; 3 | 4 | chart = new ForestD3.Chart('#example'); 5 | 6 | legend = new ForestD3.Legend('#legend'); 7 | 8 | chart.chartLabel('Trading Volume').xLabel('Date').yLabel('Volume').yTickFormat(d3.format(',.3f')).tooltipType('hover').xTickFormat(function(d) { 9 | if (d != null) { 10 | return d3.time.format('%Y-%m-%d')(new Date(d)); 11 | } else { 12 | return ''; 13 | } 14 | }).addPlugin(legend); 15 | 16 | getStocks = function(startPrice, volatility) { 17 | var changePct, i, j, result, startDate; 18 | result = []; 19 | startDate = new Date(2012, 0, 1); 20 | for (i = j = 0; j < 15; i = ++j) { 21 | result.push([startDate.getTime(), startPrice - 0.3]); 22 | changePct = 2 * volatility * Math.random(); 23 | if (changePct > volatility) { 24 | changePct -= 2 * volatility; 25 | } 26 | startPrice += startPrice * changePct; 27 | startDate.setDate(startDate.getDate() + 1); 28 | } 29 | return result; 30 | }; 31 | 32 | data = [ 33 | { 34 | key: 'series1', 35 | label: 'AAPL', 36 | type: 'bar', 37 | values: getStocks(0.75, 0.47) 38 | }, { 39 | key: 'series2', 40 | label: 'GOLDMAN', 41 | type: 'bar', 42 | values: getStocks(0.6, 0.32) 43 | }, { 44 | key: 'series3', 45 | label: 'CITI', 46 | type: 'bar', 47 | values: getStocks(0.45, 0.76) 48 | }, { 49 | key: 'marker1', 50 | label: 'High Volume', 51 | type: 'marker', 52 | axis: 'y', 53 | value: 0.21 54 | } 55 | ]; 56 | 57 | chart.data(data).render(); 58 | 59 | chartSingle = new ForestD3.Chart('#example-single'); 60 | 61 | chartSingle.chartLabel('Single Bar'); 62 | 63 | dataSingle = [ 64 | { 65 | key: 'k1', 66 | type: 'bar', 67 | label: 'Series 1', 68 | values: [['Population', 234]] 69 | } 70 | ]; 71 | 72 | chartSingle.data(dataSingle).render(); 73 | 74 | chartOneGroup = new ForestD3.Chart('#example-onegroup'); 75 | 76 | chartOneGroup.yPadding(0.3).chartLabel('One Group'); 77 | 78 | dataOneGroup = [ 79 | { 80 | key: 'k1', 81 | type: 'bar', 82 | label: 'Series 1', 83 | values: [['Population', 234]] 84 | }, { 85 | key: 'k2', 86 | type: 'bar', 87 | label: 'Series 2', 88 | values: [['Population', 341]] 89 | } 90 | ]; 91 | 92 | chartOneGroup.data(dataOneGroup).render(); 93 | 94 | chartTwoGroups = new ForestD3.Chart('#example-twogroups'); 95 | 96 | chartTwoGroups.xPadding(1.5).yPadding(0).forceDomain({ 97 | y: 0 98 | }).chartLabel('Two Groups'); 99 | 100 | dataTwoGroups = [ 101 | { 102 | key: 'k1', 103 | type: 'bar', 104 | label: 'Series 1', 105 | values: [['Exp. 1', 234], ['Exp. 2', 245]] 106 | }, { 107 | key: 'k2', 108 | type: 'bar', 109 | label: 'Series 2', 110 | values: [['Exp. 1', 341], ['Exp. 2', 321]] 111 | } 112 | ]; 113 | 114 | chartTwoGroups.data(dataTwoGroups).render(); 115 | 116 | months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 117 | 118 | getVals = function(list) { 119 | return list.map(function(d, i) { 120 | return { 121 | month: months[i], 122 | val: d 123 | }; 124 | }); 125 | }; 126 | 127 | dataSingleSeries = { 128 | monthlyData: { 129 | color: '#aaa', 130 | type: 'bar', 131 | classed: function(d) { 132 | if (d.val > 22) { 133 | return '-highlight-bar'; 134 | } else { 135 | return ''; 136 | } 137 | }, 138 | values: getVals([3.4, 4.5, 6.7, 8.0, 13.5, 22.4, 19.8, 15.4, 11.3, 8.3, 6.5, 2.4]) 139 | }, 140 | average: { 141 | color: '#aaf', 142 | type: 'line', 143 | interpolate: 'cardinal', 144 | values: getVals([3.9, 5.5, 2.7, 8.9, 30.5, 36.4, 23.8, 14.4, 11.3, 7.3, 8.5, 5.4]) 145 | } 146 | }; 147 | 148 | chartSingleSeries = new ForestD3.Chart('#example-single-series'); 149 | 150 | chartSingleSeries.getX(function(d) { 151 | return d.month; 152 | }).getY(function(d) { 153 | return d.val; 154 | }).reduceXTicks(false).chartLabel('Monthly Calculations').data(dataSingleSeries).addPlugin(new ForestD3.Legend('#legend-single-series')).render(); 155 | 156 | dataStacked = [ 157 | { 158 | label: 'Apples', 159 | values: getVals([-1.3, 1, 3, 4.6, 8.81, 7.6, 4, 1.3]) 160 | }, { 161 | label: 'Pears', 162 | values: getVals([-1.7, 1, 1.3, 2.4, 5.6, 7.6, 4.5, 1.4]) 163 | }, { 164 | label: 'Grapes', 165 | values: getVals([-2.3, 0.4, 0.9, 1.2, 3.4, 2.4, 0.6, 0.3]) 166 | }, { 167 | label: 'Strawberries', 168 | values: getVals([-6.7, 1.9, 3, 4.6, 7.3, 5.5, 4.3, 0.6]) 169 | }, { 170 | type: 'marker', 171 | axis: 'y', 172 | value: 7.1, 173 | label: 'Threshold' 174 | } 175 | ]; 176 | 177 | chartStackedBar = new ForestD3.StackedChart('#example-stacked'); 178 | 179 | legendStacked = new ForestD3.Legend('#legend-stacked'); 180 | 181 | chartStackedBar.getX(function(d) { 182 | return d.month; 183 | }).getY(function(d) { 184 | return d.val; 185 | }).chartLabel('Stacked Bar Example').xPadding(0.2).barPaddingPercent(0.0).stacked(true).stackType('bar').addPlugin(legendStacked).data(dataStacked).render(); 186 | 187 | document.getElementById('toggle-stacked-button').addEventListener('click', function() { 188 | return chartStackedBar.stacked(!chartStackedBar.stacked()).render(); 189 | }); 190 | 191 | }).call(this); 192 | -------------------------------------------------------------------------------- /demo/app-line.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var chart, chartLog, chartNonOrdinal, chartRandom, chartSwitcher, chartUpdate, data, dataLog, dataNonOrdinal, dataRandom, dataUpdate, getRandom, getStocks, legend, rand, switchData; 3 | 4 | chart = new ForestD3.Chart(d3.select('#example')); 5 | 6 | legend = new ForestD3.Legend(d3.select('#legend')); 7 | 8 | chart.ordinal(true).margin({ 9 | left: 90 10 | }).xPadding(0).xLabel('Date').yLabel('Price').yTickFormat(d3.format(',.2f')).xTickFormat(function(d) { 11 | if (d != null) { 12 | return d3.time.format('%Y-%m-%d')(new Date(d)); 13 | } else { 14 | return ''; 15 | } 16 | }).addPlugin(legend); 17 | 18 | getStocks = function(startPrice, volatility, points) { 19 | var changePct, i, j, ref, result, startDate; 20 | if (points == null) { 21 | points = 80; 22 | } 23 | result = []; 24 | startDate = new Date(2012, 0, 1); 25 | for (i = j = 0, ref = points; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) { 26 | result.push([startDate.getTime(), startPrice - 0.3]); 27 | changePct = 2 * volatility * Math.random(); 28 | if (changePct > volatility) { 29 | changePct -= 2 * volatility; 30 | } 31 | startPrice += startPrice * changePct; 32 | startDate.setDate(startDate.getDate() + 1); 33 | } 34 | return result; 35 | }; 36 | 37 | data = [ 38 | { 39 | key: 'series1', 40 | label: 'AAPL', 41 | type: 'line', 42 | interpolate: 'cardinal', 43 | values: getStocks(5.75, 0.47) 44 | }, { 45 | key: 'series2', 46 | label: 'MSFT', 47 | type: 'line', 48 | area: true, 49 | values: getStocks(5, 1.1) 50 | }, { 51 | key: 'series3', 52 | label: 'FACEBOOK', 53 | type: 'line', 54 | area: true, 55 | interpolate: 'cardinal', 56 | values: getStocks(6.56, 0.13) 57 | }, { 58 | key: 'series4', 59 | label: 'AMAZON', 60 | type: 'line', 61 | area: false, 62 | values: getStocks(7.89, 0.37) 63 | }, { 64 | key: 'marker1', 65 | label: 'Profit', 66 | type: 'marker', 67 | axis: 'y', 68 | value: 0.2 69 | }, { 70 | key: 'region1', 71 | label: 'Earnings Season', 72 | type: 'region', 73 | axis: 'x', 74 | values: [5, 9] 75 | } 76 | ]; 77 | 78 | chart.data(data).render(); 79 | 80 | chartUpdate = new ForestD3.Chart('#example-update'); 81 | 82 | chartUpdate.ordinal(true).chartLabel('Citi Bank (NYSE)').xTickFormat(function(d) { 83 | if (d != null) { 84 | return d3.time.format('%Y-%m-%d')(new Date(d)); 85 | } else { 86 | return ''; 87 | } 88 | }); 89 | 90 | dataUpdate = [ 91 | { 92 | key: 'series1', 93 | type: 'line', 94 | label: 'CITI', 95 | values: getStocks(206, 0.07, 200) 96 | } 97 | ]; 98 | 99 | chartUpdate.data(dataUpdate).render(); 100 | 101 | document.getElementById('update-data').addEventListener('click', function() { 102 | dataUpdate[0].values = getStocks(206, 0.07, 200); 103 | return chartUpdate.data(dataUpdate).render(); 104 | }); 105 | 106 | chartLog = new ForestD3.Chart('#example-log-scale'); 107 | 108 | chartLog.ordinal(true).yScaleType(d3.scale.log).yPadding(0).chartLabel('Logarithmic Scale Example').tooltipType('spatial').xTickFormat(function(d) { 109 | if (d != null) { 110 | return d3.time.format('%Y-%m')(new Date(d)); 111 | } else { 112 | return ''; 113 | } 114 | }); 115 | 116 | dataLog = [ 117 | { 118 | key: 'series1', 119 | label: 'AAPL', 120 | type: 'line', 121 | color: '#efefef', 122 | values: getStocks(200, 0.4, 100).map(function(p) { 123 | if (p[1] <= 0) { 124 | p[1] = 1; 125 | } 126 | return p; 127 | }) 128 | } 129 | ]; 130 | 131 | chartLog.data(dataLog).render(); 132 | 133 | chartRandom = new ForestD3.Chart('#example-random'); 134 | 135 | chartRandom.ordinal(false).getX(function(d) { 136 | return d.x; 137 | }).getY(function(d) { 138 | return d.y; 139 | }).chartLabel('Random Data Points'); 140 | 141 | getRandom = function() { 142 | var j, points, rand, results; 143 | rand = d3.random.normal(0, 0.6); 144 | points = (function() { 145 | results = []; 146 | for (j = 0; j < 50; j++){ results.push(j); } 147 | return results; 148 | }).apply(this).map(function(i) { 149 | return { 150 | x: i, 151 | y: rand() 152 | }; 153 | }); 154 | return d3.shuffle(points); 155 | }; 156 | 157 | dataRandom = { 158 | series1: { 159 | type: 'line', 160 | values: getRandom() 161 | }, 162 | series2: { 163 | type: 'line', 164 | values: getRandom() 165 | } 166 | }; 167 | 168 | chartRandom.data(dataRandom).render(); 169 | 170 | chartNonOrdinal = new ForestD3.Chart('#example-non-ordinal'); 171 | 172 | chartNonOrdinal.ordinal(false).tooltipType('spatial').xTickFormat(d3.format('.2f')).chartLabel('Non-Ordinal Chart'); 173 | 174 | rand = d3.random.normal(0, 0.6); 175 | 176 | dataNonOrdinal = [ 177 | { 178 | type: 'scatter', 179 | symbol: 'circle', 180 | color: 'yellow', 181 | values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map(function(d) { 182 | return [rand(), rand()]; 183 | }) 184 | }, { 185 | type: 'line', 186 | color: 'white', 187 | values: [[-1, -1], [-0.8, -0.7], [-0.3, -0.56], [0.4, 0.7], [0.2, 0.5], [0.5, 0.8], [1, 1.1]] 188 | } 189 | ]; 190 | 191 | chartNonOrdinal.data(dataNonOrdinal).render(); 192 | 193 | document.getElementById('update-x-sort').addEventListener('click', function() { 194 | chartRandom.autoSortXValues(!chartRandom.autoSortXValues()); 195 | return chartRandom.data(dataRandom).render(); 196 | }); 197 | 198 | chartSwitcher = new ForestD3.Chart('#example-type-switch'); 199 | 200 | chartSwitcher.xTickFormat(function(d) { 201 | if (d != null) { 202 | return d3.time.format('%Y-%m-%d')(new Date(d)); 203 | } else { 204 | return ''; 205 | } 206 | }); 207 | 208 | switchData = { 209 | series: { 210 | color: 'springgreen', 211 | type: 'line', 212 | values: getStocks(345, 0.2) 213 | } 214 | }; 215 | 216 | chartSwitcher.data(switchData).render(); 217 | 218 | document.getElementById('switch-to-line').addEventListener('click', function() { 219 | switchData.series.type = 'line'; 220 | switchData.series.area = false; 221 | return chartSwitcher.data(switchData).render(); 222 | }); 223 | 224 | document.getElementById('switch-to-scatter').addEventListener('click', function() { 225 | switchData.series.type = 'scatter'; 226 | return chartSwitcher.data(switchData).render(); 227 | }); 228 | 229 | document.getElementById('switch-to-bar').addEventListener('click', function() { 230 | switchData.series.type = 'bar'; 231 | return chartSwitcher.data(switchData).render(); 232 | }); 233 | 234 | document.getElementById('switch-to-area').addEventListener('click', function() { 235 | switchData.series.type = 'line'; 236 | switchData.series.area = true; 237 | return chartSwitcher.data(switchData).render(); 238 | }); 239 | 240 | }).call(this); 241 | -------------------------------------------------------------------------------- /demo/app-ohlc.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var chart, data, getStocks, legend, stocks; 3 | 4 | chart = new ForestD3.Chart('#example'); 5 | 6 | legend = new ForestD3.Legend('#legend'); 7 | 8 | chart.ordinal(true).getX(function(d) { 9 | return d.date; 10 | }).getY(function(d) { 11 | return d.value; 12 | }).xLabel('Date').yLabel('Quote').yTickFormat(d3.format(',.3f')).xTickFormat(function(d) { 13 | if (d != null) { 14 | return d3.time.format('%Y-%m-%d')(new Date(d)); 15 | } else { 16 | return ''; 17 | } 18 | }).addPlugin(legend); 19 | 20 | getStocks = function(startPrice, volatility) { 21 | var changePct, close, hi, i, j, lo, result, startDate; 22 | result = []; 23 | startDate = new Date(2012, 0, 1); 24 | for (i = j = 0; j < 40; i = ++j) { 25 | hi = startPrice + Math.random() * 5; 26 | lo = startPrice - Math.random() * 5; 27 | close = Math.random() * (lo - hi) + hi; 28 | result.push({ 29 | date: startDate.getTime(), 30 | open: startPrice, 31 | hi: hi, 32 | lo: lo, 33 | close: close 34 | }); 35 | changePct = 2 * volatility * Math.random(); 36 | if (changePct > volatility) { 37 | changePct -= 2 * volatility; 38 | } 39 | startPrice += startPrice * changePct; 40 | startDate.setDate(startDate.getDate() + 1); 41 | } 42 | return result; 43 | }; 44 | 45 | stocks = getStocks(75, 0.047); 46 | 47 | data = [ 48 | { 49 | key: 'series1', 50 | label: 'AAPL', 51 | type: 'ohlc', 52 | values: stocks, 53 | getOpen: function(d) { 54 | return d.open; 55 | }, 56 | getClose: function(d) { 57 | return d.close; 58 | }, 59 | getHi: function(d) { 60 | return d.hi; 61 | }, 62 | getLo: function(d) { 63 | return d.lo; 64 | } 65 | }, { 66 | key: 'series2', 67 | label: 'AAPL Low', 68 | type: 'line', 69 | color: 'orange', 70 | getY: function(d) { 71 | return d.lo; 72 | }, 73 | interpolate: 'cardinal', 74 | values: (function() { 75 | return stocks.map(function(d) { 76 | return { 77 | date: d.date, 78 | value: d.lo 79 | }; 80 | }); 81 | })() 82 | } 83 | ]; 84 | 85 | chart.data(data).render(); 86 | 87 | }).call(this); 88 | -------------------------------------------------------------------------------- /demo/app-pie.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var data, pieChart; 3 | 4 | pieChart = new ForestD3.PieChart('#example'); 5 | 6 | data = [ 7 | { 8 | label: 'Apples', 9 | value: 100 10 | }, { 11 | label: 'Pears', 12 | value: 34 13 | }, { 14 | label: 'Bananas', 15 | value: 6 16 | }, { 17 | label: 'Oranges', 18 | value: 87 19 | }, { 20 | label: 'Grapes', 21 | value: 54 22 | }, { 23 | label: 'Melons', 24 | value: 2 25 | }, { 26 | label: 'Strawberries', 27 | value: 32 28 | } 29 | ]; 30 | 31 | pieChart.getLabel(function(d) { 32 | return d.label; 33 | }).getValue(function(d) { 34 | return d.value; 35 | }).data(data).render(); 36 | 37 | }).call(this); 38 | -------------------------------------------------------------------------------- /demo/app-scatter.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var chart, chartHover, data, dataHover, getValues, legend; 3 | 4 | chart = new ForestD3.Chart('#example'); 5 | 6 | legend = new ForestD3.Legend('#legend'); 7 | 8 | chart.ordinal(false).chartLabel('Spatial Tooltips').tooltipType('spatial').xTickFormat(d3.format('.2f')).addPlugin(legend); 9 | 10 | getValues = function(deviation) { 11 | var i, rand, results, values; 12 | if (deviation == null) { 13 | deviation = 1.0; 14 | } 15 | rand = d3.random.normal(0, deviation); 16 | return values = (function() { 17 | results = []; 18 | for (i = 0; i < 30; i++){ results.push(i); } 19 | return results; 20 | }).apply(this).map(function(_) { 21 | return [rand(), rand()]; 22 | }); 23 | }; 24 | 25 | data = [ 26 | { 27 | shape: 'square', 28 | color: 'orange', 29 | values: getValues() 30 | }, { 31 | values: getValues(1.9) 32 | }, { 33 | shape: 'circle', 34 | values: getValues(0.7) 35 | } 36 | ]; 37 | 38 | chart.data(data).render(); 39 | 40 | chartHover = new ForestD3.Chart('#example-hover'); 41 | 42 | chartHover.ordinal(false).chartLabel('Hover Tooltips').tooltipType('hover').xTickFormat(d3.format('.2f')).addPlugin(new ForestD3.Legend('#legend-hover')); 43 | 44 | dataHover = [ 45 | { 46 | values: getValues(1.1) 47 | }, { 48 | values: getValues(1.3) 49 | }, { 50 | values: getValues(1.4) 51 | } 52 | ]; 53 | 54 | chartHover.data(dataHover).render(); 55 | 56 | }).call(this); 57 | -------------------------------------------------------------------------------- /demo/app-stacked-area.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var chart, chartBar, data, getStocks; 3 | 4 | getStocks = function(startPrice, volatility) { 5 | var changePct, i, j, result, startDate; 6 | result = []; 7 | startDate = new Date(2012, 0, 1); 8 | for (i = j = 0; j < 200; i = ++j) { 9 | result.push([startDate.getTime(), startPrice - 0.3]); 10 | changePct = 2 * volatility * Math.random(); 11 | if (changePct > volatility) { 12 | changePct -= 2 * volatility; 13 | } 14 | startPrice += startPrice * changePct; 15 | startDate.setDate(startDate.getDate() + 1); 16 | } 17 | return result; 18 | }; 19 | 20 | chart = new ForestD3.StackedChart('#example'); 21 | 22 | data = [ 23 | { 24 | label: 'Consumer Discretionary', 25 | values: getStocks(30, 0.05) 26 | }, { 27 | label: 'Health Care', 28 | values: getStocks(40, 0.05) 29 | }, { 30 | label: 'Industrials', 31 | values: getStocks(45, 0.05) 32 | }, { 33 | label: 'Financial', 34 | values: getStocks(100, 0.07) 35 | }, { 36 | label: 'Oil', 37 | values: getStocks(10, 0.04) 38 | }, { 39 | label: 'Mid Cap', 40 | values: getStocks(32, 0.01) 41 | }, { 42 | label: 'Real Estate', 43 | values: getStocks(70, 0.03) 44 | } 45 | ]; 46 | 47 | chart.stacked(true).stackType('area').xPadding(0).xTickFormat(function(d) { 48 | if (d != null) { 49 | return d3.time.format('%Y-%m-%d')(new Date(d)); 50 | } else { 51 | return ''; 52 | } 53 | }).addPlugin(new ForestD3.Legend('#legend')).data(data).render(); 54 | 55 | chartBar = new ForestD3.StackedChart('#example-bar'); 56 | 57 | chartBar.stacked(true).stackType('bar').xTickFormat(function(d) { 58 | if (d != null) { 59 | return d3.time.format('%Y-%m-%d')(new Date(d)); 60 | } else { 61 | return ''; 62 | } 63 | }).addPlugin(new ForestD3.Legend('#legend-bar')).data(data).render(); 64 | 65 | }).call(this); 66 | -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var barChart, barData, getStocks, legend, lineChart, lineData; 3 | 4 | lineChart = new ForestD3.Chart('#line-plot'); 5 | 6 | barChart = new ForestD3.Chart('#bar-plot'); 7 | 8 | legend = new ForestD3.Legend('#legend'); 9 | 10 | getStocks = function(startPrice, volatility, points) { 11 | var changePct, i, j, ref, result, startDate; 12 | if (points == null) { 13 | points = 20; 14 | } 15 | result = []; 16 | startDate = new Date(2012, 0, 1); 17 | for (i = j = 0, ref = points; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) { 18 | result.push([startDate.getTime(), startPrice - 0.3]); 19 | changePct = 2 * volatility * Math.random(); 20 | if (changePct > volatility) { 21 | changePct -= 2 * volatility; 22 | } 23 | startPrice += startPrice * changePct; 24 | startDate.setDate(startDate.getDate() + 1); 25 | } 26 | return result; 27 | }; 28 | 29 | lineData = { 30 | series1: { 31 | label: 'AAPL', 32 | type: 'line', 33 | color: 'rgb(143,228,94)', 34 | values: getStocks(320, 0.23, 100) 35 | }, 36 | series2: { 37 | label: 'AAPL Volatility', 38 | type: 'line', 39 | area: true, 40 | values: getStocks(304, 0.34, 100) 41 | }, 42 | series3: { 43 | label: 'Benchmark S&P', 44 | type: 'scatter', 45 | shape: 'triangle-down', 46 | size: 64, 47 | color: 'rgb(108, 109, 186)', 48 | values: getStocks(306, 0.289, 100) 49 | }, 50 | marker1: { 51 | label: 'DOW Average', 52 | type: 'marker', 53 | axis: 'y', 54 | value: 404 55 | } 56 | }; 57 | 58 | legend.onlyDataSeries(false); 59 | 60 | lineChart.ordinal(true).xTickFormat(function(d) { 61 | if (d != null) { 62 | return d3.time.format('%Y-%m-%d')(new Date(d)); 63 | } else { 64 | return ''; 65 | } 66 | }).showXAxis(false).duration(500).addPlugin(legend).on('tooltipBisect.test', function(evt) { 67 | return barChart.renderBisectTooltipAt(evt.index, evt.clientMouse[0]); 68 | }).on('tooltipHidden.test', function() { 69 | return barChart.hideTooltips(); 70 | }).data(lineData).render(); 71 | 72 | barData = [ 73 | { 74 | key: 'bar1', 75 | label: 'Volume', 76 | type: 'bar', 77 | values: getStocks(100, 0.35, 100) 78 | }, { 79 | key: 'marker1', 80 | label: 'VOL Average', 81 | type: 'marker', 82 | axis: 'y', 83 | value: 230 84 | } 85 | ]; 86 | 87 | barChart.ordinal(true).yTicks(3).xTickFormat(function(d) { 88 | if (d != null) { 89 | return d3.time.format('%Y-%m-%d')(new Date(d)); 90 | } else { 91 | return ''; 92 | } 93 | }).on('tooltipBisect.test', function(evt) { 94 | return lineChart.renderBisectTooltipAt(evt.index, evt.clientMouse[0]); 95 | }).on('tooltipHidden.test', function() { 96 | return lineChart.hideTooltips(); 97 | }).data(barData).render(); 98 | 99 | }).call(this); 100 | -------------------------------------------------------------------------------- /demo/bar-horizontal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Forest D3 - By Robin Hu 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 |
18 | 26 |
27 | 38 |
39 |
40 | 50 |
51 |
Sort
52 |
53 |
54 | 55 |
56 | 59 |
60 | 64 | 65 | -------------------------------------------------------------------------------- /demo/bar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Forest D3 - By Robin Hu 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 |
18 | 26 |
27 | 38 |
39 |
40 |
41 |

Bar Chart

42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | 55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | 91 |
92 | 95 |
96 | 100 | 101 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Forest D3 - By Robin Hu 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 |
18 | 26 |
27 | 38 |
39 |
40 |
41 |

Line plus bar chart

42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 52 |
53 | 56 |
57 | 61 | 62 | -------------------------------------------------------------------------------- /demo/line.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Forest D3 - By Robin Hu 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 |
18 | 26 |
27 | 38 |
39 |
40 |
41 |

Line Chart

42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | 55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | 75 | 76 | 77 | 78 |
79 |
80 |
81 |
82 | 83 |
84 | 87 |
88 | 92 | 93 | -------------------------------------------------------------------------------- /demo/ohlc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Forest D3 - By Robin Hu 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 |
18 | 26 |
27 | 38 |
39 |
40 |
41 |

OHLC Chart

42 |
43 |
44 |
45 |
46 |
47 | 48 |
49 | 52 |
53 | 57 | 58 | -------------------------------------------------------------------------------- /demo/pie.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Forest D3 - By Robin Hu 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 |
18 | 26 |
27 | 38 |
39 |
40 |

Pie Chart

41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 |
49 | 52 |
53 | 57 | 58 | -------------------------------------------------------------------------------- /demo/scatter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Forest D3 - By Robin Hu 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 |
18 | 26 |
27 | 38 |
39 |
40 |
41 |

Scatter Chart

42 |
43 | Using spatial tooltips 44 |   |  Read about quadtrees 45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | 61 |
62 | 65 |
66 | 70 | 71 | -------------------------------------------------------------------------------- /demo/stacked-area.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Forest D3 - By Robin Hu 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 |
18 | 26 |
27 | 38 |
39 |
40 |
41 |

Stacked Area Chart

42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | 55 |
56 | 59 |
60 | 64 | 65 | -------------------------------------------------------------------------------- /dist/forest-d3-dark.css: -------------------------------------------------------------------------------- 1 | .forest-d3 rect.bar { 2 | fill-opacity: 0.7; 3 | stroke-width: 1; 4 | stroke: #fff; 5 | stroke-opacity: 0.1; 6 | transition: fill-opacity 0.25s, stroke-opacity 0.25s; 7 | } 8 | .forest-d3 .highlight rect.bar { 9 | fill-opacity: 0.9; 10 | stroke-opacity: 0.9; 11 | } 12 | .forest-d3.bar-chart text { 13 | font-size: 20px; 14 | fill: #dcdcdc; 15 | } 16 | .forest-d3 .bars rect { 17 | stroke: #fff; 18 | } 19 | .forest-d3 .bars line.zero-line { 20 | stroke: #fff; 21 | stroke-opacity: 0.7; 22 | } 23 | 24 | .forest-d3 svg { 25 | width: 100%; 26 | height: 100%; 27 | } 28 | .forest-d3.auto-height svg { 29 | height: auto; 30 | } 31 | .forest-d3 rect.backdrop { 32 | opacity: 0; 33 | pointer-events: all; 34 | } 35 | .forest-d3 .axis .tick line { 36 | stroke: #fff; 37 | } 38 | .forest-d3 .axis .tick text { 39 | fill: #dcdcdc; 40 | } 41 | .forest-d3 .axis.x-axis .tick line { 42 | stroke-opacity: 0.2; 43 | } 44 | .forest-d3 .axis.y-axis .tick line { 45 | stroke-opacity: 0.1; 46 | } 47 | .forest-d3 .axis > path { 48 | fill: none; 49 | stroke: #fff; 50 | stroke-opacity: 0.2; 51 | } 52 | .forest-d3 .axes-labels { 53 | fill: #dcdcdc; 54 | pointer-events: none; 55 | } 56 | .forest-d3 .canvas > rect { 57 | pointer-events: all; 58 | fill: none; 59 | stroke: #fff; 60 | stroke-opacity: 0.2; 61 | } 62 | .forest-d3 .canvas path.point { 63 | fill-opacity: 0.75; 64 | stroke: #fff; 65 | stroke-opacity: 0.5; 66 | transition: fill-opacity 0.25s, stroke-opacity 0.25s, stroke-width 0.25s; 67 | } 68 | .forest-d3 .canvas .highlight path.point { 69 | fill-opacity: 0.9; 70 | stroke-opacity: 0.9; 71 | stroke-width: 1.5; 72 | } 73 | .forest-d3 .canvas line.guideline, 74 | .forest-d3 .canvas line.crosshair-x, 75 | .forest-d3 .canvas line.crosshair-y { 76 | stroke: #fff; 77 | pointer-events: none; 78 | } 79 | .forest-d3 .canvas .guideline-markers circle.marker { 80 | stroke: #fff; 81 | stroke-opacity: 0.7; 82 | } 83 | .forest-d3 .canvas .series { 84 | pointer-events: none; 85 | } 86 | .forest-d3 .canvas .series.interactive { 87 | pointer-events: all; 88 | } 89 | 90 | .forest-d3.legend .item { 91 | min-width: 200px; 92 | padding: 5px; 93 | cursor: pointer; 94 | transition: all 0.25s; 95 | } 96 | .forest-d3.legend .item:hover { 97 | background-color: rgba(124,124,244,0.23); 98 | } 99 | .forest-d3.legend .item:hover .show-only { 100 | display: inline-block; 101 | } 102 | .forest-d3.legend .item.disabled { 103 | color: #ccc; 104 | font-style: italic; 105 | opacity: 0.5; 106 | } 107 | .forest-d3.legend .item .color-square { 108 | position: relative; 109 | top: 3px; 110 | display: inline-block; 111 | width: 18px; 112 | height: 18px; 113 | margin-right: 8px; 114 | border: solid 1px #ccc; 115 | border-radius: 3px; 116 | } 117 | .forest-d3.legend .item .show-only { 118 | display: none; 119 | margin-left: 15px; 120 | } 121 | .forest-d3.legend .button { 122 | cursor: pointer; 123 | color: #498fcb; 124 | } 125 | .forest-d3.legend .button:hover { 126 | text-decoration: underline; 127 | } 128 | 129 | .forest-d3 path.line { 130 | fill: none; 131 | stroke-opacity: 0.8; 132 | stroke-width: 2; 133 | pointer-events: none; 134 | transition: stroke-opacity 0.25s, stroke-width 0.25s; 135 | } 136 | .forest-d3 path.area { 137 | fill-opacity: 0.3; 138 | transition: fill-opacity 0.25s; 139 | } 140 | .forest-d3 .highlight path.line { 141 | stroke-opacity: 1; 142 | stroke-width: 3; 143 | } 144 | .forest-d3 .highlight path.area { 145 | fill-opacity: 0.5; 146 | } 147 | 148 | .forest-d3 line.marker { 149 | stroke: #fff; 150 | stroke-opacity: 0.5; 151 | stroke-dasharray: 5; 152 | } 153 | .forest-d3 .marker-label { 154 | fill: #dcdcdc; 155 | font-size: 11px; 156 | } 157 | 158 | .forest-d3 .ohlc line { 159 | stroke: #fff; 160 | } 161 | .forest-d3 .ohlc line.ohlc-range { 162 | stroke-width: 2px; 163 | } 164 | .forest-d3 .ohlc line.ohlc-open, 165 | .forest-d3 .ohlc line.ohlc-close { 166 | stroke-width: 3px; 167 | } 168 | 169 | .forest-d3 rect.region { 170 | fill: #dcdcdc; 171 | fill-opacity: 0.15; 172 | } 173 | 174 | .forest-d3.tooltip-box { 175 | position: absolute; 176 | pointer-events: none; 177 | min-width: 20px; 178 | min-height: 20px; 179 | margin: 0 10px 0 10px; 180 | padding: 7px; 181 | background: #201f1d; 182 | color: #dcdcdc; 183 | border: solid 1px #ccc; 184 | border-radius: 3px; 185 | z-index: 100; 186 | } 187 | .forest-d3.tooltip-box .header { 188 | font-weight: bold; 189 | margin-bottom: 8px; 190 | } 191 | .forest-d3.tooltip-box table td { 192 | white-space: nowrap; 193 | } 194 | .forest-d3.tooltip-box table td.series-label { 195 | padding-right: 20px; 196 | } 197 | .forest-d3.tooltip-box table td.series-value { 198 | text-align: right; 199 | font-weight: bold; 200 | } 201 | .forest-d3.tooltip-box table td .series-color { 202 | width: 13px; 203 | height: 12px; 204 | border-radius: 3px; 205 | margin-right: 10px; 206 | } 207 | -------------------------------------------------------------------------------- /dist/forest-d3.css: -------------------------------------------------------------------------------- 1 | .forest-d3 rect.bar { 2 | fill-opacity: 0.7; 3 | stroke-width: 1; 4 | stroke: #000; 5 | stroke-opacity: 0.1; 6 | transition: fill-opacity 0.25s, stroke-opacity 0.25s; 7 | } 8 | .forest-d3 .highlight rect.bar { 9 | fill-opacity: 0.9; 10 | stroke-opacity: 0.9; 11 | } 12 | .forest-d3.bar-chart text { 13 | font-size: 20px; 14 | fill: #333; 15 | } 16 | .forest-d3 .bars rect { 17 | stroke: #000; 18 | } 19 | .forest-d3 .bars line.zero-line { 20 | stroke: #000; 21 | stroke-opacity: 0.7; 22 | } 23 | 24 | .forest-d3 svg { 25 | width: 100%; 26 | height: 100%; 27 | } 28 | .forest-d3.auto-height svg { 29 | height: auto; 30 | } 31 | .forest-d3 rect.backdrop { 32 | opacity: 0; 33 | pointer-events: all; 34 | } 35 | .forest-d3 .axis .tick line { 36 | stroke: #000; 37 | } 38 | .forest-d3 .axis .tick text { 39 | fill: #333; 40 | } 41 | .forest-d3 .axis.x-axis .tick line { 42 | stroke-opacity: 0.2; 43 | } 44 | .forest-d3 .axis.y-axis .tick line { 45 | stroke-opacity: 0.07; 46 | } 47 | .forest-d3 .axis > path { 48 | fill: none; 49 | stroke: #000; 50 | stroke-opacity: 0.2; 51 | } 52 | .forest-d3 .axes-labels { 53 | fill: #333; 54 | pointer-events: none; 55 | } 56 | .forest-d3 .canvas > rect { 57 | pointer-events: all; 58 | fill: none; 59 | stroke: #000; 60 | stroke-opacity: 0.2; 61 | } 62 | .forest-d3 .canvas path.point { 63 | fill-opacity: 0.75; 64 | stroke: #000; 65 | stroke-opacity: 0.5; 66 | transition: fill-opacity 0.25s, stroke-opacity 0.25s, stroke-width 0.25s; 67 | } 68 | .forest-d3 .canvas .highlight path.point { 69 | fill-opacity: 0.9; 70 | stroke-opacity: 0.9; 71 | stroke-width: 1.5; 72 | } 73 | .forest-d3 .canvas line.guideline, 74 | .forest-d3 .canvas line.crosshair-x, 75 | .forest-d3 .canvas line.crosshair-y { 76 | stroke: #000; 77 | pointer-events: none; 78 | } 79 | .forest-d3 .canvas .guideline-markers circle.marker { 80 | stroke: #000; 81 | stroke-opacity: 0.7; 82 | } 83 | .forest-d3 .canvas .series { 84 | pointer-events: none; 85 | } 86 | .forest-d3 .canvas .series.interactive { 87 | pointer-events: all; 88 | } 89 | 90 | .forest-d3.legend .item { 91 | min-width: 200px; 92 | padding: 5px; 93 | cursor: pointer; 94 | transition: all 0.25s; 95 | } 96 | .forest-d3.legend .item:hover { 97 | background-color: rgba(10,10,10,0.23); 98 | } 99 | .forest-d3.legend .item:hover .show-only { 100 | display: inline-block; 101 | } 102 | .forest-d3.legend .item.disabled { 103 | color: #ccc; 104 | font-style: italic; 105 | opacity: 0.5; 106 | } 107 | .forest-d3.legend .item .color-square { 108 | position: relative; 109 | top: 3px; 110 | display: inline-block; 111 | width: 18px; 112 | height: 18px; 113 | margin-right: 8px; 114 | border: solid 1px #ccc; 115 | border-radius: 3px; 116 | } 117 | .forest-d3.legend .item .show-only { 118 | display: none; 119 | margin-left: 15px; 120 | } 121 | .forest-d3.legend .button { 122 | cursor: pointer; 123 | color: #498fcb; 124 | } 125 | .forest-d3.legend .button:hover { 126 | text-decoration: underline; 127 | } 128 | 129 | .forest-d3 path.line { 130 | fill: none; 131 | stroke-opacity: 0.8; 132 | stroke-width: 2; 133 | pointer-events: none; 134 | transition: stroke-opacity 0.25s, stroke-width 0.25s; 135 | } 136 | .forest-d3 path.area { 137 | fill-opacity: 0.3; 138 | transition: fill-opacity 0.25s; 139 | } 140 | .forest-d3 .highlight path.line { 141 | stroke-opacity: 1; 142 | stroke-width: 3; 143 | } 144 | .forest-d3 .highlight path.area { 145 | fill-opacity: 0.5; 146 | } 147 | 148 | .forest-d3 line.marker { 149 | stroke: #000; 150 | stroke-opacity: 0.5; 151 | stroke-dasharray: 5; 152 | } 153 | .forest-d3 .marker-label { 154 | fill: #333; 155 | font-size: 11px; 156 | } 157 | 158 | .forest-d3 .ohlc line { 159 | stroke: #000; 160 | } 161 | .forest-d3 .ohlc line.ohlc-range { 162 | stroke-width: 2px; 163 | } 164 | .forest-d3 .ohlc line.ohlc-open, 165 | .forest-d3 .ohlc line.ohlc-close { 166 | stroke-width: 3px; 167 | } 168 | 169 | .forest-d3 rect.region { 170 | fill: #333; 171 | fill-opacity: 0.15; 172 | } 173 | 174 | .forest-d3.tooltip-box { 175 | position: absolute; 176 | pointer-events: none; 177 | min-width: 20px; 178 | min-height: 20px; 179 | margin: 0 10px 0 10px; 180 | padding: 7px; 181 | background: #fff; 182 | color: #333; 183 | border: solid 1px #ccc; 184 | border-radius: 3px; 185 | z-index: 100; 186 | } 187 | .forest-d3.tooltip-box .header { 188 | font-weight: bold; 189 | margin-bottom: 8px; 190 | } 191 | .forest-d3.tooltip-box table td { 192 | white-space: nowrap; 193 | } 194 | .forest-d3.tooltip-box table td.series-label { 195 | padding-right: 20px; 196 | } 197 | .forest-d3.tooltip-box table td.series-value { 198 | text-align: right; 199 | font-weight: bold; 200 | } 201 | .forest-d3.tooltip-box table td .series-color { 202 | width: 13px; 203 | height: 12px; 204 | border-radius: 3px; 205 | margin-right: 10px; 206 | } 207 | -------------------------------------------------------------------------------- /documentation/README.md: -------------------------------------------------------------------------------- 1 | # Forest D3 2 | ## Documentation 3 | 4 | ### What is it? 5 | Forest D3 is a simple charting library built on top of d3.js. 6 | 7 | ### How to install 8 | 9 | The library exists as a single .js distributable and two CSS files. 10 | You need the files: 11 | 12 | * dist/forest-d3.js 13 | * dist/forest-d3.css 14 | * dist/forest-d3-dark.css 15 | 16 | The library is dependent on d3.js version 3.5.x. 17 | 18 | ### Basic Usage 19 | 20 | Charts need to be placed inside of a predefined DOM element. The chart 21 | will automatically size itself to match the container element's dimensions. 22 | 23 | ``` 24 |
25 | ``` 26 | 27 | Next, create a new ForestD3.Chart object and pass in some data: 28 | 29 | ``` 30 | var data = { 31 | 'series1': { 32 | label: 'My First Series', 33 | values: [ 34 | [1, 10], 35 | [2, 30], 36 | [3, 60], 37 | [4, 80] 38 | ] 39 | } 40 | }; 41 | 42 | var chart = new ForestD3.Chart('#my-chart'); 43 | chart.data(data).render(); 44 | ``` 45 | 46 | This will render a scatter chart by default. 47 | -------------------------------------------------------------------------------- /examples/bar-horizontal.jade: -------------------------------------------------------------------------------- 1 | extends ./template.jade 2 | 3 | block content 4 | style. 5 | text.negative { 6 | fill: #f22 !important; 7 | } 8 | 9 | rect.negative { 10 | fill: #622 !important; 11 | } 12 | 13 | .col-md-6.col-md-offset-3 14 | 15 | #sort-button.btn.btn-default Sort 16 | #example(style='border: solid 1px #555;') 17 | 18 | script(src="app-bar-horizontal.js") -------------------------------------------------------------------------------- /examples/bar.jade: -------------------------------------------------------------------------------- 1 | extends ./template.jade 2 | 3 | block content 4 | .col-xs-8 5 | h3.text-center Bar Chart 6 | #example(style="height: 400px;") 7 | .col-xs-2 8 | #legend(style="margin-top:50px;") 9 | 10 | .clearfix 11 | hr 12 | style. 13 | .-highlight-bar { 14 | fill: rgb(93, 60, 224) !important; 15 | } 16 | 17 | .col-md-12 18 | .row 19 | .col-xs-8 20 | #example-stacked(style='height: 380px;') 21 | 22 | .col-xs-2 23 | #legend-stacked(style="margin-top:50px;") 24 | 25 | button.btn.btn-default#toggle-stacked-button Toggle Stacked 26 | 27 | .col-md-12 28 | .row 29 | .col-xs-8 30 | #example-single-series(style='height: 380px;') 31 | .col-xs-2 32 | #legend-single-series(style="margin-top:50px;") 33 | 34 | .col-md-12 35 | hr 36 | .row 37 | .col-xs-4 38 | #example-single(style='height: 300px;') 39 | 40 | .col-xs-4 41 | #example-onegroup(style='height: 300px;') 42 | 43 | .col-xs-4 44 | #example-twogroups(style='height: 300px;') 45 | 46 | script(src="app-bar.js") 47 | -------------------------------------------------------------------------------- /examples/index.jade: -------------------------------------------------------------------------------- 1 | extends ./template.jade 2 | 3 | block content 4 | .col-xs-10 5 | h3.text-center Line plus bar chart 6 | #line-plot(style="height: 400px;") 7 | .col-xs-2 8 | #legend(style="margin-top:50px;") 9 | 10 | .clearfix 11 | .col-xs-10 12 | #bar-plot(style="height: 210px;") 13 | 14 | script(src="app.js") 15 | -------------------------------------------------------------------------------- /examples/line.jade: -------------------------------------------------------------------------------- 1 | extends ./template.jade 2 | 3 | block content 4 | .col-xs-10 5 | h3.text-center Line Chart 6 | #example(style="height: 400px;") 7 | .col-xs-2 8 | #legend(style="margin-top:50px;") 9 | 10 | .clearfix 11 | .col-md-12 12 | hr 13 | .row 14 | .col-xs-4 15 | #example-update(style='height:400px;') 16 | .text-center 17 | button.btn.btn-default#update-data Update Data 18 | 19 | .col-xs-4 20 | #example-log-scale(style='height:400px;') 21 | 22 | .col-xs-4 23 | #example-random(style='height:400px;') 24 | .text-center 25 | button.btn.btn-default#update-x-sort Toggle X Sort 26 | 27 | .row 28 | .col-xs-4 29 | #example-non-ordinal(style='height:400px;') 30 | 31 | .col-xs-4 32 | #example-type-switch(style='height:400px;') 33 | .text-center 34 | button.btn.btn-primary#switch-to-line Line 35 | button.btn.btn-primary#switch-to-scatter Scatter 36 | button.btn.btn-primary#switch-to-bar Bar 37 | button.btn.btn-primary#switch-to-area Area 38 | 39 | script(src="app-line.js") 40 | -------------------------------------------------------------------------------- /examples/ohlc.jade: -------------------------------------------------------------------------------- 1 | extends ./template.jade 2 | 3 | block content 4 | .col-xs-8 5 | h3.text-center OHLC Chart 6 | #example(style="height: 400px;") 7 | .col-xs-2 8 | #legend(style="margin-top:50px;") 9 | 10 | script(src="app-ohlc.js") 11 | -------------------------------------------------------------------------------- /examples/pie.jade: -------------------------------------------------------------------------------- 1 | extends ./template.jade 2 | 3 | block content 4 | h3.text-center Pie Chart 5 | 6 | .col-xs-6 7 | #example(style="height: 500px; width: 500px;") 8 | 9 | .col-xs-6 10 | #legend 11 | 12 | script(src='app-pie.js') 13 | -------------------------------------------------------------------------------- /examples/scatter.jade: -------------------------------------------------------------------------------- 1 | extends ./template.jade 2 | 3 | block content 4 | .col-md-12 5 | h3.text-center Scatter Chart 6 | .text-center 7 | | Using spatial tooltips 8 | |   |   9 | a(href='http://bl.ocks.org/robinfhu/c159eca5249af366c307') Read about quadtrees 10 | 11 | .col-xs-8 12 | #example(style='height: 480px;') 13 | 14 | .col-xs-2 15 | #legend 16 | 17 | .clearfix 18 | .col-xs-8 19 | #example-hover(style='height: 480px;') 20 | .col-xs-2 21 | #legend-hover 22 | 23 | 24 | 25 | script(src='app-scatter.js') 26 | 27 | -------------------------------------------------------------------------------- /examples/src/app-bar-horizontal.coffee: -------------------------------------------------------------------------------- 1 | chart = new ForestD3.BarChart '#example' 2 | data = [ 3 | key: 'series1' 4 | label: 'Long' 5 | color: '#555' 6 | values: [ 7 | ['Toyota', 100] 8 | ['Honda', 80] 9 | ['Mazda', 70] 10 | ['Prius', 10] 11 | ['Ford F150', 87] 12 | ['Hyundai', 23.4] 13 | ['Chrysler', 1] 14 | ['Lincoln', 102] 15 | ['Accord', -60] 16 | ['Hummer', -5.6] 17 | ['Dodge', -11] 18 | ] 19 | ] 20 | 21 | chart.data(data).render() 22 | chart.sortBy((d)-> d[1]) 23 | 24 | sortDir = 0 25 | 26 | document.getElementById('sort-button').addEventListener 'click', -> 27 | chart.sortDirection(if sortDir is 0 then 'asc' else 'desc').render() 28 | sortDir = 1 - sortDir 29 | -------------------------------------------------------------------------------- /examples/src/app-bar.coffee: -------------------------------------------------------------------------------- 1 | chart = new ForestD3.Chart '#example' 2 | legend = new ForestD3.Legend '#legend' 3 | 4 | chart 5 | .chartLabel('Trading Volume') 6 | .xLabel('Date') 7 | .yLabel('Volume') 8 | .yTickFormat(d3.format(',.3f')) 9 | .tooltipType('hover') 10 | .xTickFormat((d)-> 11 | if d? 12 | d3.time.format('%Y-%m-%d')(new Date d) 13 | else 14 | '' 15 | ) 16 | .addPlugin(legend) 17 | 18 | getStocks = (startPrice, volatility)-> 19 | result = [] 20 | startDate = new Date 2012, 0, 1 21 | 22 | for i in [0...15] 23 | result.push [ 24 | startDate.getTime(), 25 | startPrice - 0.3 26 | ] 27 | changePct = 2 * volatility * Math.random() 28 | if changePct > volatility 29 | changePct -= 2*volatility 30 | 31 | startPrice += startPrice * changePct 32 | startDate.setDate(startDate.getDate()+1) 33 | 34 | result 35 | 36 | data = [ 37 | key: 'series1' 38 | label: 'AAPL' 39 | type: 'bar' 40 | values: getStocks(0.75, 0.47) 41 | , 42 | key: 'series2' 43 | label: 'GOLDMAN' 44 | type: 'bar' 45 | values: getStocks(0.6, 0.32) 46 | , 47 | key: 'series3' 48 | label: 'CITI' 49 | type: 'bar' 50 | values: getStocks(0.45, 0.76) 51 | , 52 | key: 'marker1' 53 | label: 'High Volume' 54 | type: 'marker' 55 | axis: 'y' 56 | value: 0.21 57 | ] 58 | 59 | chart.data(data).render() 60 | 61 | # ******************* SINGLE BAR EXAMPLE **************** # 62 | chartSingle = new ForestD3.Chart '#example-single' 63 | chartSingle.chartLabel('Single Bar') 64 | dataSingle = [ 65 | key: 'k1' 66 | type: 'bar' 67 | label: 'Series 1' 68 | values: [ 69 | ['Population', 234] 70 | ] 71 | ] 72 | 73 | chartSingle.data(dataSingle).render() 74 | 75 | # ******************* ONE GROUP BAR EXAMPLE **************** # 76 | chartOneGroup = new ForestD3.Chart '#example-onegroup' 77 | chartOneGroup.yPadding(0.3).chartLabel('One Group') 78 | dataOneGroup = [ 79 | key: 'k1' 80 | type: 'bar' 81 | label: 'Series 1' 82 | values: [ 83 | ['Population', 234] 84 | ] 85 | , 86 | key: 'k2' 87 | type: 'bar' 88 | label: 'Series 2' 89 | values: [ 90 | ['Population', 341] 91 | ] 92 | ] 93 | 94 | chartOneGroup.data(dataOneGroup).render() 95 | 96 | # ******************* TWO GROUPS BAR EXAMPLE **************** # 97 | chartTwoGroups = new ForestD3.Chart '#example-twogroups' 98 | chartTwoGroups 99 | .xPadding(1.5) 100 | .yPadding(0) 101 | .forceDomain({y: 0}) 102 | .chartLabel('Two Groups') 103 | 104 | dataTwoGroups = [ 105 | key: 'k1' 106 | type: 'bar' 107 | label: 'Series 1' 108 | values: [ 109 | ['Exp. 1', 234] 110 | ['Exp. 2', 245] 111 | ] 112 | , 113 | key: 'k2' 114 | type: 'bar' 115 | label: 'Series 2' 116 | values: [ 117 | ['Exp. 1', 341] 118 | ['Exp. 2', 321] 119 | ] 120 | ] 121 | 122 | chartTwoGroups.data(dataTwoGroups).render() 123 | 124 | # ************************* Just a regular single series 125 | months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] 126 | getVals = (list)-> list.map (d,i)-> {month: months[i], val: d} 127 | 128 | dataSingleSeries = 129 | monthlyData: 130 | color: '#aaa' 131 | type: 'bar' 132 | classed: (d)-> 133 | if d.val > 22 then '-highlight-bar' else '' 134 | values: getVals([3.4,4.5,6.7,8.0,13.5,22.4,19.8,15.4,11.3,8.3,6.5,2.4]) 135 | average: 136 | color: '#aaf' 137 | type: 'line' 138 | interpolate: 'cardinal' 139 | values: getVals([3.9,5.5,2.7,8.9,30.5,36.4,23.8,14.4,11.3,7.3,8.5,5.4]) 140 | 141 | 142 | chartSingleSeries = new ForestD3.Chart '#example-single-series' 143 | chartSingleSeries 144 | .getX((d)-> d.month) 145 | .getY((d)-> d.val) 146 | .reduceXTicks(false) 147 | .chartLabel('Monthly Calculations') 148 | .data(dataSingleSeries) 149 | .addPlugin(new ForestD3.Legend('#legend-single-series')) 150 | .render() 151 | 152 | # ******************** Stacked Bar Example **************** 153 | dataStacked = [ 154 | label: 'Apples' 155 | values: getVals([-1.3,1,3,4.6,8.81,7.6,4,1.3]) 156 | , 157 | label: 'Pears' 158 | values: getVals([-1.7,1,1.3,2.4,5.6,7.6,4.5,1.4]) 159 | , 160 | label: 'Grapes' 161 | values: getVals([-2.3,0.4,0.9,1.2,3.4,2.4,0.6,0.3]) 162 | , 163 | label: 'Strawberries' 164 | values: getVals([-6.7,1.9,3,4.6,7.3,5.5,4.3,0.6]) 165 | , 166 | type: 'marker' 167 | axis: 'y' 168 | value: 7.1 169 | label: 'Threshold' 170 | ] 171 | 172 | chartStackedBar = new ForestD3.StackedChart '#example-stacked' 173 | legendStacked = new ForestD3.Legend '#legend-stacked' 174 | 175 | chartStackedBar 176 | .getX((d)-> d.month) 177 | .getY((d)-> d.val) 178 | .chartLabel('Stacked Bar Example') 179 | .xPadding(0.2) 180 | .barPaddingPercent(0.0) 181 | .stacked(true) 182 | .stackType('bar') 183 | .addPlugin(legendStacked) 184 | .data(dataStacked) 185 | .render() 186 | 187 | document.getElementById('toggle-stacked-button').addEventListener 'click', -> 188 | chartStackedBar.stacked(not chartStackedBar.stacked()).render() 189 | -------------------------------------------------------------------------------- /examples/src/app-line.coffee: -------------------------------------------------------------------------------- 1 | chart = new ForestD3.Chart d3.select('#example') 2 | legend = new ForestD3.Legend d3.select('#legend') 3 | 4 | chart 5 | .ordinal(true) 6 | .margin({left: 90}) 7 | .xPadding(0) 8 | .xLabel('Date') 9 | .yLabel('Price') 10 | .yTickFormat(d3.format(',.2f')) 11 | .xTickFormat((d)-> 12 | if d? 13 | d3.time.format('%Y-%m-%d')(new Date d) 14 | else 15 | '' 16 | ) 17 | .addPlugin(legend) 18 | 19 | getStocks = (startPrice, volatility, points=80)-> 20 | result = [] 21 | startDate = new Date 2012, 0, 1 22 | 23 | for i in [0...points] 24 | result.push [ 25 | startDate.getTime(), 26 | startPrice - 0.3 27 | ] 28 | changePct = 2 * volatility * Math.random() 29 | if changePct > volatility 30 | changePct -= 2*volatility 31 | 32 | startPrice += startPrice * changePct 33 | startDate.setDate(startDate.getDate()+1) 34 | 35 | result 36 | 37 | data = [ 38 | key: 'series1' 39 | label: 'AAPL' 40 | type: 'line' 41 | interpolate: 'cardinal' 42 | values: getStocks(5.75, 0.47) 43 | , 44 | key: 'series2' 45 | label: 'MSFT' 46 | type: 'line' 47 | area: true 48 | values: getStocks(5, 1.1) 49 | , 50 | key: 'series3' 51 | label: 'FACEBOOK' 52 | type: 'line' 53 | area: true 54 | interpolate: 'cardinal' 55 | values: getStocks(6.56, 0.13) 56 | , 57 | key: 'series4' 58 | label: 'AMAZON' 59 | type: 'line' 60 | area: false 61 | values: getStocks(7.89, 0.37) 62 | , 63 | key: 'marker1' 64 | label: 'Profit' 65 | type: 'marker' 66 | axis: 'y' 67 | value: 0.2 68 | , 69 | key: 'region1' 70 | label: 'Earnings Season' 71 | type: 'region' 72 | axis: 'x' 73 | values: [5, 9] 74 | ] 75 | 76 | chart.data(data).render() 77 | 78 | # ******************** Update Data Example ********************* 79 | chartUpdate = new ForestD3.Chart '#example-update' 80 | 81 | chartUpdate 82 | .ordinal(true) 83 | .chartLabel('Citi Bank (NYSE)') 84 | .xTickFormat((d)-> 85 | if d? 86 | d3.time.format('%Y-%m-%d')(new Date d) 87 | else 88 | '' 89 | ) 90 | 91 | dataUpdate = [ 92 | key: 'series1' 93 | type: 'line' 94 | label: 'CITI' 95 | values: getStocks(206, 0.07, 200) 96 | ] 97 | 98 | chartUpdate.data(dataUpdate).render() 99 | 100 | document.getElementById('update-data').addEventListener 'click', -> 101 | dataUpdate[0].values = getStocks(206, 0.07, 200) 102 | chartUpdate 103 | .data(dataUpdate) 104 | .render() 105 | 106 | # ******************* Log Scale Chart Example ******************* 107 | chartLog = new ForestD3.Chart '#example-log-scale' 108 | chartLog 109 | .ordinal(true) 110 | .yScaleType(d3.scale.log) 111 | .yPadding(0) 112 | .chartLabel('Logarithmic Scale Example') 113 | .tooltipType('spatial') 114 | .xTickFormat((d)-> 115 | if d? 116 | d3.time.format('%Y-%m')(new Date d) 117 | else 118 | '' 119 | ) 120 | 121 | dataLog = [ 122 | key: 'series1' 123 | label: 'AAPL' 124 | type: 'line' 125 | color: '#efefef' 126 | values: getStocks(200, 0.4, 100).map (p)-> 127 | if p[1] <= 0 128 | p[1] = 1 129 | 130 | p 131 | ] 132 | 133 | chartLog.data(dataLog).render() 134 | 135 | # ****************** Randomized data points and auto sort *********** 136 | chartRandom = new ForestD3.Chart '#example-random' 137 | chartRandom 138 | .ordinal(false) 139 | .getX((d)-> d.x) 140 | .getY((d)-> d.y) 141 | .chartLabel('Random Data Points') 142 | 143 | getRandom = -> 144 | rand = d3.random.normal(0, 0.6) 145 | points = [0...50].map (i)-> 146 | x: i 147 | y: rand() 148 | 149 | d3.shuffle points 150 | 151 | dataRandom = 152 | series1: 153 | type: 'line' 154 | values: getRandom() 155 | series2: 156 | type: 'line' 157 | values: getRandom() 158 | 159 | chartRandom.data(dataRandom).render() 160 | 161 | # ********************** Non Ordinal Line and scatter ************** 162 | chartNonOrdinal = new ForestD3.Chart '#example-non-ordinal' 163 | chartNonOrdinal 164 | .ordinal(false) 165 | .tooltipType('spatial') 166 | .xTickFormat(d3.format('.2f')) 167 | .chartLabel('Non-Ordinal Chart') 168 | 169 | rand = d3.random.normal(0, 0.6) 170 | dataNonOrdinal = [ 171 | type: 'scatter' 172 | symbol: 'circle' 173 | color: 'yellow' 174 | values: [0..20].map (d)-> [rand(), rand()] 175 | , 176 | type: 'line' 177 | color: 'white' 178 | values: [ 179 | [-1, -1] 180 | [-0.8, -0.7] 181 | [-0.3, -0.56] 182 | [0.4, 0.7] 183 | [0.2, 0.5] 184 | [0.5, 0.8] 185 | [1, 1.1] 186 | ] 187 | ] 188 | 189 | chartNonOrdinal.data(dataNonOrdinal).render() 190 | 191 | document.getElementById('update-x-sort').addEventListener 'click', -> 192 | chartRandom.autoSortXValues(not chartRandom.autoSortXValues()) 193 | chartRandom.data(dataRandom).render() 194 | 195 | # ********************* Switching series type example *************** 196 | chartSwitcher = new ForestD3.Chart '#example-type-switch' 197 | chartSwitcher 198 | .xTickFormat((d)-> 199 | if d? 200 | d3.time.format('%Y-%m-%d')(new Date d) 201 | else 202 | '' 203 | ) 204 | 205 | switchData = 206 | series: 207 | color: 'springgreen' 208 | type: 'line' 209 | values: getStocks(345,0.2) 210 | 211 | chartSwitcher.data(switchData).render() 212 | 213 | document.getElementById('switch-to-line').addEventListener 'click', -> 214 | switchData.series.type = 'line' 215 | switchData.series.area = false 216 | chartSwitcher.data(switchData).render() 217 | 218 | document.getElementById('switch-to-scatter').addEventListener 'click', -> 219 | switchData.series.type = 'scatter' 220 | chartSwitcher.data(switchData).render() 221 | 222 | document.getElementById('switch-to-bar').addEventListener 'click', -> 223 | switchData.series.type = 'bar' 224 | chartSwitcher.data(switchData).render() 225 | 226 | document.getElementById('switch-to-area').addEventListener 'click', -> 227 | switchData.series.type = 'line' 228 | switchData.series.area = true 229 | chartSwitcher.data(switchData).render() -------------------------------------------------------------------------------- /examples/src/app-ohlc.coffee: -------------------------------------------------------------------------------- 1 | chart = new ForestD3.Chart '#example' 2 | legend = new ForestD3.Legend '#legend' 3 | 4 | chart 5 | .ordinal(true) 6 | .getX((d)-> d.date) 7 | .getY((d)-> d.value) 8 | .xLabel('Date') 9 | .yLabel('Quote') 10 | .yTickFormat(d3.format(',.3f')) 11 | .xTickFormat((d)-> 12 | if d? 13 | d3.time.format('%Y-%m-%d')(new Date d) 14 | else 15 | '' 16 | ) 17 | .addPlugin(legend) 18 | 19 | getStocks = (startPrice, volatility)-> 20 | result = [] 21 | startDate = new Date 2012, 0, 1 22 | 23 | for i in [0...40] 24 | hi = startPrice + Math.random() * 5 25 | lo = startPrice - Math.random() * 5 26 | 27 | close = Math.random() * (lo - hi) + hi 28 | 29 | result.push { 30 | date: startDate.getTime() 31 | open: startPrice 32 | hi 33 | lo 34 | close 35 | } 36 | 37 | changePct = 2 * volatility * Math.random() 38 | if changePct > volatility 39 | changePct -= 2*volatility 40 | 41 | startPrice += startPrice * changePct 42 | startDate.setDate(startDate.getDate()+1) 43 | 44 | result 45 | 46 | stocks = getStocks(75, 0.047) 47 | 48 | data = [ 49 | key: 'series1' 50 | label: 'AAPL' 51 | type: 'ohlc' 52 | values: stocks 53 | getOpen: (d)-> d.open 54 | getClose: (d)-> d.close 55 | getHi: (d)-> d.hi 56 | getLo: (d)-> d.lo 57 | , 58 | key: 'series2' 59 | label: 'AAPL Low' 60 | type: 'line' 61 | color: 'orange' 62 | getY: (d)-> d.lo 63 | interpolate: 'cardinal' 64 | values: do-> 65 | stocks.map (d)-> 66 | date: d.date 67 | value: d.lo 68 | ] 69 | 70 | chart.data(data).render() 71 | -------------------------------------------------------------------------------- /examples/src/app-pie.coffee: -------------------------------------------------------------------------------- 1 | pieChart = new ForestD3.PieChart '#example' 2 | 3 | data = [ 4 | label: 'Apples' 5 | value: 100 6 | , 7 | label: 'Pears' 8 | value: 34 9 | , 10 | label: 'Bananas' 11 | value: 6 12 | , 13 | label: 'Oranges' 14 | value: 87 15 | , 16 | label: 'Grapes' 17 | value: 54 18 | , 19 | label: 'Melons' 20 | value: 2 21 | , 22 | label: 'Strawberries' 23 | value: 32 24 | ] 25 | 26 | pieChart 27 | .getLabel((d)-> d.label) 28 | .getValue((d)-> d.value) 29 | .data(data) 30 | .render() 31 | -------------------------------------------------------------------------------- /examples/src/app-scatter.coffee: -------------------------------------------------------------------------------- 1 | chart = new ForestD3.Chart '#example' 2 | legend = new ForestD3.Legend '#legend' 3 | chart 4 | .ordinal(false) 5 | .chartLabel('Spatial Tooltips') 6 | .tooltipType('spatial') 7 | .xTickFormat(d3.format('.2f')) 8 | .addPlugin(legend) 9 | 10 | 11 | getValues = (deviation=1.0)-> 12 | rand = d3.random.normal 0, deviation 13 | values = [0...30].map (_)-> [rand(), rand()] 14 | 15 | data = [ 16 | shape: 'square' 17 | color: 'orange' 18 | values: getValues() 19 | , 20 | values: getValues(1.9) 21 | , 22 | shape: 'circle' 23 | values: getValues(0.7) 24 | ] 25 | 26 | chart.data(data).render() 27 | 28 | # ************************* Hover example ****************** 29 | chartHover = new ForestD3.Chart '#example-hover' 30 | chartHover 31 | .ordinal(false) 32 | .chartLabel('Hover Tooltips') 33 | .tooltipType('hover') 34 | .xTickFormat(d3.format('.2f')) 35 | .addPlugin(new ForestD3.Legend '#legend-hover') 36 | 37 | dataHover = [ 38 | values: getValues(1.1) 39 | , 40 | values: getValues(1.3) 41 | , 42 | values: getValues(1.4) 43 | ] 44 | 45 | chartHover.data(dataHover).render() -------------------------------------------------------------------------------- /examples/src/app-stacked-area.coffee: -------------------------------------------------------------------------------- 1 | getStocks = (startPrice, volatility)-> 2 | result = [] 3 | startDate = new Date 2012, 0, 1 4 | 5 | for i in [0...200] 6 | result.push [ 7 | startDate.getTime(), 8 | startPrice - 0.3 9 | ] 10 | changePct = 2 * volatility * Math.random() 11 | if changePct > volatility 12 | changePct -= 2*volatility 13 | 14 | startPrice += startPrice * changePct 15 | startDate.setDate(startDate.getDate()+1) 16 | 17 | result 18 | 19 | chart = new ForestD3.StackedChart '#example' 20 | 21 | data = [ 22 | label: 'Consumer Discretionary' 23 | values: getStocks(30, 0.05) 24 | , 25 | label: 'Health Care' 26 | values: getStocks(40, 0.05) 27 | , 28 | label: 'Industrials' 29 | values: getStocks(45, 0.05) 30 | , 31 | label: 'Financial' 32 | values: getStocks(100, 0.07) 33 | , 34 | label: 'Oil' 35 | values: getStocks(10, 0.04) 36 | , 37 | label: 'Mid Cap' 38 | values: getStocks(32, 0.01) 39 | , 40 | label: 'Real Estate' 41 | values: getStocks(70, 0.03) 42 | ] 43 | 44 | chart 45 | .stacked(true) 46 | .stackType('area') 47 | .xPadding(0) 48 | .xTickFormat((d)-> 49 | if d? 50 | d3.time.format('%Y-%m-%d')(new Date d) 51 | else 52 | '' 53 | ) 54 | .addPlugin(new ForestD3.Legend('#legend')) 55 | .data(data) 56 | .render() 57 | 58 | # ******************** A stacked bar ******************** 59 | chartBar = new ForestD3.StackedChart '#example-bar' 60 | chartBar 61 | .stacked(true) 62 | .stackType('bar') 63 | .xTickFormat((d)-> 64 | if d? 65 | d3.time.format('%Y-%m-%d')(new Date d) 66 | else 67 | '' 68 | ) 69 | .addPlugin(new ForestD3.Legend('#legend-bar')) 70 | .data(data) 71 | .render() 72 | -------------------------------------------------------------------------------- /examples/src/app.coffee: -------------------------------------------------------------------------------- 1 | lineChart = new ForestD3.Chart '#line-plot' 2 | barChart = new ForestD3.Chart '#bar-plot' 3 | legend = new ForestD3.Legend '#legend' 4 | 5 | getStocks = (startPrice, volatility, points=20)-> 6 | result = [] 7 | startDate = new Date 2012, 0, 1 8 | 9 | for i in [0...points] 10 | result.push [ 11 | startDate.getTime(), 12 | startPrice - 0.3 13 | ] 14 | changePct = 2 * volatility * Math.random() 15 | if changePct > volatility 16 | changePct -= 2*volatility 17 | 18 | startPrice += startPrice * changePct 19 | startDate.setDate(startDate.getDate()+1) 20 | 21 | result 22 | 23 | lineData = 24 | series1: 25 | label: 'AAPL' 26 | type: 'line' 27 | color: 'rgb(143,228,94)' 28 | values: getStocks(320, 0.23, 100) 29 | series2: 30 | label: 'AAPL Volatility' 31 | type: 'line' 32 | area: true 33 | values: getStocks(304, 0.34, 100) 34 | series3: 35 | label: 'Benchmark S&P' 36 | type: 'scatter' 37 | shape: 'triangle-down' 38 | size: 64 39 | color: 'rgb(108, 109, 186)' 40 | values: getStocks(306, 0.289, 100) 41 | marker1: 42 | label: 'DOW Average' 43 | type: 'marker' 44 | axis: 'y' 45 | value: 404 46 | 47 | legend.onlyDataSeries false 48 | 49 | lineChart 50 | .ordinal(true) 51 | .xTickFormat((d)-> 52 | if d? 53 | d3.time.format('%Y-%m-%d')(new Date d) 54 | else 55 | '' 56 | ) 57 | .showXAxis(false) 58 | .duration(500) 59 | .addPlugin(legend) 60 | .on('tooltipBisect.test', (evt)-> 61 | barChart.renderBisectTooltipAt evt.index, evt.clientMouse[0] 62 | ) 63 | .on('tooltipHidden.test', -> 64 | barChart.hideTooltips() 65 | ) 66 | .data(lineData) 67 | .render() 68 | 69 | barData = [ 70 | key: 'bar1' 71 | label: 'Volume' 72 | type: 'bar' 73 | values: getStocks(100, 0.35, 100) 74 | , 75 | key: 'marker1' 76 | label: 'VOL Average' 77 | type: 'marker' 78 | axis: 'y' 79 | value: 230 80 | ] 81 | 82 | barChart 83 | .ordinal(true) 84 | .yTicks(3) 85 | .xTickFormat((d)-> 86 | if d? 87 | d3.time.format('%Y-%m-%d')(new Date d) 88 | else 89 | '' 90 | ) 91 | .on('tooltipBisect.test', (evt)-> 92 | lineChart.renderBisectTooltipAt evt.index, evt.clientMouse[0] 93 | ) 94 | .on('tooltipHidden.test', -> 95 | lineChart.hideTooltips() 96 | ) 97 | .data(barData) 98 | .render() -------------------------------------------------------------------------------- /examples/stacked-area.jade: -------------------------------------------------------------------------------- 1 | extends ./template.jade 2 | 3 | block content 4 | .col-xs-10 5 | h3.text-center Stacked Area Chart 6 | #example(style="height: 400px;") 7 | .col-xs-2 8 | #legend(style="margin-top:50px;") 9 | 10 | .clearfix 11 | 12 | .col-xs-10 13 | #example-bar(style="height: 400px;") 14 | .col-xs-2 15 | #legend-bar(style="margin-top:50px;") 16 | 17 | script(src="app-stacked-area.js") 18 | -------------------------------------------------------------------------------- /examples/template.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title Forest D3 - By Robin Hu 5 | 6 | script(src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.13/d3.js", charset="utf-8") 7 | script(src="forest-d3.js") 8 | link(rel="stylesheet", href="forest-d3-dark.css") 9 | link(rel="stylesheet", href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css") 10 | 11 | style. 12 | body.dark { 13 | background-color: #201F1D; 14 | color: #DCDCDC; 15 | } 16 | body.dark 17 | .container-fluid 18 | .page-header 19 | h1 20 | span#forest-d3-title Forest D3 21 | small    A Javascript charting library by Robin Hu 22 | 23 | h4. 24 | This webpage showcases some live examples of my charting library. Please enjoy! 25 | 26 | h3 27 | a(href="https://github.com/robinfhu/forest-d3") Github Page 28 | 29 | .row 30 | ul.nav.nav-pills 31 | li 32 | a(href="index.html") Home 33 | li.disabled 34 | a 35 | strong Chart Types: 36 | li 37 | a(href="line.html") Line 38 | li 39 | a(href="stacked-area.html") Stacked Area 40 | li 41 | a(href="bar.html") Bar 42 | li 43 | a(href="bar-horizontal.html") Horizontal Bar 44 | li 45 | a(href="scatter.html") Scatter 46 | li 47 | a(href="pie.html") Pie 48 | li 49 | a(href="ohlc.html") OHLC 50 | 51 | .row 52 | block content 53 | 54 | footer 55 | .text-center 56 | small © 2015-2016 Robin Hu 57 | 58 | script(type="text/javascript"). 59 | var version = ForestD3.version; 60 | document.getElementById('forest-d3-title').textContent = "Forest D3 " + version; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forest-d3", 3 | "version": "0.4.0-beta", 4 | "description": "A charting library", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "grunt", 8 | "start": "node ./node_modules/http-server/bin/http-server" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/robinfhu/forest-d3.git" 13 | }, 14 | "author": "Robin Hu", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/robinfhu/forest-d3/issues" 18 | }, 19 | "homepage": "https://github.com/robinfhu/forest-d3", 20 | "devDependencies": { 21 | "bootstrap": "^3.3.4", 22 | "coffeelint": "^1.10.1", 23 | "d3": "^3.5.5", 24 | "grunt": "^0.4.5", 25 | "grunt-coffeelint": "0.0.13", 26 | "grunt-contrib-coffee": "^0.13.0", 27 | "grunt-contrib-copy": "^0.8.0", 28 | "grunt-contrib-jade": "^0.14.1", 29 | "grunt-contrib-stylus": "^0.21.0", 30 | "grunt-karma": "^0.11.0", 31 | "http-server": "^0.8.0", 32 | "jquery": "^2.1.4", 33 | "karma": "^0.12.36", 34 | "karma-coffee-preprocessor": "^0.2.1", 35 | "karma-coverage": "^0.4.2", 36 | "karma-firefox-launcher": "^0.1.6", 37 | "karma-junit-reporter": "^0.2.2", 38 | "karma-mocha": "^0.1.10", 39 | "karma-sinon-chai": "^1.0.0", 40 | "karma-spec-reporter": "0.0.19", 41 | "mocha": "^2.2.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/bar-chart.coffee: -------------------------------------------------------------------------------- 1 | chartProperties = [ 2 | ['autoResize', true] 3 | ['getX', (d)-> d[0]] 4 | ['getY', (d)-> d[1]] 5 | ['height', null] 6 | ['barHeight', 40] 7 | ['barPadding', 10] 8 | ['sortBy', (d)-> d[0]] 9 | ['sortDirection', null] 10 | ] 11 | 12 | @ForestD3.BarChart = class BarChart extends ForestD3.BaseChart 13 | constructor: (domContainer)-> 14 | super domContainer 15 | d3.select(@container()).classed('auto-height bar-chart', true) 16 | @_setProperties chartProperties 17 | 18 | @getXInternal = (d)-> d.x 19 | @getYInternal = (d)-> d.y 20 | 21 | ### 22 | Set chart data. 23 | ### 24 | data: (d)-> 25 | unless d? 26 | return ForestD3.DataAPI.call @, @chartData 27 | else 28 | @chartData = ForestD3.Utils.normalize d, { 29 | getX: @getX() 30 | getY: @getY() 31 | ordinal: yes 32 | } 33 | return @ 34 | 35 | _barData: -> 36 | @data().get()[0].values 37 | 38 | _barDataSorted: -> 39 | unless @sortDirection() 40 | return @_barData() 41 | 42 | result = [] 43 | 44 | # Creates a copy of the values array, so original is not touched 45 | for d in @_barData() 46 | result.push d 47 | 48 | getVal = @sortBy() 49 | if @sortDirection() is 'asc' 50 | result.sort (a,b)-> 51 | d3.ascending getVal(a.data), getVal(b.data) 52 | else 53 | result.sort (a,b)-> 54 | d3.descending getVal(a.data), getVal(b.data) 55 | 56 | result 57 | ### 58 | A function that finds the longest label and computes an approximate 59 | pixel width for it. Used to determine how much left margin there should 60 | be. 61 | The technique used is: create a temporary element and put 62 | the longest label in it. Then use getBoundingClientRect to find the width. 63 | Quickly remove it after. 64 | ### 65 | calcMaxTextWidth: -> 66 | labels = @_barData().map (d,i)=> 67 | @getX()(d.data,i) 68 | 69 | maxL = 0 70 | maxLabel = '' 71 | 72 | for label in labels 73 | if label.length > maxL 74 | maxL = label.length 75 | maxLabel = label 76 | 77 | text = @svg.append('text').text(maxLabel) 78 | size = text.node().getBoundingClientRect().width 79 | text.remove() 80 | 81 | size + 20 82 | 83 | render: -> 84 | return unless @svg? 85 | return unless @chartData? 86 | @updateDimensions() 87 | @updateChartScale() 88 | @updateChartFrame() 89 | 90 | barY = (i)=> @barHeight()*i + @barPadding()*i 91 | 92 | chart = @ 93 | 94 | color = @data().get()[0].color 95 | 96 | labels = @labelGroup 97 | .selectAll('text') 98 | .data(@_barDataSorted(), @getXInternal) 99 | 100 | labels 101 | .enter() 102 | .append('text') 103 | .attr('text-anchor', 'end') 104 | .attr('x', 0) 105 | .attr('y', 0) 106 | .style('fill-opacity', 0) 107 | 108 | labels 109 | .exit() 110 | .remove() 111 | 112 | labels.each (d,i)-> 113 | isNegative = chart.getYInternal(d) < 0 114 | 115 | d3.select(@) 116 | .classed('positive', not isNegative) 117 | .classed('negative', isNegative) 118 | .text(chart.getX()(d.data,i)) 119 | .transition() 120 | .duration(700) 121 | .delay(i*20) 122 | .attr('y', barY(i)) 123 | .style('fill-opacity', 1) 124 | 125 | zeroPosition = chart.yScale 0 126 | 127 | bars = @barGroup 128 | .selectAll('rect') 129 | .data(@_barDataSorted(), @getXInternal) 130 | 131 | bars 132 | .enter() 133 | .append('rect') 134 | .attr('x', zeroPosition) 135 | .attr('y', 0) 136 | .style('fill-opacity', 0) 137 | .style('stroke-opacity', 0) 138 | 139 | bars 140 | .exit() 141 | .remove() 142 | 143 | bars.each (d,i)-> 144 | width = do-> 145 | yPos = chart.yScale chart.getYInternal(d) 146 | Math.abs (yPos - zeroPosition) 147 | 148 | isNegative = chart.getYInternal(d) < 0 149 | 150 | translate = 151 | if isNegative 152 | "translate(#{-width}, 0)" 153 | else 154 | '' 155 | 156 | d3.select(@) 157 | .attr('height', chart.barHeight()) 158 | .attr('transform', translate) 159 | .classed('positive', not isNegative) 160 | .classed('negative', isNegative) 161 | .transition() 162 | .attr('width', width) 163 | .style('fill', color) 164 | .duration(700) 165 | .delay(i*50) 166 | .attr('x', zeroPosition) 167 | .attr('y', barY(i)) 168 | .style('fill-opacity', 1) 169 | .style('stroke-opacity', 0.7) 170 | 171 | valueLabels = @valueGroup 172 | .selectAll('text') 173 | .data(@_barDataSorted(), @getXInternal) 174 | 175 | valueLabels 176 | .enter() 177 | .append('text') 178 | .attr('x', 0) 179 | 180 | valueLabels 181 | .exit() 182 | .remove() 183 | 184 | valueLabels.each (d,i)-> 185 | yVal = chart.getYInternal(d,i) 186 | isNegative = yVal < 0 187 | 188 | xPos = 189 | if isNegative 190 | zeroPosition 191 | else 192 | chart.yScale yVal 193 | 194 | d3.select(@) 195 | .classed('positive', not isNegative) 196 | .classed('negative', isNegative) 197 | .transition() 198 | .duration(700) 199 | .attr('y', barY(i)) 200 | .delay(i*20) 201 | .text(yVal) 202 | .attr('x', xPos) 203 | 204 | zeroLine = @barGroup.selectAll('line.zero-line').data([0]) 205 | zeroLine.enter().append('line').classed('zero-line', true) 206 | 207 | zeroLine 208 | .transition() 209 | .attr('x1', zeroPosition) 210 | .attr('x2', zeroPosition) 211 | .attr('y1', 0) 212 | .attr('y2', @canvasHeight) 213 | 214 | @ 215 | 216 | ### 217 | Get the chart's dimensions, based on the parent container
. 218 | Calculate chart margins and canvas dimensions. 219 | ### 220 | updateDimensions: -> 221 | container = @container() 222 | if container? 223 | bounds = container.getBoundingClientRect() 224 | 225 | @margin = 226 | left: @calcMaxTextWidth() 227 | right: 50 228 | 229 | @canvasWidth = bounds.width - @margin.left - @margin.right 230 | 231 | unless @height() 232 | barCount = @_barData().length 233 | @canvasHeight = barCount * (@barHeight() + @barPadding()) 234 | 235 | @svg.attr('height', @canvasHeight) 236 | 237 | updateChartScale: -> 238 | extent = ForestD3.Utils.extent @data().get(), {y: 0} 239 | 240 | @yScale = d3.scale.linear() 241 | .domain(extent.y) 242 | .range([0, @canvasWidth]) 243 | 244 | ### 245 | Draws the chart frame. Things like backdrop and canvas. 246 | ### 247 | updateChartFrame: -> 248 | padding = 10 249 | barCenter = @barHeight() / 2 + 5 250 | @labelGroup = @svg.selectAll('g.bar-labels').data([0]) 251 | @labelGroup.enter().append('g').classed('bar-labels', true) 252 | @labelGroup 253 | .attr('transform', 254 | "translate(#{@margin.left - padding},#{barCenter})" 255 | ) 256 | 257 | @barGroup = @svg.selectAll('g.bars').data([0]) 258 | @barGroup.enter().append('g').classed('bars', true) 259 | @barGroup 260 | .attr('transform', "translate(#{@margin.left},0)") 261 | 262 | @valueGroup = @svg.selectAll('g.bar-values').data([0]) 263 | @valueGroup.enter().append('g').classed('bar-values', true) 264 | @valueGroup 265 | .attr('transform', 266 | "translate(#{@margin.left + padding},#{barCenter})" 267 | ) -------------------------------------------------------------------------------- /src/base.coffee: -------------------------------------------------------------------------------- 1 | @ForestD3.BaseChart = class BaseChart 2 | constructor: (domContainer)-> 3 | @properties = {} 4 | 5 | @container domContainer 6 | 7 | @_metadata = {} 8 | @_dispatch = d3.dispatch( 9 | 'rendered', 10 | 'stateUpdate', 11 | 'tooltipBisect' 12 | 'tooltipHidden' 13 | ) 14 | 15 | @plugins = {} 16 | 17 | ### 18 | Auto resize the chart if user resizes the browser window. 19 | ### 20 | @resize = => 21 | if @autoResize() 22 | @render() 23 | 24 | window.addEventListener 'resize', @resize 25 | @_attachStateHandlers() 26 | 27 | ### 28 | Call this method to remove chart from the document and any artifacts 29 | it has (like tooltips) and event handlers. 30 | ### 31 | destroy: -> 32 | domContainer = @container() 33 | if domContainer?.parentNode? 34 | domContainer.parentNode.removeChild domContainer 35 | 36 | window.removeEventListener 'resize', @resize 37 | 38 | on: (type, listener)-> 39 | try 40 | @_dispatch.on type, listener 41 | catch e 42 | throw new Error "Chart does not recognize the event '#{type}'." 43 | 44 | @ 45 | 46 | trigger: (type)-> 47 | @_dispatch[type].apply @, Array.prototype.slice.call(arguments, 1) 48 | 49 | @ 50 | 51 | _attachStateHandlers: -> 52 | 53 | container: (d)-> 54 | unless d? 55 | return @properties['container'] 56 | else 57 | if d.select? and d.node? 58 | # This is a d3 selection 59 | d = d.node() 60 | else if typeof(d) is 'string' 61 | d = document.querySelector d 62 | 63 | @properties['container'] = d 64 | @svg = @createSvg() 65 | 66 | return @ 67 | 68 | ### 69 | Create an element to start rendering the chart. 70 | ### 71 | createSvg: -> 72 | container = @container() 73 | if container? 74 | exists = d3.select(container) 75 | .classed('forest-d3',true) 76 | .select 'svg' 77 | if exists.empty() 78 | return d3.select(container).append('svg') 79 | else 80 | return exists 81 | 82 | return null 83 | 84 | _setProperties: (chartProperties)-> 85 | ForestD3.Utils.setProperties @, @properties, chartProperties 86 | -------------------------------------------------------------------------------- /src/data.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Returns an API object that performs calculations and operations on a chart 3 | data object. 4 | 5 | Some operations can mutate the original chart data. 6 | 7 | Example usage: 8 | api = ForestD3.DataAPI.call chart, myData 9 | api.show('key1').render() 10 | internal = api.get() 11 | ### 12 | @ForestD3.DataAPI = (data)-> 13 | chart = @ 14 | 15 | # Returns the entire raw data object. 16 | get: -> data 17 | 18 | # Mark a given data series as hidden. 19 | hide: (keys, flag = true)-> 20 | if not (keys instanceof Array) 21 | keys = [keys] 22 | 23 | for d in data 24 | if d.key in keys 25 | d.hidden = flag 26 | 27 | @ 28 | 29 | # Un-hide data series. 30 | show: (keys)-> 31 | @hide keys, false 32 | 33 | # Flip data series on/off. 34 | toggle: (keys)-> 35 | if not (keys instanceof Array) 36 | keys = [keys] 37 | 38 | for d in data 39 | if d.key in keys 40 | d.hidden = not d.hidden 41 | 42 | @ 43 | 44 | # Turn everything off except for the given data series. 45 | # if 'onlyDataSeries' option is true, then markers and regions are ignored. 46 | showOnly: (key, options={})-> 47 | options.onlyDataSeries ?= false 48 | 49 | for d in data 50 | if options.onlyDataSeries and not d.isDataSeries 51 | continue 52 | 53 | d.hidden = not (d.key is key) 54 | 55 | @ 56 | 57 | # Turn everything on. 58 | showAll: -> 59 | for d in data 60 | d.hidden = false 61 | 62 | @ 63 | 64 | # Get list of data series' that are not hidden 65 | visible: -> 66 | data.filter (d)-> not d.hidden 67 | 68 | _getSliceable: -> 69 | data.filter (d)-> d.isDataSeries 70 | 71 | _xValues: (getX)-> 72 | dataObjs = @._getSliceable() 73 | return [] unless dataObjs[0]? 74 | 75 | dataObjs[0].values.map getX 76 | 77 | # Get array of all X-axis data points 78 | # Returns natural ordered indices if chart.ordinal is true 79 | xValues: -> 80 | @._xValues (d)-> d.x 81 | 82 | # Get array of all X-axis data points, returning the raw x value 83 | xValuesRaw: -> 84 | @._xValues (d)-> d.xValueRaw 85 | 86 | # Get the x raw value at a certain position 87 | xValueAt: (i)-> 88 | series = @._getSliceable() 89 | return null unless series[0]? 90 | 91 | point = series[0].values[i]?.xValueRaw 92 | 93 | ### 94 | For a set of data series, grabs a slice of the data at a certain index. 95 | Useful for making the 'bisect' tooltip. 96 | ### 97 | sliced: (idx)-> 98 | @._getSliceable().filter((d)-> not d.hidden).map (d)-> 99 | point = d.values[idx] 100 | point 101 | 102 | _barItems: -> @.visible().filter((d)-> d.type is 'bar') 103 | ### 104 | Count how many visible bar series items there are. 105 | Used for doing bar chart math. 106 | ### 107 | barCount: -> 108 | @._barItems().length 109 | 110 | ### 111 | Returns the index of the bar item given a key. 112 | Only takes into account visible bar items. 113 | Returns null if the key specified is not a bar item 114 | ### 115 | barIndex: (key)-> 116 | for item, i in @._barItems() 117 | if item.key is key 118 | return i 119 | 120 | return null 121 | 122 | quadtree: -> 123 | allPoints = @._getSliceable() 124 | .filter((d)-> not d.hidden) 125 | .map (s, i)-> s.values 126 | 127 | allPoints = d3.merge allPoints 128 | 129 | d3.geom.quadtree() 130 | .x((d)-> d.x) 131 | .y((d)-> d.y)(allPoints) 132 | 133 | ### 134 | Alias to chart.render(). Allows you to do things like: 135 | chart.data().show('mySeries').render() 136 | ### 137 | render: -> chart.render() 138 | -------------------------------------------------------------------------------- /src/features/crosshairs.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Handles the guideline that moves along the x-axis 3 | ### 4 | @ForestD3.Crosshairs = class Crosshairs 5 | constructor: (@chart)-> 6 | 7 | # Creates the guideline on the canvas selection 8 | create: (canvas)-> 9 | return unless @chart.showGuideline() 10 | 11 | # Add a guideline 12 | @xLine = canvas.selectAll('line.crosshair-x') 13 | .data([@chart.canvasHeight]) 14 | @yLine = canvas.selectAll('line.crosshair-y') 15 | .data([@chart.canvasWidth]) 16 | 17 | @xLine.enter() 18 | .append('line') 19 | .classed('crosshair-x', true) 20 | .style('stroke-opacity', 0) 21 | 22 | @xLine 23 | .attr('y1', 0) 24 | .attr('y2', (d)-> d) 25 | 26 | @yLine.enter() 27 | .append('line') 28 | .classed('crosshair-y', true) 29 | .style('stroke-opacity', 0) 30 | 31 | @yLine 32 | .attr('x1', 0) 33 | .attr('x2', (d)-> d) 34 | 35 | ### 36 | canvasMouse: the pixel coordinates on the chart canvas to draw the 37 | cross hairs. Array of [x,y] values. 38 | ### 39 | render: (canvasMouse)-> 40 | return unless @chart.showGuideline() 41 | return unless @xLine? 42 | 43 | [x, y] = canvasMouse 44 | # Show the crosshairs and position it. 45 | @xLine 46 | .transition() 47 | .duration(50) 48 | .attr('x1', x) 49 | .attr('x2', x) 50 | .style('stroke-opacity', 0.5) 51 | 52 | @yLine 53 | .transition() 54 | .duration(50) 55 | .attr('y1', y) 56 | .attr('y2', y) 57 | .style('stroke-opacity', 0.5) 58 | 59 | hide: -> 60 | return unless @chart.showGuideline() 61 | return unless @xLine? 62 | 63 | @xLine 64 | .transition() 65 | .delay(250) 66 | .style('stroke-opacity', 0) 67 | 68 | @yLine 69 | .transition() 70 | .delay(250) 71 | .style('stroke-opacity', 0) 72 | -------------------------------------------------------------------------------- /src/features/guideline.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Handles the guideline that moves along the x-axis. 3 | This likely only works for ordinal charts. 4 | ### 5 | @ForestD3.Guideline = class Guideline 6 | constructor: (@chart)-> 7 | 8 | # Creates the guideline on the canvas selection 9 | create: (canvas)-> 10 | return unless @chart.showGuideline() 11 | # Add a guideline 12 | @line = canvas.selectAll('line.guideline').data([@chart.canvasHeight]) 13 | 14 | @line.enter() 15 | .append('line') 16 | .classed('guideline', true) 17 | .style('opacity', 0) 18 | 19 | @line 20 | .attr('y1', 0) 21 | .attr('y2', (d)-> d) 22 | 23 | # Add container to put markers 24 | @markerContainer = canvas.selectAll('g.guideline-markers').data([0]) 25 | 26 | @markerContainer 27 | .enter() 28 | .append('g') 29 | .classed('guideline-markers', true) 30 | 31 | render: (xPosition, markerPoints=[])-> 32 | return unless @chart.showGuideline() 33 | return unless @line? 34 | 35 | # Show the guideline and position it. 36 | @line 37 | .attr('x1', xPosition) 38 | .attr('x2', xPosition) 39 | .transition() 40 | .style('opacity', 0.5) 41 | 42 | @markerContainer 43 | .transition() 44 | .style('opacity', 1) 45 | 46 | markers = @markerContainer 47 | .selectAll('circle.marker') 48 | .data(markerPoints) 49 | 50 | markers 51 | .enter() 52 | .append('circle') 53 | .classed('marker', true) 54 | .attr('r', 3) 55 | 56 | markers.exit().remove() 57 | 58 | markers 59 | .attr('cx', xPosition) 60 | .attr('cy', (d)-> d.y) 61 | .style('fill', (d)-> d.color) 62 | 63 | hide: -> 64 | return unless @chart.showGuideline() 65 | return unless @line? 66 | 67 | @line 68 | .transition() 69 | .delay(250) 70 | .style('opacity', 0) 71 | 72 | @markerContainer 73 | .transition() 74 | .delay(250) 75 | .style('opacity', 0) 76 | -------------------------------------------------------------------------------- /src/features/tooltip-content.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Library of tooltip rendering utilities 3 | ### 4 | @ForestD3.TooltipContent = 5 | multiple: (xValue, points)-> 6 | rows = points.map (d)-> 7 | bgColor = "background-color: #{d.series.color};" 8 | """ 9 |
10 |
11 | 12 | #{d.series.label or d.series.key} 13 | 14 | #{d.yFormatted} 15 | 16 | """ 17 | 18 | rows = rows.join '' 19 | 20 | """ 21 |
#{xValue}
22 | 23 | #{rows} 24 |
25 | """ 26 | 27 | single: (chart, point)-> 28 | series = point.series 29 | color = series.color 30 | bgColor = "background-color: #{color};" 31 | label = series.label or series.key 32 | 33 | """ 34 |
#{chart.xTickFormat()(point.xValueRaw)}
35 | 36 | 37 | 38 | 39 | 42 | 43 |
#{label} 40 | #{chart.yTickFormat()(point.yValueRaw)} 41 |
44 | """ -------------------------------------------------------------------------------- /src/features/tooltip.coffee: -------------------------------------------------------------------------------- 1 | @ForestD3.Tooltip = class Tooltip 2 | constructor: (chart)-> 3 | @container = null 4 | @chart = chart 5 | 6 | ### 7 | Lets you define a DOM id for the tooltip. Makes it so that 8 | you can perform DOM manipulation on it later on. 9 | ### 10 | id: (s)-> 11 | if arguments.length is 0 12 | return @_id 13 | else 14 | @_id = s 15 | return @ 16 | 17 | ### 18 | content: string or DOM object or d3 object representing tooltip content. 19 | clientMouse: Array of [mouse screen x, mouse screen y] positions 20 | ### 21 | render: (content, clientMouse)-> 22 | unless @container? 23 | @container = document.createElement 'div' 24 | document.body.appendChild @container 25 | 26 | # Populate the tooltip container with content. 27 | # Needs to be done early on so we can do positioning calculations. 28 | content ?= '' 29 | unless (typeof content) is 'string' 30 | content = content.toString() 31 | 32 | d3.select(@container) 33 | .classed('forest-d3 tooltip-box', true) 34 | .html(content) 35 | 36 | ### 37 | xPos and yPos are the relative coordinates of the mouse in the 38 | browser window. 39 | 40 | Adding page offset to it takes into account any scrolling. 41 | 42 | Because the tooltip DIV is placed on document.body, this should give 43 | us the absolute correct position. 44 | ### 45 | [xPos, yPos] = do (clientMouse)=> 46 | if (typeof clientMouse) is 'number' 47 | # put the tooltip at the top of the chart container 48 | chartClient = @chart.container().getBoundingClientRect().top 49 | return [clientMouse, chartClient] 50 | else if clientMouse instanceof Array 51 | return clientMouse 52 | else 53 | throw new Error """ 54 | Tooltip.render: no valid client mouse coordinates. 55 | """ 56 | 57 | xPos += window.pageXOffset 58 | yPos += window.pageYOffset 59 | 60 | ### 61 | Adjust tooltip so that it is centered on the mouse. 62 | Accomplish this by calculating container height and dividing it by 2. 63 | ### 64 | dimensions = @container.getBoundingClientRect() 65 | containerCenter = dimensions.height / 2 66 | yPos -= containerCenter 67 | 68 | ### 69 | Check to see if the tooltip will render past the right side of the 70 | browser window. If so, then move it to the left of the mouse. 71 | ### 72 | edgeThreshold = 40 73 | if (xPos + dimensions.width + edgeThreshold) > window.innerWidth 74 | xPos -= dimensions.width + edgeThreshold 75 | 76 | d3.select(@container) 77 | .attr('id', @id()) 78 | .style('left', "#{xPos}px") 79 | .style('top', "#{yPos}px") 80 | .transition() 81 | .style('opacity', 0.9) 82 | 83 | # Hide tooltip from view by making it transparent. 84 | hide: -> 85 | d3.select(@container) 86 | .transition() 87 | .delay(250) 88 | .style('opacity', 0) 89 | .each('end', -> 90 | # Moves tooltip to top left corner after transition is done. 91 | # This is so that the container doesn't disrupt the browser 92 | # if the window is resized. 93 | d3.select(@).style('left','0px').style('top', '0px') 94 | ) 95 | 96 | # Call this to remove the tooltip DIV from the page. 97 | destroy: -> 98 | if @container? 99 | document.body.removeChild @container 100 | -------------------------------------------------------------------------------- /src/main.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | ******************************************************** 3 | Forest D3 - a charting library using d3.js 4 | 5 | Author: Robin Hu 6 | 7 | ******************************************************** 8 | ### 9 | unless d3? 10 | throw new Error """ 11 | d3.js has not been included. See http://d3js.org/ for how to include it. 12 | """ 13 | 14 | @ForestD3 = 15 | version: '0.4.0-beta' 16 | 17 | @ForestD3.Visualizations = {} -------------------------------------------------------------------------------- /src/pie-chart.coffee: -------------------------------------------------------------------------------- 1 | chartProperties = [ 2 | ['getLabel', (d)-> d.label] 3 | ['getValue', (d)-> d.value] 4 | ] 5 | 6 | @ForestD3.PieChart = class PieChart extends ForestD3.BaseChart 7 | constructor: (domContainer)-> 8 | super domContainer 9 | d3.select(@container()).classed('pie-chart', true) 10 | @_setProperties chartProperties 11 | 12 | data: (d)-> 13 | if arguments.length is 0 14 | return ForestD3.DataAPI.call @, @_internalData 15 | else 16 | @_internalData = d.slice() 17 | 18 | @_internalData.sort (a,b)=> 19 | d3.ascending @getValue()(a), @getValue()(b) 20 | 21 | return @ 22 | 23 | render: -> 24 | return @ unless @svg? 25 | return @ unless @data().get()? 26 | 27 | @updateDimensions() 28 | @updateChartFrame() 29 | 30 | radius = d3.min([@canvasWidth,@canvasHeight]) / 2 31 | arc = d3.svg.arc() 32 | .outerRadius(radius) 33 | .innerRadius(0) 34 | 35 | pieData = d3.layout.pie() 36 | .sort(null) 37 | .value(@getValue())(@data().get()) 38 | 39 | slicesContainer = @canvas.selectAll('g.slices-container').data([0]) 40 | slicesContainer.enter().append('g').classed('slices-container') 41 | 42 | slicesContainer 43 | .attr('transform', 44 | "translate(#{@canvasWidth/2}, #{@canvasHeight/2})" 45 | ) 46 | 47 | slices = slicesContainer.selectAll('path.slice').data(pieData) 48 | 49 | slices 50 | .enter() 51 | .append('path') 52 | .classed('slice', true) 53 | .attr('d', (d)-> 54 | arc({startAngle: d.startAngle, endAngle: d.startAngle}) 55 | ) 56 | 57 | slices 58 | .transition() 59 | .duration(1000) 60 | .attr('d', arc) 61 | .style('fill', (d,i)-> ForestD3.Utils.defaultColor(i)) 62 | 63 | updateChartFrame: -> 64 | # Create a canvas, where all data points will be plotted. 65 | @canvas = @svg.selectAll('g.canvas').data([0]) 66 | 67 | canvasEnter = @canvas.enter().append('g').classed('canvas', true) 68 | 69 | @canvas 70 | .attr('transform',"translate(#{@margin}, #{@margin})") 71 | 72 | canvasEnter 73 | .append('rect') 74 | .classed('canvas-backdrop', true) 75 | 76 | @canvas.select('rect.canvas-backdrop') 77 | .attr('width', @canvasWidth) 78 | .attr('height', @canvasHeight) 79 | 80 | ### 81 | Get the chart's dimensions, based on the parent container
. 82 | Calculate chart margins and canvas dimensions. 83 | ### 84 | updateDimensions: -> 85 | container = @container() 86 | if container? 87 | bounds = container.getBoundingClientRect() 88 | 89 | @height = bounds.height 90 | @width = bounds.width 91 | 92 | @margin = 30 93 | 94 | ### 95 | Calculates the chart canvas dimensions. Uses the parent 96 | container's dimensions, and subtracts off any margins. 97 | ### 98 | @canvasHeight = @height - @margin * 2 99 | @canvasWidth = @width - @margin * 2 100 | 101 | # Ensures that charts cannot get smaller than 50x50 pixels. 102 | @canvasWidth = d3.max [@canvasWidth, 50] 103 | @canvasHeight = d3.max [@canvasHeight, 50] 104 | -------------------------------------------------------------------------------- /src/plugins/legend.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Draws a chart legend using HTML. 3 | It acts as a plugin to a main chart instance. 4 | ### 5 | 6 | legendProperties = [ 7 | ['onlyDataSeries', true] # if false, will show markers and regions in legend 8 | ] 9 | 10 | @ForestD3.Legend = class Legend 11 | constructor: (domContainer)-> 12 | @name = 'legend' 13 | 14 | @properties = {} 15 | ForestD3.Utils.setProperties @, @properties, legendProperties 16 | 17 | if domContainer.select? 18 | @container = domContainer 19 | else 20 | @container = d3.select domContainer 21 | 22 | @container.classed('forest-d3 legend', true) 23 | 24 | ### 25 | This is a technique to distinguish between single and double clicks. 26 | 27 | When any kind of click happens, the last event handler gets stored in 28 | @lastClickEvent. 29 | 30 | After a brief delay of about 200 ms, the lastClickEvent is executed. 31 | ### 32 | @lastClickEvent = -> 33 | processClickEvent = => 34 | @lastClickEvent.call @ 35 | 36 | @legendClickHandler = ForestD3.Utils.debounce processClickEvent, 200 37 | 38 | chart: (chart)-> 39 | @chartInstance = chart 40 | 41 | @ 42 | 43 | destroy: -> 44 | if @container? 45 | @container.remove() 46 | 47 | render: -> 48 | return unless @chartInstance? 49 | 50 | showAll = @container.selectAll('div.show-all').data([0]) 51 | showAll 52 | .enter() 53 | .append('div') 54 | .classed('show-all button', true) 55 | .text('show all') 56 | .on('click', (d)=> @chartInstance.data().showAll().render()) 57 | 58 | data = @chartInstance.data().get() 59 | if @onlyDataSeries() 60 | data = data.filter (d)-> d.isDataSeries 61 | 62 | items = @container.selectAll('div.item').data(data, (d)-> d.key) 63 | itemsEnter = items 64 | .enter() 65 | .append('div') 66 | .classed('item', true) 67 | 68 | items 69 | .on('click.legend', (d)=> 70 | @lastClickEvent = => 71 | @chartInstance.data().toggle(d.key).render() 72 | @legendClickHandler() 73 | ) 74 | .on('dblclick.legend', (d)=> 75 | @lastClickEvent = => 76 | @chartInstance 77 | .data() 78 | .showOnly(d.key, {onlyDataSeries: @onlyDataSeries()}) 79 | .render() 80 | @legendClickHandler() 81 | ) 82 | .on('mouseover.legend', (d)=> 83 | @chartInstance.highlightSeries d.key 84 | ) 85 | .on('mouseout.legend', (d)=> 86 | @chartInstance.highlightSeries null 87 | ) 88 | 89 | items.classed('disabled', (d)-> d.hidden) 90 | 91 | itemsEnter 92 | .append('span') 93 | .classed('color-square', true) 94 | .style('background-color', (d)-> d.color) 95 | 96 | itemsEnter 97 | .append('span') 98 | .classed('description', true) 99 | .text((d)-> d.label) 100 | 101 | itemsEnter 102 | .append('span') 103 | .classed('show-only button', true) 104 | .text('only') 105 | .on('click.showOnly', (d)=> 106 | d3.event.stopPropagation() 107 | @chartInstance 108 | .data() 109 | .showOnly(d.key, {onlyDataSeries: @onlyDataSeries()}) 110 | .render() 111 | ) 112 | -------------------------------------------------------------------------------- /src/stacked-chart.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | A StackedChart is responsible for rendering a chart with 'layers'. 3 | Examples include stacked bar and stacked area charts. 4 | 5 | Due to the unique nature of the stacked visualization, you are not 6 | allowed to combine it with lines and scatters. 7 | ### 8 | chartProperties = [ 9 | ['stackType','bar'] 10 | ['stacked', true] 11 | ] 12 | 13 | @ForestD3.StackedChart = class StackedChart extends ForestD3.Chart 14 | constructor: (domContainer)-> 15 | super domContainer 16 | @_setProperties chartProperties 17 | 18 | # Overrides parent class 19 | init: -> 20 | internalData = @data().visible().filter (d)-> d.isDataSeries 21 | d3.layout.stack() 22 | .offset('zero') 23 | .order('reverse') 24 | .values((d)-> d.values)(internalData) 25 | 26 | # Calculate the y-extent for each series 27 | yOffsetVal = 28 | if @stacked() 29 | (d)-> d.y + d.y0 30 | else 31 | (d)-> d.y 32 | 33 | seriesType = 34 | if @stackType() is 'bar' 35 | 'bar' 36 | else 37 | 'area' 38 | 39 | internalData.forEach (series)-> 40 | if series.isDataSeries 41 | # Set type=bar, so that data().barCount() returns valid length. 42 | series.type = seriesType 43 | 44 | yVals = series.values.map yOffsetVal 45 | 46 | ### 47 | Add 0 to the extent always, because stacked bar charts 48 | should be based on the zero axis 49 | ### 50 | yVals = yVals.concat [0] 51 | series.extent.y = d3.extent yVals 52 | 53 | ### 54 | Override the parent class' method. 55 | ### 56 | getVisualization: (series)-> 57 | renderFn = super series 58 | 59 | if series.type is 'bar' 60 | if @stacked() 61 | ForestD3.Visualizations.barStacked 62 | else 63 | ForestD3.Visualizations.bar 64 | else if series.type is 'area' 65 | if @stacked() 66 | ForestD3.Visualizations.areaStacked 67 | else 68 | ForestD3.Visualizations.line 69 | else 70 | renderFn 71 | 72 | ### 73 | Override parent method, to add the 'y0' base value. 74 | ### 75 | renderBisectGuideline: (xValue, xIndex)-> 76 | xPosition = @xScale xValue 77 | markerPoints = @data().sliced(xIndex).map (d)=> 78 | y: @yScale(d.y + d.y0) 79 | color: d.series.color 80 | 81 | @guideline.render xPosition, markerPoints 82 | -------------------------------------------------------------------------------- /src/utils.coffee: -------------------------------------------------------------------------------- 1 | @ForestD3.Utils = do -> 2 | colors20 = d3.scale.category20().range() 3 | 4 | setProperties: (chart, target, chartProperties)-> 5 | for propPair in chartProperties 6 | [prop, defaultVal] = propPair 7 | target[prop] = defaultVal 8 | 9 | chart[prop] = do (prop)-> 10 | (d)-> 11 | if typeof(d) is 'undefined' 12 | return target[prop] 13 | 14 | else 15 | target[prop] = d 16 | return chart 17 | ### 18 | Calculates the minimum and maximum point across all series'. 19 | Useful for setting the domain for a d3.scale() 20 | 21 | data: chart data that has been passed through normalization function. 22 | It should be an array of objects, where each object contains an extent 23 | property. Example: 24 | [ 25 | key: 'line1' 26 | extent: 27 | x: [1,3] 28 | y: [3,4] 29 | , 30 | key: 'line2' 31 | extent: 32 | x: [1,3] 33 | y: [3,4] 34 | ] 35 | 36 | the 'force' argument allows you to force certain values onto the final 37 | extent. Example: 38 | {y: [0], x: [0]} 39 | 40 | Returns an object with the x,y axis extents: 41 | { 42 | x: [min, max] 43 | y: [min, max] 44 | } 45 | ### 46 | extent: (data, force)-> 47 | defaultExtent = [-1, 1] 48 | if not data or data.length is 0 49 | return { 50 | x: defaultExtent 51 | y: defaultExtent 52 | } 53 | 54 | xExt = d3.extent d3.merge data.map((series)-> series.extent?.x or []) 55 | yExt = d3.extent d3.merge data.map((series)-> series.extent?.y or []) 56 | 57 | # Factor in any forced domain values 58 | force ?= {} 59 | force.x ?= [] 60 | force.y ?= [] 61 | force.x = [force.x] if not (force.x instanceof Array) 62 | force.y = [force.y] if not (force.y instanceof Array) 63 | 64 | xExt = xExt.concat force.x 65 | yExt = yExt.concat force.y 66 | 67 | xExt = d3.extent xExt 68 | yExt = d3.extent yExt 69 | 70 | clearNaN = (d,i)-> 71 | if isNaN d 72 | return if i is 0 then -1 else 1 73 | else 74 | return d 75 | 76 | xExt = xExt.map clearNaN 77 | yExt = yExt.map clearNaN 78 | 79 | x: xExt 80 | y: yExt 81 | 82 | ### 83 | Increases an extent by a certain percentage. Useful for padding the 84 | edges of a chart so the points are not right against the axis. 85 | 86 | extent: Object of form: 87 | { 88 | x: [-10, 10] 89 | y: [-1, 1] 90 | } 91 | 92 | padding: Object of form: 93 | { 94 | x: 0.1 # percentage to pad by 95 | y: 0.05 96 | } 97 | ### 98 | extentPadding: (extent, padding)-> 99 | result = {} 100 | 101 | for key, domain of extent 102 | padPercent = padding[key] 103 | if padPercent? 104 | if domain[0] is 0 and domain[1] is 0 105 | result[key] = [-1, 1] 106 | else 107 | range = Math.abs(domain[0] - domain[1]) or domain[0] 108 | amount = range * padPercent 109 | amount /= 2 110 | 111 | result[key] = [domain[0] - amount, domain[1] + amount] 112 | 113 | 114 | result 115 | 116 | ### 117 | Utility class that uses d3.bisect to find the index in a given array, 118 | where a search value can be inserted. 119 | This is different from normal bisectLeft; this function finds the nearest 120 | index to insert the search value. 121 | For instance, lets say your array is [1,2,3,5,10,30], and you search for 28. 122 | Normal d3.bisectLeft will return 4, because 28 is inserted after the number 123 | 10. 124 | 125 | But smartBisect will return 5 126 | because 28 is closer to 30 than 10. 127 | Has the following known issues: 128 | * Will not work if the data points move backwards (ie, 10,9,8,7, etc) or 129 | if the data points are in random order. 130 | * Won't work if there are duplicate x coordinate values. 131 | ### 132 | smartBisect: (values, search, getX = (d)-> d[0])-> 133 | return null unless values instanceof Array 134 | return null if values.length is 0 135 | return 0 if values.length is 1 136 | 137 | if search >= values[values.length - 1] 138 | return values.length-1 139 | 140 | if search <= values[0] 141 | return 0 142 | 143 | bisect = (vals, sch)-> 144 | lo = 0 145 | hi = vals.length 146 | while lo < hi 147 | mid = (lo + hi) >>> 1 148 | if getX(vals[mid],mid) < sch 149 | lo = mid + 1 150 | else 151 | hi = mid 152 | 153 | lo 154 | 155 | index = bisect values,search 156 | 157 | index = d3.min [index, values.length-1] 158 | if index > 0 159 | prevIndex = index-1 160 | prevVal = getX(values[prevIndex], prevIndex) 161 | nextVal = getX(values[index], index) 162 | 163 | if Math.abs(search-prevVal) < Math.abs(search-nextVal) 164 | index = prevIndex 165 | 166 | index 167 | 168 | defaultColor: (i)-> colors20[i % colors20.length] 169 | 170 | debounce: (fn, delay)-> 171 | promise = null 172 | -> 173 | args = arguments 174 | window.clearTimeout promise 175 | 176 | promise = window.setTimeout => 177 | promise = null 178 | fn.apply @, args 179 | , delay 180 | 181 | # Approximate the max width in pixels that a label can use up in x-axis. 182 | # Used to calculate how many ticks to show in x-axis. 183 | textWidthApprox: (xValues, format)-> 184 | return 100 unless xValues? 185 | sample = '' + format(xValues[0] or '') 186 | sample.length * 10 + 40 187 | 188 | ### 189 | Returns an array that is a good approximation for what ticks should 190 | be shown on x-axis. 191 | 192 | xValues - array of all available x-axis values 193 | numTicks - max number of ticks that can fit on the axis 194 | widthThreshold - minimum distance between ticks allowed. 195 | ### 196 | tickValues: (xValues, numTicks, widthThreshold = 1)-> 197 | if numTicks is 0 198 | return [] 199 | 200 | L = xValues.length 201 | if L <= 2 202 | return xValues 203 | 204 | result = [xValues[0]] 205 | 206 | counter = 0 207 | increment = Math.ceil(L / numTicks) 208 | 209 | while counter < L - 1 210 | counter += increment 211 | break if counter >= L - 1 212 | result.push xValues[counter] 213 | 214 | dist = xValues[L-1] - result[result.length-1] 215 | if dist < widthThreshold 216 | result.pop() 217 | 218 | result.push xValues[L-1] 219 | 220 | result 221 | 222 | convertObjectToArray: (obj)-> 223 | if obj instanceof Array 224 | return obj.slice() 225 | else 226 | array = [] 227 | for key, data of obj 228 | data.key ?= key 229 | array.push data 230 | 231 | return array 232 | 233 | ### 234 | Create a clone of a chart data object. 235 | ### 236 | clone: (data)-> 237 | copy = @convertObjectToArray data 238 | 239 | copy = copy.map (d)-> 240 | newObj = {} 241 | newObj[key] = val for key, val of d 242 | 243 | newObj 244 | copy 245 | 246 | ### 247 | Converts the input data into a normalized format. 248 | Also clones the data so the chart operates on copy of the data. 249 | It converts the 'values' array into a normal format, that looks like this: 250 | { 251 | x: (raw x value, or an index if ordinal=true) 252 | y: (the raw y value) 253 | data: (reference to the original data point) 254 | } 255 | 256 | It also adds an 'extent' property to the series data. 257 | 258 | @param data - the chart data to normalize. 259 | @param options - object with the following properties: 260 | getX: function to get the raw x value 261 | getY: function to get the raw y value 262 | ordinal: boolean describing whether the data is uniformly distributed 263 | on the x-axis or not. 264 | ### 265 | normalize: (data, options={})-> 266 | data = @clone data 267 | 268 | getX = options.getX 269 | getY = options.getY 270 | ordinal = options.ordinal 271 | colorPalette = options.colorPalette or colors20 272 | autoSortXValues = options.autoSortXValues 273 | 274 | colorIndex = 0 275 | seriesIndex = 0 276 | 277 | data.forEach (series,i)-> 278 | series.key ?= "series#{i}" 279 | series.label ?= "Series ##{i}" 280 | series.type ?= if series.value? then 'marker' else 'scatter' 281 | 282 | ### 283 | An internal only unique identifier. 284 | This is necessary to ensure each chart series has a 285 | unique key when doing a d3.selectAll.data join. 286 | ### 287 | series._uniqueKey = "#{series.key}_#{series.type}_#{i}" 288 | 289 | if series.type is 'region' 290 | series.extent = 291 | x: if series.axis is 'x' then series.values else [] 292 | y: if series.axis isnt 'x' then series.values else [] 293 | return 294 | 295 | if series.type is 'marker' 296 | series.extent = 297 | x: if series.axis is 'x' then [series.value] else [] 298 | y: if series.axis isnt 'x' then [series.value] else [] 299 | return 300 | 301 | unless series.color? 302 | series.color = colorPalette[colorIndex % colorPalette.length] 303 | colorIndex++ 304 | 305 | series.index = seriesIndex 306 | seriesIndex++ 307 | 308 | if series.values instanceof Array 309 | series.isDataSeries = true 310 | series.values = series.values.map (d,i)-> 311 | xRaw = getX(d,i) 312 | yRaw = getY(d,i) 313 | 314 | x: if ordinal then i else xRaw 315 | y: yRaw 316 | xValueRaw: xRaw 317 | yValueRaw: yRaw 318 | data: d 319 | series: series 320 | 321 | ### 322 | Calculates the extent (in x and y directions) of the data in 323 | each series. 324 | The 'extent' is basically the highest and lowest values, 325 | used to figure out the chart's scale. 326 | ### 327 | series.extent = 328 | x: d3.extent(series.values, (d)-> d.x) 329 | y: d3.extent(series.values, (d)-> d.y) 330 | 331 | ### 332 | Sort all the data points in ascending order, by x-value. 333 | This prevents any scrambled lines being drawn. 334 | 335 | This only needs to happen for 336 | non-ordinal data series (scatter plots for example). 337 | Ordinal data is always sorted by default. 338 | ### 339 | if autoSortXValues and not ordinal 340 | series.values.sort (a,b)-> 341 | d3.ascending a.xValueRaw, b.xValueRaw 342 | 343 | data 344 | -------------------------------------------------------------------------------- /src/visualizations/bar.coffee: -------------------------------------------------------------------------------- 1 | renderBars = (selection, selectionData, options={})-> 2 | chart = @ 3 | stacked = options.stacked 4 | 5 | bars = selection.selectAll('rect.bar').data(selectionData.values) 6 | 7 | ### 8 | Ensure the bars are based at the zero line, but does not extend past 9 | canvas boundaries. 10 | ### 11 | barBase = chart.yScale 0 12 | if barBase > chart.canvasHeight 13 | barBase = chart.canvasHeight 14 | else if barBase < 0 15 | barBase = 0 16 | 17 | # Figure out an optimal width for each bar. 18 | 19 | # Calculates how much available space there is between each x-axis tick mark 20 | fullSpace = chart.canvasWidth / selectionData.values.length 21 | 22 | # Ensure the bars don't get too wide either. 23 | maxFullSpace = chart.xScale(1) / 2 24 | 25 | fullSpace = d3.min [maxFullSpace, fullSpace] 26 | 27 | maxPadding = 35 28 | #add some padding between groups of bars (default to 10% of the full space) 29 | #Padding is maxed out after a certain threshold 30 | fullSpace -= d3.min [(fullSpace * chart.barPaddingPercent()), maxPadding] 31 | 32 | # Ensure we don't get negative bar widths 33 | fullSpace = d3.max [1, fullSpace] 34 | 35 | ### 36 | This is used to ensure that the bar group is centered around the x-axis 37 | tick mark. 38 | ### 39 | xCentered = fullSpace / 2 40 | 41 | bars 42 | .enter() 43 | .append('rect') 44 | .classed('bar', true) 45 | .attr('x', (d)-> chart.xScale(d.x) - xCentered) 46 | .attr('y', barBase) 47 | .attr('height', 0) 48 | 49 | bars 50 | .exit() 51 | .remove() 52 | 53 | # Gets total number of visible bars and figures out bar width (if grouped) 54 | barCount = chart.data().barCount() 55 | barIndex = chart.data().barIndex selectionData.key 56 | barWidth = 57 | if stacked 58 | fullSpace 59 | else 60 | fullSpace / barCount 61 | 62 | barOffset = 63 | if stacked 64 | 0 65 | else 66 | barWidth * barIndex 67 | 68 | barYPosition = 69 | if stacked 70 | (d)-> 71 | ### 72 | For negative stacked bars, place the top of the at y0. 73 | 74 | For positive bars, place the top of the at y0 + y 75 | ### 76 | if d.y0 <= 0 and d.y < 0 77 | chart.yScale d.y0 78 | else 79 | chart.yScale(d.y0 + d.y) 80 | else 81 | (d)-> 82 | if d.y < 0 83 | barBase 84 | else 85 | chart.yScale(d.y) 86 | 87 | delayFactor = 150 / selectionData.values.length 88 | delayFactor = d3.max [3, delayFactor] 89 | 90 | bars 91 | .transition() 92 | .duration(selectionData.duration or chart.duration()) 93 | .delay((d,i)-> i * delayFactor) 94 | .attr('x', (d)-> 95 | ### 96 | Calculates the x position of each bar. Shifts the bar along x-axis 97 | depending on which series index the bar belongs to. 98 | ### 99 | chart.xScale(d.x) - xCentered + barOffset 100 | ) 101 | .attr('y', barYPosition) 102 | .attr('height', (d)-> 103 | Math.abs(chart.yScale(d.y) - barBase) 104 | ) 105 | .attr('width', barWidth) 106 | .style('fill', selectionData.color) 107 | .attr('class', (d,i)-> 108 | additionalClass = 109 | if (typeof selectionData.classed) is 'function' 110 | selectionData.classed d.data, i, selectionData 111 | else 112 | '' 113 | 114 | "bar #{additionalClass}" 115 | ) 116 | 117 | # Add hover events, but only if tooltipType is 'hover' 118 | if chart.tooltipType() is 'hover' 119 | selection.classed 'interactive', true 120 | 121 | bars 122 | .on('mousemove.tooltip', (d)-> 123 | clientMouse = [d3.event.clientX, d3.event.clientY] 124 | content = ForestD3.TooltipContent.single chart, d 125 | 126 | chart.renderSpatialTooltip { 127 | content 128 | clientMouse 129 | } 130 | ) 131 | .on('mouseout.tooltip', -> 132 | chart.renderSpatialTooltip {hide: true} 133 | ) 134 | 135 | 136 | ForestD3.Visualizations.bar = (selection, selectionData)-> 137 | renderBars.call @, selection, selectionData, {stacked: false} 138 | 139 | ForestD3.Visualizations.barStacked = (selection, selectionData)-> 140 | renderBars.call @, selection, selectionData, {stacked: true} 141 | -------------------------------------------------------------------------------- /src/visualizations/line-area.coffee: -------------------------------------------------------------------------------- 1 | renderArea = (selection, selectionData, options={})-> 2 | chart = @ 3 | 4 | drawArea = options.area 5 | stacked = options.stacked 6 | # Draw an area graph if area option is turned on 7 | if drawArea 8 | # Ensure the base of the area doesn't extend outside the cavnas bounds. 9 | areaBase = chart.yScale 0 10 | if areaBase > chart.canvasHeight 11 | areaBase = chart.canvasHeight 12 | else if areaBase < 0 13 | areaBase = 0 14 | 15 | areaFn = d3.svg.area() 16 | .interpolate(selectionData.interpolate or 'linear') 17 | .x((d)-> chart.xScale(d.x)) 18 | 19 | area = selection 20 | .selectAll('path.area') 21 | .data([selectionData.values]) 22 | 23 | areaOffsetEnter = 24 | if stacked 25 | areaFn 26 | .y0((d)-> chart.yScale(d.y0)) 27 | .y1((d)-> chart.yScale(d.y0)) 28 | else 29 | areaFn 30 | .y0(areaBase) 31 | .y1(areaBase) 32 | 33 | area 34 | .enter() 35 | .append('path') 36 | .classed('area', true) 37 | .attr('d', areaOffsetEnter) 38 | 39 | areaOffset = 40 | if stacked 41 | areaOffsetEnter.y1((d)-> chart.yScale(d.y + d.y0)) 42 | else 43 | areaOffsetEnter.y1((d)-> chart.yScale(d.y)) 44 | 45 | area 46 | .transition() 47 | .duration(selectionData.duration or chart.duration()) 48 | .style('fill', selectionData.color) 49 | .attr('d', areaOffset) 50 | else 51 | selection.selectAll('path.area').remove() 52 | ### 53 | Draws a simple line graph. 54 | If you set area=true, turns it into an area graph 55 | ### 56 | @ForestD3.Visualizations.line = (selection, selectionData)-> 57 | chart = @ 58 | 59 | selection.style 'stroke', selectionData.color 60 | 61 | interpolate = selectionData.interpolate or 'linear' 62 | 63 | lineFn = d3.svg.line() 64 | .interpolate(interpolate) 65 | .x((d)-> chart.xScale(d.x)) 66 | 67 | path = selection.selectAll('path.line').data([selectionData.values]) 68 | 69 | path 70 | .enter() 71 | .append('path') 72 | .classed('line', true) 73 | .attr('d',lineFn.y(chart.canvasHeight)) 74 | 75 | duration = selectionData.duration or chart.duration() 76 | 77 | path 78 | .transition() 79 | .duration(duration) 80 | .attr('d', lineFn.y((d)-> chart.yScale(d.y))) 81 | 82 | renderArea.call @, selection, selectionData, {area: selectionData.area} 83 | 84 | @ForestD3.Visualizations.areaStacked = (selection, selectionData)-> 85 | selection.style 'stroke', selectionData.color 86 | 87 | renderArea.call @, selection, selectionData, {area: true, stacked: true} 88 | -------------------------------------------------------------------------------- /src/visualizations/marker-line.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Draws a horizontal or vertical line at the specified x or y location. 3 | ### 4 | @ForestD3.Visualizations.markerLine = (selection, selectionData)-> 5 | chart = @ 6 | 7 | line = selection.selectAll('line.marker').data((d)-> [d.value]) 8 | label = selection.selectAll('text.marker-label').data([selectionData.label]) 9 | 10 | labelEnter = label 11 | .enter() 12 | .append('text') 13 | .classed('marker-label', true) 14 | .text((d)-> d) 15 | .attr('x', 0) 16 | .attr('y', 0) 17 | 18 | labelPadding = 10 19 | 20 | duration = selectionData.duration or chart.duration() 21 | 22 | if selectionData.axis is 'x' 23 | x = chart.xScale selectionData.value 24 | 25 | line 26 | .enter() 27 | .append('line') 28 | .classed('marker', true) 29 | .attr('x1', 0) 30 | .attr('x2', 0) 31 | .attr('y1', 0) 32 | 33 | line 34 | .attr('y2', chart.canvasHeight) 35 | .transition() 36 | .duration(duration) 37 | .attr('x1', x) 38 | .attr('x2', x) 39 | 40 | # Rotates the x marker label 90 degrees. 41 | labelRotate = "rotate(-90 #{x} #{chart.canvasHeight})" 42 | labelOffset = "translate(0 #{-labelPadding})" 43 | 44 | labelEnter.attr('transform', labelRotate) 45 | label 46 | .attr('y', chart.canvasHeight) 47 | .transition() 48 | .duration(duration) 49 | .attr('transform', "#{labelRotate} #{labelOffset}") 50 | .attr('x', x) 51 | 52 | else 53 | y = chart.yScale selectionData.value 54 | 55 | line 56 | .enter() 57 | .append('line') 58 | .classed('marker', true) 59 | .attr('x1', 0) 60 | .attr('y1', 0) 61 | .attr('y2', 0) 62 | 63 | line 64 | .attr('x2', chart.canvasWidth) 65 | .transition() 66 | .duration(duration) 67 | .attr('y1', y) 68 | .attr('y2', y) 69 | 70 | label 71 | .attr('text-anchor', 'end') 72 | .transition() 73 | .duration(duration) 74 | .attr('x', chart.canvasWidth) 75 | .attr('y', y + labelPadding) 76 | -------------------------------------------------------------------------------- /src/visualizations/ohlc.coffee: -------------------------------------------------------------------------------- 1 | @ForestD3.Visualizations.ohlc = (selection, selectionData)-> 2 | chart = @ 3 | 4 | selection.classed('ohlc', true) 5 | 6 | rangeLines = selection 7 | .selectAll('line.ohlc-range') 8 | .data(selectionData.values) 9 | 10 | x = chart.getXInternal 11 | open = selectionData.getOpen or (d,i)-> d[1] 12 | hi = selectionData.getHi or (d,i)-> d[2] 13 | lo = selectionData.getLo or (d,i)-> d[3] 14 | close = selectionData.getClose or (d,i)-> d[4] 15 | duration = selectionData.duration or chart.duration() 16 | 17 | rangeLines 18 | .enter() 19 | .append('line') 20 | .classed('ohlc-range', true) 21 | .attr('x1', (d,i)-> chart.xScale(x(d,i))) 22 | .attr('x2', (d,i)-> chart.xScale(x(d,i))) 23 | .attr('y1', 0) 24 | .attr('y2', 0) 25 | 26 | rangeLines 27 | .exit() 28 | .remove() 29 | 30 | rangeLines 31 | .transition() 32 | .duration(duration) 33 | .delay((d,i)-> i*20) 34 | .attr('x1', (d,i)-> chart.xScale(x(d,i))) 35 | .attr('x2', (d,i)-> chart.xScale(x(d,i))) 36 | .attr('y1', (d,i)-> chart.yScale(hi(d.data,i))) 37 | .attr('y2', (d,i)-> chart.yScale(lo(d.data,i))) 38 | 39 | openMarks = selection 40 | .selectAll('line.ohlc-open') 41 | .data(selectionData.values) 42 | 43 | openMarks 44 | .enter() 45 | .append('line') 46 | .classed('ohlc-open', true) 47 | .attr('y1', 0) 48 | .attr('y2', 0) 49 | 50 | openMarks 51 | .exit() 52 | .remove() 53 | 54 | openMarks 55 | .transition() 56 | .duration(duration) 57 | .delay((d,i)-> i*20) 58 | .attr('y1', (d,i)-> chart.yScale(open(d.data,i))) 59 | .attr('y2', (d,i)-> chart.yScale(open(d.data,i))) 60 | .attr('x1', (d,i)-> chart.xScale(x(d,i))) 61 | .attr('x2', (d,i)-> chart.xScale(x(d,i)) - 5) 62 | 63 | closeMarks = selection 64 | .selectAll('line.ohlc-close') 65 | .data(selectionData.values) 66 | 67 | closeMarks 68 | .enter() 69 | .append('line') 70 | .classed('ohlc-close', true) 71 | .attr('y1', 0) 72 | .attr('y2', 0) 73 | 74 | closeMarks 75 | .exit() 76 | .remove() 77 | 78 | closeMarks 79 | .transition() 80 | .duration(duration) 81 | .delay((d,i)-> i*20) 82 | .attr('y1', (d,i)-> chart.yScale(close(d.data,i))) 83 | .attr('y2', (d,i)-> chart.yScale(close(d.data,i))) 84 | .attr('x1', (d,i)-> chart.xScale(x(d,i))) 85 | .attr('x2', (d,i)-> chart.xScale(x(d,i)) + 5) 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/visualizations/region.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Draws a transparent rectangle across the canvas signifying an important 3 | region. 4 | ### 5 | @ForestD3.Visualizations.region = (selection, selectionData)-> 6 | chart = @ 7 | 8 | region = selection.selectAll('rect.region').data([selectionData]) 9 | 10 | regionEnter = region 11 | .enter() 12 | .append('rect') 13 | .classed('region', true) 14 | 15 | start = d3.min selectionData.values 16 | end = d3.max selectionData.values 17 | 18 | duration = selectionData.duration or chart.duration() 19 | 20 | if selectionData.axis is 'x' 21 | x = chart.xScale start 22 | width = Math.abs(chart.xScale(start) - chart.xScale(end)) 23 | regionEnter 24 | .attr('width', 0) 25 | 26 | region 27 | .attr('x', x) 28 | .attr('y', 0) 29 | .attr('height', chart.canvasHeight) 30 | .transition() 31 | .duration(duration) 32 | .attr('width', width) 33 | else 34 | y = chart.yScale end 35 | height = Math.abs(chart.yScale(start) - chart.yScale(end)) 36 | regionEnter 37 | .attr('height', 0) 38 | 39 | region 40 | .attr('x', 0) 41 | .attr('y', y) 42 | .transition() 43 | .duration(duration) 44 | .attr('width', chart.canvasWidth) 45 | .attr('height', height) 46 | -------------------------------------------------------------------------------- /src/visualizations/scatter.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Function responsible for rendering a scatter plot inside a d3 selection. 3 | Must have reference to a chart instance. 4 | 5 | Example call: 6 | ForestD3.Visualizations.scatter.call chartInstance, d3.select(this) 7 | 8 | ### 9 | @ForestD3.Visualizations.scatter = (selection, selectionData)-> 10 | chart = @ 11 | 12 | selection.style('fill', selectionData.color) 13 | 14 | all = d3.svg.symbolTypes 15 | seriesIndex = selectionData.index 16 | shape = 17 | selectionData.shape or all[seriesIndex % all.length] 18 | 19 | symbol = d3.svg.symbol().type shape 20 | 21 | points = selection 22 | .selectAll('path.point') 23 | .data((d)-> d.values) 24 | 25 | base = Math.min(chart.yScale(0), chart.canvasHeight) 26 | 27 | points.enter() 28 | .append('path') 29 | .classed('point', true) 30 | .attr('transform', (d)-> 31 | "translate(#{chart.xScale(d.x)},#{base})" 32 | ) 33 | .attr('d', symbol.size(0)) 34 | 35 | points.exit().remove() 36 | 37 | delayFactor = 150 / selectionData.values.length 38 | delayFactor = d3.max [3, delayFactor] 39 | 40 | points 41 | .transition() 42 | .duration(selectionData.duration or chart.duration()) 43 | .delay((d,i)-> i * delayFactor) 44 | .ease('quad') 45 | .attr('transform', (d)-> 46 | "translate(#{chart.xScale(d.x)},#{chart.yScale(d.y)})" 47 | ) 48 | .attr('d', symbol.size(selectionData.size or 96)) 49 | 50 | # Add hover events, but only if tooltipType is 'hover' 51 | if chart.tooltipType() is 'hover' 52 | selection.classed 'interactive', true 53 | points 54 | .on('mouseover.tooltipHover', (d)-> 55 | clientMouse = [d3.event.clientX, d3.event.clientY] 56 | canvasMouse = [chart.xScale(d.x), chart.yScale(d.y)] 57 | content = ForestD3.TooltipContent.single chart, d 58 | 59 | chart.renderSpatialTooltip { 60 | content 61 | clientMouse 62 | canvasMouse 63 | } 64 | ) 65 | .on('mouseout.tooltipHover', -> 66 | chart.renderSpatialTooltip {hide: true} 67 | ) -------------------------------------------------------------------------------- /style/bar.styl: -------------------------------------------------------------------------------- 1 | .forest-d3 2 | rect 3 | &.bar 4 | fill-opacity 0.7 5 | stroke-width 1 6 | stroke $axisLineColor 7 | stroke-opacity 0.1 8 | transition fill-opacity 0.25s, stroke-opacity 0.25s 9 | 10 | .highlight 11 | rect 12 | &.bar 13 | fill-opacity 0.9 14 | stroke-opacity 0.9 15 | 16 | &.bar-chart 17 | text 18 | font-size 20px 19 | fill $axisTextColor 20 | 21 | .bars 22 | rect 23 | stroke $axisLineColor 24 | 25 | line 26 | &.zero-line 27 | stroke $axisLineColor 28 | stroke-opacity 0.7 -------------------------------------------------------------------------------- /style/chart.styl: -------------------------------------------------------------------------------- 1 | .forest-d3 2 | svg 3 | width 100% 4 | height 100% 5 | 6 | &.auto-height 7 | svg 8 | height auto 9 | 10 | rect 11 | &.backdrop 12 | opacity 0 13 | pointer-events all 14 | 15 | .axis 16 | .tick 17 | line 18 | stroke $axisLineColor 19 | 20 | text 21 | fill $axisTextColor 22 | 23 | &.x-axis 24 | .tick 25 | line 26 | stroke-opacity 0.2 27 | 28 | &.y-axis 29 | .tick 30 | line 31 | stroke-opacity $yAxisOpacity 32 | 33 | > path 34 | fill none 35 | stroke $axisLineColor 36 | stroke-opacity 0.2 37 | 38 | .axes-labels 39 | fill $axisTextColor 40 | pointer-events none 41 | 42 | .canvas 43 | & > rect 44 | pointer-events all 45 | fill none 46 | stroke $axisLineColor 47 | stroke-opacity 0.2 48 | 49 | path 50 | &.point 51 | fill-opacity 0.75 52 | stroke $axisLineColor 53 | stroke-opacity 0.5 54 | transition fill-opacity 0.25s, stroke-opacity 0.25s, stroke-width 0.25s 55 | 56 | .highlight 57 | path 58 | &.point 59 | fill-opacity 0.9 60 | stroke-opacity 0.9 61 | stroke-width 1.5 62 | 63 | line 64 | &.guideline, &.crosshair-x, &.crosshair-y 65 | stroke $axisLineColor 66 | pointer-events none 67 | 68 | .guideline-markers 69 | circle 70 | &.marker 71 | stroke $axisLineColor 72 | stroke-opacity 0.7 73 | 74 | .series 75 | pointer-events none 76 | 77 | &.interactive 78 | pointer-events all 79 | -------------------------------------------------------------------------------- /style/legend.styl: -------------------------------------------------------------------------------- 1 | .forest-d3 2 | &.legend 3 | .item 4 | min-width 200px 5 | padding 5px 6 | cursor pointer 7 | transition all 0.25s 8 | 9 | &:hover 10 | background-color $legendHoverColor 11 | 12 | .show-only 13 | display inline-block 14 | 15 | &.disabled 16 | color #ccc 17 | font-style italic 18 | opacity 0.5 19 | 20 | .color-square 21 | position relative 22 | top 3px 23 | display inline-block 24 | width 18px 25 | height 18px 26 | margin-right 8px 27 | border solid 1px #ccc 28 | border-radius 3px 29 | 30 | .show-only 31 | display none 32 | margin-left 15px 33 | 34 | .button 35 | cursor pointer 36 | color #498FCB 37 | 38 | &:hover 39 | text-decoration underline 40 | -------------------------------------------------------------------------------- /style/line.styl: -------------------------------------------------------------------------------- 1 | .forest-d3 2 | path 3 | &.line 4 | fill none 5 | stroke-opacity 0.8 6 | stroke-width 2 7 | pointer-events none 8 | transition stroke-opacity 0.25s, stroke-width 0.25s 9 | 10 | &.area 11 | fill-opacity 0.3 12 | transition fill-opacity 0.25s 13 | 14 | .highlight 15 | path 16 | &.line 17 | stroke-opacity 1.0 18 | stroke-width 3 19 | 20 | &.area 21 | fill-opacity 0.5 -------------------------------------------------------------------------------- /style/marker-line.styl: -------------------------------------------------------------------------------- 1 | .forest-d3 2 | line 3 | &.marker 4 | stroke $axisLineColor 5 | stroke-opacity 0.5 6 | stroke-dasharray 5 7 | 8 | .marker-label 9 | fill $axisTextColor 10 | font-size 11px 11 | 12 | -------------------------------------------------------------------------------- /style/ohlc.styl: -------------------------------------------------------------------------------- 1 | .forest-d3 2 | .ohlc 3 | line 4 | stroke $axisLineColor 5 | 6 | &.ohlc-range 7 | stroke-width 2px 8 | 9 | &.ohlc-open, &.ohlc-close 10 | stroke-width 3px 11 | -------------------------------------------------------------------------------- /style/region.styl: -------------------------------------------------------------------------------- 1 | .forest-d3 2 | rect 3 | &.region 4 | fill $regionFill 5 | fill-opacity 0.15 -------------------------------------------------------------------------------- /style/tooltip.styl: -------------------------------------------------------------------------------- 1 | .forest-d3 2 | &.tooltip-box 3 | position absolute 4 | pointer-events none 5 | min-width 20px 6 | min-height 20px 7 | 8 | margin 0 10px 0 10px 9 | padding 7px 10 | 11 | background $tooltipBackground 12 | color $tooltipColor 13 | border solid 1px #ccc 14 | border-radius 3px 15 | 16 | z-index 100 17 | 18 | .header 19 | font-weight bold 20 | margin-bottom 8px 21 | 22 | table 23 | td 24 | white-space nowrap 25 | &.series-label 26 | padding-right 20px 27 | 28 | &.series-value 29 | text-align right 30 | font-weight bold 31 | 32 | .series-color 33 | width 13px 34 | height 12px 35 | border-radius 3px 36 | margin-right 10px 37 | -------------------------------------------------------------------------------- /style/variables/dark.styl: -------------------------------------------------------------------------------- 1 | $axisLineColor = #fff 2 | $axisTextColor = #DCDCDC 3 | 4 | $tooltipBackground = #201F1D 5 | $tooltipColor = $axisTextColor 6 | 7 | $yAxisOpacity = 0.1 8 | 9 | $regionFill = #DCDCDC 10 | $legendHoverColor = rgba(124, 124, 244, 0.23) -------------------------------------------------------------------------------- /style/variables/light.styl: -------------------------------------------------------------------------------- 1 | $axisLineColor = #000 2 | $axisTextColor = #333 3 | 4 | $tooltipBackground = #fff 5 | $tooltipColor = $axisTextColor 6 | 7 | $yAxisOpacity = 0.07 8 | 9 | $regionFill = #333 10 | $legendHoverColor = rgba(10,10,10, 0.23) -------------------------------------------------------------------------------- /test/bar-chart.coffee: -------------------------------------------------------------------------------- 1 | describe 'Horizontal Bar Chart', -> 2 | container = null 3 | chart = null 4 | 5 | beforeEach -> 6 | container = document.createElement 'div' 7 | container.style.width = '500px' 8 | container.style.height = '400px' 9 | document.body.appendChild container 10 | 11 | chart = new ForestD3.BarChart container 12 | 13 | afterEach -> 14 | chart.destroy() 15 | 16 | it 'creates an tag and adds forest-d3 class to container', -> 17 | $(container).hasClass('forest-d3').should.be.true 18 | $(container).find('svg').length.should.equal 1 19 | 20 | it 'has autoResize function', -> 21 | should.exist chart.autoResize 22 | 23 | it 'can render a chart frame', -> 24 | data = [ 25 | key: 'series1' 26 | label: 'Long' 27 | values: [ 28 | ['Population', 100] 29 | ] 30 | ] 31 | 32 | chart.data(data).render() 33 | 34 | labels = $(container).find('svg g.bar-labels') 35 | bars = $(container).find('svg g.bars') 36 | values = $(container).find('svg g.bar-values') 37 | 38 | labels.length.should.equal 1 39 | bars.length.should.equal 1 40 | values.length.should.equal 1 41 | 42 | it 'can render a single bar', -> 43 | data = [ 44 | key: 'series1' 45 | label: 'Long' 46 | values: [ 47 | ['Population', 100] 48 | ] 49 | ] 50 | 51 | chart.data(data).render() 52 | 53 | labels = $(container).find('svg g.bar-labels text') 54 | bars = $(container).find('svg g.bars rect') 55 | values = $(container).find('svg g.bar-values text') 56 | 57 | labels.length.should.equal 1 58 | bars.length.should.equal 1 59 | values.length.should.equal 1 60 | 61 | labels.get(0).textContent.should.equal 'Population' 62 | 63 | it 'automatically figures out SVG height', -> 64 | data = [ 65 | key: 'series1' 66 | label: 'Short' 67 | values: [ 68 | ['Experiment 1', 100] 69 | ['Experiment 2', 90] 70 | ['Experiment 3', 80] 71 | ] 72 | ] 73 | 74 | chart 75 | .barHeight(40) 76 | .barPadding(10) 77 | .height(null) 78 | .data(data) 79 | .render() 80 | 81 | svg = $(container).find('svg') 82 | 83 | height = "#{50*3}" 84 | svg.get(0).getAttribute('height').should.equal height 85 | 86 | it 'automatically figures out left margin', (done)-> 87 | data = [ 88 | key: 'series1' 89 | label: 'Short' 90 | values: [ 91 | ['A', 1] 92 | ['BBBBBBBBBB', 2] 93 | ] 94 | ] 95 | 96 | chart.data(data).render() 97 | 98 | chart.margin.left.should.be.greaterThan 100 99 | 100 | data = [ 101 | key: 'series1' 102 | label: 'Short' 103 | values: [ 104 | ['A', 1] 105 | ['B', 2] 106 | ] 107 | ] 108 | 109 | chart.data(data).render() 110 | 111 | chart.margin.left.should.be.lessThan 40 112 | 113 | setTimeout -> 114 | done() 115 | , 600 116 | 117 | it 'can sort things by label, ascending', (done)-> 118 | data = [ 119 | key: 'series1' 120 | label: 'Short' 121 | values: [ 122 | ['A', 1] 123 | ['E', 2] 124 | ['B', 3] 125 | ['Z', 4] 126 | ['C', 5] 127 | ] 128 | ] 129 | 130 | chart.data(data) 131 | chart.sortBy((d)-> d[0]).sortDirection('asc').render() 132 | 133 | labels = $(container).find('.bar-labels text') 134 | 135 | for text, i in ['A','B','C','E','Z'] 136 | labels.get(i).textContent.should.equal text 137 | 138 | # original data kept intact 139 | 140 | data[0].values.should.deep.equal [ 141 | ['A', 1] 142 | ['E', 2] 143 | ['B', 3] 144 | ['Z', 4] 145 | ['C', 5] 146 | ] 147 | 148 | setTimeout -> 149 | done() 150 | , 600 151 | 152 | it 'translates negative bars and labels', (done)-> 153 | data = [ 154 | key: 'series1' 155 | label: 'Short' 156 | values: [ 157 | ['A', 100] 158 | ['B', 60] 159 | ['C', -60] 160 | ['D', -100] 161 | ] 162 | ] 163 | 164 | chart.data(data).render() 165 | 166 | bars = $(container).find('.bars rect') 167 | bars.get(2).getAttribute('transform').should.contain 'translate' 168 | bars.get(3).getAttribute('transform').should.contain 'translate' 169 | 170 | bars.get(1).getAttribute('class').should.contain 'positive' 171 | bars.get(2).getAttribute('class').should.contain 'negative' 172 | 173 | values = $(container).find('.bar-values text') 174 | values.get(1).getAttribute('class').should.contain 'positive' 175 | values.get(2).getAttribute('class').should.contain 'negative' 176 | 177 | values.get(2).getAttribute('x').should.equal values.get(3).getAttribute('x') 178 | 179 | setTimeout -> 180 | done() 181 | , 600 182 | -------------------------------------------------------------------------------- /test/data.coffee: -------------------------------------------------------------------------------- 1 | describe 'Data API', -> 2 | it 'should have ability to get raw data', -> 3 | api = ForestD3.DataAPI [1,2,3] 4 | 5 | api.get().should.deep.equal [1,2,3] 6 | 7 | it 'has methods to show/hide data series`', -> 8 | data = [ 9 | key: 'series1' 10 | label: 'Hello' 11 | values: [] 12 | , 13 | key: 'series2' 14 | label: 'World' 15 | values: [] 16 | , 17 | key: 'series3' 18 | color: '#00f' 19 | label: 'Foo' 20 | values: [] 21 | ] 22 | 23 | chart = new ForestD3.Chart() 24 | chart.data(data) 25 | 26 | api = chart.data() 27 | 28 | api.hide(['series1', 'series2']) 29 | 30 | visible = api.visible() 31 | visible.length.should.equal 1 32 | 33 | api.show('series2') 34 | 35 | visible = api.visible() 36 | visible.length.should.equal 2 37 | 38 | api.toggle('series2') 39 | 40 | visible = api.visible() 41 | visible.length.should.equal 1 42 | 43 | it 'has methods for showOnly and showAll data', -> 44 | data = [ 45 | key: 'series1' 46 | label: 'Hello' 47 | values: [] 48 | , 49 | key: 'series2' 50 | label: 'World' 51 | values: [] 52 | , 53 | key: 'series3' 54 | color: '#00f' 55 | label: 'Foo' 56 | values: [] 57 | ] 58 | 59 | chart = new ForestD3.Chart() 60 | chart.data(data) 61 | 62 | api = chart.data() 63 | 64 | api.showOnly 'series2' 65 | visible = api.visible() 66 | visible.length.should.equal 1 67 | visible[0].key.should.equal 'series2' 68 | 69 | api.showAll() 70 | visible = api.visible() 71 | visible.length.should.equal 3 72 | 73 | it 'showOnly accepts "onlyDataSeries" option', -> 74 | data = [ 75 | key: 'series1' 76 | label: 'Hello' 77 | values: [] 78 | , 79 | key: 'series2' 80 | label: 'World' 81 | values: [] 82 | , 83 | key: 'seriesMarker' 84 | type: 'marker' 85 | value: 0 86 | , 87 | key: 'seriesRegion' 88 | type: 'region' 89 | values: [0,1] 90 | , 91 | key: 'series3' 92 | color: '#00f' 93 | label: 'Foo' 94 | values: [] 95 | ] 96 | 97 | chart = new ForestD3.Chart() 98 | chart.data(data) 99 | 100 | api = chart.data() 101 | 102 | api.showOnly 'series2', {onlyDataSeries: true} 103 | visible = api.visible() 104 | 105 | visible.length.should.equal 3, '3 visible' 106 | visible[0].key.should.equal 'series2' 107 | visible[1].key.should.equal 'seriesMarker' 108 | visible[2].key.should.equal 'seriesRegion' 109 | 110 | it 'has method to get visible data only', -> 111 | data = [ 112 | key: 'series1' 113 | label: 'Hello' 114 | values: [] 115 | , 116 | key: 'series2' 117 | label: 'World' 118 | values: [] 119 | , 120 | key: 'series3' 121 | color: '#00f' 122 | label: 'Foo' 123 | values: [] 124 | ] 125 | 126 | chart = new ForestD3.Chart() 127 | chart.data(data) 128 | 129 | chart.data().hide(['series2','series3']) 130 | 131 | visible = chart.data().visible() 132 | visible.length.should.equal 1 133 | visible[0].key.should.equal 'series1' 134 | 135 | it 'has method to get list of x values', -> 136 | data = [ 137 | values: [ 138 | [2, 10] 139 | [80, 100] 140 | [90, 101] 141 | ] 142 | ] 143 | chart = new ForestD3.Chart() 144 | chart.ordinal(false).data(data) 145 | 146 | chart.data().xValues().should.deep.equal [2, 80, 90] 147 | 148 | data = [ 149 | value: [1,2] 150 | ] 151 | 152 | chart.data(data) 153 | 154 | chart.data().xValues().should.deep.equal [] 155 | 156 | chart.getX (d,i)-> i 157 | 158 | data = [ 159 | values: [ 160 | [2, 10] 161 | [80, 100] 162 | [90, 101] 163 | ] 164 | ] 165 | 166 | chart.data(data) 167 | 168 | chart.data().xValues().should.deep.equal [0,1,2] 169 | 170 | it 'can get raw x values for an ordinal chart', -> 171 | data = [ 172 | values: [ 173 | [2, 10] 174 | [80, 100] 175 | [90, 101] 176 | ] 177 | ] 178 | chart = new ForestD3.Chart() 179 | chart.ordinal(true).data(data) 180 | 181 | chart.data().xValuesRaw().should.deep.equal [2,80,90] 182 | 183 | it 'can get x value at certain index', -> 184 | data = [ 185 | values: [ 186 | [2, 10] 187 | [80, 100] 188 | [90, 101] 189 | ] 190 | ] 191 | chart = new ForestD3.Chart() 192 | chart.ordinal(true).data(data) 193 | 194 | chart.data().xValueAt(0).should.equal 2 195 | chart.data().xValueAt(1).should.equal 80 196 | chart.data().xValueAt(2).should.equal 90 197 | should.not.exist chart.data().xValueAt(4) 198 | 199 | it 'makes a copy of the data', -> 200 | data = [ 201 | key: 'line1' 202 | type: 'line' 203 | values: [ 204 | [0,0] 205 | [1,1] 206 | [2,4] 207 | ] 208 | , 209 | key: 'line2' 210 | type: 'line' 211 | values: [ 212 | [0,7] 213 | [1,8] 214 | [2,9] 215 | ] 216 | ] 217 | 218 | chart = new ForestD3.Chart() 219 | chart.data(data) 220 | 221 | internalData = chart.data().get() 222 | 223 | (internalData is data).should.be.false 224 | (internalData[0].values is data[0].values).should.be.false 225 | (internalData[1].values is data[1].values).should.be.false 226 | 227 | it 'accepts an object of objects as chart data', -> 228 | data = 229 | 'line1': 230 | type: 'line' 231 | values: [ 232 | [0,0] 233 | [1,1] 234 | [2,4] 235 | ] 236 | 'bar1': 237 | type: 'bar' 238 | values: [ 239 | [0,0] 240 | [1,1] 241 | [2,4] 242 | ] 243 | 244 | chart = new ForestD3.Chart() 245 | chart.data(data) 246 | 247 | internalData = chart.data().get() 248 | 249 | internalData.should.be.instanceof Array 250 | internalData[0].key.should.equal 'line1' 251 | internalData[0].type.should.equal 'line' 252 | 253 | internalData[1].key.should.equal 'bar1' 254 | internalData[1].type.should.equal 'bar' 255 | 256 | it 'converts data to consistent format internally', -> 257 | data = 258 | 'line1': 259 | type: 'line' 260 | values: [ 261 | [0,0] 262 | [1,1] 263 | [2,4] 264 | ] 265 | 'bar1': 266 | type: 'bar' 267 | values: [ 268 | [0,0] 269 | [1,1] 270 | [2,4] 271 | ] 272 | 273 | chart = new ForestD3.Chart() 274 | chart.data(data) 275 | 276 | internalData = chart.data().get() 277 | internalData[0].values[0].x.should.equal 0 278 | internalData[0].values[0].y.should.equal 0 279 | 280 | internalData[1].values[2].x.should.equal 2 281 | internalData[1].values[2].y.should.equal 4 282 | 283 | internalData[1].values[2].data.should.deep.equal [2,4] 284 | 285 | it 'calculates the x,y extent of each series', -> 286 | data = [ 287 | type: 'line' 288 | values: [ 289 | [-3,4] 290 | [0,6] 291 | [4,8] 292 | ] 293 | , 294 | type: 'line' 295 | values: [ 296 | [4,-1] 297 | [5,3] 298 | [6,2] 299 | ] 300 | , 301 | type: 'marker' 302 | axis: 'x' 303 | value: 30 304 | , 305 | type: 'marker' 306 | axis: 'y' 307 | value: 40 308 | , 309 | type: 'region' 310 | axis: 'x' 311 | values: [3,10] 312 | , 313 | type: 'region' 314 | axis: 'y' 315 | values: [-10,11] 316 | ] 317 | 318 | chart = new ForestD3.Chart() 319 | chart.ordinal(true).data(data) 320 | internalData = chart.data().get() 321 | 322 | internalData[0].extent.should.deep.equal 323 | x: [0,2] 324 | y: [4,8] 325 | 326 | internalData[1].extent.should.deep.equal 327 | x: [0,2] 328 | y: [-1,3] 329 | 330 | internalData[2].extent.should.deep.equal 331 | x: [30] 332 | y: [] 333 | 334 | internalData[3].extent.should.deep.equal 335 | x: [] 336 | y: [40] 337 | 338 | internalData[4].extent.should.deep.equal 339 | x: [3,10] 340 | y: [] 341 | 342 | internalData[5].extent.should.deep.equal 343 | x: [] 344 | y: [-10,11] 345 | 346 | it 'fills in key and label if not defined', -> 347 | data = [ 348 | values: [] 349 | , 350 | values: [] 351 | , 352 | values: [] 353 | ] 354 | 355 | chart = new ForestD3.Chart() 356 | chart.data(data) 357 | internalData = chart.data().get() 358 | 359 | internalData[0].key.should.equal 'series0' 360 | internalData[0].label.should.equal 'Series #0' 361 | 362 | internalData[1].key.should.equal 'series1' 363 | internalData[1].label.should.equal 'Series #1' 364 | 365 | 366 | internalData[2].key.should.equal 'series2' 367 | internalData[2].label.should.equal 'Series #2' 368 | internalData[2].type.should.equal 'scatter' 369 | 370 | it 'automatically adds color and index field to each series', -> 371 | data = [ 372 | values: [] 373 | , 374 | color: '#00f' 375 | values: [] 376 | , 377 | type: 'marker' 378 | value: 30 379 | , 380 | values: [] 381 | , 382 | values: [] 383 | , 384 | values: [] 385 | ] 386 | 387 | colors = [ 388 | "#1f77b4", 389 | "#aec7e8", 390 | "#ff7f0e", 391 | "#ffbb78", 392 | "#2ca02c", 393 | "#98df8a" 394 | ] 395 | 396 | chart = new ForestD3.Chart() 397 | chart.colorPalette(colors).data(data) 398 | internalData = chart.data().get() 399 | internalData[0].color.should.equal '#1f77b4' 400 | internalData[1].color.should.equal '#00f' 401 | should.not.exist internalData[2].color 402 | internalData[3].color.should.equal '#aec7e8' 403 | internalData[4].color.should.equal '#ff7f0e' 404 | internalData[5].color.should.equal '#ffbb78' 405 | 406 | internalData[0].index.should.equal 0 407 | internalData[1].index.should.equal 1 408 | should.not.exist internalData[2].index 409 | internalData[3].index.should.equal 2 410 | internalData[4].index.should.equal 3 411 | internalData[5].index.should.equal 4 412 | 413 | it 'auto sorts data by x value ascending', -> 414 | getPoints = -> 415 | points = [0...50].map (i)-> 416 | x: i 417 | y: Math.random() 418 | 419 | d3.shuffle points 420 | points 421 | data = 422 | series1: 423 | type: 'line' 424 | values: getPoints() 425 | series2: 426 | type: 'line' 427 | values: getPoints() 428 | 429 | chart = new ForestD3.Chart() 430 | chart 431 | .getX((d)->d.x) 432 | .getY((d)->d.y) 433 | .ordinal(false) 434 | .autoSortXValues(true) 435 | .data(data) 436 | 437 | internal = chart.data().get() 438 | 439 | internalXVals = internal[0].values.map (d)-> d.x 440 | internalXVals.should.deep.equal [0...50] 441 | 442 | internalXVals = internal[1].values.map (d)-> d.x 443 | internalXVals.should.deep.equal [0...50] 444 | 445 | describe 'Data Slice', -> 446 | it 'can get a slice of data at an index', -> 447 | data = [ 448 | key: 'series1' 449 | label: 'Foo' 450 | color: '#000' 451 | values: [ 452 | [70, 10] 453 | [80, 100] 454 | [90, 101] 455 | ] 456 | , 457 | key: 'series2' 458 | label: 'Bar' 459 | values: [ 460 | [70, 11] 461 | [80, 800] 462 | [90, 709] 463 | ] 464 | , 465 | key: 'series3' 466 | label: 'Maz' 467 | color: '#0f0' 468 | values: [ 469 | [70, 12] 470 | [80, 300] 471 | [90, 749] 472 | ] 473 | ] 474 | 475 | chart = new ForestD3.Chart() 476 | chart.data(data) 477 | 478 | slice = chart.data().sliced(0) 479 | 480 | slice.length.should.equal 3, '3 items' 481 | 482 | slice[0].y.should.equal 10 483 | slice[0].series.label.should.equal 'Foo' 484 | slice[0].series.color.should.equal '#000' 485 | 486 | slice[1].y.should.equal 11 487 | slice[2].y.should.equal 12 488 | 489 | slice = chart.data().sliced(2) 490 | 491 | yData = slice.map (d)-> d.y 492 | 493 | yData.should.deep.equal [101, 709, 749] 494 | 495 | it 'keeps hidden data out of slice', -> 496 | it 'can get a slice of data at an index', -> 497 | data = [ 498 | key: 's1' 499 | values: [ 500 | [70, 10] 501 | [80, 100] 502 | [90, 101] 503 | ] 504 | , 505 | key: 's2' 506 | values: [ 507 | [70, 10] 508 | [80, 800] 509 | [90, 709] 510 | ] 511 | , 512 | key: 's3' 513 | values: [ 514 | [70, 12] 515 | [80, 300] 516 | [90, 749] 517 | ] 518 | ] 519 | 520 | chart = new ForestD3.Chart() 521 | chart.data(data) 522 | chart.data().hide(['s2','s3']) 523 | 524 | slice = chart.data().sliced(0) 525 | slice.length.should.equal 1, 'only one item' 526 | 527 | describe 'bar item api', -> 528 | data = [ 529 | key: 's1' 530 | type: 'line' 531 | values: [] 532 | , 533 | key: 's2' 534 | type: 'bar' 535 | values: [] 536 | , 537 | key: 's3' 538 | type: 'line' 539 | values: [] 540 | , 541 | key: 's4' 542 | type: 'bar' 543 | values: [] 544 | , 545 | key: 's5' 546 | type: 'bar' 547 | values: [] 548 | ] 549 | 550 | it 'can count number of bar items', -> 551 | chart = new ForestD3.Chart() 552 | chart.data(data) 553 | api = chart.data() 554 | 555 | api.hide 's5' 556 | api.barCount().should.equal 2, 'two visible bars' 557 | 558 | api.show 's5' 559 | api.barCount().should.equal 3, 'three bars visible' 560 | 561 | it 'can get the relative bar index', -> 562 | chart = new ForestD3.Chart() 563 | chart.data(data) 564 | api = chart.data() 565 | 566 | api.barIndex('s5').should.equal 2 567 | api.barIndex('s4').should.equal 1 568 | api.barIndex('s2').should.equal 0 569 | should.not.exist api.barIndex('s1') 570 | 571 | api.hide 's2' 572 | 573 | api.barIndex('s4').should.equal 0 574 | api.barIndex('s5').should.equal 1 575 | -------------------------------------------------------------------------------- /test/guideline.coffee: -------------------------------------------------------------------------------- 1 | describe 'Chart', -> 2 | describe 'Guideline', -> 3 | chart = null 4 | container = null 5 | 6 | beforeEach -> 7 | container = document.createElement 'div' 8 | container.style.width = '500px' 9 | container.style.height = '400px' 10 | document.body.appendChild container 11 | 12 | data = [ 13 | key: 'line1' 14 | type: 'line' 15 | values: [ 16 | [0,0] 17 | [1,1] 18 | [2,2] 19 | ] 20 | , 21 | key: 'line2' 22 | type: 'line' 23 | values: [ 24 | [0,2] 25 | [1,4] 26 | [2,6] 27 | ] 28 | ] 29 | 30 | chart = new ForestD3.Chart container 31 | 32 | chart 33 | .showGuideline(true) 34 | .data(data) 35 | .render() 36 | 37 | afterEach -> 38 | chart.destroy() 39 | 40 | it 'can render a guideline', -> 41 | chart.updateTooltip 42 | canvasMouse: [250, 200] 43 | clientMouse: [0,0] 44 | 45 | line = $(container).find('g.canvas line.guideline') 46 | 47 | line.length.should.equal 1, 'line exists' 48 | 49 | line = line.get(0) 50 | line.getAttribute('x1').should.not.equal '0' 51 | 52 | line.getAttribute('x1').should.equal line.getAttribute('x2') 53 | 54 | it 'renders guideline marker circles along the guideline', -> 55 | chart.updateTooltip 56 | canvasMouse: [250, 200] 57 | clientMouse: [0,0] 58 | 59 | markerContainer = $(container).find('g.canvas g.guideline-markers') 60 | markerContainer.length.should.equal 1, 'container exists' 61 | 62 | markers = markerContainer.find('circle.marker') 63 | markers.length.should.equal 2, 'two markers' 64 | 65 | it 'can hide guideline', (done)-> 66 | chart.updateTooltip 67 | canvasMouse: [250, 200] 68 | clientMouse: [0,0] 69 | 70 | chart.updateTooltip 71 | hide: true 72 | 73 | setTimeout -> 74 | line = $(container).find('line.guideline') 75 | line.css('opacity').should.equal '0' 76 | done() 77 | , 1000 78 | -------------------------------------------------------------------------------- /test/legend.coffee: -------------------------------------------------------------------------------- 1 | describe 'Plugin: Legend', -> 2 | chartContainer = null 3 | legendContainer = null 4 | chart = null 5 | sampleData = null 6 | 7 | beforeEach -> 8 | sampleData = [ 9 | key: 'hello' 10 | label: 'Hello World' 11 | values: [] 12 | , 13 | key: 'bye' 14 | label: 'Good Bye' 15 | values: [] 16 | , 17 | key: 'adios' 18 | label: 'Good Bye Again' 19 | color: '#000' 20 | values: [] 21 | ] 22 | 23 | chartContainer = document.createElement 'div' 24 | legendContainer = document.createElement 'div' 25 | 26 | chart = new ForestD3.Chart chartContainer 27 | 28 | document.body.appendChild chartContainer 29 | document.body.appendChild legendContainer 30 | 31 | afterEach -> 32 | chart.destroy() 33 | 34 | it 'sets forest-d3 class on parent div', -> 35 | legend = new ForestD3.Legend legendContainer 36 | 37 | $(legendContainer).hasClass('forest-d3').should.be.true 38 | $(legendContainer).hasClass('legend').should.be.true 39 | 40 | it 'renders legend items given chart data', -> 41 | chart.data sampleData 42 | legend = new ForestD3.Legend legendContainer 43 | 44 | legend.chart(chart).render() 45 | 46 | items = $(legendContainer).find('.item') 47 | items.length.should.equal 3, 'three legend items' 48 | 49 | items.eq(0).find('.color-square').length.should.equal 1, 'color square' 50 | items.eq(0).text().should.contain 'Hello World' 51 | 52 | items.eq(1).text().should.contain 'Good Bye' 53 | items.eq(2).text().should.contain 'Good Bye Again' 54 | 55 | items.eq(2).find('.color-square') 56 | .css('background-color').should.equal 'rgb(0, 0, 0)' 57 | 58 | it 'renders via chart plugin API', (done)-> 59 | chart.data sampleData 60 | legend = new ForestD3.Legend legendContainer 61 | 62 | chart.addPlugin.should.exist 63 | chart.addPlugin legend 64 | 65 | chart.render() 66 | 67 | setTimeout -> 68 | items = $(legendContainer).find('.item') 69 | items.length.should.equal 3, 'three legend items' 70 | done() 71 | , 300 72 | 73 | it 'marks legend item as disabled', -> 74 | chart.data sampleData 75 | legend = new ForestD3.Legend legendContainer 76 | chart.addPlugin legend 77 | 78 | chart.render() 79 | chart.data().hide('hello').render() 80 | 81 | items = $(legendContainer).find('.item') 82 | items.eq(0).hasClass('disabled').should.be.true 83 | 84 | it 'marks legend item as disabled', -> 85 | data2 = sampleData.concat [ 86 | type: 'region' 87 | values: [3,4] 88 | , 89 | type: 'marker' 90 | value: 1 91 | ] 92 | 93 | chart.data data2 94 | legend = new ForestD3.Legend legendContainer 95 | legend.onlyDataSeries(true) 96 | chart.addPlugin legend 97 | 98 | chart.render() 99 | items = $(legendContainer).find '.item' 100 | items.length.should.equal 3, '3 items only' 101 | -------------------------------------------------------------------------------- /test/ohlc.coffee: -------------------------------------------------------------------------------- 1 | describe 'Chart', -> 2 | describe 'OHLC (open high low close) Chart', -> 3 | it 'can render', -> 4 | container = document.createElement 'div' 5 | chart = new ForestD3.Chart container 6 | data = [ 7 | key: 's1' 8 | type: 'ohlc' 9 | values: [ 10 | [0, 0.01, 0.2, 0.01, 0.15] 11 | [1, 0.02, 0.21, 0.016, 0.17] 12 | ] 13 | ] 14 | 15 | chart.data(data).render() 16 | 17 | lines = $(container).find('line.ohlc-range') 18 | lines.length.should.equal 2, 'two range lines' 19 | -------------------------------------------------------------------------------- /test/smoke-tests.coffee: -------------------------------------------------------------------------------- 1 | describe 'Smoke Tests', -> 2 | it 'd3 should exist', -> 3 | expect(d3).to.exist 4 | d3.version.should.match /^3\.5/ 5 | -------------------------------------------------------------------------------- /test/stacked-chart.coffee: -------------------------------------------------------------------------------- 1 | describe 'Chart', -> 2 | describe 'Stacked Charts', -> 3 | chart = null 4 | container = null 5 | 6 | data = [ 7 | values: [ 8 | [1, 3] 9 | [2, 2] 10 | [3, 6] 11 | ] 12 | , 13 | values: [ 14 | [1, 1] 15 | [2, 3] 16 | [3, 1] 17 | ] 18 | , 19 | values: [ 20 | [1, 1] 21 | [2, 1] 22 | [3, 1] 23 | ] 24 | ] 25 | 26 | beforeEach -> 27 | container = document.createElement 'div' 28 | container.style.width = '500px' 29 | container.style.height = '400px' 30 | document.body.appendChild container 31 | 32 | afterEach -> 33 | chart.destroy() 34 | 35 | it 'computes the stacked offsets and extents', -> 36 | chart = new ForestD3.StackedChart container 37 | 38 | chart.stacked(true).stackType('bar').data(data).render() 39 | 40 | internal = chart.data().get() 41 | 42 | internal[2].values[0].y0.should.equal 0 43 | internal[2].values[1].y0.should.equal 0 44 | internal[2].values[2].y0.should.equal 0 45 | 46 | internal[1].values[0].y0.should.equal 1 47 | internal[1].values[1].y0.should.equal 1 48 | internal[1].values[2].y0.should.equal 1 49 | 50 | internal[0].values[0].y0.should.equal 2 51 | internal[0].values[1].y0.should.equal 4 52 | internal[0].values[2].y0.should.equal 2 53 | 54 | internal[2].extent.y.should.deep.equal [0, 1] 55 | internal[1].extent.y.should.deep.equal [0, 4] 56 | internal[0].extent.y.should.deep.equal [0, 8] 57 | 58 | it 'negative stacked values', (done)-> 59 | chart = new ForestD3.StackedChart container 60 | 61 | dataNeg = [ 62 | values: [[0, -1]] 63 | , 64 | values: [[0, -2]] 65 | , 66 | values: [[0, -3]] 67 | , 68 | values: [[0, -2]] 69 | ] 70 | 71 | chart.stacked(true).stackType('bar').data(dataNeg).render() 72 | 73 | internal = chart.data().get() 74 | 75 | internal[3].values[0].y0.should.equal 0 76 | internal[2].values[0].y0.should.equal -2 77 | internal[1].values[0].y0.should.equal -5 78 | internal[0].values[0].y0.should.equal -7 79 | 80 | setTimeout -> 81 | series = $(container).find('g.series').eq(0) 82 | bar = series.find('rect') 83 | parseInt(bar.attr('y')).should.equal(parseInt(chart.yScale(-7))) 84 | 85 | series = $(container).find('g.series').eq(1) 86 | bar = series.find('rect') 87 | parseInt(bar.attr('y')).should.equal(parseInt(chart.yScale(-5))) 88 | 89 | series = $(container).find('g.series').eq(2) 90 | bar = series.find('rect') 91 | parseInt(bar.attr('y')).should.equal(parseInt(chart.yScale(-2))) 92 | 93 | series = $(container).find('g.series').eq(3) 94 | bar = series.find('rect') 95 | parseInt(bar.attr('y')).should.equal(parseInt(chart.yScale(0))) 96 | 97 | done() 98 | , 300 99 | 100 | it 'renders stacked bars and markers', (done)-> 101 | chart = new ForestD3.StackedChart container 102 | 103 | data2 = data.concat [ 104 | type: 'marker' 105 | label: 'Marker 1' 106 | axis: 'y' 107 | value: 4.5 108 | ] 109 | 110 | chart.stacked(true).stackType('bar').data(data2).render() 111 | 112 | internal = chart.data().get() 113 | for series,i in internal 114 | if i < 3 115 | series.type.should.equal 'bar' 116 | else if i is 3 117 | series.type.should.equal 'marker' 118 | 119 | setTimeout -> 120 | series = $(container).find('g.series') 121 | series.length.should.equal 4, '4 series' 122 | 123 | series.eq(0).find('rect.bar').length.should.equal 3, '3 bars' 124 | series.eq(1).find('rect.bar').length.should.equal 3, '3 bars' 125 | series.eq(2).find('rect.bar').length.should.equal 3, '3 bars' 126 | 127 | series.eq(3).find('line.marker').length.should.equal 1,'marker' 128 | done() 129 | , 500 130 | 131 | it 'calculates offset only for visible bars', -> 132 | chart = new ForestD3.StackedChart container 133 | 134 | chart.stacked(true).stackType('bar').data(data) 135 | 136 | chart.data().hide('series1').render() 137 | 138 | internal = chart.data().visible() 139 | 140 | internal[0].values[0].y0.should.equal 1 141 | internal[0].values[1].y0.should.equal 1 142 | internal[0].values[2].y0.should.equal 1 143 | 144 | it 'renders stacked area chart', (done)-> 145 | chart = new ForestD3.StackedChart container 146 | data = [ 147 | values: [0...20].map (i)-> [i, Math.random()] 148 | , 149 | values: [0...20].map (i)-> [i, Math.random()] 150 | , 151 | values: [0...20].map (i)-> [i, Math.random()] 152 | ] 153 | 154 | chart.stacked(true).stackType('area').data(data).render() 155 | 156 | setTimeout -> 157 | series = $(container).find('g.series path.area') 158 | series.length.should.equal 3, 'three area paths' 159 | done() 160 | , 300 -------------------------------------------------------------------------------- /test/state.coffee: -------------------------------------------------------------------------------- 1 | describe 'Chart', -> 2 | describe 'State and Event management', -> 3 | chart = null 4 | beforeEach -> 5 | data = [ 6 | key: 's1' 7 | values: [ 8 | [1,1] 9 | ] 10 | , 11 | key: 's2' 12 | values: [ 13 | [1,2] 14 | ] 15 | , 16 | key: 's3' 17 | values: [ 18 | [1,3] 19 | ] 20 | ] 21 | 22 | container = document.createElement 'div' 23 | container.style.width = '500px' 24 | container.style.height = '400px' 25 | chart = new ForestD3.Chart container 26 | chart.data(data) 27 | 28 | afterEach -> chart.destroy() 29 | 30 | it 'has `on` and `trigger` methods', -> 31 | should.exist chart.on 32 | should.exist chart.trigger 33 | 34 | it 'fires `rendered` event which contains current state', -> 35 | spy = sinon.spy() 36 | 37 | chart.on 'rendered', spy 38 | 39 | chart.data().hide('s2') 40 | chart.render() 41 | 42 | spy.should.have.been.calledOnce 43 | -------------------------------------------------------------------------------- /test/tooltip.coffee: -------------------------------------------------------------------------------- 1 | describe 'Tooltip and Guideline', -> 2 | chart = null 3 | container = null 4 | data = null 5 | beforeEach -> 6 | container = document.createElement 'div' 7 | container.style.width = '500px' 8 | container.style.height = '500px' 9 | document.body.appendChild container 10 | chart = new ForestD3.Chart container 11 | 12 | data = [ 13 | key: 'series1' 14 | values: [ 15 | [1,1] 16 | [2,2] 17 | ] 18 | , 19 | key: 'series2' 20 | values: [ 21 | [4,0] 22 | [5,0] 23 | ] 24 | ] 25 | 26 | afterEach -> 27 | chart.destroy() 28 | 29 | it 'rendered a guideline on the chart canvas', -> 30 | chart.data(data).render() 31 | line = $(container).find('.canvas line.guideline') 32 | line.length.should.equal 1 33 | 34 | it 'renders tooltip onto document.body', -> 35 | chart.data(data).render() 36 | chart.tooltip.id 'my-tooltip' 37 | chart.updateTooltip 38 | canvasMouse: [0,0] 39 | clientMouse: [10,10] 40 | 41 | tooltip = $('.forest-d3.tooltip-box') 42 | tooltip.length.should.equal 1, 'tooltip exists' 43 | 44 | $('#my-tooltip').length.should.equal 1, 'select by id' 45 | 46 | it 'can render spatial tooltips', (done)-> 47 | chart.ordinal(false).tooltipType('spatial').data(data).render() 48 | 49 | chart.updateTooltip 50 | canvasMouse: [40, 400] 51 | clientMouse: [10, 10] 52 | 53 | x = chart.xScale 1 54 | y = chart.yScale 1 55 | 56 | chart.updateTooltip 57 | canvasMouse: [x,y] 58 | clientMouse: [10,10] 59 | 60 | setTimeout -> 61 | crosshair = $(container).find('line.crosshair-x') 62 | crosshair.css('stroke-opacity').should.equal '0.5' 63 | done() 64 | , 200 65 | 66 | it 'adds "interactive" class to series when tooltipType=="hover"', (done)-> 67 | data2 = [ 68 | type: 'scatter' 69 | values: [] 70 | , 71 | type: 'bar' 72 | values: [] 73 | ] 74 | 75 | chart.ordinal(false).tooltipType('hover').data(data2).render() 76 | 77 | setTimeout -> 78 | series = $(container).find('g.series.interactive') 79 | series.length.should.equal 2, 'two interactive series' 80 | done() 81 | , 200 82 | 83 | it 'emits event when a bisect guideline is shown', (done)-> 84 | data2 = 85 | series1: 86 | type: 'line' 87 | values: [0..50].map (i)-> [i, Math.random()] 88 | 89 | chart.tooltipType('bisect').data(data2).render() 90 | 91 | chart.on 'tooltipBisect', (e)-> 92 | e.index.should.equal 32, 'index val found through testing' 93 | e.clientMouse.should.deep.equal [10,10] 94 | e.canvasMouse.should.deep.equal [250, 400] 95 | done() 96 | 97 | chart.updateTooltip 98 | canvasMouse: [250, 400] 99 | clientMouse: [10, 10] 100 | 101 | it 'emits event when tooltip hidden', (done)-> 102 | chart.tooltipType('bisect').data(data).render() 103 | chart.on 'tooltipHidden', (e)-> 104 | done() 105 | 106 | chart.updateTooltip {hide: true} 107 | -------------------------------------------------------------------------------- /test/utils.coffee: -------------------------------------------------------------------------------- 1 | describe 'Utilities', -> 2 | describe 'Extent', -> 3 | extent = null 4 | beforeEach -> 5 | extent = ForestD3.Utils.extent 6 | 7 | it 'exists and is a function', -> 8 | extent.should.exist 9 | extent.should.be.a.Function 10 | 11 | it 'handles empty case', -> 12 | result = extent [] 13 | result.should.deep.equal 14 | x: [-1, 1] 15 | y: [-1, 1] 16 | 17 | it 'handles simple case', -> 18 | data = [ 19 | extent: 20 | x: [1, 5] 21 | y: [1, 2] 22 | ] 23 | 24 | result = extent data 25 | 26 | result.should.deep.equal 27 | x: [1, 5] 28 | y: [1, 2] 29 | 30 | it 'handles multiple series', -> 31 | data = [ 32 | extent: 33 | x: [1,5] 34 | y: [2,3] 35 | , 36 | extent: 37 | x: [1,9] 38 | y: [-2,3] 39 | , 40 | extent: 41 | x: [-1,5] 42 | y: [2,30] 43 | ] 44 | 45 | result = extent data 46 | 47 | result.should.deep.equal 48 | x: [-1, 9] 49 | y: [-2, 30] 50 | 51 | it 'skips the integer rounding if extent range is small', -> 52 | data = [ 53 | extent: 54 | x: [0, 1] 55 | y: [-0.02, 0.8] 56 | ] 57 | 58 | result = extent data 59 | result.should.deep.equal 60 | x: [0, 1] 61 | y: [-0.02, 0.8] 62 | 63 | it 'factors in chart markers as part of the computation', -> 64 | data = [ 65 | extent: 66 | x: [-3, 0] 67 | y: [5, 8] 68 | , 69 | extent: 70 | y: [10] 71 | , 72 | extent: 73 | x: [1] 74 | ] 75 | 76 | result = extent data 77 | 78 | result.should.deep.equal 79 | x: [-3, 1] 80 | y: [5, 10] 81 | 82 | it 'defaults to [-1,1] extent if no valid values', -> 83 | data = [ 84 | extent: 85 | y: [0.5] 86 | ] 87 | 88 | result = extent data 89 | result.should.deep.equal 90 | x: [-1, 1] 91 | y: [0.5,0.5] 92 | 93 | it 'accepts a "force" property, forcing values onto the extent', -> 94 | data = [ 95 | extent: 96 | x: [-1, 1] 97 | y: [1, 3] 98 | ] 99 | 100 | force = 101 | x: [0] 102 | y: [0] 103 | 104 | result = extent data, force 105 | 106 | result.should.deep.equal 107 | x: [-1, 1] 108 | y: [0, 3] 109 | 110 | force = 111 | x: -4 112 | y: 0 113 | 114 | result = extent data, force 115 | result.should.deep.equal 116 | x: [-4, 1] 117 | y: [0, 3] 118 | 119 | describe 'extentPadding', -> 120 | extPadding = null 121 | beforeEach -> 122 | extPadding = ForestD3.Utils.extentPadding 123 | 124 | it 'increases the extent by a certain percentage', -> 125 | xyExtent = 126 | x: [-10, 10] 127 | y: [2, 5] 128 | 129 | padding = 130 | x: 0.1 # 10 percent 131 | y: 0.05 # 5 percent 132 | 133 | newExtent = extPadding xyExtent, padding 134 | 135 | newExtent.should.deep.equal 136 | x: [-11, 11] 137 | y: [1.925, 5.075] 138 | 139 | it 'handles cases where the extent values are the same', -> 140 | xyExtent = 141 | x: [0.2, 0.2] 142 | y: [0.2, 0.2] 143 | 144 | padding = 145 | x: 0.1 146 | y: 0.1 147 | 148 | newExtent = extPadding xyExtent, padding 149 | newExtent.x[0].toFixed(2).should.equal '0.19' 150 | newExtent.x[1].toFixed(2).should.equal '0.21' 151 | 152 | it 'handles case where extent is [0,0]', -> 153 | xyExtent = 154 | x: [0,0] 155 | y: [0,1] 156 | 157 | padding = 158 | x: 0.1 159 | y: 0 160 | 161 | newExtent = extPadding xyExtent, padding 162 | newExtent.should.deep.equal 163 | x: [-1, 1] 164 | y: [0, 1] 165 | 166 | describe 'smartBisect', -> 167 | smartBisect = null 168 | getX = (d,i)-> i 169 | beforeEach -> smartBisect = ForestD3.Utils.smartBisect 170 | 171 | it 'handles basic case', -> 172 | data = [0, 1, 2, 3, 4, 5] 173 | 174 | tests = [ 175 | [0,0] 176 | [2,2] 177 | [2.5, 3] 178 | [2.51, 3] 179 | [5, 5] 180 | [7, 5] 181 | [-1, 0] 182 | ] 183 | 184 | for test, i in tests 185 | [search, expected] = test 186 | 187 | result = smartBisect data, search, getX 188 | 189 | result.should.equal expected, "Test case #{i}" 190 | 191 | it 'edge cases', -> 192 | result = smartBisect 'blah' 193 | should.not.exist result, 'result is null' 194 | 195 | result = smartBisect [] 196 | should.not.exist result, 'result null if input empty array' 197 | 198 | result = smartBisect [1] 199 | result.should.equal 0, 'result is 0' 200 | 201 | describe 'tickValues', -> 202 | tickValues = null 203 | beforeEach -> tickValues = ForestD3.Utils.tickValues 204 | 205 | it 'handles one or two ticks', -> 206 | result = tickValues [0], 100 207 | 208 | result.should.deep.equal [0] 209 | 210 | result = tickValues [0,1], 100 211 | 212 | result.should.deep.equal [0,1] 213 | 214 | it 'xValues is less than numTicks', -> 215 | result = tickValues [0,1,2,3,4], 10 216 | result.should.deep.equal [0,1,2,3,4] 217 | 218 | it 'numTicks is 3', -> 219 | result = tickValues [0,1,2,3,4], 3 220 | result.should.deep.equal [0,2,4] 221 | 222 | it 'numTicks is 5', -> 223 | width = 2 224 | result = tickValues [0..10], 4, width 225 | result.should.deep.equal [0, 3, 6, 10] 226 | 227 | --------------------------------------------------------------------------------