├── examples ├── lib │ ├── example.js │ └── example.css ├── _template.html ├── donut.html ├── bar-no-axes.html ├── goals.html ├── stacked_bars.html ├── dst.html ├── area-as-line.html ├── bar.html ├── donut-formatter.html ├── area.html ├── donut-colors.html ├── negative.html ├── decimal-custom-hover.html ├── bar-highlight-hover.html ├── non-date.html ├── bar-colors.html ├── updating.html ├── years.html ├── days.html ├── months-no-smooth.html ├── no-grid.html ├── diagonal-xlabels.html ├── diagonal-xlabels-bar.html ├── timestamps.html ├── resize.html ├── non-continuous.html ├── weeks.html ├── quarters.html └── events.html ├── spec ├── viz │ ├── exemplary │ │ ├── area0.png │ │ ├── bar0.png │ │ ├── line0.png │ │ └── stacked_bar0.png │ ├── run.sh │ ├── test.html │ ├── examples.js │ └── visual_specs.js ├── support │ └── placeholder.coffee ├── lib │ ├── grid │ │ ├── y_label_format_spec.coffee │ │ ├── auto_grid_lines_spec.coffee │ │ └── set_data_spec.coffee │ ├── pad_spec.coffee │ ├── commas_spec.coffee │ ├── bar │ │ ├── colours.coffee │ │ └── bar_spec.coffee │ ├── parse_time_spec.coffee │ ├── area │ │ └── area_spec.coffee │ ├── hover_spec.coffee │ ├── donut │ │ └── donut_spec.coffee │ ├── label_series_spec.coffee │ └── line │ │ └── line_spec.coffee └── specs.html ├── .gitignore ├── bower.json ├── morris.css ├── bower.travis.json ├── less └── morris.core.less ├── .travis.yml ├── package.json ├── lib ├── morris.coffee ├── morris.hover.coffee ├── morris.area.coffee ├── morris.donut.coffee ├── morris.bar.coffee ├── morris.line.coffee └── morris.grid.coffee ├── Gruntfile.js ├── README.md └── morris.min.js /examples/lib/example.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | eval($('#code').text()); 3 | prettyPrint(); 4 | }); -------------------------------------------------------------------------------- /spec/viz/exemplary/area0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arch/morris.js/HEAD/spec/viz/exemplary/area0.png -------------------------------------------------------------------------------- /spec/viz/exemplary/bar0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arch/morris.js/HEAD/spec/viz/exemplary/bar0.png -------------------------------------------------------------------------------- /spec/viz/exemplary/line0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arch/morris.js/HEAD/spec/viz/exemplary/line0.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | spec/viz/output/ 4 | spec/viz/diff/ 5 | bower_components 6 | .idea 7 | -------------------------------------------------------------------------------- /spec/viz/exemplary/stacked_bar0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arch/morris.js/HEAD/spec/viz/exemplary/stacked_bar0.png -------------------------------------------------------------------------------- /spec/support/placeholder.coffee: -------------------------------------------------------------------------------- 1 | beforeEach -> 2 | placeholder = $('
') 3 | $('#test').append(placeholder) 4 | 5 | afterEach -> 6 | $('#test').empty() 7 | -------------------------------------------------------------------------------- /examples/lib/example.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 800px; 3 | margin: 0 auto; 4 | } 5 | #graph { 6 | width: 800px; 7 | height: 250px; 8 | margin: 20px auto 0 auto; 9 | } 10 | pre { 11 | height: 250px; 12 | overflow: auto; 13 | } 14 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "morris.js", 3 | "main": [ 4 | "./morris.js", 5 | "./morris.css" 6 | ], 7 | "dependencies": { 8 | "jquery": ">= 1.7.0", 9 | "raphael": ">= 2.0" 10 | }, 11 | "devDependencies": { 12 | "mocha": "~1.17.1", 13 | "chai": "~1.9.0", 14 | "chai-jquery": "~1.2.1", 15 | "sinon": "http://sinonjs.org/releases/sinon-1.8.1.js", 16 | "sinon-chai": "~2.5.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /morris.css: -------------------------------------------------------------------------------- 1 | .morris-hover{position:absolute;z-index:1000}.morris-hover.morris-default-style{border-radius:10px;padding:6px;color:#666;background:rgba(255,255,255,0.8);border:solid 2px rgba(230,230,230,0.8);font-family:sans-serif;font-size:12px;text-align:center}.morris-hover.morris-default-style .morris-hover-row-label{font-weight:bold;margin:0.25em 0} 2 | .morris-hover.morris-default-style .morris-hover-point{white-space:nowrap;margin:0.1em 0} 3 | -------------------------------------------------------------------------------- /bower.travis.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "morris.js", 3 | "version": "0.5.1", 4 | "main": [ 5 | "./morris.js", 6 | "./morris.css" 7 | ], 8 | "dependencies": { 9 | "jquery": "JQUERY", 10 | "raphael": "RAPHAEL" 11 | }, 12 | "devDependencies": { 13 | "mocha": "~1.17.1", 14 | "chai": "~1.9.0", 15 | "chai-jquery": "~1.2.1", 16 | "sinon": "http://sinonjs.org/releases/sinon-1.8.1.js", 17 | "sinon-chai": "~2.5.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spec/lib/grid/y_label_format_spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'Morris.Grid#yLabelFormat', -> 2 | 3 | it 'should use custom formatter for y labels', -> 4 | formatter = (label) -> 5 | flabel = parseFloat(label) / 1000 6 | "#{flabel.toFixed(1)}k" 7 | line = Morris.Line 8 | element: 'graph' 9 | data: [{x: 1, y: 1500}, {x: 2, y: 2500}] 10 | xkey: 'x' 11 | ykeys: ['y'] 12 | labels: ['dontcare'] 13 | preUnits: "$" 14 | yLabelFormat: formatter 15 | line.yLabelFormat(1500).should.equal "1.5k" 16 | -------------------------------------------------------------------------------- /spec/viz/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # visual_specs.js creates output in output/XXX.png 4 | phantomjs visual_specs.js 5 | 6 | # clear out old diffs 7 | mkdir -p diff 8 | rm -f diff/* 9 | 10 | # generate diffs 11 | PASS=1 12 | for i in exemplary/*.png 13 | do 14 | FN=`basename $i` 15 | perceptualdiff $i output/$FN -output diff/$FN 16 | if [ $? -eq 0 ] 17 | then 18 | echo "OK: $FN" 19 | else 20 | echo "FAIL: $FN" 21 | PASS=0 22 | fi 23 | done 24 | 25 | # pass / fail 26 | if [ $PASS -eq 1 ] 27 | then 28 | echo "Success." 29 | else 30 | echo "Failed." 31 | exit 1 32 | fi 33 | -------------------------------------------------------------------------------- /less/morris.core.less: -------------------------------------------------------------------------------- 1 | .morris-hover { 2 | position: absolute; 3 | z-index: 1000; 4 | 5 | &.morris-default-style { 6 | border-radius: 10px; 7 | padding: 6px; 8 | color: #666; 9 | background: rgba(255, 255, 255, 0.8); 10 | border: solid 2px rgba(230, 230, 230, 0.8); 11 | 12 | font-family: sans-serif; 13 | font-size: 12px; 14 | text-align: center; 15 | 16 | .morris-hover-row-label { 17 | font-weight: bold; 18 | margin: 0.25em 0; 19 | } 20 | 21 | .morris-hover-point { 22 | white-space: nowrap; 23 | margin: 0.1em 0; 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /spec/lib/pad_spec.coffee: -------------------------------------------------------------------------------- 1 | describe '#pad', -> 2 | 3 | it 'should pad numbers', -> 4 | Morris.pad2(0).should.equal("00") 5 | Morris.pad2(1).should.equal("01") 6 | Morris.pad2(2).should.equal("02") 7 | Morris.pad2(3).should.equal("03") 8 | Morris.pad2(4).should.equal("04") 9 | Morris.pad2(5).should.equal("05") 10 | Morris.pad2(6).should.equal("06") 11 | Morris.pad2(7).should.equal("07") 12 | Morris.pad2(8).should.equal("08") 13 | Morris.pad2(9).should.equal("09") 14 | Morris.pad2(10).should.equal("10") 15 | Morris.pad2(12).should.equal("12") 16 | Morris.pad2(34).should.equal("34") 17 | Morris.pad2(123).should.equal("123") -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | before_script: 5 | - "npm install -g grunt-cli" 6 | - "npm install" 7 | - "cp -f bower.travis.json bower.json" 8 | - 'sed -i -e "s/JQUERY/$JQUERY/" bower.json' 9 | - 'sed -i -e "s/RAPHAEL/$RAPHAEL/" bower.json' 10 | - "bower install" 11 | env: 12 | - JQUERY="~> 1.7.0" RAPHAEL="~> 2.0.0" 13 | - JQUERY="~> 1.8.0" RAPHAEL="~> 2.0.0" 14 | - JQUERY="~> 1.9.0" RAPHAEL="~> 2.0.0" 15 | - JQUERY="~> 2.0.0" RAPHAEL="~> 2.0.0" 16 | - JQUERY="~> 2.1.0" RAPHAEL="~> 2.0.0" 17 | - JQUERY="~> 1.8.0" RAPHAEL="~> 2.1.0" 18 | - JQUERY="~> 1.9.0" RAPHAEL="~> 2.1.0" 19 | - JQUERY="~> 2.0.0" RAPHAEL="~> 2.1.0" 20 | - JQUERY="~> 2.1.0" RAPHAEL="~> 2.1.0" 21 | -------------------------------------------------------------------------------- /examples/_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Title

13 |
14 |
15 | // Insert code here:
16 | // it'll get eval()-ed and prettyprinted.
17 | 
18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "morris.js", 3 | "version": "0.5.1", 4 | "homepage": "http://morrisjs.github.com/morris.js", 5 | "license": "BSD-2-Clause", 6 | "description": "Easy, pretty charts", 7 | "author": { 8 | "name": "Olly Smith", 9 | "email": "olly@oesmith.co.uk" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/morrisjs/morris.js.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/morrisjs/morris.js/issues" 17 | }, 18 | "devDependencies": { 19 | "matchdep": "~0.1.2", 20 | "grunt": "~0.4.1", 21 | "grunt-mocha": "~0.4.10", 22 | "grunt-contrib-concat": "~0.3.0", 23 | "grunt-contrib-coffee": "~0.7.0", 24 | "grunt-contrib-uglify": "~0.2.4", 25 | "grunt-contrib-less": "~0.7.0", 26 | "grunt-contrib-watch": "~0.5.3", 27 | "grunt-shell": "~0.5.0", 28 | "bower": "1.3.8" 29 | }, 30 | "scripts": { 31 | "test": "grunt concat coffee mocha" 32 | }, 33 | "engines": { 34 | "node": ">=0.8 <0.11" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /spec/viz/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 20 | 31 | 32 | 33 |
34 | 35 | -------------------------------------------------------------------------------- /examples/donut.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Donut Chart

14 |
15 |
16 | Morris.Donut({
17 |   element: 'graph',
18 |   data: [
19 |     {value: 70, label: 'foo'},
20 |     {value: 15, label: 'bar'},
21 |     {value: 10, label: 'baz'},
22 |     {value: 5, label: 'A really really long label'}
23 |   ],
24 |   formatter: function (x) { return x + "%"}
25 | }).on('click', function(i, row){
26 |   console.log(i, row);
27 | });
28 | 
29 | 30 | -------------------------------------------------------------------------------- /examples/bar-no-axes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Bar charts

14 |
15 |
16 | // Use Morris.Bar
17 | Morris.Bar({
18 |   element: 'graph',
19 |   axes: false,
20 |   data: [
21 |     {x: '2011 Q1', y: 3, z: 2, a: 3},
22 |     {x: '2011 Q2', y: 2, z: null, a: 1},
23 |     {x: '2011 Q3', y: 0, z: 2, a: 4},
24 |     {x: '2011 Q4', y: 2, z: 4, a: 3}
25 |   ],
26 |   xkey: 'x',
27 |   ykeys: ['y', 'z', 'a'],
28 |   labels: ['Y', 'Z', 'A']
29 | });
30 | 
31 | 32 | -------------------------------------------------------------------------------- /examples/goals.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Value Goals

14 |
15 |
16 | var decimal_data = [];
17 | for (var x = 0; x <= 360; x += 10) {
18 |   decimal_data.push({
19 |     x: x,
20 |     y: Math.sin(Math.PI * x / 180).toFixed(4)
21 |   });
22 | }
23 | window.m = Morris.Line({
24 |   element: 'graph',
25 |   data: decimal_data,
26 |   xkey: 'x',
27 |   ykeys: ['y'],
28 |   labels: ['sin(x)'],
29 |   parseTime: false,
30 |   goals: [-1, 0, 1]
31 | });
32 | 
33 | 34 | -------------------------------------------------------------------------------- /examples/stacked_bars.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Stacked Bars chart

14 |
15 |
16 | // Use Morris.Bar
17 | Morris.Bar({
18 |   element: 'graph',
19 |   data: [
20 |     {x: '2011 Q1', y: 3, z: 2, a: 3},
21 |     {x: '2011 Q2', y: 2, z: null, a: 1},
22 |     {x: '2011 Q3', y: 0, z: 2, a: 4},
23 |     {x: '2011 Q4', y: 2, z: 4, a: 3}
24 |   ],
25 |   xkey: 'x',
26 |   ykeys: ['y', 'z', 'a'],
27 |   labels: ['Y', 'Z', 'A'],
28 |   stacked: true
29 | });
30 | 
31 | 32 | -------------------------------------------------------------------------------- /examples/dst.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Daylight-savings time

14 |
15 |
16 | // This crosses a DST boundary in the UK.
17 | Morris.Area({
18 |   element: 'graph',
19 |   data: [
20 |     {x: '2013-03-30 22:00:00', y: 3, z: 3},
21 |     {x: '2013-03-31 00:00:00', y: 2, z: 0},
22 |     {x: '2013-03-31 02:00:00', y: 0, z: 2},
23 |     {x: '2013-03-31 04:00:00', y: 4, z: 4}
24 |   ],
25 |   xkey: 'x',
26 |   ykeys: ['y', 'z'],
27 |   labels: ['Y', 'Z']
28 | });
29 | 
30 | 31 | -------------------------------------------------------------------------------- /examples/area-as-line.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Area charts behaving like line charts

14 |
15 |
16 | // Use Morris.Area instead of Morris.Line
17 | Morris.Area({
18 |   element: 'graph',
19 |   behaveLikeLine: true,
20 |   data: [
21 |     {x: '2011 Q1', y: 3, z: 3},
22 |     {x: '2011 Q2', y: 2, z: 1},
23 |     {x: '2011 Q3', y: 2, z: 4},
24 |     {x: '2011 Q4', y: 3, z: 3}
25 |   ],
26 |   xkey: 'x',
27 |   ykeys: ['y', 'z'],
28 |   labels: ['Y', 'Z']
29 | });
30 | 
31 | 32 | -------------------------------------------------------------------------------- /examples/bar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Bar charts

14 |
15 |
16 | // Use Morris.Bar
17 | Morris.Bar({
18 |   element: 'graph',
19 |   data: [
20 |     {x: '2011 Q1', y: 3, z: 2, a: 3},
21 |     {x: '2011 Q2', y: 2, z: null, a: 1},
22 |     {x: '2011 Q3', y: 0, z: 2, a: 4},
23 |     {x: '2011 Q4', y: 2, z: 4, a: 3}
24 |   ],
25 |   xkey: 'x',
26 |   ykeys: ['y', 'z', 'a'],
27 |   labels: ['Y', 'Z', 'A']
28 | }).on('click', function(i, row){
29 |   console.log(i, row);
30 | });
31 | 
32 | 33 | -------------------------------------------------------------------------------- /examples/donut-formatter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Donut Chart

14 |
15 |
16 | Morris.Donut({
17 |   element: 'graph',
18 |   data: [
19 |     {value: 70, label: 'foo', formatted: 'at least 70%' },
20 |     {value: 15, label: 'bar', formatted: 'approx. 15%' },
21 |     {value: 10, label: 'baz', formatted: 'approx. 10%' },
22 |     {value: 5, label: 'A really really long label', formatted: 'at most 5%' }
23 |   ],
24 |   formatter: function (x, data) { return data.formatted; }
25 | });
26 | 
27 | 28 | -------------------------------------------------------------------------------- /lib/morris.coffee: -------------------------------------------------------------------------------- 1 | Morris = window.Morris = {} 2 | 3 | $ = jQuery 4 | 5 | # Very simple event-emitter class. 6 | # 7 | # @private 8 | class Morris.EventEmitter 9 | on: (name, handler) -> 10 | unless @handlers? 11 | @handlers = {} 12 | unless @handlers[name]? 13 | @handlers[name] = [] 14 | @handlers[name].push(handler) 15 | @ 16 | 17 | fire: (name, args...) -> 18 | if @handlers? and @handlers[name]? 19 | for handler in @handlers[name] 20 | handler(args...) 21 | 22 | # Make long numbers prettier by inserting commas. 23 | # 24 | # @example 25 | # Morris.commas(1234567) -> '1,234,567' 26 | Morris.commas = (num) -> 27 | if num? 28 | ret = if num < 0 then "-" else "" 29 | absnum = Math.abs(num) 30 | intnum = Math.floor(absnum).toFixed(0) 31 | ret += intnum.replace(/(?=(?:\d{3})+$)(?!^)/g, ',') 32 | strabsnum = absnum.toString() 33 | if strabsnum.length > intnum.length 34 | ret += strabsnum.slice(intnum.length) 35 | ret 36 | else 37 | '-' 38 | 39 | # Zero-pad numbers to two characters wide. 40 | # 41 | # @example 42 | # Morris.pad2(1) -> '01' 43 | Morris.pad2 = (number) -> (if number < 10 then '0' else '') + number 44 | -------------------------------------------------------------------------------- /examples/area.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Area charts

14 |
15 |
16 | // Use Morris.Area instead of Morris.Line
17 | Morris.Area({
18 |   element: 'graph',
19 |   data: [
20 |     {x: '2010 Q4', y: 3, z: 7},
21 |     {x: '2011 Q1', y: 3, z: 4},
22 |     {x: '2011 Q2', y: null, z: 1},
23 |     {x: '2011 Q3', y: 2, z: 5},
24 |     {x: '2011 Q4', y: 8, z: 2},
25 |     {x: '2012 Q1', y: 4, z: 4}
26 |   ],
27 |   xkey: 'x',
28 |   ykeys: ['y', 'z'],
29 |   labels: ['Y', 'Z']
30 | }).on('click', function(i, row){
31 |   console.log(i, row);
32 | });
33 | 
34 | 35 | -------------------------------------------------------------------------------- /spec/lib/grid/auto_grid_lines_spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'Morris.Grid#autoGridLines', -> 2 | 3 | beforeEach -> 4 | @subject = Morris.Grid.prototype.autoGridLines 5 | 6 | it 'should draw at fixed intervals', -> 7 | @subject(0, 4, 5).should.deep.equal [0, 1, 2, 3, 4] 8 | @subject(0, 400, 5).should.deep.equal [0, 100, 200, 300, 400] 9 | 10 | it 'should pick intervals that show significant numbers', -> 11 | @subject(102, 499, 5).should.deep.equal [100, 200, 300, 400, 500] 12 | 13 | it 'should draw zero when it falls within [ymin..ymax]', -> 14 | @subject(-100, 300, 5).should.deep.equal [-100, 0, 100, 200, 300] 15 | @subject(-50, 350, 5).should.deep.equal [-125, 0, 125, 250, 375] 16 | @subject(-400, 400, 5).should.deep.equal [-400, -200, 0, 200, 400] 17 | @subject(100, 500, 5).should.deep.equal [100, 200, 300, 400, 500] 18 | @subject(-500, -100, 5).should.deep.equal [-500, -400, -300, -200, -100] 19 | 20 | it 'should generate decimal labels to 2 significant figures', -> 21 | @subject(0, 1, 5).should.deep.equal [0, 0.25, 0.5, 0.75, 1] 22 | @subject(0.1, 0.5, 5).should.deep.equal [0.1, 0.2, 0.3, 0.4, 0.5] 23 | 24 | it 'should use integer intervals for intervals larger than 1', -> 25 | @subject(0, 9, 5).should.deep.equal [0, 3, 6, 9, 12] 26 | -------------------------------------------------------------------------------- /examples/donut-colors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 |

Donut Chart

17 |
18 |
19 | Morris.Donut({
20 |   element: 'graph',
21 |   data: [
22 |     {value: 70, label: 'foo'},
23 |     {value: 15, label: 'bar'},
24 |     {value: 10, label: 'baz'},
25 |     {value: 5, label: 'A really really long label'}
26 |   ],
27 |   backgroundColor: '#ccc',
28 |   labelColor: '#060',
29 |   colors: [
30 |     '#0BA462',
31 |     '#39B580',
32 |     '#67C69D',
33 |     '#95D7BB'
34 |   ],
35 |   formatter: function (x) { return x + "%"}
36 | });
37 | 
38 | 39 | -------------------------------------------------------------------------------- /examples/negative.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Negative values

14 |
15 |
16 | var neg_data = [
17 |   {"period": "2011-08-12", "a": 100},
18 |   {"period": "2011-03-03", "a": 75},
19 |   {"period": "2010-08-08", "a": 50},
20 |   {"period": "2010-05-10", "a": 25},
21 |   {"period": "2010-03-14", "a": 0},
22 |   {"period": "2010-01-10", "a": -25},
23 |   {"period": "2009-12-10", "a": -50},
24 |   {"period": "2009-10-07", "a": -75},
25 |   {"period": "2009-09-25", "a": -100}
26 | ];
27 | Morris.Line({
28 |   element: 'graph',
29 |   data: neg_data,
30 |   xkey: 'period',
31 |   ykeys: ['a'],
32 |   labels: ['Series A'],
33 |   units: '%'
34 | });
35 | 
36 | 37 | -------------------------------------------------------------------------------- /examples/decimal-custom-hover.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Decimal Data

14 |
15 |
16 | var decimal_data = [];
17 | for (var x = 0; x <= 360; x += 10) {
18 |   decimal_data.push({
19 |     x: x,
20 |     y: 1.5 + 1.5 * Math.sin(Math.PI * x / 180).toFixed(4)
21 |   });
22 | }
23 | window.m = Morris.Line({
24 |   element: 'graph',
25 |   data: decimal_data,
26 |   xkey: 'x',
27 |   ykeys: ['y'],
28 |   labels: ['sin(x)'],
29 |   parseTime: false,
30 |   hoverCallback: function (index, options, default_content, row) {
31 |     return default_content.replace("sin(x)", "1.5 + 1.5 sin(" + row.x + ")");
32 |   },
33 |   xLabelMargin: 10,
34 |   integerYLabels: true
35 | });
36 | 
37 | 38 | -------------------------------------------------------------------------------- /examples/bar-highlight-hover.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 |

Title

18 |
19 |
20 | Morris.Bar({
21 |   element: 'graph',
22 |   data: [
23 |     {x: 'One', y: 3, z: 2, a: 1, q: 2},
24 |     {x: 'Two', y: 2, z: null, a: 1, q: 2},
25 |     {x: 'Three', y: 0, z: 2, a: 1, q: 2},
26 |     {x: 'Four', y: 2, z: 4, a: 1, q: 2}
27 |   ],
28 |   xkey: 'x',
29 |   ykeys: ['y', 'z'],
30 |   labels: [],
31 |   barColors: ['#1fbba6', '#f8aa33', '#4da74d', '#afd8f8', '#edc240', '#cb4b4b', '#9440ed'],
32 |   barOpacity: 0.5,
33 |   resize: true,
34 |   gridTextColor: '#666',
35 |   grid: false
36 | 
37 | });
38 | 
39 | 40 | -------------------------------------------------------------------------------- /examples/non-date.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Formatting Non-date Arbitrary X-axis

14 |
15 |
16 | var day_data = [
17 |   {"elapsed": "I", "value": 34},
18 |   {"elapsed": "II", "value": 24},
19 |   {"elapsed": "III", "value": 3},
20 |   {"elapsed": "IV", "value": 12},
21 |   {"elapsed": "V", "value": 13},
22 |   {"elapsed": "VI", "value": 22},
23 |   {"elapsed": "VII", "value": 5},
24 |   {"elapsed": "VIII", "value": 26},
25 |   {"elapsed": "IX", "value": 12},
26 |   {"elapsed": "X", "value": 19}
27 | ];
28 | Morris.Line({
29 |   element: 'graph',
30 |   data: day_data,
31 |   xkey: 'elapsed',
32 |   ykeys: ['value'],
33 |   labels: ['value'],
34 |   parseTime: false
35 | });
36 | 
37 | 38 | -------------------------------------------------------------------------------- /examples/bar-colors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Bar charts

14 |
15 |
16 | // Use Morris.Bar
17 | Morris.Bar({
18 |   element: 'graph',
19 |   data: [
20 |     {x: '2011 Q1', y: 0},
21 |     {x: '2011 Q2', y: 1},
22 |     {x: '2011 Q3', y: 2},
23 |     {x: '2011 Q4', y: 3},
24 |     {x: '2012 Q1', y: 4},
25 |     {x: '2012 Q2', y: 5},
26 |     {x: '2012 Q3', y: 6},
27 |     {x: '2012 Q4', y: 7},
28 |     {x: '2013 Q1', y: 8}
29 |   ],
30 |   xkey: 'x',
31 |   ykeys: ['y'],
32 |   labels: ['Y'],
33 |   barColors: function (row, series, type) {
34 |     if (type === 'bar') {
35 |       var red = Math.ceil(255 * row.y / this.ymax);
36 |       return 'rgb(' + red + ',0,0)';
37 |     }
38 |     else {
39 |       return '#000';
40 |     }
41 |   }
42 | });
43 | 
44 | 45 | -------------------------------------------------------------------------------- /lib/morris.hover.coffee: -------------------------------------------------------------------------------- 1 | class Morris.Hover 2 | # Displays contextual information in a floating HTML div. 3 | 4 | @defaults: 5 | class: 'morris-hover morris-default-style' 6 | 7 | constructor: (options = {}) -> 8 | @options = $.extend {}, Morris.Hover.defaults, options 9 | @el = $ "
" 10 | @el.hide() 11 | @options.parent.append(@el) 12 | 13 | update: (html, x, y, centre_y) -> 14 | if not html 15 | @hide() 16 | else 17 | @html(html) 18 | @show() 19 | @moveTo(x, y, centre_y) 20 | 21 | html: (content) -> 22 | @el.html(content) 23 | 24 | moveTo: (x, y, centre_y) -> 25 | parentWidth = @options.parent.innerWidth() 26 | parentHeight = @options.parent.innerHeight() 27 | hoverWidth = @el.outerWidth() 28 | hoverHeight = @el.outerHeight() 29 | left = Math.min(Math.max(0, x - hoverWidth / 2), parentWidth - hoverWidth) 30 | if y? 31 | if centre_y is true 32 | top = y - hoverHeight / 2 33 | if top < 0 34 | top = 0 35 | else 36 | top = y - hoverHeight - 10 37 | if top < 0 38 | top = y + 10 39 | if top + hoverHeight > parentHeight 40 | top = parentHeight / 2 - hoverHeight / 2 41 | else 42 | top = parentHeight / 2 - hoverHeight / 2 43 | @el.css(left: left + "px", top: parseInt(top) + "px") 44 | 45 | show: -> 46 | @el.show() 47 | 48 | hide: -> 49 | @el.hide() 50 | -------------------------------------------------------------------------------- /spec/lib/commas_spec.coffee: -------------------------------------------------------------------------------- 1 | describe '#commas', -> 2 | 3 | it 'should insert commas into long numbers', -> 4 | # zero 5 | Morris.commas(0).should.equal("0") 6 | 7 | # positive integers 8 | Morris.commas(1).should.equal("1") 9 | Morris.commas(12).should.equal("12") 10 | Morris.commas(123).should.equal("123") 11 | Morris.commas(1234).should.equal("1,234") 12 | Morris.commas(12345).should.equal("12,345") 13 | Morris.commas(123456).should.equal("123,456") 14 | Morris.commas(1234567).should.equal("1,234,567") 15 | 16 | # negative integers 17 | Morris.commas(-1).should.equal("-1") 18 | Morris.commas(-12).should.equal("-12") 19 | Morris.commas(-123).should.equal("-123") 20 | Morris.commas(-1234).should.equal("-1,234") 21 | Morris.commas(-12345).should.equal("-12,345") 22 | Morris.commas(-123456).should.equal("-123,456") 23 | Morris.commas(-1234567).should.equal("-1,234,567") 24 | 25 | # positive decimals 26 | Morris.commas(1.2).should.equal("1.2") 27 | Morris.commas(12.34).should.equal("12.34") 28 | Morris.commas(123.456).should.equal("123.456") 29 | Morris.commas(1234.56).should.equal("1,234.56") 30 | 31 | # negative decimals 32 | Morris.commas(-1.2).should.equal("-1.2") 33 | Morris.commas(-12.34).should.equal("-12.34") 34 | Morris.commas(-123.456).should.equal("-123.456") 35 | Morris.commas(-1234.56).should.equal("-1,234.56") 36 | 37 | # null 38 | Morris.commas(null).should.equal('-') 39 | -------------------------------------------------------------------------------- /spec/lib/bar/colours.coffee: -------------------------------------------------------------------------------- 1 | describe 'Morris.Bar#colorFor', -> 2 | 3 | defaults = 4 | element: 'graph' 5 | data: [{x: 'foo', y: 2, z: 3}, {x: 'bar', y: 4, z: 6}] 6 | xkey: 'x' 7 | ykeys: ['y', 'z'] 8 | labels: ['Y', 'Z'] 9 | 10 | it 'should fetch colours from an array', -> 11 | chart = Morris.Bar $.extend {}, defaults, barColors: ['#f00', '#0f0', '#00f'] 12 | chart.colorFor(chart.data[0], 0, 'bar').should.equal '#f00' 13 | chart.colorFor(chart.data[0], 0, 'hover').should.equal '#f00' 14 | chart.colorFor(chart.data[0], 1, 'bar').should.equal '#0f0' 15 | chart.colorFor(chart.data[0], 1, 'hover').should.equal '#0f0' 16 | chart.colorFor(chart.data[0], 2, 'bar').should.equal '#00f' 17 | chart.colorFor(chart.data[0], 2, 'hover').should.equal '#00f' 18 | chart.colorFor(chart.data[0], 3, 'bar').should.equal '#f00' 19 | chart.colorFor(chart.data[0], 4, 'hover').should.equal '#0f0' 20 | 21 | it 'should defer to a callback', -> 22 | stub = sinon.stub().returns '#f00' 23 | chart = Morris.Bar $.extend {}, defaults, barColors: stub 24 | stub.reset() 25 | 26 | chart.colorFor(chart.data[0], 0, 'bar') 27 | stub.should.have.been.calledWith( 28 | {x:0, y:2, label:'foo', src: { x: "foo", y: 2, z: 3 }}, 29 | {index:0, key:'y', label:'Y'}, 30 | 'bar') 31 | 32 | chart.colorFor(chart.data[0], 1, 'hover') 33 | stub.should.have.been.calledWith( 34 | {x:0, y:3, label:'foo', src: { x: "foo", y: 2, z: 3 }}, 35 | {index:1, key:'z', label:'Z'}, 36 | 'hover') 37 | -------------------------------------------------------------------------------- /spec/specs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | morris.js tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 |
28 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/updating.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Updating data

14 |
15 |
16 |
17 | 
18 | var nReloads = 0;
19 | function data(offset) {
20 |   var ret = [];
21 |   for (var x = 0; x <= 360; x += 10) {
22 |     var v = (offset + x) % 360;
23 |     ret.push({
24 |       x: x,
25 |       y: Math.sin(Math.PI * v / 180).toFixed(4),
26 |       z: Math.cos(Math.PI * v / 180).toFixed(4)
27 |     });
28 |   }
29 |   return ret;
30 | }
31 | var graph = Morris.Line({
32 |     element: 'graph',
33 |     data: data(0),
34 |     xkey: 'x',
35 |     ykeys: ['y', 'z'],
36 |     labels: ['sin()', 'cos()'],
37 |     parseTime: false,
38 |     ymin: -1.0,
39 |     ymax: 1.0,
40 |     hideHover: true
41 | });
42 | function update() {
43 |   nReloads++;
44 |   graph.setData(data(5 * nReloads));
45 |   $('#reloadStatus').text(nReloads + ' reloads');
46 | }
47 | setInterval(update, 100);
48 | 
49 | 50 | -------------------------------------------------------------------------------- /spec/viz/examples.js: -------------------------------------------------------------------------------- 1 | var webpage = require("webpage"), 2 | fs = require("fs"); 3 | 4 | var html_path = fs.absolute("test.html"); 5 | var examples = []; 6 | 7 | function run_example(example_index) { 8 | if (example_index >= examples.length) { 9 | phantom.exit(0); 10 | return; 11 | } 12 | 13 | var example = examples[example_index]; 14 | var snapshot_index = 0; 15 | var page = webpage.create(); 16 | 17 | page.viewportSize = { width: 500, height: 300 }; 18 | page.clipRect = { width: 500, height: 300 }; 19 | page.onAlert = function (msg) { 20 | var e = JSON.parse(msg); 21 | if (e.fn == "snapshot") { 22 | page.render("output/" + example.name + snapshot_index + ".png"); 23 | snapshot_index += 1; 24 | } else if (e.fn == "mousemove") { 25 | page.sendEvent("mousemove", e.x, e.y); 26 | } 27 | }; 28 | 29 | page.open(html_path, function (status) { 30 | if (status == "fail") { 31 | console.log("Failed to load test page: " + example.name); 32 | phantom.exit(1); 33 | } else { 34 | page.evaluate(example.runner); 35 | } 36 | page.close(); 37 | run_example(example_index + 1); 38 | }); 39 | } 40 | 41 | exports.def = function (name, runner) { 42 | examples.push({ name: name, runner: runner }); 43 | }; 44 | 45 | exports.run = function () { 46 | if (fs.isDirectory("output")) { 47 | fs.list("output").forEach(function (path) { 48 | if (path != "." && path != "..") { 49 | fs.remove("output/" + path); 50 | } 51 | }); 52 | } else { 53 | fs.makeDirectory("output"); 54 | } 55 | run_example(0); 56 | }; 57 | -------------------------------------------------------------------------------- /examples/years.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Formatting Dates YYYY

14 |
15 |
16 | /* data stolen from http://howmanyleft.co.uk/vehicle/jaguar_'e'_type */
17 | var year_data = [
18 |   {"period": "2012", "licensed": 3407, "sorned": 660},
19 |   {"period": "2011", "licensed": 3351, "sorned": 629},
20 |   {"period": "2010", "licensed": 3269, "sorned": 618},
21 |   {"period": "2009", "licensed": 3246, "sorned": 661},
22 |   {"period": "2008", "licensed": 3257, "sorned": 667},
23 |   {"period": "2007", "licensed": 3248, "sorned": 627},
24 |   {"period": "2006", "licensed": 3171, "sorned": 660},
25 |   {"period": "2005", "licensed": 3171, "sorned": 676},
26 |   {"period": "2004", "licensed": 3201, "sorned": 656},
27 |   {"period": "2003", "licensed": 3215, "sorned": 622}
28 | ];
29 | Morris.Line({
30 |   element: 'graph',
31 |   data: year_data,
32 |   xkey: 'period',
33 |   ykeys: ['licensed', 'sorned'],
34 |   labels: ['Licensed', 'SORN']
35 | });
36 | 
37 | 38 | -------------------------------------------------------------------------------- /examples/days.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Formatting Dates YYYY-MM-DD

14 |
15 |
16 | /* data stolen from http://howmanyleft.co.uk/vehicle/jaguar_'e'_type */
17 | var day_data = [
18 |   {"period": "2012-10-01", "licensed": 3407, "sorned": 660},
19 |   {"period": "2012-09-30", "licensed": 3351, "sorned": 629},
20 |   {"period": "2012-09-29", "licensed": 3269, "sorned": 618},
21 |   {"period": "2012-09-20", "licensed": 3246, "sorned": 661},
22 |   {"period": "2012-09-19", "licensed": 3257, "sorned": 667},
23 |   {"period": "2012-09-18", "licensed": 3248, "sorned": 627},
24 |   {"period": "2012-09-17", "licensed": 3171, "sorned": 660},
25 |   {"period": "2012-09-16", "licensed": 3171, "sorned": 676},
26 |   {"period": "2012-09-15", "licensed": 3201, "sorned": 656},
27 |   {"period": "2012-09-10", "licensed": 3215, "sorned": 622}
28 | ];
29 | Morris.Line({
30 |   element: 'graph',
31 |   data: day_data,
32 |   xkey: 'period',
33 |   ykeys: ['licensed', 'sorned'],
34 |   labels: ['Licensed', 'SORN']
35 | });
36 | 
37 | 38 | -------------------------------------------------------------------------------- /examples/months-no-smooth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Formatting Dates with YYYY-MM

14 |
15 |
16 | /* data stolen from http://howmanyleft.co.uk/vehicle/jaguar_'e'_type */
17 | var month_data = [
18 |   {"period": "2012-10", "licensed": 3407, "sorned": 660},
19 |   {"period": "2011-08", "licensed": 3351, "sorned": 629},
20 |   {"period": "2011-03", "licensed": 3269, "sorned": 618},
21 |   {"period": "2010-08", "licensed": 3246, "sorned": 661},
22 |   {"period": "2010-05", "licensed": 3257, "sorned": 667},
23 |   {"period": "2010-03", "licensed": 3248, "sorned": 627},
24 |   {"period": "2010-01", "licensed": 3171, "sorned": 660},
25 |   {"period": "2009-12", "licensed": 3171, "sorned": 676},
26 |   {"period": "2009-10", "licensed": 3201, "sorned": 656},
27 |   {"period": "2009-09", "licensed": 3215, "sorned": 622}
28 | ];
29 | Morris.Line({
30 |   element: 'graph',
31 |   data: month_data,
32 |   xkey: 'period',
33 |   ykeys: ['licensed', 'sorned'],
34 |   labels: ['Licensed', 'SORN'],
35 |   smooth: false
36 | });
37 | 
38 | 39 | -------------------------------------------------------------------------------- /examples/no-grid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Formatting Dates YYYY-MM-DD

14 |
15 |
16 | /* data stolen from http://howmanyleft.co.uk/vehicle/jaguar_'e'_type */
17 | var day_data = [
18 |   {"period": "2012-10-01", "licensed": 3407, "sorned": 660},
19 |   {"period": "2012-09-30", "licensed": 3351, "sorned": 629},
20 |   {"period": "2012-09-29", "licensed": 3269, "sorned": 618},
21 |   {"period": "2012-09-20", "licensed": 3246, "sorned": 661},
22 |   {"period": "2012-09-19", "licensed": 3257, "sorned": 667},
23 |   {"period": "2012-09-18", "licensed": 3248, "sorned": 627},
24 |   {"period": "2012-09-17", "licensed": 3171, "sorned": 660},
25 |   {"period": "2012-09-16", "licensed": 3171, "sorned": 676},
26 |   {"period": "2012-09-15", "licensed": 3201, "sorned": 656},
27 |   {"period": "2012-09-10", "licensed": 3215, "sorned": 622}
28 | ];
29 | Morris.Line({
30 |   element: 'graph',
31 |   grid: false,
32 |   data: day_data,
33 |   xkey: 'period',
34 |   ykeys: ['licensed', 'sorned'],
35 |   labels: ['Licensed', 'SORN']
36 | });
37 | 
38 | 39 | -------------------------------------------------------------------------------- /examples/diagonal-xlabels.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Displaying X Labels Diagonally

14 |
15 |
16 | /* data stolen from http://howmanyleft.co.uk/vehicle/jaguar_'e'_type */
17 | var day_data = [
18 |   {"period": "2012-10-30", "licensed": 3407, "sorned": 660},
19 |   {"period": "2012-09-30", "licensed": 3351, "sorned": 629},
20 |   {"period": "2012-09-29", "licensed": 3269, "sorned": 618},
21 |   {"period": "2012-09-20", "licensed": 3246, "sorned": 661},
22 |   {"period": "2012-09-19", "licensed": 3257, "sorned": 667},
23 |   {"period": "2012-09-18", "licensed": 3248, "sorned": 627},
24 |   {"period": "2012-09-17", "licensed": 3171, "sorned": 660},
25 |   {"period": "2012-09-16", "licensed": 3171, "sorned": 676},
26 |   {"period": "2012-09-15", "licensed": 3201, "sorned": 656},
27 |   {"period": "2012-09-10", "licensed": 3215, "sorned": 622}
28 | ];
29 | Morris.Line({
30 |   element: 'graph',
31 |   data: day_data,
32 |   xkey: 'period',
33 |   ykeys: ['licensed', 'sorned'],
34 |   labels: ['Licensed', 'SORN'],
35 |   xLabelAngle: 60
36 | });
37 | 
38 | 39 | -------------------------------------------------------------------------------- /examples/diagonal-xlabels-bar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Displaying X Labels Diagonally (Bar Chart)

14 |
15 |
16 | /* data stolen from http://howmanyleft.co.uk/vehicle/jaguar_'e'_type */
17 | var day_data = [
18 |   {"period": "2012-10-01", "licensed": 3407, "sorned": 660},
19 |   {"period": "2012-09-30", "licensed": 3351, "sorned": 629},
20 |   {"period": "2012-09-29", "licensed": 3269, "sorned": 618},
21 |   {"period": "2012-09-20", "licensed": 3246, "sorned": 661},
22 |   {"period": "2012-09-19", "licensed": 3257, "sorned": 667},
23 |   {"period": "2012-09-18", "licensed": 3248, "sorned": 627},
24 |   {"period": "2012-09-17", "licensed": 3171, "sorned": 660},
25 |   {"period": "2012-09-16", "licensed": 3171, "sorned": 676},
26 |   {"period": "2012-09-15", "licensed": 3201, "sorned": 656},
27 |   {"period": "2012-09-10", "licensed": 3215, "sorned": 622}
28 | ];
29 | Morris.Bar({
30 |   element: 'graph',
31 |   data: day_data,
32 |   xkey: 'period',
33 |   ykeys: ['licensed', 'sorned'],
34 |   labels: ['Licensed', 'SORN'],
35 |   xLabelAngle: 60
36 | });
37 | 
38 | 39 | -------------------------------------------------------------------------------- /spec/viz/visual_specs.js: -------------------------------------------------------------------------------- 1 | var examples = require('./examples'); 2 | 3 | examples.def('line', function () { 4 | Morris.Line({ 5 | element: 'chart', 6 | data: [ 7 | { x: 0, y: 10, z: 30 }, { x: 1, y: 20, z: 20 }, 8 | { x: 2, y: 30, z: 10 }, { x: 3, y: 30, z: 10 }, 9 | { x: 4, y: 20, z: 20 }, { x: 5, y: 10, z: 30 } 10 | ], 11 | xkey: 'x', 12 | ykeys: ['y', 'z'], 13 | labels: ['y', 'z'], 14 | parseTime: false 15 | }); 16 | window.snapshot(); 17 | }); 18 | 19 | examples.def('area', function () { 20 | Morris.Area({ 21 | element: 'chart', 22 | data: [ 23 | { x: 0, y: 1, z: 1 }, { x: 1, y: 2, z: 1 }, 24 | { x: 2, y: 3, z: 1 }, { x: 3, y: 3, z: 1 }, 25 | { x: 4, y: 2, z: 1 }, { x: 5, y: 1, z: 1 } 26 | ], 27 | xkey: 'x', 28 | ykeys: ['y', 'z'], 29 | labels: ['y', 'z'], 30 | parseTime: false 31 | }); 32 | window.snapshot(); 33 | }); 34 | 35 | examples.def('bar', function () { 36 | Morris.Bar({ 37 | element: 'chart', 38 | data: [ 39 | { x: 0, y: 1, z: 3 }, { x: 1, y: 2, z: 2 }, 40 | { x: 2, y: 3, z: 1 }, { x: 3, y: 3, z: 1 }, 41 | { x: 4, y: 2, z: 2 }, { x: 5, y: 1, z: 3 } 42 | ], 43 | xkey: 'x', 44 | ykeys: ['y', 'z'], 45 | labels: ['y', 'z'] 46 | }); 47 | window.snapshot(); 48 | }); 49 | 50 | examples.def('stacked_bar', function () { 51 | Morris.Bar({ 52 | element: 'chart', 53 | data: [ 54 | { x: 0, y: 1, z: 1 }, { x: 1, y: 2, z: 1 }, 55 | { x: 2, y: 3, z: 1 }, { x: 3, y: 3, z: 1 }, 56 | { x: 4, y: 2, z: 1 }, { x: 5, y: 1, z: 1 } 57 | ], 58 | xkey: 'x', 59 | ykeys: ['y', 'z'], 60 | labels: ['y', 'z'], 61 | stacked: true 62 | }); 63 | window.snapshot(); 64 | }); 65 | 66 | examples.run(); 67 | -------------------------------------------------------------------------------- /examples/timestamps.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Timestamps

14 |
15 |
16 | /* data stolen from http://howmanyleft.co.uk/vehicle/jaguar_'e'_type */
17 | var timestamp_data = [
18 |   {"period": 1349046000000, "licensed": 3407, "sorned": 660},
19 |   {"period": 1313103600000, "licensed": 3351, "sorned": 629},
20 |   {"period": 1299110400000, "licensed": 3269, "sorned": 618},
21 |   {"period": 1281222000000, "licensed": 3246, "sorned": 661},
22 |   {"period": 1273446000000, "licensed": 3257, "sorned": 667},
23 |   {"period": 1268524800000, "licensed": 3248, "sorned": 627},
24 |   {"period": 1263081600000, "licensed": 3171, "sorned": 660},
25 |   {"period": 1260403200000, "licensed": 3171, "sorned": 676},
26 |   {"period": 1254870000000, "licensed": 3201, "sorned": 656},
27 |   {"period": 1253833200000, "licensed": 3215, "sorned": 622}
28 | ];
29 | Morris.Line({
30 |   element: 'graph',
31 |   data: timestamp_data,
32 |   xkey: 'period',
33 |   ykeys: ['licensed', 'sorned'],
34 |   labels: ['Licensed', 'SORN'],
35 |   dateFormat: function (x) { return new Date(x).toDateString(); }
36 | });
37 | 
38 | 39 | -------------------------------------------------------------------------------- /examples/resize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 |

Formatting Dates YYYY-MM-DD

18 |
19 |
20 | /* data stolen from http://howmanyleft.co.uk/vehicle/jaguar_'e'_type */
21 | var day_data = [
22 |   {"period": "2012-10-01", "licensed": 3407, "sorned": 660},
23 |   {"period": "2012-09-30", "licensed": 3351, "sorned": 629},
24 |   {"period": "2012-09-29", "licensed": 3269, "sorned": 618},
25 |   {"period": "2012-09-20", "licensed": 3246, "sorned": 661},
26 |   {"period": "2012-09-19", "licensed": 3257, "sorned": 667},
27 |   {"period": "2012-09-18", "licensed": 3248, "sorned": 627},
28 |   {"period": "2012-09-17", "licensed": 3171, "sorned": 660},
29 |   {"period": "2012-09-16", "licensed": 3171, "sorned": 676},
30 |   {"period": "2012-09-15", "licensed": 3201, "sorned": 656},
31 |   {"period": "2012-09-10", "licensed": 3215, "sorned": 622}
32 | ];
33 | Morris.Line({
34 |   element: 'graph',
35 |   data: day_data,
36 |   xkey: 'period',
37 |   ykeys: ['licensed', 'sorned'],
38 |   labels: ['Licensed', 'SORN'],
39 |   resize: true
40 | });
41 | 
42 | 43 | -------------------------------------------------------------------------------- /examples/non-continuous.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Non-continuous data

14 |

Null series values will break the line when rendering, missing values will be skipped

15 |
16 |
17 | /* data stolen from http://howmanyleft.co.uk/vehicle/jaguar_'e'_type */
18 | var day_data = [
19 |   {"period": "2012-10-01", "licensed": 3407},
20 |   {"period": "2012-09-30", "sorned": 0},
21 |   {"period": "2012-09-29", "sorned": 618},
22 |   {"period": "2012-09-20", "licensed": 3246, "sorned": 661},
23 |   {"period": "2012-09-19", "licensed": 3257, "sorned": null},
24 |   {"period": "2012-09-18", "licensed": 3248, "other": 1000},
25 |   {"period": "2012-09-17", "sorned": 0},
26 |   {"period": "2012-09-16", "sorned": 0},
27 |   {"period": "2012-09-15", "licensed": 3201, "sorned": 656},
28 |   {"period": "2012-09-10", "licensed": 3215}
29 | ];
30 | Morris.Line({
31 |   element: 'graph',
32 |   data: day_data,
33 |   xkey: 'period',
34 |   ykeys: ['licensed', 'sorned', 'other'],
35 |   labels: ['Licensed', 'SORN', 'Other'],
36 |   /* custom label formatting with `xLabelFormat` */
37 |   xLabelFormat: function(d) { return (d.getMonth()+1)+'/'+d.getDate()+'/'+d.getFullYear(); },
38 |   /* setting `xLabels` is recommended when using xLabelFormat */
39 |   xLabels: 'day'
40 | });
41 | 
42 | 43 | -------------------------------------------------------------------------------- /lib/morris.area.coffee: -------------------------------------------------------------------------------- 1 | class Morris.Area extends Morris.Line 2 | # Initialise 3 | # 4 | areaDefaults = 5 | fillOpacity: 'auto' 6 | behaveLikeLine: false 7 | 8 | constructor: (options) -> 9 | return new Morris.Area(options) unless (@ instanceof Morris.Area) 10 | areaOptions = $.extend {}, areaDefaults, options 11 | 12 | @cumulative = not areaOptions.behaveLikeLine 13 | 14 | if areaOptions.fillOpacity is 'auto' 15 | areaOptions.fillOpacity = if areaOptions.behaveLikeLine then .8 else 1 16 | 17 | super(areaOptions) 18 | 19 | # calculate series data point coordinates 20 | # 21 | # @private 22 | calcPoints: -> 23 | for row in @data 24 | row._x = @transX(row.x) 25 | total = 0 26 | row._y = for y in row.y 27 | if @options.behaveLikeLine 28 | @transY(y) 29 | else 30 | total += (y || 0) 31 | @transY(total) 32 | row._ymax = Math.max row._y... 33 | 34 | # draw the data series 35 | # 36 | # @private 37 | drawSeries: -> 38 | @seriesPoints = [] 39 | if @options.behaveLikeLine 40 | range = [0..@options.ykeys.length-1] 41 | else 42 | range = [@options.ykeys.length-1..0] 43 | 44 | for i in range 45 | @_drawFillFor i 46 | @_drawLineFor i 47 | @_drawPointFor i 48 | 49 | _drawFillFor: (index) -> 50 | path = @paths[index] 51 | if path isnt null 52 | path = path + "L#{@transX(@xmax)},#{@bottom}L#{@transX(@xmin)},#{@bottom}Z" 53 | @drawFilledPath path, @fillForSeries(index) 54 | 55 | fillForSeries: (i) -> 56 | color = Raphael.rgb2hsl @colorFor(@data[i], i, 'line') 57 | Raphael.hsl( 58 | color.h, 59 | if @options.behaveLikeLine then color.s * 0.9 else color.s * 0.75, 60 | Math.min(0.98, if @options.behaveLikeLine then color.l * 1.2 else color.l * 1.25)) 61 | 62 | drawFilledPath: (path, fill) -> 63 | @raphael.path(path) 64 | .attr('fill', fill) 65 | .attr('fill-opacity', @options.fillOpacity) 66 | .attr('stroke', 'none') 67 | -------------------------------------------------------------------------------- /spec/lib/parse_time_spec.coffee: -------------------------------------------------------------------------------- 1 | describe '#parseTime', -> 2 | 3 | it 'should parse years', -> 4 | Morris.parseDate('2012').should.equal(new Date(2012, 0, 1).getTime()) 5 | 6 | it 'should parse quarters', -> 7 | Morris.parseDate('2012 Q1').should.equal(new Date(2012, 2, 1).getTime()) 8 | 9 | it 'should parse months', -> 10 | Morris.parseDate('2012-09').should.equal(new Date(2012, 8, 1).getTime()) 11 | Morris.parseDate('2012-10').should.equal(new Date(2012, 9, 1).getTime()) 12 | 13 | it 'should parse dates', -> 14 | Morris.parseDate('2012-09-15').should.equal(new Date(2012, 8, 15).getTime()) 15 | Morris.parseDate('2012-10-15').should.equal(new Date(2012, 9, 15).getTime()) 16 | 17 | it 'should parse times', -> 18 | Morris.parseDate("2012-10-15 12:34").should.equal(new Date(2012, 9, 15, 12, 34).getTime()) 19 | Morris.parseDate("2012-10-15T12:34").should.equal(new Date(2012, 9, 15, 12, 34).getTime()) 20 | Morris.parseDate("2012-10-15 12:34:55").should.equal(new Date(2012, 9, 15, 12, 34, 55).getTime()) 21 | Morris.parseDate("2012-10-15T12:34:55").should.equal(new Date(2012, 9, 15, 12, 34, 55).getTime()) 22 | 23 | it 'should parse times with timezones', -> 24 | Morris.parseDate("2012-10-15T12:34+0100").should.equal(Date.UTC(2012, 9, 15, 11, 34)) 25 | Morris.parseDate("2012-10-15T12:34+02:00").should.equal(Date.UTC(2012, 9, 15, 10, 34)) 26 | Morris.parseDate("2012-10-15T12:34-0100").should.equal(Date.UTC(2012, 9, 15, 13, 34)) 27 | Morris.parseDate("2012-10-15T12:34-02:00").should.equal(Date.UTC(2012, 9, 15, 14, 34)) 28 | Morris.parseDate("2012-10-15T12:34:55Z").should.equal(Date.UTC(2012, 9, 15, 12, 34, 55)) 29 | Morris.parseDate("2012-10-15T12:34:55+0600").should.equal(Date.UTC(2012, 9, 15, 6, 34, 55)) 30 | Morris.parseDate("2012-10-15T12:34:55+04:00").should.equal(Date.UTC(2012, 9, 15, 8, 34, 55)) 31 | Morris.parseDate("2012-10-15T12:34:55-0600").should.equal(Date.UTC(2012, 9, 15, 18, 34, 55)) 32 | 33 | it 'should pass-through timestamps', -> 34 | Morris.parseDate(new Date(2012, 9, 15, 12, 34, 55, 123).getTime()) 35 | .should.equal(new Date(2012, 9, 15, 12, 34, 55, 123).getTime()) -------------------------------------------------------------------------------- /spec/lib/area/area_spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'Morris.Area', -> 2 | 3 | describe 'svg structure', -> 4 | defaults = 5 | element: 'graph' 6 | data: [{x: '2012 Q1', y: 1}, {x: '2012 Q2', y: 1}] 7 | lineColors: [ '#0b62a4', '#7a92a3'] 8 | gridLineColor: '#aaa' 9 | xkey: 'x' 10 | ykeys: ['y'] 11 | labels: ['Y'] 12 | 13 | it 'should contain a line path for each line', -> 14 | chart = Morris.Area $.extend {}, defaults 15 | $('#graph').find("path[stroke='#0b62a4']").size().should.equal 1 16 | 17 | it 'should contain a path with stroke-width 0 for each line', -> 18 | chart = Morris.Area $.extend {}, defaults 19 | $('#graph').find("path[stroke='#0b62a4']").size().should.equal 1 20 | 21 | it 'should contain 5 grid lines', -> 22 | chart = Morris.Area $.extend {}, defaults 23 | $('#graph').find("path[stroke='#aaaaaa']").size().should.equal 5 24 | 25 | it 'should contain 9 text elements', -> 26 | chart = Morris.Area $.extend {}, defaults 27 | $('#graph').find("text").size().should.equal 9 28 | 29 | describe 'svg attributes', -> 30 | defaults = 31 | element: 'graph' 32 | data: [{x: '2012 Q1', y: 1}, {x: '2012 Q2', y: 1}] 33 | xkey: 'x' 34 | ykeys: ['y'] 35 | labels: ['Y'] 36 | lineColors: [ '#0b62a4', '#7a92a3'] 37 | lineWidth: 3 38 | pointWidths: [5] 39 | pointStrokeColors: ['#ffffff'] 40 | gridLineColor: '#aaa' 41 | gridStrokeWidth: 0.5 42 | gridTextColor: '#888' 43 | gridTextSize: 12 44 | 45 | it 'should not be cumulative if behaveLikeLine', -> 46 | chart = Morris.Area $.extend {}, defaults, behaveLikeLine: true 47 | chart.cumulative.should.equal false 48 | 49 | it 'should have a line with transparent fill if behaveLikeLine', -> 50 | chart = Morris.Area $.extend {}, defaults, behaveLikeLine: true 51 | $('#graph').find("path[fill-opacity='0.8']").size().should.equal 1 52 | 53 | it 'should not have a line with transparent fill', -> 54 | chart = Morris.Area $.extend {}, defaults 55 | $('#graph').find("path[fill-opacity='0.8']").size().should.equal 0 56 | 57 | it 'should have a line with the fill of a modified line color', -> 58 | chart = Morris.Area $.extend {}, defaults 59 | $('#graph').find("path[fill='#0b62a4']").size().should.equal 0 60 | $('#graph').find("path[fill='#7a92a3']").size().should.equal 0 61 | -------------------------------------------------------------------------------- /spec/lib/hover_spec.coffee: -------------------------------------------------------------------------------- 1 | describe "Morris.Hover", -> 2 | 3 | describe "with dummy content", -> 4 | 5 | beforeEach -> 6 | parent = $('
') 7 | .appendTo($('#test')) 8 | @hover = new Morris.Hover(parent: parent) 9 | @element = $('#test .morris-hover') 10 | 11 | it "should initialise a hidden, empty popup", -> 12 | @element.should.exist 13 | @element.should.be.hidden 14 | @element.should.be.empty 15 | 16 | describe "#show", -> 17 | it "should show the popup", -> 18 | @hover.show() 19 | @element.should.be.visible 20 | 21 | describe "#hide", -> 22 | it "should hide the popup", -> 23 | @hover.show() 24 | @hover.hide() 25 | @element.should.be.hidden 26 | 27 | describe "#html", -> 28 | it "should replace the contents of the element", -> 29 | @hover.html('
Foobarbaz
') 30 | @element.should.have.html('
Foobarbaz
') 31 | 32 | describe "#moveTo", -> 33 | beforeEach -> 34 | @hover.html('
') 35 | 36 | it "should place the popup directly above the given point", -> 37 | @hover.moveTo(100, 150) 38 | @element.should.have.css('left', '50px') 39 | @element.should.have.css('top', '40px') 40 | 41 | it "should place the popup below the given point if it does not fit above", -> 42 | @hover.moveTo(100, 50) 43 | @element.should.have.css('left', '50px') 44 | @element.should.have.css('top', '60px') 45 | 46 | it "should center the popup vertically if it will not fit above or below", -> 47 | @hover.moveTo(100, 100) 48 | @element.should.have.css('left', '50px') 49 | @element.should.have.css('top', '40px') 50 | 51 | it "should center the popup vertically if no y value is supplied", -> 52 | @hover.moveTo(100) 53 | @element.should.have.css('left', '50px') 54 | @element.should.have.css('top', '40px') 55 | 56 | describe "#update", -> 57 | it "should update content, show and reposition the popup", -> 58 | hover = new Morris.Hover(parent: $('#test')) 59 | html = "
Hello, Everyone!
" 60 | hover.update(html, 150, 200) 61 | el = $('#test .morris-hover') 62 | el.should.have.css('left', '100px') 63 | el.should.have.css('top', '90px') 64 | el.should.have.text('Hello, Everyone!') 65 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 3 | 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | coffee: { 7 | lib: { 8 | options: { bare: false }, 9 | files: { 10 | 'morris.js': ['build/morris.coffee'] 11 | } 12 | }, 13 | spec: { 14 | options: { bare: true }, 15 | files: { 16 | 'build/spec.js': ['build/spec.coffee'] 17 | } 18 | }, 19 | }, 20 | concat: { 21 | 'build/morris.coffee': { 22 | options: { 23 | banner: "### @license\n"+ 24 | "<%= pkg.name %> v<%= pkg.version %>\n"+ 25 | "Copyright <%= (new Date()).getFullYear() %> <%= pkg.author.name %> All rights reserved.\n" + 26 | "Licensed under the <%= pkg.license %> License.\n" + 27 | "###\n", 28 | }, 29 | src: [ 30 | 'lib/morris.coffee', 31 | 'lib/morris.grid.coffee', 32 | 'lib/morris.hover.coffee', 33 | 'lib/morris.line.coffee', 34 | 'lib/morris.area.coffee', 35 | 'lib/morris.bar.coffee', 36 | 'lib/morris.donut.coffee' 37 | ], 38 | dest: 'build/morris.coffee' 39 | }, 40 | 'build/spec.coffee': ['spec/support/**/*.coffee', 'spec/lib/**/*.coffee'] 41 | }, 42 | less: { 43 | all: { 44 | src: 'less/*.less', 45 | dest: 'morris.css', 46 | options: { 47 | compress: true 48 | } 49 | } 50 | }, 51 | uglify: { 52 | build: { 53 | options: { 54 | preserveComments: 'some' 55 | }, 56 | files: { 57 | 'morris.min.js': 'morris.js' 58 | } 59 | } 60 | }, 61 | mocha: { 62 | index: ['spec/specs.html'], 63 | options: {run: true} 64 | }, 65 | watch: { 66 | all: { 67 | files: ['lib/**/*.coffee', 'spec/lib/**/*.coffee', 'spec/support/**/*.coffee', 'less/**/*.less'], 68 | tasks: 'default' 69 | }, 70 | dev: { 71 | files: 'lib/*.coffee' , 72 | tasks: ['concat:build/morris.coffee', 'coffee:lib'] 73 | } 74 | }, 75 | shell: { 76 | visual_spec: { 77 | command: './run.sh', 78 | options: { 79 | stdout: true, 80 | failOnError: true, 81 | execOptions: { 82 | cwd: 'spec/viz' 83 | } 84 | } 85 | } 86 | } 87 | }); 88 | 89 | grunt.registerTask('default', ['concat', 'coffee', 'less', 'uglify', 'mocha', 'shell:visual_spec']); 90 | }; 91 | -------------------------------------------------------------------------------- /examples/weeks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Formatting Dates With Weeks

14 |
15 |
16 | var week_data = [
17 |   {"period": "2011 W27", "licensed": 3407, "sorned": 660},
18 |   {"period": "2011 W26", "licensed": 3351, "sorned": 629},
19 |   {"period": "2011 W25", "licensed": 3269, "sorned": 618},
20 |   {"period": "2011 W24", "licensed": 3246, "sorned": 661},
21 |   {"period": "2011 W23", "licensed": 3257, "sorned": 667},
22 |   {"period": "2011 W22", "licensed": 3248, "sorned": 627},
23 |   {"period": "2011 W21", "licensed": 3171, "sorned": 660},
24 |   {"period": "2011 W20", "licensed": 3171, "sorned": 676},
25 |   {"period": "2011 W19", "licensed": 3201, "sorned": 656},
26 |   {"period": "2011 W18", "licensed": 3215, "sorned": 622},
27 |   {"period": "2011 W17", "licensed": 3148, "sorned": 632},
28 |   {"period": "2011 W16", "licensed": 3155, "sorned": 681},
29 |   {"period": "2011 W15", "licensed": 3190, "sorned": 667},
30 |   {"period": "2011 W14", "licensed": 3226, "sorned": 620},
31 |   {"period": "2011 W13", "licensed": 3245, "sorned": null},
32 |   {"period": "2011 W12", "licensed": 3289, "sorned": null},
33 |   {"period": "2011 W11", "licensed": 3263, "sorned": null},
34 |   {"period": "2011 W10", "licensed": 3189, "sorned": null},
35 |   {"period": "2011 W09", "licensed": 3079, "sorned": null},
36 |   {"period": "2011 W08", "licensed": 3085, "sorned": null},
37 |   {"period": "2011 W07", "licensed": 3055, "sorned": null},
38 |   {"period": "2011 W06", "licensed": 3063, "sorned": null},
39 |   {"period": "2011 W05", "licensed": 2943, "sorned": null},
40 |   {"period": "2011 W04", "licensed": 2806, "sorned": null},
41 |   {"period": "2011 W03", "licensed": 2674, "sorned": null},
42 |   {"period": "2011 W02", "licensed": 1702, "sorned": null},
43 |   {"period": "2011 W01", "licensed": 1732, "sorned": null}
44 | ];
45 | Morris.Line({
46 |   element: 'graph',
47 |   data: week_data,
48 |   xkey: 'period',
49 |   ykeys: ['licensed', 'sorned'],
50 |   labels: ['Licensed', 'SORN']
51 | });
52 | 
53 | 54 | -------------------------------------------------------------------------------- /examples/quarters.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Formatting Dates with Quarters

14 |
15 |
16 | /* data stolen from http://howmanyleft.co.uk/vehicle/jaguar_e_type */
17 | var quarter_data = [
18 |   {"period": "2011 Q3", "licensed": 3407, "sorned": 660},
19 |   {"period": "2011 Q2", "licensed": 3351, "sorned": 629},
20 |   {"period": "2011 Q1", "licensed": 3269, "sorned": 618},
21 |   {"period": "2010 Q4", "licensed": 3246, "sorned": 661},
22 |   {"period": "2010 Q3", "licensed": 3257, "sorned": 667},
23 |   {"period": "2010 Q2", "licensed": 3248, "sorned": 627},
24 |   {"period": "2010 Q1", "licensed": 3171, "sorned": 660},
25 |   {"period": "2009 Q4", "licensed": 3171, "sorned": 676},
26 |   {"period": "2009 Q3", "licensed": 3201, "sorned": 656},
27 |   {"period": "2009 Q2", "licensed": 3215, "sorned": 622},
28 |   {"period": "2009 Q1", "licensed": 3148, "sorned": 632},
29 |   {"period": "2008 Q4", "licensed": 3155, "sorned": 681},
30 |   {"period": "2008 Q3", "licensed": 3190, "sorned": 667},
31 |   {"period": "2007 Q4", "licensed": 3226, "sorned": 620},
32 |   {"period": "2006 Q4", "licensed": 3245, "sorned": null},
33 |   {"period": "2005 Q4", "licensed": 3289, "sorned": null},
34 |   {"period": "2004 Q4", "licensed": 3263, "sorned": null},
35 |   {"period": "2003 Q4", "licensed": 3189, "sorned": null},
36 |   {"period": "2002 Q4", "licensed": 3079, "sorned": null},
37 |   {"period": "2001 Q4", "licensed": 3085, "sorned": null},
38 |   {"period": "2000 Q4", "licensed": 3055, "sorned": null},
39 |   {"period": "1999 Q4", "licensed": 3063, "sorned": null},
40 |   {"period": "1998 Q4", "licensed": 2943, "sorned": null},
41 |   {"period": "1997 Q4", "licensed": 2806, "sorned": null},
42 |   {"period": "1996 Q4", "licensed": 2674, "sorned": null},
43 |   {"period": "1995 Q4", "licensed": 1702, "sorned": null},
44 |   {"period": "1994 Q4", "licensed": 1732, "sorned": null}
45 | ];
46 | Morris.Line({
47 |   element: 'graph',
48 |   data: quarter_data,
49 |   xkey: 'period',
50 |   ykeys: ['licensed', 'sorned'],
51 |   labels: ['Licensed', 'SORN']
52 | });
53 | 
54 | 55 | -------------------------------------------------------------------------------- /examples/events.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Time Events

14 |
15 |
16 | var week_data = [
17 |   {"period": "2011 W27", "licensed": 3407, "sorned": 660},
18 |   {"period": "2011 W26", "licensed": 3351, "sorned": 629},
19 |   {"period": "2011 W25", "licensed": 3269, "sorned": 618},
20 |   {"period": "2011 W24", "licensed": 3246, "sorned": 661},
21 |   {"period": "2011 W23", "licensed": 3257, "sorned": 667},
22 |   {"period": "2011 W22", "licensed": 3248, "sorned": 627},
23 |   {"period": "2011 W21", "licensed": 3171, "sorned": 660},
24 |   {"period": "2011 W20", "licensed": 3171, "sorned": 676},
25 |   {"period": "2011 W19", "licensed": 3201, "sorned": 656},
26 |   {"period": "2011 W18", "licensed": 3215, "sorned": 622},
27 |   {"period": "2011 W17", "licensed": 3148, "sorned": 632},
28 |   {"period": "2011 W16", "licensed": 3155, "sorned": 681},
29 |   {"period": "2011 W15", "licensed": 3190, "sorned": 667},
30 |   {"period": "2011 W14", "licensed": 3226, "sorned": 620},
31 |   {"period": "2011 W13", "licensed": 3245, "sorned": null},
32 |   {"period": "2011 W12", "licensed": 3289, "sorned": null},
33 |   {"period": "2011 W11", "licensed": 3263, "sorned": null},
34 |   {"period": "2011 W10", "licensed": 3189, "sorned": null},
35 |   {"period": "2011 W09", "licensed": 3079, "sorned": null},
36 |   {"period": "2011 W08", "licensed": 3085, "sorned": null},
37 |   {"period": "2011 W07", "licensed": 3055, "sorned": null},
38 |   {"period": "2011 W06", "licensed": 3063, "sorned": null},
39 |   {"period": "2011 W05", "licensed": 2943, "sorned": null},
40 |   {"period": "2011 W04", "licensed": 2806, "sorned": null},
41 |   {"period": "2011 W03", "licensed": 2674, "sorned": null},
42 |   {"period": "2011 W02", "licensed": 1702, "sorned": null},
43 |   {"period": "2011 W01", "licensed": 1732, "sorned": null}
44 | ];
45 | Morris.Line({
46 |   element: 'graph',
47 |   data: week_data,
48 |   xkey: 'period',
49 |   ykeys: ['licensed', 'sorned'],
50 |   labels: ['Licensed', 'SORN'],
51 |   events: [
52 |     '2011-04',
53 |     ['2011-05', '2011-06'],
54 |     '2011-08'
55 |   ]
56 | });
57 | 
58 | 59 | -------------------------------------------------------------------------------- /spec/lib/donut/donut_spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'Morris.Donut', -> 2 | 3 | describe 'svg structure', -> 4 | defaults = 5 | element: 'graph' 6 | data: [ {label: 'Jam', value: 25 }, 7 | {label: 'Frosted', value: 40 }, 8 | {label: 'Custard', value: 25 }, 9 | {label: 'Sugar', value: 10 } ] 10 | formatter: (y) -> "#{y}%" 11 | 12 | it 'should contain 2 paths for each segment', -> 13 | chart = Morris.Donut $.extend {}, defaults 14 | $('#graph').find("path").size().should.equal 8 15 | 16 | it 'should contain 2 text elements for the label', -> 17 | chart = Morris.Donut $.extend {}, defaults 18 | $('#graph').find("text").size().should.equal 2 19 | 20 | describe 'svg attributes', -> 21 | defaults = 22 | element: 'graph' 23 | data: [ {label: 'Jam', value: 25 }, 24 | {label: 'Frosted', value: 40 }, 25 | {label: 'Custard', value: 25 }, 26 | {label: 'Sugar', value: 10 } ] 27 | formatter: (y) -> "#{y}%" 28 | colors: [ '#0B62A4', '#3980B5', '#679DC6', '#95BBD7'] 29 | 30 | it 'should have a label with font size 15', -> 31 | chart = Morris.Donut $.extend {}, defaults 32 | $('#graph').find("text[font-size='15px']").size().should.equal 1 33 | 34 | it 'should have a label with font size 14', -> 35 | chart = Morris.Donut $.extend {}, defaults 36 | $('#graph').find("text[font-size='14px']").size().should.equal 1 37 | 38 | it 'should have a label with font-weight 800', -> 39 | chart = Morris.Donut $.extend {}, defaults 40 | $('#graph').find("text[font-weight='800']").size().should.equal 1 41 | 42 | it 'should have 1 paths with fill of first color', -> 43 | chart = Morris.Donut $.extend {}, defaults 44 | $('#graph').find("path[fill='#0b62a4']").size().should.equal 1 45 | 46 | it 'should have 1 paths with stroke of first color', -> 47 | chart = Morris.Donut $.extend {}, defaults 48 | $('#graph').find("path[stroke='#0b62a4']").size().should.equal 1 49 | 50 | it 'should have a path with white stroke', -> 51 | chart = Morris.Donut $.extend {}, defaults 52 | $('#graph').find("path[stroke='#ffffff']").size().should.equal 4 53 | 54 | it 'should have a path with stroke-width 3', -> 55 | chart = Morris.Donut $.extend {}, defaults 56 | $('#graph').find("path[stroke-width='3']").size().should.equal 4 57 | 58 | it 'should have a path with stroke-width 2', -> 59 | chart = Morris.Donut $.extend {}, defaults 60 | $('#graph').find("path[stroke-width='2']").size().should.equal 4 61 | 62 | describe 'setData', -> 63 | defaults = 64 | element: 'graph' 65 | data: [ {label: 'One', value: 25 }, {label: "Two", value: 30} ] 66 | colors: ['#ff0000', '#00ff00', '#0000ff'] 67 | 68 | it 'should update the chart', -> 69 | chart = Morris.Donut $.extend {}, defaults 70 | $('#graph').find("path[stroke='#0000ff']").size().should.equal 0 71 | chart.setData [ 72 | { label: 'One', value: 25 } 73 | { label: 'Two', value: 30 } 74 | { label: 'Three', value: 35 } 75 | ] 76 | $('#graph').find("path[stroke='#0000ff']").size().should.equal 1 77 | -------------------------------------------------------------------------------- /spec/lib/bar/bar_spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'Morris.Bar', -> 2 | describe 'when using vertical grid', -> 3 | defaults = 4 | element: 'graph' 5 | data: [{x: 'foo', y: 2, z: 3}, {x: 'bar', y: 4, z: 6}] 6 | xkey: 'x' 7 | ykeys: ['y', 'z'] 8 | labels: ['Y', 'Z'] 9 | barColors: [ '#0b62a4', '#7a92a3'] 10 | gridLineColor: '#aaa' 11 | gridStrokeWidth: 0.5 12 | gridTextColor: '#888' 13 | gridTextSize: 12 14 | verticalGridCondition: (index) -> index % 2 15 | verticalGridColor: '#888888' 16 | verticalGridOpacity: '0.2' 17 | 18 | describe 'svg structure', -> 19 | it 'should contain extra rectangles for vertical grid', -> 20 | $('#graph').css('height', '250px').css('width', '800px') 21 | chart = Morris.Bar $.extend {}, defaults 22 | $('#graph').find("rect").size().should.equal 6 23 | 24 | describe 'svg attributes', -> 25 | it 'should have to bars with verticalGrid.color', -> 26 | chart = Morris.Bar $.extend {}, defaults 27 | $('#graph').find("rect[fill='#{defaults.verticalGridColor}']").size().should.equal 2 28 | it 'should have to bars with verticalGrid.color', -> 29 | chart = Morris.Bar $.extend {}, defaults 30 | $('#graph').find("rect[fill-opacity='#{defaults.verticalGridOpacity}']").size().should.equal 2 31 | 32 | describe 'svg structure', -> 33 | defaults = 34 | element: 'graph' 35 | data: [{x: 'foo', y: 2, z: 3}, {x: 'bar', y: 4, z: 6}] 36 | xkey: 'x' 37 | ykeys: ['y', 'z'] 38 | labels: ['Y', 'Z'] 39 | 40 | it 'should contain a rect for each bar', -> 41 | chart = Morris.Bar $.extend {}, defaults 42 | $('#graph').find("rect").size().should.equal 4 43 | 44 | it 'should contain 5 grid lines', -> 45 | chart = Morris.Bar $.extend {}, defaults 46 | $('#graph').find("path").size().should.equal 5 47 | 48 | it 'should contain 7 text elements', -> 49 | chart = Morris.Bar $.extend {}, defaults 50 | $('#graph').find("text").size().should.equal 7 51 | 52 | describe 'svg attributes', -> 53 | defaults = 54 | element: 'graph' 55 | data: [{x: 'foo', y: 2, z: 3}, {x: 'bar', y: 4, z: 6}] 56 | xkey: 'x' 57 | ykeys: ['y', 'z'] 58 | labels: ['Y', 'Z'] 59 | barColors: [ '#0b62a4', '#7a92a3'] 60 | gridLineColor: '#aaa' 61 | gridStrokeWidth: 0.5 62 | gridTextColor: '#888' 63 | gridTextSize: 12 64 | 65 | it 'should have a bar with the first default color', -> 66 | chart = Morris.Bar $.extend {}, defaults 67 | $('#graph').find("rect[fill='#0b62a4']").size().should.equal 2 68 | 69 | it 'should have a bar with no stroke', -> 70 | chart = Morris.Bar $.extend {}, defaults 71 | $('#graph').find("rect[stroke='none']").size().should.equal 4 72 | 73 | it 'should have text with configured fill color', -> 74 | chart = Morris.Bar $.extend {}, defaults 75 | $('#graph').find("text[fill='#888888']").size().should.equal 7 76 | 77 | it 'should have text with configured font size', -> 78 | chart = Morris.Bar $.extend {}, defaults 79 | $('#graph').find("text[font-size='12px']").size().should.equal 7 80 | 81 | describe 'when setting bar radius', -> 82 | describe 'svg structure', -> 83 | defaults = 84 | element: 'graph' 85 | data: [{x: 'foo', y: 2, z: 3}, {x: 'bar', y: 4, z: 6}] 86 | xkey: 'x' 87 | ykeys: ['y', 'z'] 88 | labels: ['Y', 'Z'] 89 | barRadius: [5, 5, 0, 0] 90 | 91 | it 'should contain a path for each bar', -> 92 | chart = Morris.Bar $.extend {}, defaults 93 | $('#graph').find("path").size().should.equal 9 94 | 95 | it 'should use rects if radius is too big', -> 96 | delete defaults.barStyle 97 | chart = Morris.Bar $.extend {}, defaults, 98 | barRadius: [300, 300, 0, 0] 99 | $('#graph').find("rect").size().should.equal 4 100 | 101 | describe 'barSize option', -> 102 | describe 'svg attributes', -> 103 | defaults = 104 | element: 'graph' 105 | barSize: 20 106 | data: [ 107 | {x: '2011 Q1', y: 3, z: 2, a: 3} 108 | {x: '2011 Q2', y: 2, z: null, a: 1} 109 | {x: '2011 Q3', y: 0, z: 2, a: 4} 110 | {x: '2011 Q4', y: 2, z: 4, a: 3} 111 | ], 112 | xkey: 'x' 113 | ykeys: ['y', 'z', 'a'] 114 | labels: ['Y', 'Z', 'A'] 115 | 116 | it 'should calc the width if too narrow for barSize', -> 117 | $('#graph').width('200px') 118 | chart = Morris.Bar $.extend {}, defaults 119 | $('#graph').find("rect").filter((i) -> 120 | parseFloat($(@).attr('width'), 10) < 10 121 | ).size().should.equal 11 122 | 123 | it 'should set width to @options.barSize if possible', -> 124 | chart = Morris.Bar $.extend {}, defaults 125 | $('#graph').find("rect[width='#{defaults.barSize}']").size().should.equal 11 126 | 127 | 128 | -------------------------------------------------------------------------------- /lib/morris.donut.coffee: -------------------------------------------------------------------------------- 1 | # Donut charts. 2 | # 3 | # @example 4 | # Morris.Donut({ 5 | # el: $('#donut-container'), 6 | # data: [ 7 | # { label: 'yin', value: 50 }, 8 | # { label: 'yang', value: 50 } 9 | # ] 10 | # }); 11 | class Morris.Donut extends Morris.EventEmitter 12 | defaults: 13 | colors: [ 14 | '#0B62A4' 15 | '#3980B5' 16 | '#679DC6' 17 | '#95BBD7' 18 | '#B0CCE1' 19 | '#095791' 20 | '#095085' 21 | '#083E67' 22 | '#052C48' 23 | '#042135' 24 | ], 25 | backgroundColor: '#FFFFFF', 26 | labelColor: '#000000', 27 | formatter: Morris.commas 28 | resize: false 29 | 30 | # Create and render a donut chart. 31 | # 32 | constructor: (options) -> 33 | return new Morris.Donut(options) unless (@ instanceof Morris.Donut) 34 | @options = $.extend {}, @defaults, options 35 | 36 | if typeof options.element is 'string' 37 | @el = $ document.getElementById(options.element) 38 | else 39 | @el = $ options.element 40 | 41 | if @el == null || @el.length == 0 42 | throw new Error("Graph placeholder not found.") 43 | 44 | # bail if there's no data 45 | if options.data is undefined or options.data.length is 0 46 | return 47 | 48 | @raphael = new Raphael(@el[0]) 49 | 50 | if @options.resize 51 | $(window).bind 'resize', (evt) => 52 | if @timeoutId? 53 | window.clearTimeout @timeoutId 54 | @timeoutId = window.setTimeout @resizeHandler, 100 55 | 56 | @setData options.data 57 | 58 | # Clear and redraw the chart. 59 | redraw: -> 60 | @raphael.clear() 61 | 62 | cx = @el.width() / 2 63 | cy = @el.height() / 2 64 | w = (Math.min(cx, cy) - 10) / 3 65 | 66 | total = 0 67 | total += value for value in @values 68 | 69 | min = 5 / (2 * w) 70 | C = 1.9999 * Math.PI - min * @data.length 71 | 72 | last = 0 73 | idx = 0 74 | @segments = [] 75 | for value, i in @values 76 | next = last + min + C * (value / total) 77 | seg = new Morris.DonutSegment( 78 | cx, cy, w*2, w, last, next, 79 | @data[i].color || @options.colors[idx % @options.colors.length], 80 | @options.backgroundColor, idx, @raphael) 81 | seg.render() 82 | @segments.push seg 83 | seg.on 'hover', @select 84 | seg.on 'click', @click 85 | last = next 86 | idx += 1 87 | 88 | @text1 = @drawEmptyDonutLabel(cx, cy - 10, @options.labelColor, 15, 800) 89 | @text2 = @drawEmptyDonutLabel(cx, cy + 10, @options.labelColor, 14) 90 | 91 | max_value = Math.max @values... 92 | idx = 0 93 | for value in @values 94 | if value == max_value 95 | @select idx 96 | break 97 | idx += 1 98 | 99 | setData: (data) -> 100 | @data = data 101 | @values = (parseFloat(row.value) for row in @data) 102 | @redraw() 103 | 104 | # @private 105 | click: (idx) => 106 | @fire 'click', idx, @data[idx] 107 | 108 | # Select the segment at the given index. 109 | select: (idx) => 110 | s.deselect() for s in @segments 111 | segment = @segments[idx] 112 | segment.select() 113 | row = @data[idx] 114 | @setLabels(row.label, @options.formatter(row.value, row)) 115 | 116 | 117 | 118 | # @private 119 | setLabels: (label1, label2) -> 120 | inner = (Math.min(@el.width() / 2, @el.height() / 2) - 10) * 2 / 3 121 | maxWidth = 1.8 * inner 122 | maxHeightTop = inner / 2 123 | maxHeightBottom = inner / 3 124 | @text1.attr(text: label1, transform: '') 125 | text1bbox = @text1.getBBox() 126 | text1scale = Math.min(maxWidth / text1bbox.width, maxHeightTop / text1bbox.height) 127 | @text1.attr(transform: "S#{text1scale},#{text1scale},#{text1bbox.x + text1bbox.width / 2},#{text1bbox.y + text1bbox.height}") 128 | @text2.attr(text: label2, transform: '') 129 | text2bbox = @text2.getBBox() 130 | text2scale = Math.min(maxWidth / text2bbox.width, maxHeightBottom / text2bbox.height) 131 | @text2.attr(transform: "S#{text2scale},#{text2scale},#{text2bbox.x + text2bbox.width / 2},#{text2bbox.y}") 132 | 133 | drawEmptyDonutLabel: (xPos, yPos, color, fontSize, fontWeight) -> 134 | text = @raphael.text(xPos, yPos, '') 135 | .attr('font-size', fontSize) 136 | .attr('fill', color) 137 | text.attr('font-weight', fontWeight) if fontWeight? 138 | return text 139 | 140 | resizeHandler: => 141 | @timeoutId = null 142 | @raphael.setSize @el.width(), @el.height() 143 | @redraw() 144 | 145 | 146 | # A segment within a donut chart. 147 | # 148 | # @private 149 | class Morris.DonutSegment extends Morris.EventEmitter 150 | constructor: (@cx, @cy, @inner, @outer, p0, p1, @color, @backgroundColor, @index, @raphael) -> 151 | @sin_p0 = Math.sin(p0) 152 | @cos_p0 = Math.cos(p0) 153 | @sin_p1 = Math.sin(p1) 154 | @cos_p1 = Math.cos(p1) 155 | @is_long = if (p1 - p0) > Math.PI then 1 else 0 156 | @path = @calcSegment(@inner + 3, @inner + @outer - 5) 157 | @selectedPath = @calcSegment(@inner + 3, @inner + @outer) 158 | @hilight = @calcArc(@inner) 159 | 160 | calcArcPoints: (r) -> 161 | return [ 162 | @cx + r * @sin_p0, 163 | @cy + r * @cos_p0, 164 | @cx + r * @sin_p1, 165 | @cy + r * @cos_p1] 166 | 167 | calcSegment: (r1, r2) -> 168 | [ix0, iy0, ix1, iy1] = @calcArcPoints(r1) 169 | [ox0, oy0, ox1, oy1] = @calcArcPoints(r2) 170 | return ( 171 | "M#{ix0},#{iy0}" + 172 | "A#{r1},#{r1},0,#{@is_long},0,#{ix1},#{iy1}" + 173 | "L#{ox1},#{oy1}" + 174 | "A#{r2},#{r2},0,#{@is_long},1,#{ox0},#{oy0}" + 175 | "Z") 176 | 177 | calcArc: (r) -> 178 | [ix0, iy0, ix1, iy1] = @calcArcPoints(r) 179 | return ( 180 | "M#{ix0},#{iy0}" + 181 | "A#{r},#{r},0,#{@is_long},0,#{ix1},#{iy1}") 182 | 183 | render: -> 184 | @arc = @drawDonutArc(@hilight, @color) 185 | @seg = @drawDonutSegment( 186 | @path, 187 | @color, 188 | @backgroundColor, 189 | => @fire('hover', @index), 190 | => @fire('click', @index) 191 | ) 192 | 193 | drawDonutArc: (path, color) -> 194 | @raphael.path(path) 195 | .attr(stroke: color, 'stroke-width': 2, opacity: 0) 196 | 197 | drawDonutSegment: (path, fillColor, strokeColor, hoverFunction, clickFunction) -> 198 | @raphael.path(path) 199 | .attr(fill: fillColor, stroke: strokeColor, 'stroke-width': 3) 200 | .hover(hoverFunction) 201 | .click(clickFunction) 202 | 203 | select: => 204 | unless @selected 205 | @seg.animate(path: @selectedPath, 150, '<>') 206 | @arc.animate(opacity: 1, 150, '<>') 207 | @selected = true 208 | 209 | deselect: => 210 | if @selected 211 | @seg.animate(path: @path, 150, '<>') 212 | @arc.animate(opacity: 0, 150, '<>') 213 | @selected = false 214 | -------------------------------------------------------------------------------- /spec/lib/label_series_spec.coffee: -------------------------------------------------------------------------------- 1 | describe '#labelSeries', -> 2 | 3 | it 'should generate decade intervals', -> 4 | Morris.labelSeries( 5 | new Date(1952, 0, 1).getTime(), 6 | new Date(2012, 0, 1).getTime(), 7 | 1000 8 | ).should.deep.equal([ 9 | ["1960", new Date(1960, 0, 1).getTime()], 10 | ["1970", new Date(1970, 0, 1).getTime()], 11 | ["1980", new Date(1980, 0, 1).getTime()], 12 | ["1990", new Date(1990, 0, 1).getTime()], 13 | ["2000", new Date(2000, 0, 1).getTime()], 14 | ["2010", new Date(2010, 0, 1).getTime()] 15 | ]) 16 | Morris.labelSeries( 17 | new Date(1952, 3, 1).getTime(), 18 | new Date(2012, 3, 1).getTime(), 19 | 1000 20 | ).should.deep.equal([ 21 | ["1960", new Date(1960, 0, 1).getTime()], 22 | ["1970", new Date(1970, 0, 1).getTime()], 23 | ["1980", new Date(1980, 0, 1).getTime()], 24 | ["1990", new Date(1990, 0, 1).getTime()], 25 | ["2000", new Date(2000, 0, 1).getTime()], 26 | ["2010", new Date(2010, 0, 1).getTime()] 27 | ]) 28 | 29 | it 'should generate year intervals', -> 30 | Morris.labelSeries( 31 | new Date(2007, 0, 1).getTime(), 32 | new Date(2012, 0, 1).getTime(), 33 | 1000 34 | ).should.deep.equal([ 35 | ["2007", new Date(2007, 0, 1).getTime()], 36 | ["2008", new Date(2008, 0, 1).getTime()], 37 | ["2009", new Date(2009, 0, 1).getTime()], 38 | ["2010", new Date(2010, 0, 1).getTime()], 39 | ["2011", new Date(2011, 0, 1).getTime()], 40 | ["2012", new Date(2012, 0, 1).getTime()] 41 | ]) 42 | Morris.labelSeries( 43 | new Date(2007, 3, 1).getTime(), 44 | new Date(2012, 3, 1).getTime(), 45 | 1000 46 | ).should.deep.equal([ 47 | ["2008", new Date(2008, 0, 1).getTime()], 48 | ["2009", new Date(2009, 0, 1).getTime()], 49 | ["2010", new Date(2010, 0, 1).getTime()], 50 | ["2011", new Date(2011, 0, 1).getTime()], 51 | ["2012", new Date(2012, 0, 1).getTime()] 52 | ]) 53 | 54 | it 'should generate month intervals', -> 55 | Morris.labelSeries( 56 | new Date(2012, 0, 1).getTime(), 57 | new Date(2012, 5, 1).getTime(), 58 | 1000 59 | ).should.deep.equal([ 60 | ["2012-01", new Date(2012, 0, 1).getTime()], 61 | ["2012-02", new Date(2012, 1, 1).getTime()], 62 | ["2012-03", new Date(2012, 2, 1).getTime()], 63 | ["2012-04", new Date(2012, 3, 1).getTime()], 64 | ["2012-05", new Date(2012, 4, 1).getTime()], 65 | ["2012-06", new Date(2012, 5, 1).getTime()] 66 | ]) 67 | 68 | it 'should generate week intervals', -> 69 | Morris.labelSeries( 70 | new Date(2012, 0, 1).getTime(), 71 | new Date(2012, 1, 10).getTime(), 72 | 1000 73 | ).should.deep.equal([ 74 | ["2012-01-01", new Date(2012, 0, 1).getTime()], 75 | ["2012-01-08", new Date(2012, 0, 8).getTime()], 76 | ["2012-01-15", new Date(2012, 0, 15).getTime()], 77 | ["2012-01-22", new Date(2012, 0, 22).getTime()], 78 | ["2012-01-29", new Date(2012, 0, 29).getTime()], 79 | ["2012-02-05", new Date(2012, 1, 5).getTime()] 80 | ]) 81 | 82 | it 'should generate day intervals', -> 83 | Morris.labelSeries( 84 | new Date(2012, 0, 1).getTime(), 85 | new Date(2012, 0, 6).getTime(), 86 | 1000 87 | ).should.deep.equal([ 88 | ["2012-01-01", new Date(2012, 0, 1).getTime()], 89 | ["2012-01-02", new Date(2012, 0, 2).getTime()], 90 | ["2012-01-03", new Date(2012, 0, 3).getTime()], 91 | ["2012-01-04", new Date(2012, 0, 4).getTime()], 92 | ["2012-01-05", new Date(2012, 0, 5).getTime()], 93 | ["2012-01-06", new Date(2012, 0, 6).getTime()] 94 | ]) 95 | 96 | it 'should generate hour intervals', -> 97 | Morris.labelSeries( 98 | new Date(2012, 0, 1, 0).getTime(), 99 | new Date(2012, 0, 1, 5).getTime(), 100 | 1000 101 | ).should.deep.equal([ 102 | ["00:00", new Date(2012, 0, 1, 0).getTime()], 103 | ["01:00", new Date(2012, 0, 1, 1).getTime()], 104 | ["02:00", new Date(2012, 0, 1, 2).getTime()], 105 | ["03:00", new Date(2012, 0, 1, 3).getTime()], 106 | ["04:00", new Date(2012, 0, 1, 4).getTime()], 107 | ["05:00", new Date(2012, 0, 1, 5).getTime()] 108 | ]) 109 | 110 | it 'should generate half-hour intervals', -> 111 | Morris.labelSeries( 112 | new Date(2012, 0, 1, 0, 0).getTime(), 113 | new Date(2012, 0, 1, 2, 30).getTime(), 114 | 1000 115 | ).should.deep.equal([ 116 | ["00:00", new Date(2012, 0, 1, 0, 0).getTime()], 117 | ["00:30", new Date(2012, 0, 1, 0, 30).getTime()], 118 | ["01:00", new Date(2012, 0, 1, 1, 0).getTime()], 119 | ["01:30", new Date(2012, 0, 1, 1, 30).getTime()], 120 | ["02:00", new Date(2012, 0, 1, 2, 0).getTime()], 121 | ["02:30", new Date(2012, 0, 1, 2, 30).getTime()] 122 | ]) 123 | Morris.labelSeries( 124 | new Date(2012, 4, 12, 0, 0).getTime(), 125 | new Date(2012, 4, 12, 2, 30).getTime(), 126 | 1000 127 | ).should.deep.equal([ 128 | ["00:00", new Date(2012, 4, 12, 0, 0).getTime()], 129 | ["00:30", new Date(2012, 4, 12, 0, 30).getTime()], 130 | ["01:00", new Date(2012, 4, 12, 1, 0).getTime()], 131 | ["01:30", new Date(2012, 4, 12, 1, 30).getTime()], 132 | ["02:00", new Date(2012, 4, 12, 2, 0).getTime()], 133 | ["02:30", new Date(2012, 4, 12, 2, 30).getTime()] 134 | ]) 135 | 136 | it 'should generate fifteen-minute intervals', -> 137 | Morris.labelSeries( 138 | new Date(2012, 0, 1, 0, 0).getTime(), 139 | new Date(2012, 0, 1, 1, 15).getTime(), 140 | 1000 141 | ).should.deep.equal([ 142 | ["00:00", new Date(2012, 0, 1, 0, 0).getTime()], 143 | ["00:15", new Date(2012, 0, 1, 0, 15).getTime()], 144 | ["00:30", new Date(2012, 0, 1, 0, 30).getTime()], 145 | ["00:45", new Date(2012, 0, 1, 0, 45).getTime()], 146 | ["01:00", new Date(2012, 0, 1, 1, 0).getTime()], 147 | ["01:15", new Date(2012, 0, 1, 1, 15).getTime()] 148 | ]) 149 | Morris.labelSeries( 150 | new Date(2012, 4, 12, 0, 0).getTime(), 151 | new Date(2012, 4, 12, 1, 15).getTime(), 152 | 1000 153 | ).should.deep.equal([ 154 | ["00:00", new Date(2012, 4, 12, 0, 0).getTime()], 155 | ["00:15", new Date(2012, 4, 12, 0, 15).getTime()], 156 | ["00:30", new Date(2012, 4, 12, 0, 30).getTime()], 157 | ["00:45", new Date(2012, 4, 12, 0, 45).getTime()], 158 | ["01:00", new Date(2012, 4, 12, 1, 0).getTime()], 159 | ["01:15", new Date(2012, 4, 12, 1, 15).getTime()] 160 | ]) 161 | 162 | it 'should override automatic intervals', -> 163 | Morris.labelSeries( 164 | new Date(2011, 11, 12).getTime(), 165 | new Date(2012, 0, 12).getTime(), 166 | 1000, 167 | "year" 168 | ).should.deep.equal([ 169 | ["2012", new Date(2012, 0, 1).getTime()] 170 | ]) 171 | 172 | it 'should apply custom formatters', -> 173 | Morris.labelSeries( 174 | new Date(2012, 0, 1).getTime(), 175 | new Date(2012, 0, 6).getTime(), 176 | 1000, 177 | "day", 178 | (d) -> "#{d.getMonth()+1}/#{d.getDate()}/#{d.getFullYear()}" 179 | ).should.deep.equal([ 180 | ["1/1/2012", new Date(2012, 0, 1).getTime()], 181 | ["1/2/2012", new Date(2012, 0, 2).getTime()], 182 | ["1/3/2012", new Date(2012, 0, 3).getTime()], 183 | ["1/4/2012", new Date(2012, 0, 4).getTime()], 184 | ["1/5/2012", new Date(2012, 0, 5).getTime()], 185 | ["1/6/2012", new Date(2012, 0, 6).getTime()] 186 | ]) 187 | -------------------------------------------------------------------------------- /spec/lib/grid/set_data_spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'Morris.Grid#setData', -> 2 | 3 | it 'should not alter user-supplied data', -> 4 | my_data = [{x: 1, y: 1}, {x: 2, y: 2}] 5 | expected_data = [{x: 1, y: 1}, {x: 2, y: 2}] 6 | Morris.Line 7 | element: 'graph' 8 | data: my_data 9 | xkey: 'x' 10 | ykeys: ['y'] 11 | labels: ['dontcare'] 12 | my_data.should.deep.equal expected_data 13 | 14 | describe 'ymin/ymax', -> 15 | beforeEach -> 16 | @defaults = 17 | element: 'graph' 18 | xkey: 'x' 19 | ykeys: ['y', 'z'] 20 | labels: ['y', 'z'] 21 | 22 | it 'should use a user-specified minimum and maximum value', -> 23 | line = Morris.Line $.extend @defaults, 24 | data: [{x: 1, y: 1}] 25 | ymin: 10 26 | ymax: 20 27 | line.ymin.should.equal 10 28 | line.ymax.should.equal 20 29 | 30 | describe 'auto', -> 31 | 32 | it 'should automatically calculate the minimum and maximum value', -> 33 | line = Morris.Line $.extend @defaults, 34 | data: [{x: 1, y: 10}, {x: 2, y: 15}, {x: 3, y: null}, {x: 4}] 35 | ymin: 'auto' 36 | ymax: 'auto' 37 | line.ymin.should.equal 10 38 | line.ymax.should.equal 15 39 | 40 | it 'should automatically calculate the minimum and maximum value given no y data', -> 41 | line = Morris.Line $.extend @defaults, 42 | data: [{x: 1}, {x: 2}, {x: 3}, {x: 4}] 43 | ymin: 'auto' 44 | ymax: 'auto' 45 | line.ymin.should.equal 0 46 | line.ymax.should.equal 1 47 | 48 | describe 'auto [n]', -> 49 | 50 | it 'should automatically calculate the minimum and maximum value', -> 51 | line = Morris.Line $.extend @defaults, 52 | data: [{x: 1, y: 10}, {x: 2, y: 15}, {x: 3, y: null}, {x: 4}] 53 | ymin: 'auto 11' 54 | ymax: 'auto 13' 55 | line.ymin.should.equal 10 56 | line.ymax.should.equal 15 57 | 58 | it 'should automatically calculate the minimum and maximum value given no data', -> 59 | line = Morris.Line $.extend @defaults, 60 | data: [{x: 1}, {x: 2}, {x: 3}, {x: 4}] 61 | ymin: 'auto 11' 62 | ymax: 'auto 13' 63 | line.ymin.should.equal 11 64 | line.ymax.should.equal 13 65 | 66 | it 'should use a user-specified minimum and maximum value', -> 67 | line = Morris.Line $.extend @defaults, 68 | data: [{x: 1, y: 10}, {x: 2, y: 15}, {x: 3, y: null}, {x: 4}] 69 | ymin: 'auto 5' 70 | ymax: 'auto 20' 71 | line.ymin.should.equal 5 72 | line.ymax.should.equal 20 73 | 74 | it 'should use a user-specified minimum and maximum value given no data', -> 75 | line = Morris.Line $.extend @defaults, 76 | data: [{x: 1}, {x: 2}, {x: 3}, {x: 4}] 77 | ymin: 'auto 5' 78 | ymax: 'auto 20' 79 | line.ymin.should.equal 5 80 | line.ymax.should.equal 20 81 | 82 | describe 'xmin/xmax', -> 83 | 84 | it 'should calculate the horizontal range', -> 85 | line = Morris.Line 86 | element: 'graph' 87 | data: [{x: 2, y: 2}, {x: 1, y: 1}, {x: 4, y: 4}, {x: 3, y: 3}] 88 | xkey: 'x' 89 | ykeys: ['y'] 90 | labels: ['y'] 91 | line.xmin.should == 1 92 | line.xmax.should == 4 93 | 94 | it "should pad the range if there's only one data point", -> 95 | line = Morris.Line 96 | element: 'graph' 97 | data: [{x: 2, y: 2}] 98 | xkey: 'x' 99 | ykeys: ['y'] 100 | labels: ['y'] 101 | line.xmin.should == 1 102 | line.xmax.should == 3 103 | 104 | describe 'sorting', -> 105 | 106 | it 'should sort data when parseTime is true', -> 107 | line = Morris.Line 108 | element: 'graph' 109 | data: [ 110 | {x: '2012 Q1', y: 2}, 111 | {x: '2012 Q3', y: 1}, 112 | {x: '2012 Q4', y: 4}, 113 | {x: '2012 Q2', y: 3}] 114 | xkey: 'x' 115 | ykeys: ['y'] 116 | labels: ['y'] 117 | line.data.map((row) -> row.label).should.deep.equal ['2012 Q1', '2012 Q2', '2012 Q3', '2012 Q4'] 118 | 119 | it 'should not sort data when parseTime is false', -> 120 | line = Morris.Line 121 | element: 'graph' 122 | data: [{x: 1, y: 2}, {x: 4, y: 1}, {x: 3, y: 4}, {x: 2, y: 3}] 123 | xkey: 'x' 124 | ykeys: ['y'] 125 | labels: ['y'] 126 | parseTime: false 127 | line.data.map((row) -> row.label).should.deep.equal [1, 4, 3, 2] 128 | 129 | describe 'timestamp data', -> 130 | 131 | it 'should generate default labels for timestamp x-values', -> 132 | d = [ 133 | new Date 2012, 0, 1 134 | new Date 2012, 0, 2 135 | new Date 2012, 0, 3 136 | new Date 2012, 0, 4 137 | ] 138 | line = Morris.Line 139 | element: 'graph' 140 | data: [ 141 | {x: d[0].getTime(), y: 2}, 142 | {x: d[1].getTime(), y: 1}, 143 | {x: d[2].getTime(), y: 4}, 144 | {x: d[3].getTime(), y: 3}] 145 | xkey: 'x' 146 | ykeys: ['y'] 147 | labels: ['y'] 148 | line.data.map((row) -> row.label).should.deep.equal d.map((t) -> t.toString()) 149 | 150 | it 'should use a user-supplied formatter for labels', -> 151 | line = Morris.Line 152 | element: 'graph' 153 | data: [ 154 | {x: new Date(2012, 0, 1).getTime(), y: 2}, 155 | {x: new Date(2012, 0, 2).getTime(), y: 1}, 156 | {x: new Date(2012, 0, 3).getTime(), y: 4}, 157 | {x: new Date(2012, 0, 4).getTime(), y: 3}] 158 | xkey: 'x' 159 | ykeys: ['y'] 160 | labels: ['y'] 161 | dateFormat: (ts) -> 162 | date = new Date(ts) 163 | "#{date.getFullYear()}-#{date.getMonth()+1}-#{date.getDate()}" 164 | line.data.map((row) -> row.label).should.deep.equal ['2012-1-1', '2012-1-2', '2012-1-3', '2012-1-4'] 165 | 166 | it 'should parse y-values in strings', -> 167 | line = Morris.Line 168 | element: 'graph' 169 | data: [{x: 2, y: '12'}, {x: 1, y: '13.5'}, {x: 4, y: '14'}, {x: 3, y: '16'}] 170 | xkey: 'x' 171 | ykeys: ['y'] 172 | labels: ['y'] 173 | line.ymin.should == 12 174 | line.ymax.should == 16 175 | line.data.map((row) -> row.y).should.deep.equal [[13.5], [12], [16], [14]] 176 | 177 | it 'should clear the chart when empty data is supplied', -> 178 | line = Morris.Line 179 | element: 'graph', 180 | data: [{x: 2, y: '12'}, {x: 1, y: '13.5'}, {x: 4, y: '14'}, {x: 3, y: '16'}] 181 | xkey: 'x' 182 | ykeys: ['y'] 183 | labels: ['y'] 184 | line.data.length.should.equal 4 185 | line.setData([]) 186 | line.data.length.should.equal 0 187 | line.setData([{x: 2, y: '12'}, {x: 1, y: '13.5'}, {x: 4, y: '14'}, {x: 3, y: '16'}]) 188 | line.data.length.should.equal 4 189 | 190 | it 'should be able to add data if the chart is initialised with empty data', -> 191 | line = Morris.Line 192 | element: 'graph', 193 | data: [] 194 | xkey: 'x' 195 | ykeys: ['y'] 196 | labels: ['y'] 197 | line.data.length.should.equal 0 198 | line.setData([{x: 2, y: '12'}, {x: 1, y: '13.5'}, {x: 4, y: '14'}, {x: 3, y: '16'}]) 199 | line.data.length.should.equal 4 200 | 201 | it 'should automatically choose significant numbers for y-labels', -> 202 | line = Morris.Line 203 | element: 'graph', 204 | data: [{x: 1, y: 0}, {x: 2, y: 3600}] 205 | xkey: 'x' 206 | ykeys: ['y'] 207 | labels: ['y'] 208 | line.grid.should == [0, 1000, 2000, 3000, 4000] 209 | -------------------------------------------------------------------------------- /spec/lib/line/line_spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'Morris.Line', -> 2 | 3 | it 'should raise an error when the placeholder element is not found', -> 4 | my_data = [{x: 1, y: 1}, {x: 2, y: 2}] 5 | fn = -> 6 | Morris.Line( 7 | element: "thisplacedoesnotexist" 8 | data: my_data 9 | xkey: 'x' 10 | ykeys: ['y'] 11 | labels: ['dontcare'] 12 | ) 13 | fn.should.throw(/Graph container element not found/) 14 | 15 | it 'should make point styles customizable', -> 16 | my_data = [{x: 1, y: 1}, {x: 2, y: 2}] 17 | red = '#ff0000' 18 | blue = '#0000ff' 19 | chart = Morris.Line 20 | element: 'graph' 21 | data: my_data 22 | xkey: 'x' 23 | ykeys: ['y'] 24 | labels: ['dontcare'] 25 | pointStrokeColors: [red, blue] 26 | pointStrokeWidths: [1, 2] 27 | pointFillColors: [null, red] 28 | chart.pointStrokeWidthForSeries(0).should.equal 1 29 | chart.pointStrokeColorForSeries(0).should.equal red 30 | chart.pointStrokeWidthForSeries(1).should.equal 2 31 | chart.pointStrokeColorForSeries(1).should.equal blue 32 | chart.colorFor(chart.data[0], 0, 'point').should.equal chart.colorFor(chart.data[0], 0, 'line') 33 | chart.colorFor(chart.data[1], 1, 'point').should.equal red 34 | 35 | describe 'generating column labels', -> 36 | 37 | it 'should use user-supplied x value strings by default', -> 38 | chart = Morris.Line 39 | element: 'graph' 40 | data: [{x: '2012 Q1', y: 1}, {x: '2012 Q2', y: 1}] 41 | xkey: 'x' 42 | ykeys: ['y'] 43 | labels: ['dontcare'] 44 | chart.data.map((x) -> x.label).should == ['2012 Q1', '2012 Q2'] 45 | 46 | it 'should use a default format for timestamp x-values', -> 47 | d1 = new Date(2012, 0, 1) 48 | d2 = new Date(2012, 0, 2) 49 | chart = Morris.Line 50 | element: 'graph' 51 | data: [{x: d1.getTime(), y: 1}, {x: d2.getTime(), y: 1}] 52 | xkey: 'x' 53 | ykeys: ['y'] 54 | labels: ['dontcare'] 55 | chart.data.map((x) -> x.label).should == [d2.toString(), d1.toString()] 56 | 57 | it 'should use user-defined formatters', -> 58 | d = new Date(2012, 0, 1) 59 | chart = Morris.Line 60 | element: 'graph' 61 | data: [{x: d.getTime(), y: 1}, {x: '2012-01-02', y: 1}] 62 | xkey: 'x' 63 | ykeys: ['y'] 64 | labels: ['dontcare'] 65 | dateFormat: (d) -> 66 | x = new Date(d) 67 | "#{x.getYear()+1900}/#{x.getMonth()+1}/#{x.getDay()+1}" 68 | chart.data.map((x) -> x.label).should.eql(['2012/1/1', '2012/1/2']) 69 | 70 | it 'should use user-defined labels', -> 71 | chart = Morris.Line 72 | element: 'graph' 73 | data: [{x:1,y:2}], 74 | xkey: 'x', 75 | ykeys: ['y'], 76 | labels: ['dontcare'] 77 | customLabels: [{x:3,label:'label'}] 78 | chart.options.customLabels.map((x) -> x.label).should.eql(['label']) 79 | 80 | it 'should use relative x-coordinates', -> 81 | chart = Morris.Line 82 | element: 'graph' 83 | data: [{x:1,y:2}, {x:1.2,y:2}], 84 | xkey: 'x', 85 | ykeys: ['y'], 86 | labels: ['dontcare'] 87 | parseTime: false 88 | freePosition: true 89 | [chart.data[1].x - chart.data[0].x].should.not.equal(1) 90 | 91 | describe 'rendering lines', -> 92 | beforeEach -> 93 | @defaults = 94 | element: 'graph' 95 | data: [{x:0, y:1, z:0}, {x:1, y:0, z:1}, {x:2, y:1, z:0}, {x:3, y:0, z:1}, {x:4, y:1, z:0}] 96 | xkey: 'x' 97 | ykeys: ['y', 'z'] 98 | labels: ['y', 'z'] 99 | lineColors: ['#abcdef', '#fedcba'] 100 | smooth: true 101 | 102 | shouldHavePath = (regex, color = '#abcdef') -> 103 | # Matches an SVG path element within the rendered chart. 104 | # 105 | # Sneakily uses line colors to differentiate between paths within 106 | # the chart. 107 | $('#graph').find("path[stroke='#{color}']").attr('d').should.match regex 108 | 109 | it 'should generate smooth lines when options.smooth is true', -> 110 | Morris.Line @defaults 111 | shouldHavePath /M[\d\.]+,[\d\.]+(C[\d\.]+(,[\d\.]+){5}){4}/ 112 | 113 | it 'should generate jagged lines when options.smooth is false', -> 114 | Morris.Line $.extend(@defaults, smooth: false) 115 | shouldHavePath /M[\d\.]+,[\d\.]+(L[\d\.]+,[\d\.]+){4}/ 116 | 117 | it 'should generate smooth/jagged lines according to the value for each series when options.smooth is an array', -> 118 | Morris.Line $.extend(@defaults, smooth: ['y']) 119 | shouldHavePath /M[\d\.]+,[\d\.]+(C[\d\.]+(,[\d\.]+){5}){4}/, '#abcdef' 120 | shouldHavePath /M[\d\.]+,[\d\.]+(L[\d\.]+,[\d\.]+){4}/, '#fedcba' 121 | 122 | it 'should ignore undefined values', -> 123 | @defaults.data[2].y = undefined 124 | Morris.Line @defaults 125 | shouldHavePath /M[\d\.]+,[\d\.]+(C[\d\.]+(,[\d\.]+){5}){3}/ 126 | 127 | it 'should break the line at null values', -> 128 | @defaults.data[2].y = null 129 | Morris.Line @defaults 130 | shouldHavePath /(M[\d\.]+,[\d\.]+C[\d\.]+(,[\d\.]+){5}){2}/ 131 | 132 | it 'should make line width customizable', -> 133 | chart = Morris.Line $.extend(@defaults, lineWidth: [1, 2]) 134 | chart.lineWidthForSeries(0).should.equal 1 135 | chart.lineWidthForSeries(1).should.equal 2 136 | 137 | describe '#createPath', -> 138 | 139 | it 'should generate a smooth line', -> 140 | testData = [{x: 0, y: 10}, {x: 10, y: 0}, {x: 20, y: 10}] 141 | path = Morris.Line.createPath(testData, true, 20) 142 | path.should.equal 'M0,10C2.5,7.5,7.5,0,10,0C12.5,0,17.5,7.5,20,10' 143 | 144 | it 'should generate a jagged line', -> 145 | testData = [{x: 0, y: 10}, {x: 10, y: 0}, {x: 20, y: 10}] 146 | path = Morris.Line.createPath(testData, false, 20) 147 | path.should.equal 'M0,10L10,0L20,10' 148 | 149 | it 'should prevent paths from descending below the bottom of the chart', -> 150 | testData = [{x: 0, y: 20}, {x: 10, y: 30}, {x: 20, y: 10}] 151 | path = Morris.Line.createPath(testData, true, 30) 152 | path.should.equal 'M0,20C2.5,22.5,7.5,30,10,30C12.5,28.75,17.5,15,20,10' 153 | 154 | it 'should break the line at null values', -> 155 | testData = [{x: 0, y: 10}, {x: 10, y: 0}, {x: 20, y: null}, {x: 30, y: 10}, {x: 40, y: 0}] 156 | path = Morris.Line.createPath(testData, true, 20) 157 | path.should.equal 'M0,10C2.5,7.5,7.5,2.5,10,0M30,10C32.5,7.5,37.5,2.5,40,0' 158 | 159 | it 'should ignore leading and trailing null values', -> 160 | testData = [{x: 0, y: null}, {x: 10, y: 10}, {x: 20, y: 0}, {x: 30, y: 10}, {x: 40, y: null}] 161 | path = Morris.Line.createPath(testData, true, 20) 162 | path.should.equal 'M10,10C12.5,7.5,17.5,0,20,0C22.5,0,27.5,7.5,30,10' 163 | 164 | describe 'svg structure', -> 165 | defaults = 166 | element: 'graph' 167 | data: [{x: '2012 Q1', y: 1}, {x: '2012 Q2', y: 1}] 168 | lineColors: [ '#0b62a4', '#7a92a3'] 169 | xkey: 'x' 170 | ykeys: ['y'] 171 | labels: ['dontcare'] 172 | 173 | it 'should contain a path that represents the line', -> 174 | chart = Morris.Line $.extend {}, defaults 175 | $('#graph').find("path[stroke='#0b62a4']").size().should.equal 1 176 | 177 | it 'should contain a circle for each data point', -> 178 | chart = Morris.Line $.extend {}, defaults 179 | $('#graph').find("circle").size().should.equal 2 180 | 181 | it 'should contain 5 grid lines', -> 182 | chart = Morris.Line $.extend {}, defaults 183 | $('#graph').find("path[stroke='#aaaaaa']").size().should.equal 5 184 | 185 | it 'should contain 9 text elements', -> 186 | chart = Morris.Line $.extend {}, defaults 187 | $('#graph').find("text").size().should.equal 9 188 | 189 | describe 'svg attributes', -> 190 | defaults = 191 | element: 'graph' 192 | data: [{x: '2012 Q1', y: 1}, {x: '2012 Q2', y: 1}] 193 | xkey: 'x' 194 | ykeys: ['y', 'z'] 195 | labels: ['Y', 'Z'] 196 | lineColors: [ '#0b62a4', '#7a92a3'] 197 | lineWidth: 3 198 | pointStrokeWidths: [5] 199 | pointStrokeColors: ['#ffffff'] 200 | gridLineColor: '#aaa' 201 | gridStrokeWidth: 0.5 202 | gridTextColor: '#888' 203 | gridTextSize: 12 204 | pointSize: [5] 205 | 206 | it 'should have circles with configured fill color', -> 207 | chart = Morris.Line $.extend {}, defaults 208 | $('#graph').find("circle[fill='#0b62a4']").size().should.equal 2 209 | 210 | it 'should have circles with configured stroke width', -> 211 | chart = Morris.Line $.extend {}, defaults 212 | $('#graph').find("circle[stroke-width='5']").size().should.equal 2 213 | 214 | it 'should have circles with configured stroke color', -> 215 | chart = Morris.Line $.extend {}, defaults 216 | $('#graph').find("circle[stroke='#ffffff']").size().should.equal 2 217 | 218 | it 'should have line with configured line width', -> 219 | chart = Morris.Line $.extend {}, defaults 220 | $('#graph').find("path[stroke-width='3']").size().should.equal 1 221 | 222 | it 'should have text with configured font size', -> 223 | chart = Morris.Line $.extend {}, defaults 224 | $('#graph').find("text[font-size='12px']").size().should.equal 9 225 | 226 | it 'should have text with configured font size', -> 227 | chart = Morris.Line $.extend {}, defaults 228 | $('#graph').find("text[fill='#888888']").size().should.equal 9 229 | 230 | it 'should have circle with configured size', -> 231 | chart = Morris.Line $.extend {}, defaults 232 | $('#graph').find("circle[r='5']").size().should.equal 2 233 | -------------------------------------------------------------------------------- /lib/morris.bar.coffee: -------------------------------------------------------------------------------- 1 | class Morris.Bar extends Morris.Grid 2 | constructor: (options) -> 3 | return new Morris.Bar(options) unless (@ instanceof Morris.Bar) 4 | super($.extend {}, options, parseTime: false) 5 | 6 | init: -> 7 | @cumulative = @options.stacked 8 | 9 | if @options.hideHover isnt 'always' 10 | @hover = new Morris.Hover(parent: @el) 11 | @on('hovermove', @onHoverMove) 12 | @on('hoverout', @onHoverOut) 13 | @on('gridclick', @onGridClick) 14 | 15 | # Default configuration 16 | # 17 | defaults: 18 | barSizeRatio: 0.75 19 | barGap: 3 20 | barColors: [ 21 | '#0b62a4' 22 | '#7a92a3' 23 | '#4da74d' 24 | '#afd8f8' 25 | '#edc240' 26 | '#cb4b4b' 27 | '#9440ed' 28 | ], 29 | barOpacity: 1.0 30 | barHighlightOpacity: 1.0 31 | highlightSpeed: 150 32 | barRadius: [0, 0, 0, 0] 33 | xLabelMargin: 50 34 | horizontal: false 35 | shown: true 36 | inBarValue: false 37 | inBarValueTextColor: 'white' 38 | inBarValueMinTopMargin: 1 39 | inBarValueRightMargin: 4 40 | 41 | # Do any size-related calculations 42 | # 43 | # @private 44 | calc: -> 45 | @calcBars() 46 | if @options.hideHover is false 47 | @hover.update(@hoverContentForRow(@data.length - 1)...) 48 | 49 | # calculate series data bars coordinates and sizes 50 | # 51 | # @private 52 | calcBars: -> 53 | for row, idx in @data 54 | row._x = @xStart + @xSize * (idx + 0.5) / @data.length 55 | row._y = for y in row.y 56 | if y? then @transY(y) else null 57 | 58 | # Draws the bar chart. 59 | # 60 | draw: -> 61 | @drawXAxis() if @options.axes in [true, 'both', 'x'] 62 | @drawSeries() 63 | 64 | # draw the x-axis labels 65 | # 66 | # @private 67 | drawXAxis: -> 68 | # draw x axis labels 69 | if not @options.horizontal 70 | basePos = @getXAxisLabelY() 71 | else 72 | basePos = @getYAxisLabelX() 73 | 74 | prevLabelMargin = null 75 | prevAngleMargin = null 76 | for i in [0...@data.length] 77 | row = @data[@data.length - 1 - i] 78 | if not @options.horizontal 79 | label = @drawXAxisLabel(row._x, basePos, row.label) 80 | else 81 | label = @drawYAxisLabel(basePos, row._x - 0.5 * @options.gridTextSize, row.label) 82 | 83 | 84 | if not @options.horizontal 85 | angle = @options.xLabelAngle 86 | else 87 | angle = 0 88 | 89 | textBox = label.getBBox() 90 | label.transform("r#{-angle}") 91 | labelBox = label.getBBox() 92 | label.transform("t0,#{labelBox.height / 2}...") 93 | 94 | 95 | if angle != 0 96 | offset = -0.5 * textBox.width * 97 | Math.cos(angle * Math.PI / 180.0) 98 | label.transform("t#{offset},0...") 99 | 100 | 101 | if not @options.horizontal 102 | startPos = labelBox.x 103 | size = labelBox.width 104 | maxSize = @el.width() 105 | else 106 | startPos = labelBox.y 107 | size = labelBox.height 108 | maxSize = @el.height() 109 | 110 | # try to avoid overlaps 111 | if (not prevLabelMargin? or 112 | prevLabelMargin >= startPos + size or 113 | prevAngleMargin? and prevAngleMargin >= startPos) and 114 | startPos >= 0 and (startPos + size) < maxSize 115 | if angle != 0 116 | margin = 1.25 * @options.gridTextSize / 117 | Math.sin(angle * Math.PI / 180.0) 118 | prevAngleMargin = startPos - margin 119 | if not @options.horizontal 120 | prevLabelMargin = startPos - @options.xLabelMargin 121 | else 122 | prevLabelMargin = startPos 123 | 124 | else 125 | label.remove() 126 | 127 | # get the Y position of a label on the X axis 128 | # 129 | # @private 130 | getXAxisLabelY: -> 131 | @bottom + (@options.xAxisLabelTopPadding || @options.padding / 2) 132 | 133 | # draw the data series 134 | # 135 | # @private 136 | drawSeries: -> 137 | @seriesBars = [] 138 | groupWidth = @xSize / @options.data.length 139 | 140 | if @options.stacked 141 | numBars = 1 142 | else 143 | numBars = 0 144 | for i in [0..@options.ykeys.length-1] 145 | if @hasToShow(i) 146 | numBars += 1 147 | 148 | barWidth = (groupWidth * @options.barSizeRatio - @options.barGap * (numBars - 1)) / numBars 149 | barWidth = Math.min(barWidth, @options.barSize) if @options.barSize 150 | spaceLeft = groupWidth - barWidth * numBars - @options.barGap * (numBars - 1) 151 | leftPadding = spaceLeft / 2 152 | zeroPos = if @ymin <= 0 and @ymax >= 0 then @transY(0) else null 153 | @bars = for row, idx in @data 154 | @seriesBars[idx] = [] 155 | lastTop = 0 156 | for ypos, sidx in row._y 157 | if not @hasToShow(sidx) 158 | continue 159 | if ypos != null 160 | if zeroPos 161 | top = Math.min(ypos, zeroPos) 162 | bottom = Math.max(ypos, zeroPos) 163 | else 164 | top = ypos 165 | bottom = @bottom 166 | 167 | left = @xStart + idx * groupWidth + leftPadding 168 | left += sidx * (barWidth + @options.barGap) unless @options.stacked 169 | size = bottom - top 170 | 171 | if @options.verticalGridCondition and @options.verticalGridCondition(row.x) 172 | if not @options.horizontal 173 | @drawBar(@xStart + idx * groupWidth, @yEnd, groupWidth, @ySize, @options.verticalGridColor, @options.verticalGridOpacity, @options.barRadius) 174 | else 175 | @drawBar(@yStart, @xStart + idx * groupWidth, @ySize, groupWidth, @options.verticalGridColor, @options.verticalGridOpacity, @options.barRadius) 176 | 177 | 178 | top -= lastTop if @options.stacked 179 | if not @options.horizontal 180 | lastTop += size 181 | @seriesBars[idx][sidx] = @drawBar(left, top, barWidth, size, @colorFor(row, sidx, 'bar'), 182 | @options.barOpacity, @options.barRadius) 183 | else 184 | lastTop -= size 185 | @seriesBars[idx][sidx] = @drawBar(top, left, size, barWidth, @colorFor(row, sidx, 'bar'), 186 | @options.barOpacity, @options.barRadius) 187 | 188 | if @options.inBarValue and 189 | barWidth > @options.gridTextSize + 2*@options.inBarValueMinTopMargin 190 | barMiddle = left + 0.5 * barWidth 191 | @raphael.text(bottom - @options.inBarValueRightMargin, barMiddle, @yLabelFormat(row.y[sidx], sidx)) 192 | .attr('font-size', @options.gridTextSize) 193 | .attr('font-family', @options.gridTextFamily) 194 | .attr('font-weight', @options.gridTextWeight) 195 | .attr('fill', @options.inBarValueTextColor) 196 | .attr('text-anchor', 'end') 197 | 198 | else 199 | null 200 | 201 | @flat_bars = $.map @bars, (n) -> return n 202 | @flat_bars = $.grep @flat_bars, (n) -> return n? 203 | @bar_els = $($.map @flat_bars, (n) -> return n[0]) 204 | 205 | # hightlight the bar on hover 206 | # 207 | # @private 208 | hilight: (index) -> 209 | if @seriesBars && @seriesBars[@prevHilight] && @prevHilight != null && @prevHilight != index 210 | for y,i in @seriesBars[@prevHilight] 211 | if y 212 | y.animate({'fill-opacity': @options.barOpacity}, @options.highlightSpeed) 213 | 214 | if @seriesBars && @seriesBars[index] && index != null && @prevHilight != index 215 | for y,i in @seriesBars[index] 216 | if y 217 | y.animate({'fill-opacity': @options.barHighlightOpacity}, @options.highlightSpeed) 218 | 219 | @prevHilight = index 220 | 221 | # @private 222 | # 223 | # @param row [Object] row data 224 | # @param sidx [Number] series index 225 | # @param type [String] "bar", "hover" or "label" 226 | colorFor: (row, sidx, type) -> 227 | if typeof @options.barColors is 'function' 228 | r = { x: row.x, y: row.y[sidx], label: row.label, src: row.src} 229 | s = { index: sidx, key: @options.ykeys[sidx], label: @options.labels[sidx] } 230 | @options.barColors.call(@, r, s, type) 231 | else 232 | @options.barColors[sidx % @options.barColors.length] 233 | 234 | # hit test - returns the index of the row at the given x-coordinate 235 | # 236 | hitTest: (x, y) -> 237 | return null if @data.length == 0 238 | if not @options.horizontal 239 | pos = x 240 | else 241 | pos = y 242 | 243 | pos = Math.max(Math.min(pos, @xEnd), @xStart) 244 | Math.min(@data.length - 1, 245 | Math.floor((pos - @xStart) / (@xSize / @data.length))) 246 | 247 | 248 | # click on grid event handler 249 | # 250 | # @private 251 | onGridClick: (x, y) => 252 | index = @hitTest(x, y) 253 | bar_hit = !!@bar_els.filter(() -> $(@).is(':hover')).length 254 | @fire 'click', index, @data[index].src, x, y, bar_hit 255 | 256 | # hover movement event handler 257 | # 258 | # @private 259 | onHoverMove: (x, y) => 260 | index = @hitTest(x, y) 261 | @hilight(index) 262 | if index? 263 | @hover.update(@hoverContentForRow(index)...) 264 | else 265 | @hover.hide() 266 | 267 | # hover out event handler 268 | # 269 | # @private 270 | onHoverOut: => 271 | @hilight(-1) 272 | if @options.hideHover isnt false 273 | @hover.hide() 274 | 275 | # hover content for a point 276 | # 277 | # @private 278 | hoverContentForRow: (index) -> 279 | row = @data[index] 280 | content = $("
").text(row.label) 281 | content = content.prop('outerHTML') 282 | for y, j in row.y 283 | if @options.labels[j] is false 284 | continue 285 | 286 | content += """ 287 |
288 | #{@options.labels[j]}: 289 | #{@yLabelFormat(y, j)} 290 |
291 | """ 292 | if typeof @options.hoverCallback is 'function' 293 | content = @options.hoverCallback(index, @options, content, row.src) 294 | 295 | if not @options.horizontal 296 | x = @left + (index + 0.5) * @width / @data.length 297 | [content, x] 298 | else 299 | x = @left + 0.5 * @width 300 | y = @top + (index + 0.5) * @height / @data.length 301 | [content, x, y, true] 302 | 303 | drawBar: (xPos, yPos, width, height, barColor, opacity, radiusArray) -> 304 | maxRadius = Math.max(radiusArray...) 305 | if maxRadius == 0 or maxRadius > height 306 | path = @raphael.rect(xPos, yPos, width, height) 307 | else 308 | path = @raphael.path @roundedRect(xPos, yPos, width, height, radiusArray) 309 | path 310 | .attr('fill', barColor) 311 | .attr('fill-opacity', opacity) 312 | .attr('stroke', 'none') 313 | 314 | roundedRect: (x, y, w, h, r = [0,0,0,0]) -> 315 | [ "M", x, r[0] + y, "Q", x, y, x + r[0], y, 316 | "L", x + w - r[1], y, "Q", x + w, y, x + w, y + r[1], 317 | "L", x + w, y + h - r[2], "Q", x + w, y + h, x + w - r[2], y + h, 318 | "L", x + r[3], y + h, "Q", x, y + h, x, y + h - r[3], "Z" ] 319 | 320 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Morris.js - pretty time-series line graphs 2 | 3 | [![Build Status](https://secure.travis-ci.org/morrisjs/morris.js.png?branch=master)](http://travis-ci.org/morrisjs/morris.js) 4 | 5 | Morris.js is the library that powers the graphs on http://howmanyleft.co.uk/. 6 | It's a very simple API for drawing line, bar, area and donut charts. 7 | 8 | Cheers! 9 | 10 | \- Olly (olly@oesmith.co.uk) 11 | 12 | ## Contributors wanted 13 | 14 | I'm unfortunately not able to actively support Morris.js any more. I keep an eye 15 | on the issues, but I rarely have the time to fix bugs or review pull requests. 16 | 17 | If you're interested in actively contributing to Morris.js, please contact me on 18 | the email address above. 19 | 20 | ## Requirements 21 | 22 | - [jQuery](http://jquery.com/) (>= 1.7 recommended, but it'll probably work with 23 | older versions) 24 | - [Raphael.js](http://raphaeljs.com/) (>= 2.0) 25 | 26 | ## Usage 27 | 28 | See [the website](http://morrisjs.github.com/morris.js/). 29 | 30 | ## Development 31 | 32 | Very daring. 33 | 34 | Fork, hack, possibly even add some tests, then send a pull request :) 35 | 36 | Remember that Morris.js is a coffeescript project. Please make your changes in 37 | the `.coffee` files, not in the compiled javascript files in the root directory 38 | of the project. 39 | 40 | ### Developer quick-start 41 | 42 | You'll need [node.js](https://nodejs.org). I recommend using 43 | [nvm](https://github.com/creationix/nvm) for installing node in 44 | development environments. 45 | 46 | With node installed, install [grunt](https://github.com/cowboy/grunt) using 47 | `npm install -g grunt-cli`, and then the rest of the test/build dependencies 48 | with `npm install` in the morris.js project folder. 49 | 50 | Additionally, [bower](http://bower.io/) is required for for retrieving additional test dependencies. 51 | Install it with `npm install -g bower` and then `bower install` in the morris project folder. 52 | 53 | Once you're all set up, you can compile, minify and run the tests using `grunt`. 54 | 55 | Note: I'm experimenting with using perceptual diffs to catch rendering 56 | regressions. Due to font rendering differences between platforms, the pdiff 57 | tests currently *only* pass on OS X. 58 | 59 | ## Changelog 60 | 61 | ### 0.5.1 - 15th June 2014 62 | 63 | - Fix touch event handling. 64 | - Fix stacked=false in bar chart [#275](https://github.com/morrisjs/morris.js/issues/275) 65 | - Configurable vertical segments [#297](https://github.com/morrisjs/morris.js/issues/297) 66 | - Deprecate continuousLine option. 67 | 68 | ### 0.5.0 - 19th March 2014 69 | 70 | - Update grunt dependency [#288](https://github.com/morrisjs/morris.js/issues/228) 71 | - Donut segment color config in data objects [#281](https://github.com/morrisjs/morris.js/issues/281) 72 | - Customisable line widths and point drawing [#272](https://github.com/morrisjs/morris.js/issues/272) 73 | - Bugfix for @options.smooth [#266](https://github.com/morrisjs/morris.js/issues/266) 74 | - Option to disable axes individually [#253](https://github.com/morrisjs/morris.js/issues/253) 75 | - Range selection [#252](https://github.com/morrisjs/morris.js/issues/252) 76 | - Week format for x-labels [#250](https://github.com/morrisjs/morris.js/issues/250) 77 | - Update developer quickstart instructions [#243](https://github.com/morrisjs/morris.js/issues/243) 78 | - Experimenting with perceptual diffs. 79 | - Add original data row to hover callback [#264](https://github.com/morrisjs/morris.js/issues/264) 80 | - setData method for donut charts [#211](https://github.com/morrisjs/morris.js/issues/211) 81 | - Automatic resizing [#111](https://github.com/morrisjs/morris.js/issues/111) 82 | - Fix travis builds [#298](https://github.com/morrisjs/morris.js/issues/298) 83 | - Option for rounded corners on bar charts [#305](https://github.com/morrisjs/morris.js/issues/305) 84 | - Option to set padding for X axis labels [#306](https://github.com/morrisjs/morris.js/issues/306) 85 | - Use local javascript for examples. 86 | - Events on non-time series [#314](https://github.com/morrisjs/morris.js/issues/314) 87 | 88 | ### 0.4.3 - 12th May 2013 89 | 90 | - Fix flickering hover box [#186](https://github.com/morrisjs/morris.js/issues/186) 91 | - xLabelAngle option (diagonal labels!!) [#239](https://github.com/morrisjs/morris.js/issues/239) 92 | - Fix area chart fill bug [#190](https://github.com/morrisjs/morris.js/issues/190) 93 | - Make event handlers chainable 94 | - gridTextFamily and gridTextWeight options 95 | - Fix hovers with setData [#213](https://github.com/morrisjs/morris.js/issues/213) 96 | - Fix hideHover behaviour [#236](https://github.com/morrisjs/morris.js/issues/236) 97 | 98 | ### 0.4.2 - 14th April 2013 99 | 100 | - Fix DST handling [#191](https://github.com/morrisjs/morris.js/issues/191) 101 | - Parse data values from strings in Morris.Donut [#189](https://github.com/morrisjs/morris.js/issues/189) 102 | - Non-cumulative area charts [#199](https://github.com/morrisjs/morris.js/issues/199) 103 | - Round Y-axis labels to significant numbers [#162](https://github.com/morrisjs/morris.js/issues/162) 104 | - Customising default hover content [#179](https://github.com/morrisjs/morris.js/issues/179) 105 | 106 | ### 0.4.1 - 8th February 2013 107 | 108 | - Fix goal and event rendering. [#181](https://github.com/morrisjs/morris.js/issues/181) 109 | - Don't break when empty data is passed to setData [#142](https://github.com/morrisjs/morris.js/issues/142) 110 | - labelColor option for donuts [#159](https://github.com/morrisjs/morris.js/issues/159) 111 | 112 | ### 0.4.0 - 26th January 2013 113 | 114 | - Goals and events [#103](https://github.com/morrisjs/morris.js/issues/103). 115 | - Bower package manager metadata. 116 | - More flexible formatters [#107](https://github.com/morrisjs/morris.js/issues/107). 117 | - Color callbacks. 118 | - Decade intervals for time-axis labels. 119 | - Non-continous line tweaks [#116](https://github.com/morrisjs/morris.js/issues/116). 120 | - Stacked bars [#120](https://github.com/morrisjs/morris.js/issues/120). 121 | - HTML hover [#134](https://github.com/morrisjs/morris.js/issues/134). 122 | - yLabelFormat [#139](https://github.com/morrisjs/morris.js/issues/139). 123 | - Disable axes [#114](https://github.com/morrisjs/morris.js/issues/114). 124 | 125 | ### 0.3.3 - 1st November 2012 126 | 127 | - **Bar charts!** [#101](https://github.com/morrisjs/morris.js/issues/101). 128 | 129 | ### 0.3.2 - 28th October 2012 130 | 131 | - **Area charts!** [#47](https://github.com/morrisjs/morris.js/issues/47). 132 | - Some major refactoring and test suite improvements. 133 | - Set smooth parameter per series [#91](https://github.com/morrisjs/morris.js/issues/91). 134 | - Custom dateFormat for string x-values [#90](https://github.com/morrisjs/morris.js/issues/90). 135 | 136 | ### 0.3.1 - 13th October 2012 137 | 138 | - Add `formatter` option for customising value labels in donuts [#75](https://github.com/morrisjs/morris.js/issues/75). 139 | - Cycle `lineColors` on line charts to avoid running out of colours [#78](https://github.com/morrisjs/morris.js/issues/78). 140 | - Add method to select donut segments. [#79](https://github.com/morrisjs/morris.js/issues/79). 141 | - Don't go negative on yMin when all y values are zero. [#80](https://github.com/morrisjs/morris.js/issues/80). 142 | - Don't sort data when parseTime is false [#83](https://github.com/morrisjs/morris.js/issues/83). 143 | - Customise styling for points. [#87](https://github.com/morrisjs/morris.js/issues/87). 144 | 145 | ### 0.3.0 - 15th September 2012 146 | 147 | - Donut charts! 148 | - Bugfix: ymin/ymax bug [#71](https://github.com/morrisjs/morris.js/issues/71). 149 | - Bugfix: infinite loop when data indicates horizontal line [#66](https://github.com/morrisjs/morris.js/issues/66). 150 | 151 | ### 0.2.10 - 26th June 2012 152 | 153 | - Support for decimal labels on y-axis [#58](https://github.com/morrisjs/morris.js/issues/58). 154 | - Better axis label clipping [#63](https://github.com/morrisjs/morris.js/issues/63). 155 | - Redraw graphs with updated data using `setData` method [#64](https://github.com/morrisjs/morris.js/issues/64). 156 | - Bugfix: series with zero or one non-null values [#65](https://github.com/morrisjs/morris.js/issues/65). 157 | 158 | ### 0.2.9 - 15th May 2012 159 | 160 | - Bugfix: Fix zero-value regression 161 | - Bugfix: Don't modify user-supplied data 162 | 163 | ### 0.2.8 - 10th May 2012 164 | 165 | - Customising x-axis labels with `xLabelFormat` option 166 | - Only use timezones when timezone info is specified 167 | - Fix old IE bugs (mostly in examples!) 168 | - Added `preunits` and `postunits` options 169 | - Better non-continuous series data support 170 | 171 | ### 0.2.7 - 2nd April 2012 172 | 173 | - Added `xLabels` option 174 | - Refactored x-axis labelling 175 | - Better ISO date support 176 | - Fix bug with single value in non time-series graphs 177 | 178 | ### 0.2.6 - 18th March 2012 179 | 180 | - Partial series support (see `null` y-values in `examples/quarters.html`) 181 | - `parseTime` option bugfix for non-time-series data 182 | 183 | ### 0.2.5 - 15th March 2012 184 | 185 | - Raw millisecond timestamp support (with `dateFormat` option) 186 | - YYYY-MM-DD HH:MM[:SS[.SSS]] date support 187 | - Decimal number labels 188 | 189 | ### 0.2.4 - 8th March 2012 190 | 191 | - Negative y-values support 192 | - `ymin` option 193 | - `units` options 194 | 195 | ### 0.2.3 - 6th Mar 2012 196 | 197 | - jQuery no-conflict compatibility 198 | - Support ISO week-number dates 199 | - Optionally hide hover on mouseout (`hideHover`) 200 | - Optionally skip parsing dates, treating X values as an equally-spaced series (`parseTime`) 201 | 202 | ### 0.2.2 - 29th Feb 2012 203 | 204 | - Bugfix: mouseover error when options.data.length == 2 205 | - Automatically sort options.data 206 | 207 | ### 0.2.1 - 28th Feb 2012 208 | 209 | - Accept a DOM element *or* an ID in `options.element` 210 | - Add `smooth` option 211 | - Bugfix: clone `@default` 212 | - Add `ymax` option 213 | 214 | ## License 215 | 216 | Copyright (c) 2012-2014, Olly Smith 217 | All rights reserved. 218 | 219 | Redistribution and use in source and binary forms, with or without 220 | modification, are permitted provided that the following conditions are met: 221 | 222 | 1. Redistributions of source code must retain the above copyright notice, this 223 | list of conditions and the following disclaimer. 224 | 2. Redistributions in binary form must reproduce the above copyright notice, 225 | this list of conditions and the following disclaimer in the documentation 226 | and/or other materials provided with the distribution. 227 | 228 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 229 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 230 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 231 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 232 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 233 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 234 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 235 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 236 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 237 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 238 | -------------------------------------------------------------------------------- /lib/morris.line.coffee: -------------------------------------------------------------------------------- 1 | class Morris.Line extends Morris.Grid 2 | # Initialise the graph. 3 | # 4 | constructor: (options) -> 5 | return new Morris.Line(options) unless (@ instanceof Morris.Line) 6 | super(options) 7 | 8 | init: -> 9 | # Some instance variables for later 10 | if @options.hideHover isnt 'always' 11 | @hover = new Morris.Hover(parent: @el) 12 | @on('hovermove', @onHoverMove) 13 | @on('hoverout', @onHoverOut) 14 | @on('gridclick', @onGridClick) 15 | 16 | # Default configuration 17 | # 18 | defaults: 19 | lineWidth: 3 20 | pointSize: 4 21 | lineColors: [ 22 | '#0b62a4' 23 | '#7A92A3' 24 | '#4da74d' 25 | '#afd8f8' 26 | '#edc240' 27 | '#cb4b4b' 28 | '#9440ed' 29 | ] 30 | pointStrokeWidths: [1] 31 | pointStrokeColors: ['#ffffff'] 32 | pointFillColors: [] 33 | smooth: true 34 | shown: true 35 | xLabels: 'auto' 36 | xLabelFormat: null 37 | xLabelMargin: 24 38 | verticalGrid: false 39 | verticalGridHeight: 'full' 40 | verticalGridStartOffset: 0 41 | hideHover: false 42 | trendLine: false 43 | trendLineWidth: 2 44 | trendLineWeight: false 45 | trendLineColors: [ 46 | '#689bc3' 47 | '#a2b3bf' 48 | '#64b764' 49 | ] 50 | 51 | # Do any size-related calculations 52 | # 53 | # @private 54 | calc: -> 55 | @calcPoints() 56 | @generatePaths() 57 | 58 | # calculate series data point coordinates 59 | # 60 | # @private 61 | calcPoints: -> 62 | for row in @data 63 | row._x = @transX(row.x) 64 | row._y = for y in row.y 65 | if y? then @transY(y) else y 66 | row._ymax = Math.min [@bottom].concat(y for y, i in row._y when y? and @hasToShow(i))... 67 | 68 | # hit test - returns the index of the row at the given x-coordinate 69 | # 70 | hitTest: (x) -> 71 | return null if @data.length == 0 72 | # TODO better search algo 73 | for r, index in @data.slice(1) 74 | break if x < (r._x + @data[index]._x) / 2 75 | index 76 | 77 | # click on grid event handler 78 | # 79 | # @private 80 | onGridClick: (x, y) => 81 | index = @hitTest(x) 82 | @fire 'click', index, @data[index].src, x, y 83 | 84 | # hover movement event handler 85 | # 86 | # @private 87 | onHoverMove: (x, y) => 88 | index = @hitTest(x) 89 | @displayHoverForRow(index) 90 | 91 | # hover out event handler 92 | # 93 | # @private 94 | onHoverOut: => 95 | if @options.hideHover isnt false 96 | @displayHoverForRow(null) 97 | 98 | # display a hover popup over the given row 99 | # 100 | # @private 101 | displayHoverForRow: (index) -> 102 | if index? 103 | @hover.update(@hoverContentForRow(index)...) 104 | @hilight(index) 105 | else 106 | @hover.hide() 107 | @hilight() 108 | 109 | # hover content for a point 110 | # 111 | # @private 112 | hoverContentForRow: (index) -> 113 | row = @data[index] 114 | content = $("
").text(row.label) 115 | content = content.prop('outerHTML') 116 | for y, j in row.y 117 | if @options.labels[j] is false 118 | continue 119 | 120 | content += """ 121 |
122 | #{@options.labels[j]}: 123 | #{@yLabelFormat(y, j)} 124 |
125 | """ 126 | if typeof @options.hoverCallback is 'function' 127 | content = @options.hoverCallback(index, @options, content, row.src) 128 | [content, row._x, row._ymax] 129 | 130 | 131 | # generate paths for series lines 132 | # 133 | # @private 134 | generatePaths: -> 135 | @paths = for i in [0...@options.ykeys.length] 136 | smooth = if typeof @options.smooth is "boolean" then @options.smooth else @options.ykeys[i] in @options.smooth 137 | coords = ({x: r._x, y: r._y[i]} for r in @data when r._y[i] isnt undefined) 138 | 139 | if coords.length > 1 140 | Morris.Line.createPath coords, smooth, @bottom 141 | else 142 | null 143 | 144 | # Draws the line chart. 145 | # 146 | draw: -> 147 | @drawXAxis() if @options.axes in [true, 'both', 'x'] 148 | @drawSeries() 149 | if @options.hideHover is false 150 | @displayHoverForRow(@data.length - 1) 151 | 152 | # draw the x-axis labels 153 | # 154 | # @private 155 | drawXAxis: -> 156 | # draw x axis labels 157 | ypos = @bottom + @options.padding / 2 158 | prevLabelMargin = null 159 | prevAngleMargin = null 160 | 161 | drawLabel = (labelText, xpos) => 162 | label = @drawXAxisLabel(@transX(xpos), ypos, labelText) 163 | textBox = label.getBBox() 164 | label.transform("r#{-@options.xLabelAngle}") 165 | labelBox = label.getBBox() 166 | label.transform("t0,#{labelBox.height / 2}...") 167 | if @options.xLabelAngle != 0 168 | offset = -0.5 * textBox.width * 169 | Math.cos(@options.xLabelAngle * Math.PI / 180.0) 170 | label.transform("t#{offset},0...") 171 | # try to avoid overlaps 172 | labelBox = label.getBBox() 173 | if (not prevLabelMargin? or 174 | prevLabelMargin >= labelBox.x + labelBox.width or 175 | prevAngleMargin? and prevAngleMargin >= labelBox.x) and 176 | labelBox.x >= 0 and (labelBox.x + labelBox.width) < @el.width() 177 | if @options.xLabelAngle != 0 178 | margin = 1.25 * @options.gridTextSize / 179 | Math.sin(@options.xLabelAngle * Math.PI / 180.0) 180 | prevAngleMargin = labelBox.x - margin 181 | prevLabelMargin = labelBox.x - @options.xLabelMargin 182 | if @options.verticalGrid is true 183 | @drawVerticalGridLine(xpos) 184 | 185 | else 186 | label.remove() 187 | 188 | if @options.parseTime 189 | if @data.length == 1 and @options.xLabels == 'auto' 190 | # where there's only one value in the series, we can't make a 191 | # sensible guess for an x labelling scheme, so just use the original 192 | # column label 193 | labels = [[@data[0].label, @data[0].x]] 194 | else 195 | labels = Morris.labelSeries(@xmin, @xmax, @width, @options.xLabels, @options.xLabelFormat) 196 | else if @options.customLabels 197 | labels = ([row.label, row.x] for row in @options.customLabels) 198 | else 199 | labels = ([row.label, row.x] for row in @data) 200 | labels.reverse() 201 | for l in labels 202 | drawLabel(l[0], l[1]) 203 | 204 | if typeof @options.verticalGrid is 'string' 205 | lines = Morris.labelSeries(@xmin, @xmax, @width, @options.verticalGrid) 206 | for l in lines 207 | @drawVerticalGridLine(l[1]) 208 | 209 | # Draw a vertical grid line 210 | # 211 | # @private 212 | drawVerticalGridLine: (xpos) -> 213 | xpos = Math.floor(@transX(xpos)) + 0.5 214 | yStart = @yStart + @options.verticalGridStartOffset 215 | if @options.verticalGridHeight is 'full' 216 | yEnd = @yEnd 217 | else 218 | yEnd = @yStart - @options.verticalGridHeight 219 | @drawGridLine("M#{xpos},#{yStart}V#{yEnd}") 220 | 221 | # draw the data series 222 | # 223 | # @private 224 | drawSeries: -> 225 | @seriesPoints = [] 226 | for i in [@options.ykeys.length-1..0] 227 | if @hasToShow(i) 228 | if @options.trendLine isnt false and 229 | @options.trendLine is true or @options.trendLine[i] is true 230 | @_drawTrendLine i 231 | 232 | @_drawLineFor i 233 | 234 | for i in [@options.ykeys.length-1..0] 235 | if @hasToShow(i) 236 | @_drawPointFor i 237 | 238 | _drawPointFor: (index) -> 239 | @seriesPoints[index] = [] 240 | for row in @data 241 | circle = null 242 | if row._y[index]? 243 | circle = @drawLinePoint(row._x, row._y[index], @colorFor(row, index, 'point'), index) 244 | @seriesPoints[index].push(circle) 245 | 246 | _drawLineFor: (index) -> 247 | path = @paths[index] 248 | if path isnt null 249 | @drawLinePath path, @colorFor(null, index, 'line'), index 250 | 251 | _drawTrendLine: (index) -> 252 | # Least squares fitting for y = x * a + b 253 | sum_x = 0 254 | sum_y = 0 255 | sum_xx = 0 256 | sum_xy = 0 257 | datapoints = 0 258 | 259 | for val, i in @data 260 | x = val.x 261 | y = val.y[index] 262 | if y is undefined 263 | continue 264 | if @options.trendLineWeight is false 265 | weight = 1 266 | else 267 | weight = @options.data[i][@options.trendLineWeight] 268 | datapoints += weight 269 | 270 | sum_x += x * weight 271 | sum_y += y * weight 272 | sum_xx += x * x * weight 273 | sum_xy += x * y * weight 274 | 275 | a = (datapoints*sum_xy - sum_x*sum_y) / (datapoints*sum_xx - sum_x*sum_x) 276 | b = (sum_y / datapoints) - ((a * sum_x) / datapoints) 277 | 278 | data = [{}, {}] 279 | data[0].x = @transX(@data[0].x) 280 | data[0].y = @transY(@data[0].x * a + b) 281 | data[1].x = @transX(@data[@data.length - 1].x) 282 | data[1].y = @transY(@data[@data.length - 1].x * a + b) 283 | 284 | path = Morris.Line.createPath data, false, @bottom 285 | path = @raphael.path(path) 286 | .attr('stroke', @colorFor(null, index, 'trendLine')) 287 | .attr('stroke-width', @options.trendLineWidth) 288 | 289 | 290 | # create a path for a data series 291 | # 292 | # @private 293 | @createPath: (coords, smooth, bottom) -> 294 | path = "" 295 | grads = Morris.Line.gradients(coords) if smooth 296 | 297 | prevCoord = {y: null} 298 | for coord, i in coords 299 | if coord.y? 300 | if prevCoord.y? 301 | if smooth 302 | g = grads[i] 303 | lg = grads[i - 1] 304 | ix = (coord.x - prevCoord.x) / 4 305 | x1 = prevCoord.x + ix 306 | y1 = Math.min(bottom, prevCoord.y + ix * lg) 307 | x2 = coord.x - ix 308 | y2 = Math.min(bottom, coord.y - ix * g) 309 | path += "C#{x1},#{y1},#{x2},#{y2},#{coord.x},#{coord.y}" 310 | else 311 | path += "L#{coord.x},#{coord.y}" 312 | else 313 | if not smooth or grads[i]? 314 | path += "M#{coord.x},#{coord.y}" 315 | prevCoord = coord 316 | return path 317 | 318 | # calculate a gradient at each point for a series of points 319 | # 320 | # @private 321 | @gradients: (coords) -> 322 | grad = (a, b) -> (a.y - b.y) / (a.x - b.x) 323 | for coord, i in coords 324 | if coord.y? 325 | nextCoord = coords[i + 1] or {y: null} 326 | prevCoord = coords[i - 1] or {y: null} 327 | if prevCoord.y? and nextCoord.y? 328 | grad(prevCoord, nextCoord) 329 | else if prevCoord.y? 330 | grad(prevCoord, coord) 331 | else if nextCoord.y? 332 | grad(coord, nextCoord) 333 | else 334 | null 335 | else 336 | null 337 | 338 | # @private 339 | hilight: (index) => 340 | if @prevHilight isnt null and @prevHilight isnt index 341 | for i in [0..@seriesPoints.length-1] 342 | if @hasToShow(i) and @seriesPoints[i][@prevHilight] 343 | @seriesPoints[i][@prevHilight].animate @pointShrinkSeries(i) 344 | if index isnt null and @prevHilight isnt index 345 | for i in [0..@seriesPoints.length-1] 346 | if @hasToShow(i) and @seriesPoints[i][index] 347 | @seriesPoints[i][index].animate @pointGrowSeries(i) 348 | @prevHilight = index 349 | 350 | colorFor: (row, sidx, type) -> 351 | if typeof @options.lineColors is 'function' 352 | @options.lineColors.call(@, row, sidx, type) 353 | else if type is 'point' 354 | @options.pointFillColors[sidx % @options.pointFillColors.length] || @options.lineColors[sidx % @options.lineColors.length] 355 | else if type is 'trendLine' 356 | @options.trendLineColors[sidx % @options.trendLineColors.length] 357 | else 358 | @options.lineColors[sidx % @options.lineColors.length] 359 | 360 | drawLinePath: (path, lineColor, lineIndex) -> 361 | @raphael.path(path) 362 | .attr('stroke', lineColor) 363 | .attr('stroke-width', @lineWidthForSeries(lineIndex)) 364 | 365 | drawLinePoint: (xPos, yPos, pointColor, lineIndex) -> 366 | @raphael.circle(xPos, yPos, @pointSizeForSeries(lineIndex)) 367 | .attr('fill', pointColor) 368 | .attr('stroke-width', @pointStrokeWidthForSeries(lineIndex)) 369 | .attr('stroke', @pointStrokeColorForSeries(lineIndex)) 370 | 371 | # @private 372 | pointStrokeWidthForSeries: (index) -> 373 | @options.pointStrokeWidths[index % @options.pointStrokeWidths.length] 374 | 375 | # @private 376 | pointStrokeColorForSeries: (index) -> 377 | @options.pointStrokeColors[index % @options.pointStrokeColors.length] 378 | 379 | # @private 380 | lineWidthForSeries: (index) -> 381 | if (@options.lineWidth instanceof Array) 382 | @options.lineWidth[index % @options.lineWidth.length] 383 | else 384 | @options.lineWidth 385 | 386 | # @private 387 | pointSizeForSeries: (index) -> 388 | if (@options.pointSize instanceof Array) 389 | @options.pointSize[index % @options.pointSize.length] 390 | else 391 | @options.pointSize 392 | 393 | # @private 394 | pointGrowSeries: (index) -> 395 | if @pointSizeForSeries(index) is 0 396 | return 397 | Raphael.animation r: @pointSizeForSeries(index) + 3, 25, 'linear' 398 | 399 | # @private 400 | pointShrinkSeries: (index) -> 401 | Raphael.animation r: @pointSizeForSeries(index), 25, 'linear' 402 | 403 | # generate a series of label, timestamp pairs for x-axis labels 404 | # 405 | # @private 406 | Morris.labelSeries = (dmin, dmax, pxwidth, specName, xLabelFormat) -> 407 | ddensity = 200 * (dmax - dmin) / pxwidth # seconds per `margin` pixels 408 | d0 = new Date(dmin) 409 | spec = Morris.LABEL_SPECS[specName] 410 | # if the spec doesn't exist, search for the closest one in the list 411 | if spec is undefined 412 | for name in Morris.AUTO_LABEL_ORDER 413 | s = Morris.LABEL_SPECS[name] 414 | if ddensity >= s.span 415 | spec = s 416 | break 417 | # if we run out of options, use second-intervals 418 | if spec is undefined 419 | spec = Morris.LABEL_SPECS["second"] 420 | # check if there's a user-defined formatting function 421 | if xLabelFormat 422 | spec = $.extend({}, spec, {fmt: xLabelFormat}) 423 | # calculate labels 424 | d = spec.start(d0) 425 | ret = [] 426 | while (t = d.getTime()) <= dmax 427 | if t >= dmin 428 | ret.push [spec.fmt(d), t] 429 | spec.incr(d) 430 | return ret 431 | 432 | # @private 433 | minutesSpecHelper = (interval) -> 434 | span: interval * 60 * 1000 435 | start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()) 436 | fmt: (d) -> "#{Morris.pad2(d.getHours())}:#{Morris.pad2(d.getMinutes())}" 437 | incr: (d) -> d.setUTCMinutes(d.getUTCMinutes() + interval) 438 | 439 | # @private 440 | secondsSpecHelper = (interval) -> 441 | span: interval * 1000 442 | start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes()) 443 | fmt: (d) -> "#{Morris.pad2(d.getHours())}:#{Morris.pad2(d.getMinutes())}:#{Morris.pad2(d.getSeconds())}" 444 | incr: (d) -> d.setUTCSeconds(d.getUTCSeconds() + interval) 445 | 446 | Morris.LABEL_SPECS = 447 | "decade": 448 | span: 172800000000 # 10 * 365 * 24 * 60 * 60 * 1000 449 | start: (d) -> new Date(d.getFullYear() - d.getFullYear() % 10, 0, 1) 450 | fmt: (d) -> "#{d.getFullYear()}" 451 | incr: (d) -> d.setFullYear(d.getFullYear() + 10) 452 | "year": 453 | span: 17280000000 # 365 * 24 * 60 * 60 * 1000 454 | start: (d) -> new Date(d.getFullYear(), 0, 1) 455 | fmt: (d) -> "#{d.getFullYear()}" 456 | incr: (d) -> d.setFullYear(d.getFullYear() + 1) 457 | "month": 458 | span: 2419200000 # 28 * 24 * 60 * 60 * 1000 459 | start: (d) -> new Date(d.getFullYear(), d.getMonth(), 1) 460 | fmt: (d) -> "#{d.getFullYear()}-#{Morris.pad2(d.getMonth() + 1)}" 461 | incr: (d) -> d.setMonth(d.getMonth() + 1) 462 | "week": 463 | span: 604800000 # 7 * 24 * 60 * 60 * 1000 464 | start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate()) 465 | fmt: (d) -> "#{d.getFullYear()}-#{Morris.pad2(d.getMonth() + 1)}-#{Morris.pad2(d.getDate())}" 466 | incr: (d) -> d.setDate(d.getDate() + 7) 467 | "day": 468 | span: 86400000 # 24 * 60 * 60 * 1000 469 | start: (d) -> new Date(d.getFullYear(), d.getMonth(), d.getDate()) 470 | fmt: (d) -> "#{d.getFullYear()}-#{Morris.pad2(d.getMonth() + 1)}-#{Morris.pad2(d.getDate())}" 471 | incr: (d) -> d.setDate(d.getDate() + 1) 472 | "hour": minutesSpecHelper(60) 473 | "30min": minutesSpecHelper(30) 474 | "15min": minutesSpecHelper(15) 475 | "10min": minutesSpecHelper(10) 476 | "5min": minutesSpecHelper(5) 477 | "minute": minutesSpecHelper(1) 478 | "30sec": secondsSpecHelper(30) 479 | "15sec": secondsSpecHelper(15) 480 | "10sec": secondsSpecHelper(10) 481 | "5sec": secondsSpecHelper(5) 482 | "second": secondsSpecHelper(1) 483 | 484 | Morris.AUTO_LABEL_ORDER = [ 485 | "decade", "year", "month", "week", "day", "hour", 486 | "30min", "15min", "10min", "5min", "minute", 487 | "30sec", "15sec", "10sec", "5sec", "second" 488 | ] 489 | -------------------------------------------------------------------------------- /lib/morris.grid.coffee: -------------------------------------------------------------------------------- 1 | class Morris.Grid extends Morris.EventEmitter 2 | # A generic pair of axes for line/area/bar charts. 3 | # 4 | # Draws grid lines and axis labels. 5 | # 6 | constructor: (options) -> 7 | # find the container to draw the graph in 8 | if typeof options.element is 'string' 9 | @el = $ document.getElementById(options.element) 10 | else 11 | @el = $ options.element 12 | if not @el? or @el.length == 0 13 | throw new Error("Graph container element not found") 14 | 15 | if @el.css('position') == 'static' 16 | @el.css('position', 'relative') 17 | 18 | @options = $.extend {}, @gridDefaults, (@defaults || {}), options 19 | 20 | # backwards compatibility for units -> postUnits 21 | if typeof @options.units is 'string' 22 | @options.postUnits = options.units 23 | 24 | # the raphael drawing instance 25 | @raphael = new Raphael(@el[0]) 26 | 27 | # some redraw stuff 28 | @elementWidth = null 29 | @elementHeight = null 30 | @dirty = false 31 | 32 | # range selection 33 | @selectFrom = null 34 | 35 | # more stuff 36 | @init() if @init 37 | 38 | # load data 39 | @setData @options.data 40 | 41 | # hover 42 | @el.bind 'mousemove', (evt) => 43 | offset = @el.offset() 44 | x = evt.pageX - offset.left 45 | if @selectFrom 46 | left = @data[@hitTest(Math.min(x, @selectFrom))]._x 47 | right = @data[@hitTest(Math.max(x, @selectFrom))]._x 48 | width = right - left 49 | @selectionRect.attr({ x: left, width: width }) 50 | else 51 | @fire 'hovermove', x, evt.pageY - offset.top 52 | 53 | @el.bind 'mouseleave', (evt) => 54 | if @selectFrom 55 | @selectionRect.hide() 56 | @selectFrom = null 57 | @fire 'hoverout' 58 | 59 | @el.bind 'touchstart touchmove touchend', (evt) => 60 | touch = evt.originalEvent.touches[0] or evt.originalEvent.changedTouches[0] 61 | offset = @el.offset() 62 | @fire 'hovermove', touch.pageX - offset.left, touch.pageY - offset.top 63 | 64 | @el.bind 'click', (evt) => 65 | offset = @el.offset() 66 | @fire 'gridclick', evt.pageX - offset.left, evt.pageY - offset.top 67 | 68 | if @options.rangeSelect 69 | @selectionRect = @raphael.rect(0, 0, 0, @el.innerHeight()) 70 | .attr({ fill: @options.rangeSelectColor, stroke: false }) 71 | .toBack() 72 | .hide() 73 | 74 | @el.bind 'mousedown', (evt) => 75 | offset = @el.offset() 76 | @startRange evt.pageX - offset.left 77 | 78 | @el.bind 'mouseup', (evt) => 79 | offset = @el.offset() 80 | @endRange evt.pageX - offset.left 81 | @fire 'hovermove', evt.pageX - offset.left, evt.pageY - offset.top 82 | 83 | if @options.resize 84 | $(window).bind 'resize', (evt) => 85 | if @timeoutId? 86 | window.clearTimeout @timeoutId 87 | @timeoutId = window.setTimeout @resizeHandler, 100 88 | 89 | # Disable tap highlight on iOS. 90 | @el.css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)') 91 | 92 | @postInit() if @postInit 93 | 94 | # Default options 95 | # 96 | gridDefaults: 97 | dateFormat: null 98 | axes: true 99 | freePosition: false 100 | grid: true 101 | gridLineColor: '#aaa' 102 | gridStrokeWidth: 0.5 103 | gridTextColor: '#888' 104 | gridTextSize: 12 105 | gridTextFamily: 'sans-serif' 106 | gridTextWeight: 'normal' 107 | hideHover: false 108 | yLabelFormat: null 109 | yLabelAlign: 'right' 110 | xLabelAngle: 0 111 | numLines: 5 112 | padding: 25 113 | parseTime: true 114 | postUnits: '' 115 | preUnits: '' 116 | ymax: 'auto' 117 | ymin: 'auto 0' 118 | goals: [] 119 | goalStrokeWidth: 1.0 120 | goalLineColors: [ 121 | '#666633' 122 | '#999966' 123 | '#cc6666' 124 | '#663333' 125 | ] 126 | events: [] 127 | eventStrokeWidth: 1.0 128 | eventLineColors: [ 129 | '#005a04' 130 | '#ccffbb' 131 | '#3a5f0b' 132 | '#005502' 133 | ] 134 | rangeSelect: null 135 | rangeSelectColor: '#eef' 136 | resize: false 137 | 138 | # Update the data series and redraw the chart. 139 | # 140 | setData: (data, redraw = true) -> 141 | @options.data = data 142 | 143 | if !data? or data.length == 0 144 | @data = [] 145 | @raphael.clear() 146 | @hover.hide() if @hover? 147 | return 148 | 149 | ymax = if @cumulative then 0 else null 150 | ymin = if @cumulative then 0 else null 151 | 152 | if @options.goals.length > 0 153 | minGoal = Math.min @options.goals... 154 | maxGoal = Math.max @options.goals... 155 | ymin = if ymin? then Math.min(ymin, minGoal) else minGoal 156 | ymax = if ymax? then Math.max(ymax, maxGoal) else maxGoal 157 | 158 | @data = for row, index in data 159 | ret = {src: row} 160 | 161 | ret.label = row[@options.xkey] 162 | if @options.parseTime 163 | ret.x = Morris.parseDate(ret.label) 164 | if @options.dateFormat 165 | ret.label = @options.dateFormat ret.x 166 | else if typeof ret.label is 'number' 167 | ret.label = new Date(ret.label).toString() 168 | else if @options.freePosition 169 | ret.x = parseFloat(row[@options.xkey]) 170 | if @options.xLabelFormat 171 | ret.label = @options.xLabelFormat ret 172 | else 173 | ret.x = index 174 | if @options.xLabelFormat 175 | ret.label = @options.xLabelFormat ret 176 | total = 0 177 | ret.y = for ykey, idx in @options.ykeys 178 | yval = row[ykey] 179 | yval = parseFloat(yval) if typeof yval is 'string' 180 | yval = null if yval? and typeof yval isnt 'number' 181 | if yval? and @hasToShow(idx) 182 | if @cumulative 183 | total += yval 184 | else 185 | if ymax? 186 | ymax = Math.max(yval, ymax) 187 | ymin = Math.min(yval, ymin) 188 | else 189 | ymax = ymin = yval 190 | if @cumulative and total? 191 | ymax = Math.max(total, ymax) 192 | ymin = Math.min(total, ymin) 193 | yval 194 | ret 195 | 196 | if @options.parseTime or @options.freePosition 197 | @data = @data.sort (a, b) -> (a.x > b.x) - (b.x > a.x) 198 | 199 | # calculate horizontal range of the graph 200 | @xmin = @data[0].x 201 | @xmax = @data[@data.length - 1].x 202 | 203 | @events = [] 204 | if @options.events.length > 0 205 | if @options.parseTime 206 | for e in @options.events 207 | if e instanceof Array 208 | [from, to] = e 209 | @events.push([Morris.parseDate(from), Morris.parseDate(to)]) 210 | else 211 | @events.push(Morris.parseDate(e)) 212 | else 213 | @events = @options.events 214 | flatEvents = $.map @events, (e) -> e 215 | @xmax = Math.max(@xmax, Math.max(flatEvents...)) 216 | @xmin = Math.min(@xmin, Math.min(flatEvents...)) 217 | 218 | if @xmin is @xmax 219 | @xmin -= 1 220 | @xmax += 1 221 | 222 | @ymin = @yboundary('min', ymin) 223 | @ymax = @yboundary('max', ymax) 224 | 225 | if @ymin is @ymax 226 | @ymin -= 1 if ymin 227 | @ymax += 1 228 | 229 | if @options.axes in [true, 'both', 'y'] or @options.grid is true 230 | if (@options.ymax == @gridDefaults.ymax and 231 | @options.ymin == @gridDefaults.ymin) 232 | # calculate 'magic' grid placement 233 | @grid = @autoGridLines(@ymin, @ymax, @options.numLines) 234 | @ymin = Math.min(@ymin, @grid[0]) 235 | @ymax = Math.max(@ymax, @grid[@grid.length - 1]) 236 | else 237 | step = (@ymax - @ymin) / (@options.numLines - 1) 238 | @grid = (y for y in [@ymin..@ymax] by step) 239 | 240 | @dirty = true 241 | @redraw() if redraw 242 | 243 | yboundary: (boundaryType, currentValue) -> 244 | boundaryOption = @options["y#{boundaryType}"] 245 | if typeof boundaryOption is 'string' 246 | if boundaryOption[0..3] is 'auto' 247 | if boundaryOption.length > 5 248 | suggestedValue = parseInt(boundaryOption[5..], 10) 249 | return suggestedValue unless currentValue? 250 | Math[boundaryType](currentValue, suggestedValue) 251 | else 252 | if currentValue? then currentValue else 0 253 | else 254 | parseInt(boundaryOption, 10) 255 | else 256 | boundaryOption 257 | 258 | autoGridLines: (ymin, ymax, nlines) -> 259 | span = ymax - ymin 260 | ymag = Math.floor(Math.log(span) / Math.log(10)) 261 | unit = Math.pow(10, ymag) 262 | 263 | # calculate initial grid min and max values 264 | gmin = Math.floor(ymin / unit) * unit 265 | gmax = Math.ceil(ymax / unit) * unit 266 | step = (gmax - gmin) / (nlines - 1) 267 | if unit == 1 and step > 1 and Math.ceil(step) != step 268 | step = Math.ceil(step) 269 | gmax = gmin + step * (nlines - 1) 270 | 271 | # ensure zero is plotted where the range includes zero 272 | if gmin < 0 and gmax > 0 273 | gmin = Math.floor(ymin / step) * step 274 | gmax = Math.ceil(ymax / step) * step 275 | 276 | # special case for decimal numbers 277 | if step < 1 278 | smag = Math.floor(Math.log(step) / Math.log(10)) 279 | grid = for y in [gmin..gmax] by step 280 | parseFloat(y.toFixed(1 - smag)) 281 | else 282 | grid = (y for y in [gmin..gmax] by step) 283 | grid 284 | 285 | _calc: -> 286 | w = @el.width() 287 | h = @el.height() 288 | 289 | if @elementWidth != w or @elementHeight != h or @dirty 290 | @elementWidth = w 291 | @elementHeight = h 292 | @dirty = false 293 | # recalculate grid dimensions 294 | @left = @options.padding 295 | @right = @elementWidth - @options.padding 296 | @top = @options.padding 297 | @bottom = @elementHeight - @options.padding 298 | if @options.axes in [true, 'both', 'y'] 299 | yLabelWidths = for gridLine in @grid 300 | @measureText(@yAxisFormat(gridLine)).width 301 | 302 | if not @options.horizontal 303 | @left += Math.max(yLabelWidths...) 304 | else 305 | @bottom -= Math.max(yLabelWidths...) 306 | 307 | if @options.axes in [true, 'both', 'x'] 308 | if not @options.horizontal 309 | angle = -@options.xLabelAngle 310 | else 311 | angle = -90 312 | 313 | bottomOffsets = for i in [0...@data.length] 314 | @measureText(@data[i].label, angle).height 315 | 316 | if not @options.horizontal 317 | @bottom -= Math.max(bottomOffsets...) 318 | else 319 | @left += Math.max(bottomOffsets...) 320 | 321 | @width = Math.max(1, @right - @left) 322 | @height = Math.max(1, @bottom - @top) 323 | 324 | if not @options.horizontal 325 | @dx = @width / (@xmax - @xmin) 326 | @dy = @height / (@ymax - @ymin) 327 | 328 | @yStart = @bottom 329 | @yEnd = @top 330 | @xStart = @left 331 | @xEnd = @right 332 | 333 | @xSize = @width 334 | @ySize = @height 335 | else 336 | @dx = @height / (@xmax - @xmin) 337 | @dy = @width / (@ymax - @ymin) 338 | 339 | @yStart = @left 340 | @yEnd = @right 341 | @xStart = @top 342 | @xEnd = @bottom 343 | 344 | @xSize = @height 345 | @ySize = @width 346 | 347 | @calc() if @calc 348 | 349 | # Quick translation helpers 350 | # 351 | transY: (y) -> 352 | if not @options.horizontal 353 | @bottom - (y - @ymin) * @dy 354 | else 355 | @left + (y - @ymin) * @dy 356 | transX: (x) -> 357 | if @data.length == 1 358 | (@xStart + @xEnd) / 2 359 | else 360 | @xStart + (x - @xmin) * @dx 361 | 362 | 363 | # Draw it! 364 | # 365 | # If you need to re-size your charts, call this method after changing the 366 | # size of the container element. 367 | redraw: -> 368 | @raphael.clear() 369 | @_calc() 370 | @drawGrid() 371 | @drawGoals() 372 | @drawEvents() 373 | @draw() if @draw 374 | 375 | # @private 376 | # 377 | measureText: (text, angle = 0) -> 378 | tt = @raphael.text(100, 100, text) 379 | .attr('font-size', @options.gridTextSize) 380 | .attr('font-family', @options.gridTextFamily) 381 | .attr('font-weight', @options.gridTextWeight) 382 | .rotate(angle) 383 | ret = tt.getBBox() 384 | tt.remove() 385 | ret 386 | 387 | # @private 388 | # 389 | yAxisFormat: (label) -> @yLabelFormat(label, 0) 390 | 391 | # @private 392 | # 393 | yLabelFormat: (label, i) -> 394 | if typeof @options.yLabelFormat is 'function' 395 | @options.yLabelFormat(label, i) 396 | else 397 | "#{@options.preUnits}#{Morris.commas(label)}#{@options.postUnits}" 398 | 399 | # get the X position of a label on the Y axis 400 | # 401 | # @private 402 | getYAxisLabelX: -> 403 | if @options.yLabelAlign is 'right' 404 | @left - @options.padding / 2 405 | else 406 | @options.padding / 2 407 | 408 | 409 | # draw y axis labels, horizontal lines 410 | # 411 | drawGrid: -> 412 | return if @options.grid is false and @options.axes not in [true, 'both', 'y'] 413 | 414 | if not @options.horizontal 415 | basePos = @getYAxisLabelX() 416 | else 417 | basePos = @getXAxisLabelY() 418 | 419 | for lineY in @grid 420 | pos = @transY(lineY) 421 | if @options.axes in [true, 'both', 'y'] 422 | if not @options.horizontal 423 | @drawYAxisLabel(basePos, pos, @yAxisFormat(lineY)) 424 | else 425 | @drawXAxisLabel(pos, basePos, @yAxisFormat(lineY)) 426 | 427 | if @options.grid 428 | pos = Math.floor(pos) + 0.5 429 | if not @options.horizontal 430 | @drawGridLine("M#{@xStart},#{pos}H#{@xEnd}") 431 | else 432 | @drawGridLine("M#{pos},#{@xStart}V#{@xEnd}") 433 | 434 | # draw goals horizontal lines 435 | # 436 | drawGoals: -> 437 | for goal, i in @options.goals 438 | color = @options.goalLineColors[i % @options.goalLineColors.length] 439 | @drawGoal(goal, color) 440 | 441 | # draw events vertical lines 442 | drawEvents: -> 443 | for event, i in @events 444 | color = @options.eventLineColors[i % @options.eventLineColors.length] 445 | @drawEvent(event, color) 446 | 447 | drawGoal: (goal, color) -> 448 | y = Math.floor(@transY(goal)) + 0.5 449 | if not @options.horizontal 450 | path = "M#{@xStart},#{y}H#{@xEnd}" 451 | else 452 | path = "M#{y},#{@xStart}V#{@xEnd}" 453 | 454 | @raphael.path(path) 455 | .attr('stroke', color) 456 | .attr('stroke-width', @options.goalStrokeWidth) 457 | 458 | drawEvent: (event, color) -> 459 | if event instanceof Array 460 | [from, to] = event 461 | from = Math.floor(@transX(from)) + 0.5 462 | to = Math.floor(@transX(to)) + 0.5 463 | 464 | if not @options.horizontal 465 | @raphael.rect(from, @yEnd, to-from, @yStart-@yEnd) 466 | .attr({ fill: color, stroke: false }) 467 | .toBack() 468 | else 469 | @raphael.rect(@yStart, from, @yEnd-@yStart, to-from) 470 | .attr({ fill: color, stroke: false }) 471 | .toBack() 472 | 473 | else 474 | x = Math.floor(@transX(event)) + 0.5 475 | if not @options.horizontal 476 | path = "M#{x},#{@yStart}V#{@yEnd}" 477 | else 478 | path = "M#{@yStart},#{x}H#{@yEnd}" 479 | 480 | @raphael.path(path) 481 | .attr('stroke', color) 482 | .attr('stroke-width', @options.eventStrokeWidth) 483 | 484 | drawYAxisLabel: (xPos, yPos, text) -> 485 | label = @raphael.text(xPos, yPos, text) 486 | .attr('font-size', @options.gridTextSize) 487 | .attr('font-family', @options.gridTextFamily) 488 | .attr('font-weight', @options.gridTextWeight) 489 | .attr('fill', @options.gridTextColor) 490 | if @options.yLabelAlign == 'right' 491 | label.attr('text-anchor', 'end') 492 | else 493 | label.attr('text-anchor', 'start') 494 | 495 | drawXAxisLabel: (xPos, yPos, text) -> 496 | @raphael.text(xPos, yPos, text) 497 | .attr('font-size', @options.gridTextSize) 498 | .attr('font-family', @options.gridTextFamily) 499 | .attr('font-weight', @options.gridTextWeight) 500 | .attr('fill', @options.gridTextColor) 501 | 502 | drawGridLine: (path) -> 503 | @raphael.path(path) 504 | .attr('stroke', @options.gridLineColor) 505 | .attr('stroke-width', @options.gridStrokeWidth) 506 | 507 | # Range selection 508 | # 509 | startRange: (x) -> 510 | @hover.hide() 511 | @selectFrom = x 512 | @selectionRect.attr({ x: x, width: 0 }).show() 513 | 514 | endRange: (x) -> 515 | if @selectFrom 516 | start = Math.min(@selectFrom, x) 517 | end = Math.max(@selectFrom, x) 518 | @options.rangeSelect.call @el, 519 | start: @data[@hitTest(start)].x 520 | end: @data[@hitTest(end)].x 521 | @selectFrom = null 522 | 523 | resizeHandler: => 524 | @timeoutId = null 525 | @raphael.setSize @el.width(), @el.height() 526 | @redraw() 527 | 528 | hasToShow: (i) => 529 | @options.shown is true or @options.shown[i] is true 530 | 531 | 532 | # Parse a date into a javascript timestamp 533 | # 534 | # 535 | Morris.parseDate = (date) -> 536 | if typeof date is 'number' 537 | return date 538 | m = date.match /^(\d+) Q(\d)$/ 539 | n = date.match /^(\d+)-(\d+)$/ 540 | o = date.match /^(\d+)-(\d+)-(\d+)$/ 541 | p = date.match /^(\d+) W(\d+)$/ 542 | q = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+)(Z|([+-])(\d\d):?(\d\d))?$/ 543 | r = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+):(\d+(\.\d+)?)(Z|([+-])(\d\d):?(\d\d))?$/ 544 | if m 545 | new Date( 546 | parseInt(m[1], 10), 547 | parseInt(m[2], 10) * 3 - 1, 548 | 1).getTime() 549 | else if n 550 | new Date( 551 | parseInt(n[1], 10), 552 | parseInt(n[2], 10) - 1, 553 | 1).getTime() 554 | else if o 555 | new Date( 556 | parseInt(o[1], 10), 557 | parseInt(o[2], 10) - 1, 558 | parseInt(o[3], 10)).getTime() 559 | else if p 560 | # calculate number of weeks in year given 561 | ret = new Date(parseInt(p[1], 10), 0, 1); 562 | # first thursday in year (ISO 8601 standard) 563 | if ret.getDay() isnt 4 564 | ret.setMonth(0, 1 + ((4 - ret.getDay()) + 7) % 7); 565 | # add weeks 566 | ret.getTime() + parseInt(p[2], 10) * 604800000 567 | else if q 568 | if not q[6] 569 | # no timezone info, use local 570 | new Date( 571 | parseInt(q[1], 10), 572 | parseInt(q[2], 10) - 1, 573 | parseInt(q[3], 10), 574 | parseInt(q[4], 10), 575 | parseInt(q[5], 10)).getTime() 576 | else 577 | # timezone info supplied, use UTC 578 | offsetmins = 0 579 | if q[6] != 'Z' 580 | offsetmins = parseInt(q[8], 10) * 60 + parseInt(q[9], 10) 581 | offsetmins = 0 - offsetmins if q[7] == '+' 582 | Date.UTC( 583 | parseInt(q[1], 10), 584 | parseInt(q[2], 10) - 1, 585 | parseInt(q[3], 10), 586 | parseInt(q[4], 10), 587 | parseInt(q[5], 10) + offsetmins) 588 | else if r 589 | secs = parseFloat(r[6]) 590 | isecs = Math.floor(secs) 591 | msecs = Math.round((secs - isecs) * 1000) 592 | if not r[8] 593 | # no timezone info, use local 594 | new Date( 595 | parseInt(r[1], 10), 596 | parseInt(r[2], 10) - 1, 597 | parseInt(r[3], 10), 598 | parseInt(r[4], 10), 599 | parseInt(r[5], 10), 600 | isecs, 601 | msecs).getTime() 602 | else 603 | # timezone info supplied, use UTC 604 | offsetmins = 0 605 | if r[8] != 'Z' 606 | offsetmins = parseInt(r[10], 10) * 60 + parseInt(r[11], 10) 607 | offsetmins = 0 - offsetmins if r[9] == '+' 608 | Date.UTC( 609 | parseInt(r[1], 10), 610 | parseInt(r[2], 10) - 1, 611 | parseInt(r[3], 10), 612 | parseInt(r[4], 10), 613 | parseInt(r[5], 10) + offsetmins, 614 | isecs, 615 | msecs) 616 | else 617 | new Date(parseInt(date, 10), 0, 1).getTime() 618 | 619 | -------------------------------------------------------------------------------- /morris.min.js: -------------------------------------------------------------------------------- 1 | /* @license 2 | morris.js v0.5.1 3 | Copyright 2014 Olly Smith All rights reserved. 4 | Licensed under the BSD-2-Clause License. 5 | */ 6 | (function(){var a,b,c,d,e=[].slice,f=function(a,b){return function(){return a.apply(b,arguments)}},g={}.hasOwnProperty,h=function(a,b){function c(){this.constructor=a}for(var d in b)g.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},i=[].indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(b in this&&this[b]===a)return b;return-1};b=window.Morris={},a=jQuery,b.EventEmitter=function(){function a(){}return a.prototype.on=function(a,b){return null==this.handlers&&(this.handlers={}),null==this.handlers[a]&&(this.handlers[a]=[]),this.handlers[a].push(b),this},a.prototype.fire=function(){var a,b,c,d,f,g,h;if(c=arguments[0],a=2<=arguments.length?e.call(arguments,1):[],null!=this.handlers&&null!=this.handlers[c]){for(g=this.handlers[c],h=[],d=0,f=g.length;f>d;d++)b=g[d],h.push(b.apply(null,a));return h}},a}(),b.commas=function(a){var b,c,d,e;return null!=a?(d=0>a?"-":"",b=Math.abs(a),c=Math.floor(b).toFixed(0),d+=c.replace(/(?=(?:\d{3})+$)(?!^)/g,","),e=b.toString(),e.length>c.length&&(d+=e.slice(c.length)),d):"-"},b.pad2=function(a){return(10>a?"0":"")+a},b.Grid=function(c){function d(b){this.hasToShow=f(this.hasToShow,this),this.resizeHandler=f(this.resizeHandler,this);var c=this;if(this.el=a("string"==typeof b.element?document.getElementById(b.element):b.element),null==this.el||0===this.el.length)throw new Error("Graph container element not found");"static"===this.el.css("position")&&this.el.css("position","relative"),this.options=a.extend({},this.gridDefaults,this.defaults||{},b),"string"==typeof this.options.units&&(this.options.postUnits=b.units),this.raphael=new Raphael(this.el[0]),this.elementWidth=null,this.elementHeight=null,this.dirty=!1,this.selectFrom=null,this.init&&this.init(),this.setData(this.options.data),this.el.bind("mousemove",function(a){var b,d,e,f,g;return d=c.el.offset(),g=a.pageX-d.left,c.selectFrom?(b=c.data[c.hitTest(Math.min(g,c.selectFrom))]._x,e=c.data[c.hitTest(Math.max(g,c.selectFrom))]._x,f=e-b,c.selectionRect.attr({x:b,width:f})):c.fire("hovermove",g,a.pageY-d.top)}),this.el.bind("mouseleave",function(){return c.selectFrom&&(c.selectionRect.hide(),c.selectFrom=null),c.fire("hoverout")}),this.el.bind("touchstart touchmove touchend",function(a){var b,d;return d=a.originalEvent.touches[0]||a.originalEvent.changedTouches[0],b=c.el.offset(),c.fire("hovermove",d.pageX-b.left,d.pageY-b.top)}),this.el.bind("click",function(a){var b;return b=c.el.offset(),c.fire("gridclick",a.pageX-b.left,a.pageY-b.top)}),this.options.rangeSelect&&(this.selectionRect=this.raphael.rect(0,0,0,this.el.innerHeight()).attr({fill:this.options.rangeSelectColor,stroke:!1}).toBack().hide(),this.el.bind("mousedown",function(a){var b;return b=c.el.offset(),c.startRange(a.pageX-b.left)}),this.el.bind("mouseup",function(a){var b;return b=c.el.offset(),c.endRange(a.pageX-b.left),c.fire("hovermove",a.pageX-b.left,a.pageY-b.top)})),this.options.resize&&a(window).bind("resize",function(){return null!=c.timeoutId&&window.clearTimeout(c.timeoutId),c.timeoutId=window.setTimeout(c.resizeHandler,100)}),this.el.css("-webkit-tap-highlight-color","rgba(0,0,0,0)"),this.postInit&&this.postInit()}return h(d,c),d.prototype.gridDefaults={dateFormat:null,axes:!0,grid:!0,gridLineColor:"#aaa",gridStrokeWidth:.5,gridTextColor:"#888",gridTextSize:12,gridTextFamily:"sans-serif",gridTextWeight:"normal",hideHover:!1,yLabelFormat:null,xLabelAngle:0,numLines:5,padding:25,parseTime:!0,postUnits:"",preUnits:"",ymax:"auto",ymin:"auto 0",goals:[],goalStrokeWidth:1,goalLineColors:["#666633","#999966","#cc6666","#663333"],events:[],eventStrokeWidth:1,eventLineColors:["#005a04","#ccffbb","#3a5f0b","#005502"],rangeSelect:null,rangeSelectColor:"#eef",resize:!1},d.prototype.setData=function(c,d){var e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y;if(null==d&&(d=!0),this.options.data=c,null==c||0===c.length)return this.data=[],this.raphael.clear(),void(null!=this.hover&&this.hover.hide());if(s=this.cumulative?0:null,t=this.cumulative?0:null,this.options.goals.length>0&&(k=Math.min.apply(Math,this.options.goals),j=Math.max.apply(Math,this.options.goals),t=null!=t?Math.min(t,k):k,s=null!=s?Math.max(s,j):j),this.data=function(){var a,d,e;for(e=[],i=a=0,d=c.length;d>a;i=++a)m=c[i],l={src:m},l.label=m[this.options.xkey],this.options.parseTime?(l.x=b.parseDate(l.label),this.options.dateFormat?l.label=this.options.dateFormat(l.x):"number"==typeof l.label&&(l.label=new Date(l.label).toString())):(l.x=i,this.options.xLabelFormat&&(l.label=this.options.xLabelFormat(l))),p=0,l.y=function(){var a,b,c,d;for(c=this.options.ykeys,d=[],h=a=0,b=c.length;b>a;h=++a)r=c[h],u=m[r],"string"==typeof u&&(u=parseFloat(u)),null!=u&&"number"!=typeof u&&(u=null),null!=u&&this.hasToShow(h)&&(this.cumulative?p+=u:null!=s?(s=Math.max(u,s),t=Math.min(u,t)):s=t=u),this.cumulative&&null!=p&&(s=Math.max(p,s),t=Math.min(p,t)),d.push(u);return d}.call(this),e.push(l);return e}.call(this),this.options.parseTime&&(this.data=this.data.sort(function(a,b){return(a.x>b.x)-(b.x>a.x)})),this.xmin=this.data[0].x,this.xmax=this.data[this.data.length-1].x,this.events=[],this.options.events.length>0){if(this.options.parseTime)for(x=this.options.events,v=0,w=x.length;w>v;v++)e=x[v],e instanceof Array?(g=e[0],o=e[1],this.events.push([b.parseDate(g),b.parseDate(o)])):this.events.push(b.parseDate(e));else this.events=this.options.events;f=a.map(this.events,function(a){return a}),this.xmax=Math.max(this.xmax,Math.max.apply(Math,f)),this.xmin=Math.min(this.xmin,Math.min.apply(Math,f))}return this.xmin===this.xmax&&(this.xmin-=1,this.xmax+=1),this.ymin=this.yboundary("min",t),this.ymax=this.yboundary("max",s),this.ymin===this.ymax&&(t&&(this.ymin-=1),this.ymax+=1),((y=this.options.axes)===!0||"both"===y||"y"===y||this.options.grid===!0)&&(this.options.ymax===this.gridDefaults.ymax&&this.options.ymin===this.gridDefaults.ymin?(this.grid=this.autoGridLines(this.ymin,this.ymax,this.options.numLines),this.ymin=Math.min(this.ymin,this.grid[0]),this.ymax=Math.max(this.ymax,this.grid[this.grid.length-1])):(n=(this.ymax-this.ymin)/(this.options.numLines-1),this.grid=function(){var a,b,c,d;for(d=[],q=a=b=this.ymin,c=this.ymax;n>0?c>=a:a>=c;q=a+=n)d.push(q);return d}.call(this))),this.dirty=!0,d?this.redraw():void 0},d.prototype.yboundary=function(a,b){var c,d;return c=this.options["y"+a],"string"==typeof c?"auto"===c.slice(0,4)?c.length>5?(d=parseInt(c.slice(5),10),null==b?d:Math[a](b,d)):null!=b?b:0:parseInt(c,10):c},d.prototype.autoGridLines=function(a,b,c){var d,e,f,g,h,i,j,k,l;return h=b-a,l=Math.floor(Math.log(h)/Math.log(10)),j=Math.pow(10,l),e=Math.floor(a/j)*j,d=Math.ceil(b/j)*j,i=(d-e)/(c-1),1===j&&i>1&&Math.ceil(i)!==i&&(i=Math.ceil(i),d=e+i*(c-1)),0>e&&d>0&&(e=Math.floor(a/i)*i,d=Math.ceil(b/i)*i),1>i?(g=Math.floor(Math.log(i)/Math.log(10)),f=function(){var a,b;for(b=[],k=a=e;i>0?d>=a:a>=d;k=a+=i)b.push(parseFloat(k.toFixed(1-g)));return b}()):f=function(){var a,b;for(b=[],k=a=e;i>0?d>=a:a>=d;k=a+=i)b.push(k);return b}(),f},d.prototype._calc=function(){var a,b,c,d,e,f,g,h,i;return f=this.el.width(),d=this.el.height(),(this.elementWidth!==f||this.elementHeight!==d||this.dirty)&&(this.elementWidth=f,this.elementHeight=d,this.dirty=!1,this.left=this.options.padding,this.right=this.elementWidth-this.options.padding,this.top=this.options.padding,this.bottom=this.elementHeight-this.options.padding,((h=this.options.axes)===!0||"both"===h||"y"===h)&&(g=function(){var a,b,d,e;for(d=this.grid,e=[],a=0,b=d.length;b>a;a++)c=d[a],e.push(this.measureText(this.yAxisFormat(c)).width);return e}.call(this),this.options.horizontal?this.bottom-=Math.max.apply(Math,g):this.left+=Math.max.apply(Math,g)),((i=this.options.axes)===!0||"both"===i||"x"===i)&&(a=this.options.horizontal?-90:-this.options.xLabelAngle,b=function(){var b,c,d;for(d=[],e=b=0,c=this.data.length;c>=0?c>b:b>c;e=c>=0?++b:--b)d.push(this.measureText(this.data[e].label,a).height);return d}.call(this),this.options.horizontal?this.left+=Math.max.apply(Math,b):this.bottom-=Math.max.apply(Math,b)),this.width=Math.max(1,this.right-this.left),this.height=Math.max(1,this.bottom-this.top),this.options.horizontal?(this.dx=this.height/(this.xmax-this.xmin),this.dy=this.width/(this.ymax-this.ymin),this.yStart=this.left,this.yEnd=this.right,this.xStart=this.top,this.xEnd=this.bottom,this.xSize=this.height,this.ySize=this.width):(this.dx=this.width/(this.xmax-this.xmin),this.dy=this.height/(this.ymax-this.ymin),this.yStart=this.bottom,this.yEnd=this.top,this.xStart=this.left,this.xEnd=this.right,this.xSize=this.width,this.ySize=this.height),this.calc)?this.calc():void 0},d.prototype.transY=function(a){return this.options.horizontal?this.left+(a-this.ymin)*this.dy:this.bottom-(a-this.ymin)*this.dy},d.prototype.transX=function(a){return 1===this.data.length?(this.xStart+this.xEnd)/2:this.xStart+(a-this.xmin)*this.dx},d.prototype.redraw=function(){return this.raphael.clear(),this._calc(),this.drawGrid(),this.drawGoals(),this.drawEvents(),this.draw?this.draw():void 0},d.prototype.measureText=function(a,b){var c,d;return null==b&&(b=0),d=this.raphael.text(100,100,a).attr("font-size",this.options.gridTextSize).attr("font-family",this.options.gridTextFamily).attr("font-weight",this.options.gridTextWeight).rotate(b),c=d.getBBox(),d.remove(),c},d.prototype.yAxisFormat=function(a){return this.yLabelFormat(a,0)},d.prototype.yLabelFormat=function(a,c){return"function"==typeof this.options.yLabelFormat?this.options.yLabelFormat(a,c):""+this.options.preUnits+b.commas(a)+this.options.postUnits},d.prototype.getYAxisLabelX=function(){return this.left-this.options.padding/2},d.prototype.drawGrid=function(){var a,b,c,d,e,f,g,h,i;if(this.options.grid!==!1||(f=this.options.axes)===!0||"both"===f||"y"===f){for(a=this.options.horizontal?this.getXAxisLabelY():this.getYAxisLabelX(),g=this.grid,i=[],d=0,e=g.length;e>d;d++)b=g[d],c=this.transY(b),((h=this.options.axes)===!0||"both"===h||"y"===h)&&(this.options.horizontal?this.drawXAxisLabel(c,a,this.yAxisFormat(b)):this.drawYAxisLabel(a,c,this.yAxisFormat(b))),this.options.grid?(c=Math.floor(c)+.5,i.push(this.options.horizontal?this.drawGridLine("M"+c+","+this.xStart+"V"+this.xEnd):this.drawGridLine("M"+this.xStart+","+c+"H"+this.xEnd))):i.push(void 0);return i}},d.prototype.drawGoals=function(){var a,b,c,d,e,f,g;for(f=this.options.goals,g=[],c=d=0,e=f.length;e>d;c=++d)b=f[c],a=this.options.goalLineColors[c%this.options.goalLineColors.length],g.push(this.drawGoal(b,a));return g},d.prototype.drawEvents=function(){var a,b,c,d,e,f,g;for(f=this.events,g=[],c=d=0,e=f.length;e>d;c=++d)b=f[c],a=this.options.eventLineColors[c%this.options.eventLineColors.length],g.push(this.drawEvent(b,a));return g},d.prototype.drawGoal=function(a,b){var c,d;return d=Math.floor(this.transY(a))+.5,c=this.options.horizontal?"M"+d+","+this.xStart+"V"+this.xEnd:"M"+this.xStart+","+d+"H"+this.xEnd,this.raphael.path(c).attr("stroke",b).attr("stroke-width",this.options.goalStrokeWidth)},d.prototype.drawEvent=function(a,b){var c,d,e,f;return a instanceof Array?(c=a[0],e=a[1],c=Math.floor(this.transX(c))+.5,e=Math.floor(this.transX(e))+.5,this.options.horizontal?this.raphael.rect(this.yStart,c,this.yEnd-this.yStart,e-c).attr({fill:b,stroke:!1}).toBack():this.raphael.rect(c,this.yEnd,e-c,this.yStart-this.yEnd).attr({fill:b,stroke:!1}).toBack()):(f=Math.floor(this.transX(a))+.5,d=this.options.horizontal?"M"+this.yStart+","+f+"H"+this.yEnd:"M"+f+","+this.yStart+"V"+this.yEnd,this.raphael.path(d).attr("stroke",b).attr("stroke-width",this.options.eventStrokeWidth))},d.prototype.drawYAxisLabel=function(a,b,c){return this.raphael.text(a,b,c).attr("font-size",this.options.gridTextSize).attr("font-family",this.options.gridTextFamily).attr("font-weight",this.options.gridTextWeight).attr("fill",this.options.gridTextColor).attr("text-anchor","end")},d.prototype.drawGridLine=function(a){return this.raphael.path(a).attr("stroke",this.options.gridLineColor).attr("stroke-width",this.options.gridStrokeWidth)},d.prototype.startRange=function(a){return this.hover.hide(),this.selectFrom=a,this.selectionRect.attr({x:a,width:0}).show()},d.prototype.endRange=function(a){var b,c;return this.selectFrom?(c=Math.min(this.selectFrom,a),b=Math.max(this.selectFrom,a),this.options.rangeSelect.call(this.el,{start:this.data[this.hitTest(c)].x,end:this.data[this.hitTest(b)].x}),this.selectFrom=null):void 0},d.prototype.resizeHandler=function(){return this.timeoutId=null,this.raphael.setSize(this.el.width(),this.el.height()),this.redraw()},d.prototype.hasToShow=function(a){return this.options.shown===!0||this.options.shown[a]===!0},d}(b.EventEmitter),b.parseDate=function(a){var b,c,d,e,f,g,h,i,j,k,l;return"number"==typeof a?a:(c=a.match(/^(\d+) Q(\d)$/),e=a.match(/^(\d+)-(\d+)$/),f=a.match(/^(\d+)-(\d+)-(\d+)$/),h=a.match(/^(\d+) W(\d+)$/),i=a.match(/^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+)(Z|([+-])(\d\d):?(\d\d))?$/),j=a.match(/^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+):(\d+(\.\d+)?)(Z|([+-])(\d\d):?(\d\d))?$/),c?new Date(parseInt(c[1],10),3*parseInt(c[2],10)-1,1).getTime():e?new Date(parseInt(e[1],10),parseInt(e[2],10)-1,1).getTime():f?new Date(parseInt(f[1],10),parseInt(f[2],10)-1,parseInt(f[3],10)).getTime():h?(k=new Date(parseInt(h[1],10),0,1),4!==k.getDay()&&k.setMonth(0,1+(4-k.getDay()+7)%7),k.getTime()+6048e5*parseInt(h[2],10)):i?i[6]?(g=0,"Z"!==i[6]&&(g=60*parseInt(i[8],10)+parseInt(i[9],10),"+"===i[7]&&(g=0-g)),Date.UTC(parseInt(i[1],10),parseInt(i[2],10)-1,parseInt(i[3],10),parseInt(i[4],10),parseInt(i[5],10)+g)):new Date(parseInt(i[1],10),parseInt(i[2],10)-1,parseInt(i[3],10),parseInt(i[4],10),parseInt(i[5],10)).getTime():j?(l=parseFloat(j[6]),b=Math.floor(l),d=Math.round(1e3*(l-b)),j[8]?(g=0,"Z"!==j[8]&&(g=60*parseInt(j[10],10)+parseInt(j[11],10),"+"===j[9]&&(g=0-g)),Date.UTC(parseInt(j[1],10),parseInt(j[2],10)-1,parseInt(j[3],10),parseInt(j[4],10),parseInt(j[5],10)+g,b,d)):new Date(parseInt(j[1],10),parseInt(j[2],10)-1,parseInt(j[3],10),parseInt(j[4],10),parseInt(j[5],10),b,d).getTime()):new Date(parseInt(a,10),0,1).getTime())},b.Hover=function(){function c(c){null==c&&(c={}),this.options=a.extend({},b.Hover.defaults,c),this.el=a("
"),this.el.hide(),this.options.parent.append(this.el)}return c.defaults={"class":"morris-hover morris-default-style"},c.prototype.update=function(a,b,c,d){return a?(this.html(a),this.show(),this.moveTo(b,c,d)):this.hide()},c.prototype.html=function(a){return this.el.html(a)},c.prototype.moveTo=function(a,b,c){var d,e,f,g,h,i;return h=this.options.parent.innerWidth(),g=this.options.parent.innerHeight(),e=this.el.outerWidth(),d=this.el.outerHeight(),f=Math.min(Math.max(0,a-e/2),h-e),null!=b?c===!0?(i=b-d/2,0>i&&(i=0)):(i=b-d-10,0>i&&(i=b+10,i+d>g&&(i=g/2-d/2))):i=g/2-d/2,this.el.css({left:f+"px",top:parseInt(i)+"px"})},c.prototype.show=function(){return this.el.show()},c.prototype.hide=function(){return this.el.hide()},c}(),b.Line=function(c){function d(a){return this.hilight=f(this.hilight,this),this.onHoverOut=f(this.onHoverOut,this),this.onHoverMove=f(this.onHoverMove,this),this.onGridClick=f(this.onGridClick,this),this instanceof b.Line?void d.__super__.constructor.call(this,a):new b.Line(a)}return h(d,c),d.prototype.init=function(){return"always"!==this.options.hideHover?(this.hover=new b.Hover({parent:this.el}),this.on("hovermove",this.onHoverMove),this.on("hoverout",this.onHoverOut),this.on("gridclick",this.onGridClick)):void 0},d.prototype.defaults={lineWidth:3,pointSize:4,lineColors:["#0b62a4","#7A92A3","#4da74d","#afd8f8","#edc240","#cb4b4b","#9440ed"],pointStrokeWidths:[1],pointStrokeColors:["#ffffff"],pointFillColors:[],smooth:!0,shown:!0,xLabels:"auto",xLabelFormat:null,xLabelMargin:24,hideHover:!1,trendLine:!1,trendLineWidth:2,trendLineColors:["#689bc3","#a2b3bf","#64b764"]},d.prototype.calc=function(){return this.calcPoints(),this.generatePaths()},d.prototype.calcPoints=function(){var a,b,c,d,e,f;for(e=this.data,f=[],c=0,d=e.length;d>c;c++)a=e[c],a._x=this.transX(a.x),a._y=function(){var c,d,e,f;for(e=a.y,f=[],c=0,d=e.length;d>c;c++)b=e[c],f.push(null!=b?this.transY(b):b);return f}.call(this),f.push(a._ymax=Math.min.apply(Math,[this.bottom].concat(function(){var c,d,e,f;for(e=a._y,f=[],c=0,d=e.length;d>c;c++)b=e[c],null!=b&&f.push(b);return f}())));return f},d.prototype.hitTest=function(a){var b,c,d,e,f;if(0===this.data.length)return null;for(f=this.data.slice(1),b=d=0,e=f.length;e>d&&(c=f[b],!(a<(c._x+this.data[b]._x)/2));b=++d);return b},d.prototype.onGridClick=function(a,b){var c;return c=this.hitTest(a),this.fire("click",c,this.data[c].src,a,b)},d.prototype.onHoverMove=function(a){var b;return b=this.hitTest(a),this.displayHoverForRow(b)},d.prototype.onHoverOut=function(){return this.options.hideHover!==!1?this.displayHoverForRow(null):void 0},d.prototype.displayHoverForRow=function(a){var b;return null!=a?((b=this.hover).update.apply(b,this.hoverContentForRow(a)),this.hilight(a)):(this.hover.hide(),this.hilight())},d.prototype.hoverContentForRow=function(b){var c,d,e,f,g,h,i;for(e=this.data[b],c=a("
").text(e.label),c=c.prop("outerHTML"),i=e.y,d=g=0,h=i.length;h>g;d=++g)f=i[d],this.options.labels[d]!==!1&&(c+="
\n "+this.options.labels[d]+":\n "+this.yLabelFormat(f,d)+"\n
");return"function"==typeof this.options.hoverCallback&&(c=this.options.hoverCallback(b,this.options,c,e.src)),[c,e._x,e._ymax]},d.prototype.generatePaths=function(){var a,c,d,e;return this.paths=function(){var f,g,h,j;for(j=[],c=f=0,g=this.options.ykeys.length;g>=0?g>f:f>g;c=g>=0?++f:--f)e="boolean"==typeof this.options.smooth?this.options.smooth:(h=this.options.ykeys[c],i.call(this.options.smooth,h)>=0),a=function(){var a,b,e,f;for(e=this.data,f=[],a=0,b=e.length;b>a;a++)d=e[a],void 0!==d._y[c]&&f.push({x:d._x,y:d._y[c]});return f}.call(this),j.push(a.length>1?b.Line.createPath(a,e,this.bottom):null);return j}.call(this)},d.prototype.draw=function(){var a;return((a=this.options.axes)===!0||"both"===a||"x"===a)&&this.drawXAxis(),this.drawSeries(),this.options.hideHover===!1?this.displayHoverForRow(this.data.length-1):void 0},d.prototype.drawXAxis=function(){var a,c,d,e,f,g,h,i,j,k,l=this;for(h=this.bottom+this.options.padding/2,f=null,e=null,a=function(a,b){var c,d,g,i,j;return c=l.drawXAxisLabel(l.transX(b),h,a),j=c.getBBox(),c.transform("r"+-l.options.xLabelAngle),d=c.getBBox(),c.transform("t0,"+d.height/2+"..."),0!==l.options.xLabelAngle&&(i=-.5*j.width*Math.cos(l.options.xLabelAngle*Math.PI/180),c.transform("t"+i+",0...")),d=c.getBBox(),(null==f||f>=d.x+d.width||null!=e&&e>=d.x)&&d.x>=0&&d.x+d.widtha;a++)g=c[a],d.push([g.label,g.x]);return d}.call(this),d.reverse(),k=[],i=0,j=d.length;j>i;i++)c=d[i],k.push(a(c[0],c[1]));return k},d.prototype.drawSeries=function(){var a,b,c,d,e,f;for(this.seriesPoints=[],a=b=d=this.options.ykeys.length-1;0>=d?0>=b:b>=0;a=0>=d?++b:--b)this.hasToShow(a)&&((this.options.trendLine!==!1&&this.options.trendLine===!0||this.options.trendLine[a]===!0)&&this._drawTrendLine(a),this._drawLineFor(a));for(f=[],a=c=e=this.options.ykeys.length-1;0>=e?0>=c:c>=0;a=0>=e?++c:--c)f.push(this.hasToShow(a)?this._drawPointFor(a):void 0);return f},d.prototype._drawPointFor=function(a){var b,c,d,e,f,g;for(this.seriesPoints[a]=[],f=this.data,g=[],d=0,e=f.length;e>d;d++)c=f[d],b=null,null!=c._y[a]&&(b=this.drawLinePoint(c._x,c._y[a],this.colorFor(c,a,"point"),a)),g.push(this.seriesPoints[a].push(b));return g},d.prototype._drawLineFor=function(a){var b;return b=this.paths[a],null!==b?this.drawLinePath(b,this.colorFor(null,a,"line"),a):void 0},d.prototype._drawTrendLine=function(a){var c,d,e,f,g,h,i,j,k,l,m,n,o,p,q;for(h=0,k=0,i=0,j=0,f=0,q=this.data,o=0,p=q.length;p>o;o++)l=q[o],m=l.x,n=l.y[a],void 0!==n&&(f+=1,h+=m,k+=n,i+=m*m,j+=m*n);return c=(f*j-h*k)/(f*i-h*h),d=k/f-c*h/f,e=[{},{}],e[0].x=this.transX(this.data[0].x),e[0].y=this.transY(this.data[0].x*c+d),e[1].x=this.transX(this.data[this.data.length-1].x),e[1].y=this.transY(this.data[this.data.length-1].x*c+d),g=b.Line.createPath(e,!1,this.bottom),g=this.raphael.path(g).attr("stroke",this.colorFor(null,a,"trendLine")).attr("stroke-width",this.options.trendLineWidth)},d.createPath=function(a,c,d){var e,f,g,h,i,j,k,l,m,n,o,p,q,r;for(k="",c&&(g=b.Line.gradients(a)),l={y:null},h=q=0,r=a.length;r>q;h=++q)e=a[h],null!=e.y&&(null!=l.y?c?(f=g[h],j=g[h-1],i=(e.x-l.x)/4,m=l.x+i,o=Math.min(d,l.y+i*j),n=e.x-i,p=Math.min(d,e.y-i*f),k+="C"+m+","+o+","+n+","+p+","+e.x+","+e.y):k+="L"+e.x+","+e.y:c&&null==g[h]||(k+="M"+e.x+","+e.y)),l=e;return k},d.gradients=function(a){var b,c,d,e,f,g,h,i;for(c=function(a,b){return(a.y-b.y)/(a.x-b.x)},i=[],d=g=0,h=a.length;h>g;d=++g)b=a[d],null!=b.y?(e=a[d+1]||{y:null},f=a[d-1]||{y:null},i.push(null!=f.y&&null!=e.y?c(f,e):null!=f.y?c(f,b):null!=e.y?c(b,e):null)):i.push(null);return i},d.prototype.hilight=function(a){var b,c,d,e,f;if(null!==this.prevHilight&&this.prevHilight!==a)for(b=c=0,e=this.seriesPoints.length-1;e>=0?e>=c:c>=e;b=e>=0?++c:--c)this.seriesPoints[b][this.prevHilight]&&this.seriesPoints[b][this.prevHilight].animate(this.pointShrinkSeries(b));if(null!==a&&this.prevHilight!==a)for(b=d=0,f=this.seriesPoints.length-1;f>=0?f>=d:d>=f;b=f>=0?++d:--d)this.seriesPoints[b][a]&&this.seriesPoints[b][a].animate(this.pointGrowSeries(b));return this.prevHilight=a},d.prototype.colorFor=function(a,b,c){return"function"==typeof this.options.lineColors?this.options.lineColors.call(this,a,b,c):"point"===c?this.options.pointFillColors[b%this.options.pointFillColors.length]||this.options.lineColors[b%this.options.lineColors.length]:"line"===c?this.options.lineColors[b%this.options.lineColors.length]:this.options.trendLineColors[b%this.options.trendLineColors.length]},d.prototype.drawXAxisLabel=function(a,b,c){return this.raphael.text(a,b,c).attr("font-size",this.options.gridTextSize).attr("font-family",this.options.gridTextFamily).attr("font-weight",this.options.gridTextWeight).attr("fill",this.options.gridTextColor)},d.prototype.drawLinePath=function(a,b,c){return this.raphael.path(a).attr("stroke",b).attr("stroke-width",this.lineWidthForSeries(c))},d.prototype.drawLinePoint=function(a,b,c,d){return this.raphael.circle(a,b,this.pointSizeForSeries(d)).attr("fill",c).attr("stroke-width",this.pointStrokeWidthForSeries(d)).attr("stroke",this.pointStrokeColorForSeries(d))},d.prototype.pointStrokeWidthForSeries=function(a){return this.options.pointStrokeWidths[a%this.options.pointStrokeWidths.length]},d.prototype.pointStrokeColorForSeries=function(a){return this.options.pointStrokeColors[a%this.options.pointStrokeColors.length]},d.prototype.lineWidthForSeries=function(a){return this.options.lineWidth instanceof Array?this.options.lineWidth[a%this.options.lineWidth.length]:this.options.lineWidth},d.prototype.pointSizeForSeries=function(a){return this.options.pointSize instanceof Array?this.options.pointSize[a%this.options.pointSize.length]:this.options.pointSize},d.prototype.pointGrowSeries=function(a){return 0!==this.pointSizeForSeries(a)?Raphael.animation({r:this.pointSizeForSeries(a)+3},25,"linear"):void 0},d.prototype.pointShrinkSeries=function(a){return Raphael.animation({r:this.pointSizeForSeries(a)},25,"linear")},d}(b.Grid),b.labelSeries=function(c,d,e,f,g){var h,i,j,k,l,m,n,o,p,q,r;if(j=200*(d-c)/e,i=new Date(c),n=b.LABEL_SPECS[f],void 0===n)for(r=b.AUTO_LABEL_ORDER,p=0,q=r.length;q>p;p++)if(k=r[p],m=b.LABEL_SPECS[k],j>=m.span){n=m;break}for(void 0===n&&(n=b.LABEL_SPECS.second),g&&(n=a.extend({},n,{fmt:g})),h=n.start(i),l=[];(o=h.getTime())<=d;)o>=c&&l.push([n.fmt(h),o]),n.incr(h);return l},c=function(a){return{span:60*a*1e3,start:function(a){return new Date(a.getFullYear(),a.getMonth(),a.getDate(),a.getHours())},fmt:function(a){return""+b.pad2(a.getHours())+":"+b.pad2(a.getMinutes())},incr:function(b){return b.setUTCMinutes(b.getUTCMinutes()+a)}}},d=function(a){return{span:1e3*a,start:function(a){return new Date(a.getFullYear(),a.getMonth(),a.getDate(),a.getHours(),a.getMinutes())},fmt:function(a){return""+b.pad2(a.getHours())+":"+b.pad2(a.getMinutes())+":"+b.pad2(a.getSeconds())},incr:function(b){return b.setUTCSeconds(b.getUTCSeconds()+a)}}},b.LABEL_SPECS={decade:{span:1728e8,start:function(a){return new Date(a.getFullYear()-a.getFullYear()%10,0,1)},fmt:function(a){return""+a.getFullYear()},incr:function(a){return a.setFullYear(a.getFullYear()+10)}},year:{span:1728e7,start:function(a){return new Date(a.getFullYear(),0,1)},fmt:function(a){return""+a.getFullYear()},incr:function(a){return a.setFullYear(a.getFullYear()+1)}},month:{span:24192e5,start:function(a){return new Date(a.getFullYear(),a.getMonth(),1)},fmt:function(a){return""+a.getFullYear()+"-"+b.pad2(a.getMonth()+1)},incr:function(a){return a.setMonth(a.getMonth()+1)}},week:{span:6048e5,start:function(a){return new Date(a.getFullYear(),a.getMonth(),a.getDate())},fmt:function(a){return""+a.getFullYear()+"-"+b.pad2(a.getMonth()+1)+"-"+b.pad2(a.getDate())},incr:function(a){return a.setDate(a.getDate()+7)}},day:{span:864e5,start:function(a){return new Date(a.getFullYear(),a.getMonth(),a.getDate())},fmt:function(a){return""+a.getFullYear()+"-"+b.pad2(a.getMonth()+1)+"-"+b.pad2(a.getDate())},incr:function(a){return a.setDate(a.getDate()+1)}},hour:c(60),"30min":c(30),"15min":c(15),"10min":c(10),"5min":c(5),minute:c(1),"30sec":d(30),"15sec":d(15),"10sec":d(10),"5sec":d(5),second:d(1)},b.AUTO_LABEL_ORDER=["decade","year","month","week","day","hour","30min","15min","10min","5min","minute","30sec","15sec","10sec","5sec","second"],b.Area=function(c){function d(c){var f;return this instanceof b.Area?(f=a.extend({},e,c),this.cumulative=!f.behaveLikeLine,"auto"===f.fillOpacity&&(f.fillOpacity=f.behaveLikeLine?.8:1),void d.__super__.constructor.call(this,f)):new b.Area(c)}var e;return h(d,c),e={fillOpacity:"auto",behaveLikeLine:!1},d.prototype.calcPoints=function(){var a,b,c,d,e,f,g;for(f=this.data,g=[],d=0,e=f.length;e>d;d++)a=f[d],a._x=this.transX(a.x),b=0,a._y=function(){var d,e,f,g;for(f=a.y,g=[],d=0,e=f.length;e>d;d++)c=f[d],this.options.behaveLikeLine?g.push(this.transY(c)):(b+=c||0,g.push(this.transY(b)));return g}.call(this),g.push(a._ymax=Math.max.apply(Math,a._y));return g},d.prototype.drawSeries=function(){var a,b,c,d,e,f,g,h;for(this.seriesPoints=[],b=this.options.behaveLikeLine?function(){f=[];for(var a=0,b=this.options.ykeys.length-1;b>=0?b>=a:a>=b;b>=0?a++:a--)f.push(a);return f}.apply(this):function(){g=[];for(var a=e=this.options.ykeys.length-1;0>=e?0>=a:a>=0;0>=e?a++:a--)g.push(a);return g}.apply(this),h=[],c=0,d=b.length;d>c;c++)a=b[c],this._drawFillFor(a),this._drawLineFor(a),h.push(this._drawPointFor(a));return h},d.prototype._drawFillFor=function(a){var b;return b=this.paths[a],null!==b?(b+="L"+this.transX(this.xmax)+","+this.bottom+"L"+this.transX(this.xmin)+","+this.bottom+"Z",this.drawFilledPath(b,this.fillForSeries(a))):void 0},d.prototype.fillForSeries=function(a){var b;return b=Raphael.rgb2hsl(this.colorFor(this.data[a],a,"line")),Raphael.hsl(b.h,this.options.behaveLikeLine?.9*b.s:.75*b.s,Math.min(.98,this.options.behaveLikeLine?1.2*b.l:1.25*b.l))},d.prototype.drawFilledPath=function(a,b){return this.raphael.path(a).attr("fill",b).attr("fill-opacity",this.options.fillOpacity).attr("stroke","none")},d}(b.Line),b.Bar=function(c){function d(c){return this.onHoverOut=f(this.onHoverOut,this),this.onHoverMove=f(this.onHoverMove,this),this.onGridClick=f(this.onGridClick,this),this instanceof b.Bar?void d.__super__.constructor.call(this,a.extend({},c,{parseTime:!1})):new b.Bar(c)}return h(d,c),d.prototype.init=function(){return this.cumulative=this.options.stacked,"always"!==this.options.hideHover?(this.hover=new b.Hover({parent:this.el}),this.on("hovermove",this.onHoverMove),this.on("hoverout",this.onHoverOut),this.on("gridclick",this.onGridClick)):void 0},d.prototype.defaults={barSizeRatio:.75,barGap:3,barColors:["#0b62a4","#7a92a3","#4da74d","#afd8f8","#edc240","#cb4b4b","#9440ed"],barOpacity:1,barRadius:[0,0,0,0],xLabelMargin:50,horizontal:!1,shown:!0},d.prototype.calc=function(){var a;return this.calcBars(),this.options.hideHover===!1?(a=this.hover).update.apply(a,this.hoverContentForRow(this.data.length-1)):void 0},d.prototype.calcBars=function(){var a,b,c,d,e,f,g;for(f=this.data,g=[],a=d=0,e=f.length;e>d;a=++d)b=f[a],b._x=this.xStart+this.xSize*(a+.5)/this.data.length,g.push(b._y=function(){var a,d,e,f;for(e=b.y,f=[],a=0,d=e.length;d>a;a++)c=e[a],f.push(null!=c?this.transY(c):null);return f}.call(this));return g},d.prototype.draw=function(){var a;return((a=this.options.axes)===!0||"both"===a||"x"===a)&&this.drawXAxis(),this.drawSeries()},d.prototype.drawXAxis=function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q;for(b=this.options.horizontal?this.getYAxisLabelX():this.getXAxisLabelY(),j=null,i=null,q=[],c=o=0,p=this.data.length;p>=0?p>o:o>p;c=p>=0?++o:--o)k=this.data[this.data.length-1-c],d=this.options.horizontal?this.drawYAxisLabel(b,k._x-.5*this.options.gridTextSize,k.label):this.drawXAxisLabel(k._x,b,k.label),a=this.options.horizontal?0:this.options.xLabelAngle,n=d.getBBox(),d.transform("r"+-a),e=d.getBBox(),d.transform("t0,"+e.height/2+"..."),0!==a&&(h=-.5*n.width*Math.cos(a*Math.PI/180),d.transform("t"+h+",0...")),this.options.horizontal?(m=e.y,l=e.height,g=this.el.height()):(m=e.x,l=e.width,g=this.el.width()),(null==j||j>=m+l||null!=i&&i>=m)&&m>=0&&g>m+l?(0!==a&&(f=1.25*this.options.gridTextSize/Math.sin(a*Math.PI/180),i=m-f),q.push(this.options.horizontal?j=m:j=m-this.options.xLabelMargin)):q.push(d.remove());return q},d.prototype.getXAxisLabelY=function(){return this.bottom+(this.options.xAxisLabelTopPadding||this.options.padding/2)},d.prototype.drawSeries=function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r;if(c=this.xSize/this.options.data.length,this.options.stacked)i=1;else for(i=0,d=q=0,r=this.options.ykeys.length-1;r>=0?r>=q:q>=r;d=r>=0?++q:--q)this.hasToShow(d)&&(i+=1);return a=(c*this.options.barSizeRatio-this.options.barGap*(i-1))/i,this.options.barSize&&(a=Math.min(a,this.options.barSize)),m=c-a*i-this.options.barGap*(i-1),h=m/2,p=this.ymin<=0&&this.ymax>=0?this.transY(0):null,this.bars=function(){var d,i,m,q;for(m=this.data,q=[],e=d=0,i=m.length;i>d;e=++d)j=m[e],f=0,q.push(function(){var d,i,m,q;for(m=j._y,q=[],k=d=0,i=m.length;i>d;k=++d)o=m[k],this.hasToShow(k)&&(null!==o?(p?(n=Math.min(o,p),b=Math.max(o,p)):(n=o,b=this.bottom),g=this.xStart+e*c+h,this.options.stacked||(g+=k*(a+this.options.barGap)),l=b-n,this.options.verticalGridCondition&&this.options.verticalGridCondition(j.x)&&(this.options.horizontal?this.drawBar(this.yStart,this.xStart+e*c,this.ySize,c,this.options.verticalGridColor,this.options.verticalGridOpacity,this.options.barRadius):this.drawBar(this.xStart+e*c,this.yEnd,c,this.ySize,this.options.verticalGridColor,this.options.verticalGridOpacity,this.options.barRadius)),this.options.stacked&&(n-=f),this.options.horizontal?(this.drawBar(n,g,l,a,this.colorFor(j,k,"bar"),this.options.barOpacity,this.options.barRadius),q.push(f-=l)):(this.drawBar(g,n,a,l,this.colorFor(j,k,"bar"),this.options.barOpacity,this.options.barRadius),q.push(f+=l))):q.push(null));return q}.call(this));return q}.call(this)},d.prototype.colorFor=function(a,b,c){var d,e;return"function"==typeof this.options.barColors?(d={x:a.x,y:a.y[b],label:a.label},e={index:b,key:this.options.ykeys[b],label:this.options.labels[b]},this.options.barColors.call(this,d,e,c)):this.options.barColors[b%this.options.barColors.length]},d.prototype.hitTest=function(a,b){var c;return 0===this.data.length?null:(c=this.options.horizontal?b:a,c=Math.max(Math.min(c,this.xEnd),this.xStart),Math.min(this.data.length-1,Math.floor((c-this.xStart)/(this.xSize/this.data.length)))) 7 | },d.prototype.onGridClick=function(a,b){var c;return c=this.hitTest(a,b),this.fire("click",c,this.data[c].src,a,b)},d.prototype.onHoverMove=function(a,b){var c,d;return c=this.hitTest(a,b),(d=this.hover).update.apply(d,this.hoverContentForRow(c))},d.prototype.onHoverOut=function(){return this.options.hideHover!==!1?this.hover.hide():void 0},d.prototype.hoverContentForRow=function(b){var c,d,e,f,g,h,i,j;for(e=this.data[b],c=a("
").text(e.label),c=c.prop("outerHTML"),j=e.y,d=h=0,i=j.length;i>h;d=++h)g=j[d],this.options.labels[d]!==!1&&(c+="
\n "+this.options.labels[d]+":\n "+this.yLabelFormat(g,d)+"\n
");return"function"==typeof this.options.hoverCallback&&(c=this.options.hoverCallback(b,this.options,c,e.src)),this.options.horizontal?(f=this.left+.5*this.width,g=this.top+(b+.5)*this.height/this.data.length,[c,f,g,!0]):(f=this.left+(b+.5)*this.width/this.data.length,[c,f])},d.prototype.drawXAxisLabel=function(a,b,c){var d;return d=this.raphael.text(a,b,c).attr("font-size",this.options.gridTextSize).attr("font-family",this.options.gridTextFamily).attr("font-weight",this.options.gridTextWeight).attr("fill",this.options.gridTextColor)},d.prototype.drawBar=function(a,b,c,d,e,f,g){var h,i;return h=Math.max.apply(Math,g),i=0===h||h>d?this.raphael.rect(a,b,c,d):this.raphael.path(this.roundedRect(a,b,c,d,g)),i.attr("fill",e).attr("fill-opacity",f).attr("stroke","none")},d.prototype.roundedRect=function(a,b,c,d,e){return null==e&&(e=[0,0,0,0]),["M",a,e[0]+b,"Q",a,b,a+e[0],b,"L",a+c-e[1],b,"Q",a+c,b,a+c,b+e[1],"L",a+c,b+d-e[2],"Q",a+c,b+d,a+c-e[2],b+d,"L",a+e[3],b+d,"Q",a,b+d,a,b+d-e[3],"Z"]},d}(b.Grid),b.Donut=function(c){function d(c){this.resizeHandler=f(this.resizeHandler,this),this.select=f(this.select,this),this.click=f(this.click,this);var d=this;if(!(this instanceof b.Donut))return new b.Donut(c);if(this.options=a.extend({},this.defaults,c),this.el=a("string"==typeof c.element?document.getElementById(c.element):c.element),null===this.el||0===this.el.length)throw new Error("Graph placeholder not found.");void 0!==c.data&&0!==c.data.length&&(this.raphael=new Raphael(this.el[0]),this.options.resize&&a(window).bind("resize",function(){return null!=d.timeoutId&&window.clearTimeout(d.timeoutId),d.timeoutId=window.setTimeout(d.resizeHandler,100)}),this.setData(c.data))}return h(d,c),d.prototype.defaults={colors:["#0B62A4","#3980B5","#679DC6","#95BBD7","#B0CCE1","#095791","#095085","#083E67","#052C48","#042135"],backgroundColor:"#FFFFFF",labelColor:"#000000",formatter:b.commas,resize:!1},d.prototype.redraw=function(){var a,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x;for(this.raphael.clear(),c=this.el.width()/2,d=this.el.height()/2,n=(Math.min(c,d)-10)/3,l=0,u=this.values,o=0,r=u.length;r>o;o++)m=u[o],l+=m;for(i=5/(2*n),a=1.9999*Math.PI-i*this.data.length,g=0,f=0,this.segments=[],v=this.values,e=p=0,s=v.length;s>p;e=++p)m=v[e],j=g+i+a*(m/l),k=new b.DonutSegment(c,d,2*n,n,g,j,this.data[e].color||this.options.colors[f%this.options.colors.length],this.options.backgroundColor,f,this.raphael),k.render(),this.segments.push(k),k.on("hover",this.select),k.on("click",this.click),g=j,f+=1;for(this.text1=this.drawEmptyDonutLabel(c,d-10,this.options.labelColor,15,800),this.text2=this.drawEmptyDonutLabel(c,d+10,this.options.labelColor,14),h=Math.max.apply(Math,this.values),f=0,w=this.values,x=[],q=0,t=w.length;t>q;q++){if(m=w[q],m===h){this.select(f);break}x.push(f+=1)}return x},d.prototype.setData=function(a){var b;return this.data=a,this.values=function(){var a,c,d,e;for(d=this.data,e=[],a=0,c=d.length;c>a;a++)b=d[a],e.push(parseFloat(b.value));return e}.call(this),this.redraw()},d.prototype.click=function(a){return this.fire("click",a,this.data[a])},d.prototype.select=function(a){var b,c,d,e,f,g;for(g=this.segments,e=0,f=g.length;f>e;e++)c=g[e],c.deselect();return d=this.segments[a],d.select(),b=this.data[a],this.setLabels(b.label,this.options.formatter(b.value,b))},d.prototype.setLabels=function(a,b){var c,d,e,f,g,h,i,j;return c=2*(Math.min(this.el.width()/2,this.el.height()/2)-10)/3,f=1.8*c,e=c/2,d=c/3,this.text1.attr({text:a,transform:""}),g=this.text1.getBBox(),h=Math.min(f/g.width,e/g.height),this.text1.attr({transform:"S"+h+","+h+","+(g.x+g.width/2)+","+(g.y+g.height)}),this.text2.attr({text:b,transform:""}),i=this.text2.getBBox(),j=Math.min(f/i.width,d/i.height),this.text2.attr({transform:"S"+j+","+j+","+(i.x+i.width/2)+","+i.y})},d.prototype.drawEmptyDonutLabel=function(a,b,c,d,e){var f;return f=this.raphael.text(a,b,"").attr("font-size",d).attr("fill",c),null!=e&&f.attr("font-weight",e),f},d.prototype.resizeHandler=function(){return this.timeoutId=null,this.raphael.setSize(this.el.width(),this.el.height()),this.redraw()},d}(b.EventEmitter),b.DonutSegment=function(a){function b(a,b,c,d,e,g,h,i,j,k){this.cx=a,this.cy=b,this.inner=c,this.outer=d,this.color=h,this.backgroundColor=i,this.index=j,this.raphael=k,this.deselect=f(this.deselect,this),this.select=f(this.select,this),this.sin_p0=Math.sin(e),this.cos_p0=Math.cos(e),this.sin_p1=Math.sin(g),this.cos_p1=Math.cos(g),this.is_long=g-e>Math.PI?1:0,this.path=this.calcSegment(this.inner+3,this.inner+this.outer-5),this.selectedPath=this.calcSegment(this.inner+3,this.inner+this.outer),this.hilight=this.calcArc(this.inner)}return h(b,a),b.prototype.calcArcPoints=function(a){return[this.cx+a*this.sin_p0,this.cy+a*this.cos_p0,this.cx+a*this.sin_p1,this.cy+a*this.cos_p1]},b.prototype.calcSegment=function(a,b){var c,d,e,f,g,h,i,j,k,l;return k=this.calcArcPoints(a),c=k[0],e=k[1],d=k[2],f=k[3],l=this.calcArcPoints(b),g=l[0],i=l[1],h=l[2],j=l[3],"M"+c+","+e+("A"+a+","+a+",0,"+this.is_long+",0,"+d+","+f)+("L"+h+","+j)+("A"+b+","+b+",0,"+this.is_long+",1,"+g+","+i)+"Z"},b.prototype.calcArc=function(a){var b,c,d,e,f;return f=this.calcArcPoints(a),b=f[0],d=f[1],c=f[2],e=f[3],"M"+b+","+d+("A"+a+","+a+",0,"+this.is_long+",0,"+c+","+e)},b.prototype.render=function(){var a=this;return this.arc=this.drawDonutArc(this.hilight,this.color),this.seg=this.drawDonutSegment(this.path,this.color,this.backgroundColor,function(){return a.fire("hover",a.index)},function(){return a.fire("click",a.index)})},b.prototype.drawDonutArc=function(a,b){return this.raphael.path(a).attr({stroke:b,"stroke-width":2,opacity:0})},b.prototype.drawDonutSegment=function(a,b,c,d,e){return this.raphael.path(a).attr({fill:b,stroke:c,"stroke-width":3}).hover(d).click(e)},b.prototype.select=function(){return this.selected?void 0:(this.seg.animate({path:this.selectedPath},150,"<>"),this.arc.animate({opacity:1},150,"<>"),this.selected=!0)},b.prototype.deselect=function(){return this.selected?(this.seg.animate({path:this.path},150,"<>"),this.arc.animate({opacity:0},150,"<>"),this.selected=!1):void 0},b}(b.EventEmitter)}).call(this); --------------------------------------------------------------------------------