├── src ├── scss │ └── .gitignore └── js │ ├── MG.js │ ├── common │ ├── register.js │ ├── window_listeners.js │ ├── hooks.js │ ├── chart_title.js │ ├── markers.js │ ├── init.js │ └── data_graphic.js │ ├── misc │ ├── error.js │ ├── formatters.js │ ├── transitions.js │ └── smoothers.js │ ├── charts │ ├── missing.js │ ├── table.js │ └── point.js │ └── layout │ ├── button.js │ └── bootstrap_dropdown.js ├── tests ├── common │ ├── .gitkeep │ ├── data_graphic_test.js │ ├── chart_title_test.js │ ├── hooks_test.js │ ├── markers_test.js │ ├── y_axis_test.js │ ├── x_axis_test.js │ └── init_test.js ├── misc │ ├── .gitkeep │ ├── process_test.js │ └── utility_test.js ├── helpers.js └── charts │ ├── histogram_test.js │ ├── bar_test.js │ ├── missing_test.js │ └── point_test.js ├── gulpfile.js ├── index.js ├── .jshintignore ├── examples ├── images │ ├── divider.png │ └── og-logo.png ├── css │ ├── metricsgraphics-demo-accessible.css │ ├── addons │ │ └── mg_line_brushing.css │ ├── highlightjs-default.css │ ├── railscasts.css │ └── metricsgraphics-demo.css ├── data │ ├── neg2.json │ ├── small-range.json │ ├── xnotdate.json │ ├── missing-y.json │ ├── missing-is-hidden.json │ ├── missing-is-hidden-accessor.json │ ├── make_fake_data.py │ ├── firefox_releases.json │ ├── ufo-sightings.json │ ├── brief-1.json │ └── some_currency.json ├── charts │ ├── addons.htm │ ├── other.htm │ ├── auto-time-formatting.htm │ ├── updating.htm │ └── annotations.htm ├── js │ └── main.js └── examples.htm ├── .gitignore ├── .jshintrc ├── .travis.yml ├── .editorconfig ├── bower.json ├── contribute.json ├── testem.json ├── HOOKS.md ├── package.json ├── gulp └── index.js └── dist └── metricsgraphics.css /src/scss/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/common/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/misc/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/js/MG.js: -------------------------------------------------------------------------------- 1 | window.MG = {version: '2.7.0'}; 2 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./gulp'); 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/metricsgraphics'); 2 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | src/js/common/bootstrap_tooltip_popover.js 2 | src/js/layout/bootstrap_dropdown.js 3 | -------------------------------------------------------------------------------- /examples/images/divider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antillas21/metrics-graphics/master/examples/images/divider.png -------------------------------------------------------------------------------- /examples/images/og-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antillas21/metrics-graphics/master/examples/images/og-logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | 4 | other/divider.psd 5 | 6 | other/htaccess.txt 7 | 8 | .DS_Store 9 | 10 | bare.html 11 | -------------------------------------------------------------------------------- /src/js/common/register.js: -------------------------------------------------------------------------------- 1 | function register(chartType, descriptor, defaults) { 2 | MG.charts[chartType] = { 3 | descriptor: descriptor, 4 | defaults: defaults || {} 5 | }; 6 | } 7 | 8 | MG.register = register; 9 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "$": false, 4 | "jQuery": false, 5 | "MG": false, 6 | "d3": false 7 | }, 8 | "laxbreak": true, 9 | "validthis": true, 10 | "loopfunc": true, 11 | "sub": true 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | script: npm run test-ci 5 | notifications: 6 | irc: 7 | channels: 8 | - "irc.mozilla.org#metrics" 9 | on_success: change 10 | on_failure: always 11 | use_notice: true 12 | -------------------------------------------------------------------------------- /tests/misc/process_test.js: -------------------------------------------------------------------------------- 1 | module('process'); 2 | 3 | test('args.missing_is_zero doesn\'t throw a "args.data[0][0] is undefined" error', function() { 4 | var data = [{"date": new Date('2014-02-02'), "value": 6}]; 5 | var params = { 6 | data: data, 7 | target: "#qunit-fixture", 8 | missing_is_zero: true 9 | }; 10 | 11 | MG.data_graphic(params); 12 | 13 | equal(params.data.length, 1, 'args.data is defined'); 14 | }); -------------------------------------------------------------------------------- /examples/css/metricsgraphics-demo-accessible.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 16px; 3 | } 4 | 5 | .mg-x-axis text, 6 | .mg-y-axis text, 7 | .mg-histogram .axis text { 8 | opacity: 0.9; 9 | } 10 | 11 | .mg-x-axis line, 12 | .mg-y-axis line { 13 | opacity: 1; 14 | } 15 | 16 | .mg-markers text, 17 | .mg-year-marker text, 18 | .mg-baselines text { 19 | opacity: 0.9; 20 | } 21 | 22 | .mg-markers line, 23 | .mg-year-marker line, 24 | .mg-baselines line { 25 | opacity: 1; 26 | } -------------------------------------------------------------------------------- /src/js/misc/error.js: -------------------------------------------------------------------------------- 1 | // call this to add a warning icon to a graph and log an error to the console 2 | function error (args) { 3 | console.log('ERROR : ', args.target, ' : ', args.error); 4 | 5 | d3.select(args.target).select('.mg-chart-title') 6 | .append('i') 7 | .attr('class', 'fa fa-x fa-exclamation-circle warning'); 8 | } 9 | 10 | function internal_error (args) { 11 | console.log('INTERNAL ERROR : ', args.target, ' : ', args.internal_error); 12 | } 13 | 14 | MG.error = error; 15 | -------------------------------------------------------------------------------- /examples/css/addons/mg_line_brushing.css: -------------------------------------------------------------------------------- 1 | .mg-brush-container { 2 | cursor: crosshair; } 3 | 4 | .mg-brush-container.mg-brushing { 5 | cursor: ew-resize; } 6 | 7 | .mg-brushed, .mg-brushed * { 8 | cursor: zoom-out !important; } 9 | 10 | .mg-brush rect.mg-extent { 11 | fill: rgba(0, 0, 0, 0.3); } 12 | 13 | .mg-brushing-in-progress { 14 | -webkit-touch-callout: none; 15 | -webkit-user-select: none; 16 | -khtml-user-select: none; 17 | -moz-user-select: none; 18 | -ms-user-select: none; 19 | user-select: none; } 20 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | function generateMouseEvent(type) { 2 | var event = document.createEvent('MouseEvent'); 3 | event.initEvent(type, true, true); 4 | return event; 5 | } 6 | 7 | // essentially the same as $.extend 8 | function extend(){ 9 | var result = {}, 10 | $__arguments = [].slice.call(arguments); 11 | 12 | $__arguments.forEach(function(obj) { 13 | for (var prop in obj) { 14 | if (obj.hasOwnProperty(prop)) { 15 | result[prop] = obj[prop]; 16 | } 17 | } 18 | }); 19 | return result; 20 | } 21 | -------------------------------------------------------------------------------- /examples/data/neg2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "measure": 43.34, 4 | "subject": 32.0 5 | }, 6 | { 7 | "measure": 53.3423, 8 | "subject": 20.27 9 | }, 10 | { 11 | "measure": -10.343, 12 | "subject": 12.42 13 | }, 14 | { 15 | "measure": -1, 16 | "subject": -1.69 17 | }, 18 | { 19 | "measure": 10, 20 | "subject": -10.01 21 | }, 22 | { 23 | "measure": 25, 24 | "subject": -21.59 25 | }, 26 | { 27 | "measure": -20.343, 28 | "subject": -31.86 29 | } 30 | ] -------------------------------------------------------------------------------- /examples/data/small-range.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "date": "2014-01-01", 4 | "value": 1 5 | }, 6 | { 7 | "date": "2014-01-02", 8 | "value": 3 9 | }, 10 | { 11 | "date": "2014-01-03", 12 | "value": 2 13 | }, 14 | { 15 | "date": "2014-01-04", 16 | "value": 2 17 | }, 18 | { 19 | "date": "2014-01-05", 20 | "value": 2 21 | }, 22 | { 23 | "date": "2014-01-06", 24 | "value": 1 25 | } 26 | ] -------------------------------------------------------------------------------- /examples/data/xnotdate.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "males": 50, 4 | "females": 12 5 | }, 6 | { 7 | "males": 95, 8 | "females": 66 9 | }, 10 | { 11 | "males": 143, 12 | "females": 89 13 | }, 14 | { 15 | "males": 198, 16 | "females": 105 17 | }, 18 | { 19 | "males": 244, 20 | "females": 533 21 | }, 22 | { 23 | "males": 277, 24 | "females": 175 25 | }, 26 | { 27 | "males": 344, 28 | "females": 401 29 | }, 30 | { 31 | "males": 441, 32 | "females": 1299 33 | } 34 | ] -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.js] 16 | indent_style = space 17 | indent_size = 4 18 | 19 | [*.hbs] 20 | indent_style = space 21 | indent_size = 4 22 | 23 | [*.css] 24 | indent_style = space 25 | indent_size = 4 26 | 27 | [*.html] 28 | indent_style = space 29 | indent_size = 4 30 | 31 | [*.{diff,md}] 32 | trim_trailing_whitespace = false 33 | -------------------------------------------------------------------------------- /src/js/misc/formatters.js: -------------------------------------------------------------------------------- 1 | function format_rollover_number(args) { 2 | var num; 3 | if (args.format === 'count') { 4 | num = function(d_) { 5 | var is_float = d_ % 1 !== 0; 6 | var n = d3.format("0,000"); 7 | d_ = is_float ? d3.round(d_, args.decimals) : d_; 8 | return n(d_); 9 | }; 10 | } else { 11 | num = function(d_) { 12 | var fmt_string = (args.decimals ? '.' + args.decimals : '' ) + '%'; 13 | var n = d3.format(fmt_string); 14 | return n(d_); 15 | }; 16 | } 17 | return num; 18 | } 19 | 20 | MG.format_rollover_number = format_rollover_number; 21 | -------------------------------------------------------------------------------- /src/js/common/window_listeners.js: -------------------------------------------------------------------------------- 1 | function mg_window_listeners(args) { 2 | mg_if_aspect_ratio_resize_svg(args); 3 | } 4 | 5 | function mg_if_aspect_ratio_resize_svg(args) { 6 | //have we asked the svg to fill a div, if so resize with div 7 | if (args.full_width || args.full_height) { 8 | window.addEventListener('resize', function() { 9 | var svg = d3.select(args.target).select('svg'); 10 | var aspect = svg.attr('height') / svg.attr('width'); 11 | var newWidth = get_width(args.target); 12 | 13 | svg.attr('width', newWidth); 14 | svg.attr('height', aspect * newWidth); 15 | }, true); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/data/missing-y.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "date": "2014-01-08", 5 | "value": 500 6 | }, 7 | { 8 | "date": "2014-01-12", 9 | "value": 500 10 | }, 11 | { 12 | "date": "2014-01-19", 13 | "value": 2000 14 | }, 15 | { 16 | "date": "2014-01-20", 17 | "value": 2200 18 | }, 19 | { 20 | "date": "2014-01-21", 21 | "value": 2300 22 | }, 23 | { 24 | "date": "2014-04-23", 25 | "value": 500 26 | }, 27 | { 28 | "date": "2014-04-24", 29 | "value": 1500 30 | }, 31 | { 32 | "date": "2014-04-25", 33 | "value": 3000 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /examples/data/missing-is-hidden.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "date": "2014-01-08", 5 | "value": 500 6 | }, 7 | { 8 | "date": "2014-01-09", 9 | "value": 500 10 | }, 11 | { 12 | "date": "2014-01-10", 13 | "value": 400 14 | }, 15 | { 16 | "date": "2014-02-12", 17 | "value": 500 18 | }, 19 | { 20 | "date": "2014-02-13", 21 | "value": 100 22 | }, 23 | { 24 | "date": "2014-02-14", 25 | "value": null 26 | }, 27 | { 28 | "date": "2014-02-15", 29 | "value": 30 30 | }, 31 | { 32 | "date": "2014-02-16", 33 | "value": 300 34 | }, 35 | { 36 | "date": "2014-02-17", 37 | "value": 200 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /src/js/misc/transitions.js: -------------------------------------------------------------------------------- 1 | // http://bl.ocks.org/mbostock/3916621 2 | function path_tween(d1, precision) { 3 | return function() { 4 | var path0 = this, 5 | path1 = path0.cloneNode(), 6 | n0 = path0.getTotalLength() || 0, 7 | n1 = (path1.setAttribute("d", d1), path1).getTotalLength() || 0; 8 | 9 | // Uniform sampling of distance based on specified precision. 10 | var distances = [0], i = 0, dt = precision / Math.max(n0, n1); 11 | while ((i += dt) < 1) distances.push(i); 12 | distances.push(1); 13 | 14 | // Compute point-interpolators at each distance. 15 | var points = distances.map(function(t) { 16 | var p0 = path0.getPointAtLength(t * n0), 17 | p1 = path1.getPointAtLength(t * n1); 18 | return d3.interpolate([p0.x, p0.y], [p1.x, p1.y]); 19 | }); 20 | 21 | return function(t) { 22 | return t < 1 ? "M" + points.map(function(p) { return p(t); }).join("L") : d1; 23 | }; 24 | }; 25 | } 26 | 27 | MG.path_tween = path_tween; 28 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metrics-graphics", 3 | "main": [ 4 | "dist/metricsgraphics.js", 5 | "dist/metricsgraphics.css" 6 | ], 7 | "dependencies": { 8 | "jquery": ">=1.11.1", 9 | "d3": ">=3.4.8" 10 | }, 11 | "ignore": [ 12 | ".DS_Store", 13 | ".git", 14 | ".gitignore", 15 | "examples", 16 | "gulp", 17 | "gulpfile.js", 18 | "index.js", 19 | "node_modules", 20 | "package.json", 21 | "src", 22 | "testem.json", 23 | "tests" 24 | ], 25 | "license": "MPL-2.0", 26 | "authors": [ 27 | "Ali Almossawi", "Ali Almossawi (http://twitter.com/alialmossawi)>", 28 | "Hamilton Ulmer", "Hamilton Ulmer (http://twitter.com/hamiltonulmer)>" 29 | ], 30 | "homepage": "http://metricsgraphicsjs.org", 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/mozilla/metrics-graphics.git" 34 | }, 35 | "keywords": [ 36 | "metrics-graphics", 37 | "metricsgraphicsjs", 38 | "metricsgraphics", 39 | "metricsgraphics.js", 40 | "d3 charts" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /contribute.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MetricsGraphics.js", 3 | "description": "A library optimized for concise, principled data graphics and layouts.", 4 | "repository": { 5 | "url": "https://github.com/mozilla/metrics-graphics", 6 | "license": "MPL2", 7 | "tests": "https://travis-ci.org/mozilla/metrics-graphics/" 8 | }, 9 | "participate": { 10 | "home": "http://metricsgraphicsjs.org/", 11 | "docs": "https://github.com/mozilla/metrics-graphics/wiki#resources", 12 | "irc": "irc://irc.mozilla.org/#metrics", 13 | "irc-contacts": [ 14 | "almossawi", 15 | "hulmer" 16 | ] 17 | }, 18 | "bugs": { 19 | "list": "https://github.com/mozilla/metrics-graphics/issues", 20 | "report": "https://github.com/mozilla/metrics-graphics/issues/new", 21 | "mentored": "https://github.com/mozilla/metrics-graphics/labels/help%20wanted" 22 | }, 23 | "keywords": [ 24 | "nodejs", 25 | "d3", 26 | "data", 27 | "graphics" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "qunit", 3 | "src_files": [ 4 | "node_modules/jquery/dist/jquery.js", 5 | "node_modules/d3/d3.js", 6 | 7 | "src/js/MG.js", 8 | "src/js/common/register.js", 9 | "src/js/common/hooks.js", 10 | "src/js/common/data_graphic.js", 11 | "src/js/common/bootstrap_tooltip_popover.js", 12 | "src/js/common/chart_title.js", 13 | "src/js/common/y_axis.js", 14 | "src/js/common/x_axis.js", 15 | "src/js/common/init.js", 16 | "src/js/common/markers.js", 17 | "src/js/common/window_listeners.js", 18 | "src/js/layout/bootstrap_dropdown.js", 19 | "src/js/layout/button.js", 20 | "src/js/charts/line.js", 21 | "src/js/charts/histogram.js", 22 | "src/js/charts/point.js", 23 | "src/js/charts/bar.js", 24 | "src/js/charts/table.js", 25 | "src/js/charts/missing.js", 26 | "src/js/misc/process.js", 27 | "src/js/misc/smoothers.js", 28 | "src/js/misc/formatters.js", 29 | "src/js/misc/transitions.js", 30 | "src/js/misc/utility.js", 31 | "src/js/misc/error.js", 32 | 33 | "tests/helpers.js", 34 | 35 | "tests/**/*_test.js" 36 | ], 37 | "launch_in_ci": [ 38 | "PhantomJS" 39 | ], 40 | "launch_in_dev": [ 41 | "PhantomJS", 42 | "Chrome" 43 | ], 44 | "phantomjs_debug_port": 9000 45 | } 46 | -------------------------------------------------------------------------------- /HOOKS.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | ### Global 3 | | Name | args | Description | 4 | |------|------|-------------| 5 | | `global.defaults` | `defaults` | Passes the global defaults prior to merging with args and chart-specific defaults | 6 | | `global.before_init` | `args` | Called before initializing a chart. Allows pre-processing of the arguments passed into `MG.data_graphic`. | 7 | | `x_axis.process_min_max` | `args`, `min_x`, `max_x` | Called after calculating the min and max values for the X axis | 8 | | `y_axis.process_min_max` | `args`, `min_y`, `max_y` | Called after calculating the min and max values for the Y axis | 9 | 10 | ### Line 11 | | Name | args | Description | Notes | 12 | |------|------|-------------|-------| 13 | | `line.after_init` | `lineChart` - chart descriptor | Called after intializing the chart | | 14 | | `line.after_rollover` | `args` | Called after setting up the rollover | | 15 | | `line.before_all_series` | `args` | Called before rendering the chart. | Returning `false` will prevent the default rendering process from being executed. | 16 | | `line.before_each_series` | `data[i]` - The current data in the for loop
`args` | Called within the render loop, before any other render takes place. | | 17 | | `line.after_each_series` | `data[i]` - The current data in the for loop
`args` | Called within the render loop, after the default render has taken place. | | 18 | -------------------------------------------------------------------------------- /examples/data/missing-is-hidden-accessor.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "date": "2014-01-08", 5 | "value": 500 6 | }, 7 | { 8 | "date": "2014-01-09", 9 | "value": 500 10 | }, 11 | { 12 | "date": "2014-01-10", 13 | "value": 400 14 | }, 15 | { 16 | "date": "2014-01-11", 17 | "value": 500, 18 | "dead": true 19 | }, 20 | { 21 | "date": "2014-01-12", 22 | "value": 400 23 | }, 24 | { 25 | "date": "2014-01-13", 26 | "value": 430 27 | }, 28 | { 29 | "date": "2014-01-14", 30 | "value": 410 31 | }, 32 | { 33 | "date": "2014-01-15", 34 | "value": 200, 35 | "dead": true 36 | }, 37 | { 38 | "date": "2014-01-16", 39 | "value": 500 40 | }, 41 | { 42 | "date": "2014-01-17", 43 | "value": 100 44 | }, 45 | { 46 | "date": "2014-01-18", 47 | "value": 30 48 | }, 49 | { 50 | "date": "2014-01-19", 51 | "value": 300 52 | }, 53 | { 54 | "date": "2014-01-20", 55 | "value": 200 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /src/js/common/hooks.js: -------------------------------------------------------------------------------- 1 | /** 2 | Record of all registered hooks. 3 | For internal use only. 4 | */ 5 | MG._hooks = {}; 6 | 7 | /** 8 | Add a hook callthrough to the stack. 9 | 10 | Hooks are executed in the order that they were registered. 11 | */ 12 | MG.add_hook = function(name, func, context) { 13 | var hooks; 14 | 15 | if (!MG._hooks[name]) { 16 | MG._hooks[name] = []; 17 | } 18 | 19 | hooks = MG._hooks[name]; 20 | 21 | var already_registered = 22 | hooks.filter(function(hook) { 23 | return hook.func === func; 24 | }) 25 | .length > 0; 26 | 27 | if (already_registered) { 28 | throw 'That function is already registered.'; 29 | } 30 | 31 | hooks.push({ 32 | func: func, 33 | context: context 34 | }); 35 | }; 36 | 37 | /** 38 | Execute registered hooks. 39 | 40 | Optional arguments 41 | */ 42 | MG.call_hook = function(name) { 43 | var hooks = MG._hooks[name], 44 | result = [].slice.apply(arguments, [1]), 45 | processed; 46 | 47 | if (hooks) { 48 | hooks.forEach(function(hook) { 49 | if (hook.func) { 50 | var params = processed || result; 51 | 52 | if (params && params.constructor !== Array) { 53 | params = [params]; 54 | } 55 | 56 | params = [].concat.apply([], params); 57 | processed = hook.func.apply(hook.context, params); 58 | } 59 | }); 60 | } 61 | 62 | return processed || result; 63 | }; 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metrics-graphics", 3 | "version": "2.7.0", 4 | "description": "A library optimized for concise, principled data graphics and layouts", 5 | "main": "dist/metricsgraphics.js", 6 | "scripts": { 7 | "build": "gulp build:js", 8 | "test": "gulp test", 9 | "test-ci": "./node_modules/testem/testem.js ci testem.json" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/mozilla/metrics-graphics.git" 14 | }, 15 | "keywords": [ 16 | "metrics-graphics", 17 | "metricsgraphicsjs", 18 | "metricsgraphics", 19 | "metricsgraphics.js", 20 | "d3 charts" 21 | ], 22 | "author": "Mozilla", 23 | "contributors": [ 24 | "Ali Almossawi (http://twitter.com/alialmossawi)", 25 | "Hamilton Ulmer (http://twitter.com/hamiltonulmer)" 26 | ], 27 | "license": "MPL-2.0", 28 | "bugs": { 29 | "url": "https://github.com/mozilla/metrics-graphics/issues" 30 | }, 31 | "engines": { 32 | "node": ">=0.8.0" 33 | }, 34 | "homepage": "http://metricsgraphicsjs.org", 35 | "dependencies": { 36 | "jquery": ">=1.11.1", 37 | "d3": ">=3.4.8" 38 | }, 39 | "devDependencies": { 40 | "gulp": "^3.8.10", 41 | "gulp-concat": "^2.4.2", 42 | "gulp-connect": "^2.2.0", 43 | "gulp-es6-module-transpiler": "^0.2.0", 44 | "gulp-jshint": "^1.9.0", 45 | "gulp-rename": "^1.2.0", 46 | "gulp-rimraf": "^0.1.1", 47 | "gulp-testem": "0.0.1", 48 | "gulp-uglify": "^1.0.2", 49 | "gulp-umd": "^0.1.3", 50 | "gulp-util": "^3.0.1", 51 | "qunitjs": "^1.16.0", 52 | "require-dir": "^0.1.0", 53 | "testem": "^0.6.24" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/misc/utility_test.js: -------------------------------------------------------------------------------- 1 | module('utility'); 2 | 3 | test('MG.convert.date', function() { 4 | var data = [{'date': '2014-01-01', 'value': 12}, 5 | {'date': '2014-03-01', 'value': 18}]; 6 | 7 | MG.convert.date(data, 'date'); 8 | equal($.type(data[0].date), 'date', 'First date is of type date'); 9 | equal($.type(data[0].date), 'date', 'Second date is of type date'); 10 | }); 11 | 12 | test('MG.convert.date with an alternative timestamp style', function() { 13 | var data = [{'date': '2014-20-12', 'value': 12}, 14 | {'date': '2014-21-12', 'value': 18}]; 15 | 16 | MG.convert.date(data, 'date', '%Y-%d-%m'); 17 | equal($.type(data[0].date), 'date', 'First date is of type date'); 18 | equal($.type(data[0].date), 'date', 'Second date is of type date'); 19 | }); 20 | 21 | test('MG.convert.number', function() { 22 | var data = [{'date': '2014-20-12', 'value': '12'}, 23 | {'date': '2014-21-12', 'value': '18'}]; 24 | 25 | MG.convert.number(data, 'value'); 26 | equal($.type(data[0].value), 'number', 'First value is a number'); 27 | equal($.type(data[0].value), 'number', 'Second value is a number'); 28 | }); 29 | 30 | test('mg_get_svg_child_of', function(){ 31 | d3.select('#qunit-fixture').append('svg'); 32 | 33 | var svg_element_with_node = mg_get_svg_child_of(document.querySelector('#qunit-fixture')); 34 | var svg_element_with_text = mg_get_svg_child_of('#qunit-fixture'); 35 | 36 | equal(svg_element_with_node.length, 1, 'Node-based argument should return a d3 selection with svg.'); 37 | equal(svg_element_with_node.length, 1, 'Selector-based argument should return a d3 selection with svg.'); 38 | }); 39 | 40 | 41 | test('mg_target_ref', function() { 42 | var chart_area2 = document.createElement('div'); 43 | mg_target_ref(chart_area2); 44 | ok(chart_area2.getAttribute('data-mg-uid').match(/mg-[\d]/), 'applies generated ID to DOM element'); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/charts/histogram_test.js: -------------------------------------------------------------------------------- 1 | module('histogram'); 2 | 3 | test('A solitary active datapoint exists', function() { 4 | var params = { 5 | target: '#qunit-fixture', 6 | data: d3.range(10000).map(d3.random.bates(10)), 7 | chart_type: 'histogram', 8 | linked: true 9 | }; 10 | 11 | MG.data_graphic(params); 12 | equal(document.querySelectorAll('.mg-active-datapoint').length, 1, 'One active datapoint exists'); 13 | }); 14 | 15 | test('Rollovers exist', function() { 16 | var params = { 17 | target: '#qunit-fixture', 18 | data: d3.range(10000).map(d3.random.bates(10)), 19 | chart_type: 'histogram', 20 | linked: true 21 | }; 22 | 23 | MG.data_graphic(params); 24 | ok(document.querySelector('.mg-rollover-rect'), 'Rollovers exist'); 25 | }); 26 | 27 | test('We have only one set of rollovers', function() { 28 | var params = { 29 | target: '#qunit-fixture', 30 | data: d3.range(10000).map(d3.random.bates(10)), 31 | chart_type: 'histogram', 32 | linked: true 33 | }; 34 | 35 | MG.data_graphic(params); 36 | equal(document.querySelectorAll('.mg-rollover-rect').length, 1, 'One set of rollovers exists'); 37 | }); 38 | 39 | test('Linked chart has the required class set', function() { 40 | var params = { 41 | target: '#qunit-fixture', 42 | data: d3.range(10000).map(d3.random.bates(10)), 43 | chart_type: 'histogram', 44 | linked: true 45 | }; 46 | 47 | MG.data_graphic(params); 48 | var matches = document.querySelector(params.target + ' svg').getAttribute('class').match(/linked/); 49 | ok(matches, 'Linked chart has class `linked` set'); 50 | }); 51 | 52 | test('Histogram exists', function() { 53 | var params = { 54 | target: '#qunit-fixture', 55 | data: d3.range(10000).map(d3.random.bates(10)), 56 | chart_type: 'histogram', 57 | linked: true 58 | }; 59 | 60 | MG.data_graphic(params); 61 | ok(document.querySelector('.mg-histogram'), 'Histogram exists'); 62 | }); -------------------------------------------------------------------------------- /tests/common/data_graphic_test.js: -------------------------------------------------------------------------------- 1 | module('data_graphic'); 2 | 3 | test('Required arguments are set', function() { 4 | var params = { 5 | target: '#qunit-fixture', 6 | data: [{'date': new Date('2014-11-01'), 'value': 12}, 7 | {'date': new Date('2014-11-02'), 'value': 18}] 8 | }; 9 | 10 | MG.data_graphic(params); 11 | 12 | ok(params.width, 'args.width is set'); 13 | ok(params.height, 'args.height is set'); 14 | ok(params.data, 'args.data is set'); 15 | ok(params.target, 'args.target is set'); 16 | }); 17 | 18 | test('Dom element works as target', function() { 19 | var params = { 20 | target: document.getElementById('qunit-fixture'), 21 | data: [{'date': new Date('2014-11-01'), 'value': 12}, 22 | {'date': new Date('2014-11-02'), 'value': 18}] 23 | }; 24 | 25 | MG.data_graphic(params); 26 | 27 | ok(document.querySelector('#qunit-fixture svg') != null, 'passing in dom element works properly'); 28 | }); 29 | 30 | // Can be removed in 2.x 31 | test('Correctly aliases callbacks when using 1.x-style method names', function() { 32 | var mouseoverCalled = false, 33 | mouseoutCalled = false, 34 | 35 | params = { 36 | target: '#qunit-fixture', 37 | data: [{value: 1, label: 'One'}], 38 | chart_type: 'bar', 39 | rollover_callback: function() { 40 | mouseoverCalled = true; 41 | }, 42 | rollout_callback: function() { 43 | mouseoutCalled = true; 44 | } 45 | }; 46 | 47 | MG.data_graphic(params); 48 | 49 | var bar = document.getElementsByClassName('mg-bar-rollover')[0]; 50 | 51 | bar.dispatchEvent(generateMouseEvent('mouseover')); 52 | equal(mouseoverCalled, true, 'rollover_callback was called'); 53 | 54 | bar.dispatchEvent(generateMouseEvent('mouseout')); 55 | equal(mouseoutCalled, true, 'rollout_callback was called'); 56 | 57 | ok(MG.deprecations.rollover_callback.warned, 'rollover_callback deprecation notice displayed'); 58 | ok(MG.deprecations.rollout_callback.warned, 'rollout_callback deprecation notice displayed'); 59 | }); -------------------------------------------------------------------------------- /examples/charts/addons.htm: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 |
<script src='js/addons/mg_line_brushing.js'></script>
 7 | <link rel='stylesheet' href='css/addons/mg_line_brushing.css' />
 8 | d3.json('data/fake_users2.json', function(data) {
 9 |     for (var i = 0; i < data.length; i++) {
10 |         data[i] = MG.convert.date(data[i], 'date');
11 |     }
12 | 
13 |     MG.data_graphic({
14 |         title: "Brushing Addon by Dan De Havilland",
15 |         description: "Drag the crosshair over the chart to zoom. For further details about this addon, take a look at its GitHub repo.",
16 |         data: data,
17 |         top: 70,
18 |         width: 600,
19 |         height: 240,
20 |         right: 40,
21 |         missing_is_hidden: true,
22 |         target: '#brushing'
23 |     });
24 | });
25 | 26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/data/make_fake_data.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from datetime import datetime, date, timedelta 3 | import json 4 | from random import random 5 | import argparse 6 | 7 | parser = argparse.ArgumentParser(description="Fake data maker.") 8 | parser.add_argument('-s', '--start', help="start date", nargs=1) 9 | parser.add_argument('-d', '--small', help="allow decimal values", action="store_true") 10 | parser.add_argument('-r', '--range', help="how many days from start date", nargs=1) 11 | parser.add_argument('-m', '--magnitude', help="size of change", nargs=1) 12 | parser.add_argument('-p', '--percentage', help="whether or not this is a percentage", action="store_true") 13 | parser.add_argument('-n', '--let_negative', help="let the data values go negative", action="store_true") 14 | parser.add_argument('-v', '--start_value', help='start value', nargs=1) 15 | args = parser.parse_args() 16 | 17 | doy = { 18 | 0: 1, 19 | 1:.9, 20 | 2:1, 21 | 3:1.1, 22 | 4:1.05, 23 | 5:.7, 24 | 6:.75 25 | } 26 | if args.percentage: convert = lambda x: float(x) 27 | elif args.small: convert = lambda x: float(x) 28 | else: convert = lambda x: int(x) 29 | 30 | current_date = datetime.strptime(args.start[0], '%Y-%m-%d').date() 31 | length = int(args.range[0]) 32 | 33 | baseline = convert(args.start_value[0]) 34 | 35 | if args.magnitude: magnitude = convert(args.magnitude[0]) 36 | elif args.percentage: magnitude = baseline/50. 37 | elif args.small: magnitude = baseline/20. 38 | else: magnitude = baseline/5 39 | out = [] 40 | 41 | # Needed to automatically convert dates to strings in json. 42 | dthandler = lambda obj: obj.isoformat() if isinstance(obj, date) else None 43 | 44 | for i in xrange(length): 45 | out.append({'date': current_date, 'value': baseline}) 46 | nb = convert(magnitude * (random()-.5)) 47 | if args.percentage: 48 | if (baseline+nb < 0 or baseline+nb > 1) and (not args.let_negative): pass 49 | else: baseline += nb 50 | else: 51 | if baseline+nb < 0 and not args.let_negative: pass 52 | else: baseline += nb 53 | 54 | current_date += timedelta(days=1) 55 | 56 | sys.stdout.write(json.dumps(out,default=dthandler, indent=4)) -------------------------------------------------------------------------------- /src/js/common/chart_title.js: -------------------------------------------------------------------------------- 1 | function mg_remove_element (svg, elem) { 2 | svg.select(elem).remove(); 3 | } 4 | 5 | function mg_add_chart_title_element (svg, args) { 6 | 7 | } 8 | 9 | function chart_title(args) { 10 | 'use strict'; 11 | 12 | var svg = mg_get_svg_child_of(args.target); 13 | 14 | //remove the current title if it exists 15 | //svg.select('.mg-header').remove(); 16 | mg_remove_element(svg, '.mg-header'); 17 | if (args.target && args.title) { 18 | var chartTitle = svg.insert('text') 19 | .attr('class', 'mg-header') 20 | .attr('x', (args.width + args.left - args.right) / 2) 21 | .attr('y', args.title_y_position) 22 | .attr('text-anchor', 'middle') 23 | .attr('dy', '0.55em'); 24 | 25 | //show the title 26 | chartTitle.append('tspan') 27 | .attr('class', 'mg-chart-title') 28 | .text(args.title); 29 | 30 | //show and activate the description icon if we have a description 31 | if (args.show_tooltips && args.description) { 32 | chartTitle.append('tspan') 33 | .attr('class', 'mg-chart-description') 34 | .attr('dx', '0.3em') 35 | .text('\uf059'); 36 | 37 | //now that the title is an svg text element, we'll have to trigger 38 | //mouseenter, mouseleave events manually for the popover to work properly 39 | var $chartTitle = $(chartTitle.node()); 40 | $chartTitle.popover({ 41 | html: true, 42 | animation: false, 43 | placement: 'top', 44 | content: args.description, 45 | container: args.target, 46 | trigger: 'manual', 47 | template: '

' 48 | }).on('mouseenter', function() { 49 | d3.selectAll(args.target) 50 | .selectAll('.mg-popover') 51 | .remove(); 52 | 53 | $(this).popover('show'); 54 | $(args.target).select('.popover') 55 | .on('mouseleave', function () { 56 | $chartTitle.popover('hide'); 57 | }); 58 | }).on('mouseleave', function () { 59 | setTimeout(function () { 60 | if (!$('.popover:hover').length) { 61 | $chartTitle.popover('hide'); 62 | } 63 | }, 120); 64 | }); 65 | } 66 | } 67 | 68 | if (args.error) { 69 | error(args); 70 | } 71 | } 72 | 73 | MG.chart_title = chart_title; 74 | -------------------------------------------------------------------------------- /tests/common/chart_title_test.js: -------------------------------------------------------------------------------- 1 | module('chart_title'); 2 | 3 | test('Chart title is updated', function() { 4 | var params = { 5 | title: 'foo', 6 | target: '#qunit-fixture', 7 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 8 | {'date': new Date('2014-03-01'), 'value': 18}] 9 | }; 10 | 11 | var params2 = MG.clone(params); 12 | params2.title = 'bar'; 13 | 14 | MG.data_graphic(params); 15 | MG.data_graphic(params2); 16 | 17 | equal(document.querySelector('.mg-chart-title').textContent, 'bar', 'Chart title is foo'); 18 | }); 19 | 20 | test('Chart title is removed if title is set to blank', function() { 21 | var params = { 22 | title: 'foo', 23 | target: '#qunit-fixture', 24 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 25 | {'date': new Date('2014-03-01'), 'value': 18}] 26 | }; 27 | 28 | var params2 = MG.clone(params); 29 | params2.title = ''; 30 | 31 | MG.data_graphic(params); 32 | MG.data_graphic(params2); 33 | equal(document.querySelector('.mg-chart-title'), null, 'Chart title is not added'); 34 | }); 35 | 36 | test('Chart title is removed if title is not set', function() { 37 | var params = { 38 | title: 'foo', 39 | target: '#qunit-fixture', 40 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 41 | {'date': new Date('2014-03-01'), 'value': 18}] 42 | }; 43 | 44 | var params2 = MG.clone(params); 45 | delete params2.title; 46 | 47 | MG.data_graphic(params); 48 | MG.data_graphic(params2); 49 | equal(document.querySelector('.mg-chart-title'), null, 'Chart title is not added'); 50 | }); 51 | 52 | test('When a description is set, we get a question mark', function() { 53 | var params = { 54 | title: 'foo', 55 | description: 'bar', 56 | target: '#qunit-fixture', 57 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 58 | {'date': new Date('2014-03-01'), 'value': 18}], 59 | show_tooltips: true 60 | }; 61 | 62 | MG.data_graphic(params); 63 | ok(document.querySelector('.mg-chart-description'), 'Description icon exists'); 64 | }); 65 | 66 | test('When an error is set, we get an exclamation icon', function() { 67 | var params = { 68 | title: 'foo', 69 | description: 'bar', 70 | target: '#qunit-fixture', 71 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 72 | {'date': new Date('2014-03-01'), 'value': 18}], 73 | error: 'lorem ipsum' 74 | }; 75 | 76 | MG.data_graphic(params); 77 | ok(document.querySelector('.mg-chart-title .warning'), 'Error icon exists'); 78 | }); 79 | 80 | test('Chart title is not duplicated on redraw', function() { 81 | var params = { 82 | title: 'foo', 83 | target: '#qunit-fixture', 84 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 85 | {'date': new Date('2014-03-01'), 'value': 18}] 86 | }; 87 | 88 | var params2 = MG.clone(params); 89 | MG.data_graphic(params); 90 | MG.data_graphic(params2); 91 | 92 | equal(document.querySelectorAll('.mg-chart-title').length, 1, 'there is once chart title'); 93 | }); 94 | -------------------------------------------------------------------------------- /examples/data/firefox_releases.json: -------------------------------------------------------------------------------- 1 | { 2 | "releases": [ 3 | { 4 | "version": "30.0", 5 | "date": "2014-06-01", 6 | "end": "2014-08-01" 7 | }, 8 | { 9 | "version": "29.0", 10 | "date": "2014-04-29", 11 | "end": "2014-06-01" 12 | 13 | }, 14 | { 15 | "version": "28.0", 16 | "date": "2014-03-18", 17 | "end": "2014-04-29" 18 | }, 19 | { 20 | "version": "27.0", 21 | "date": "2014-01-21", 22 | "end": "2014-03-18" 23 | }, 24 | { 25 | "version": "26.0", 26 | "date": "2013-12-10", 27 | "end": "2014-01-21" 28 | }, 29 | { 30 | "version": "25.0", 31 | "date": "2013-10-29", 32 | "end": "2013-12-10" 33 | 34 | }, 35 | { 36 | "version": "24.0", 37 | "date": "2013-09-17", 38 | "end": "2013-10-29" 39 | 40 | }, 41 | { 42 | "version": "23.0", 43 | "date": "2013-08-06", 44 | "end": "2013-09-17" 45 | 46 | }, 47 | { 48 | "version": "22.0", 49 | "date": "2013-06-25" 50 | }, 51 | { 52 | "version": "21.0", 53 | "date": "2013-05-14" 54 | }, 55 | { 56 | "version": "20.0", 57 | "date": "2013-04-02" 58 | }, 59 | { 60 | "version": "19.0", 61 | "date": "2013-02-19" 62 | }, 63 | { 64 | "version": "18.0", 65 | "date": "2013-01-08" 66 | }, 67 | { 68 | "version": "17.0.1", 69 | "date": "2012-11-30" 70 | }, 71 | { 72 | "version": "17.0", 73 | "date": "2012-11-20" 74 | }, 75 | { 76 | "version": "16.0.2", 77 | "date": "2012-10-26" 78 | }, 79 | { 80 | "version": "16.0.1", 81 | "date": "2012-10-11" 82 | }, 83 | { 84 | "version": "16.0", 85 | "date": "2012-10-09" 86 | }, 87 | { 88 | "version": "15.0.1", 89 | "date": "2012-09-06" 90 | }, 91 | { 92 | "version": "15.0", 93 | "date": "2012-08-28" 94 | }, 95 | { 96 | "version": "14.0", 97 | "date": "2012-07-17" 98 | }, 99 | { 100 | "version": "13.0.1", 101 | "date": "2012-06-15" 102 | }, 103 | { 104 | "version": "13.0", 105 | "date": "2012-06-05" 106 | }, 107 | { 108 | "version": "12.0", 109 | "date": "2012-04-24" 110 | }, 111 | { 112 | "version": "11.0", 113 | "date": "2012-03-13" 114 | }, 115 | { 116 | "version": "10.0", 117 | "date": "2012-01-31" 118 | }, 119 | { 120 | "version": "9.0", 121 | "date": "2011-12-20" 122 | }, 123 | { 124 | "version": "8.0", 125 | "date": "2011-11-08" 126 | }, 127 | { 128 | "version": "7.0", 129 | "date": "2011-09-27" 130 | }, 131 | { 132 | "version": "6.0", 133 | "date": "2011-08-16" 134 | }, 135 | { 136 | "version": "5.0", 137 | "date": "2011-06-21" 138 | }, 139 | { 140 | "version": "4.0", 141 | "date": "2011-03-22" 142 | }, 143 | { 144 | "version": "3.6", 145 | "date": "2010-01-21" 146 | }, 147 | { 148 | "version": "3.5", 149 | "date": "2009-06-30" 150 | }, 151 | { 152 | "version": "3.0", 153 | "date": "2008-06-17" 154 | }, 155 | { 156 | "version": "2.0", 157 | "date": "2006-10-24" 158 | }, 159 | { 160 | "version": "1.5", 161 | "date": "2005-11-29" 162 | }, 163 | { 164 | "version": "1.0", 165 | "date": "2004-11-09" 166 | } 167 | ] 168 | } 169 | -------------------------------------------------------------------------------- /tests/charts/bar_test.js: -------------------------------------------------------------------------------- 1 | var target = '#qunit-fixture', 2 | defaults; 3 | 4 | module('bar', { 5 | setup: function() { 6 | defaults = { 7 | target: target, 8 | chart_type: 'bar', 9 | x_accessor: 'value', 10 | y_accessor: 'label', 11 | transition_on_update: false, 12 | data: [{ 13 | label: 'Bar 1', 14 | value: 100 15 | },{ 16 | label: 'Bar 2', 17 | value: 200 18 | },{ 19 | label: 'Bar 3', 20 | value: 300 21 | }] 22 | }; 23 | } 24 | }); 25 | 26 | test('Correct number of bars are added', function() { 27 | expect(1); 28 | MG.data_graphic(defaults); 29 | equal(document.querySelectorAll('.mg-bar').length, 3, 'Should have 3 bars'); 30 | }); 31 | 32 | test('Triggers callbacks when provided', function() { 33 | var mouseoverCalled = false, 34 | mousemoveCalled = false, 35 | mouseoutCalled = false, 36 | 37 | params = extend(defaults, { 38 | mouseover: function() { 39 | mouseoverCalled = true; 40 | }, 41 | mousemove: function() { 42 | mousemoveCalled = true; 43 | }, 44 | mouseout: function() { 45 | mouseoutCalled = true; 46 | } 47 | }); 48 | 49 | MG.data_graphic(params); 50 | 51 | var bar = document.getElementsByClassName('mg-bar-rollover')[0]; 52 | 53 | bar.dispatchEvent(generateMouseEvent('mouseover')); 54 | equal(mouseoverCalled, true, 'mouseover was called'); 55 | 56 | bar.dispatchEvent(generateMouseEvent('mousemove')); 57 | equal(mousemoveCalled, true, 'mousemove was called'); 58 | 59 | bar.dispatchEvent(generateMouseEvent('mouseout')); 60 | equal(mouseoutCalled, true, 'mouseout was called'); 61 | }); 62 | 63 | test('When updating', function() { 64 | var bars = [{ 65 | label: 'Bar 1', 66 | value: 100, 67 | predictor: 75, 68 | baseline: 50 69 | }]; 70 | 71 | var params = extend(defaults, { 72 | data: bars, 73 | height: 100, 74 | width: 300, 75 | orientation: 'vertical', 76 | predictor_accessor: 'predictor', 77 | baseline_accessor: 'baseline', 78 | animate_on_load: false, 79 | transition_on_update: false 80 | }); 81 | 82 | MG.data_graphic(params); 83 | equal(164, d3.select(target).select('.mg-barplot .mg-bar').attr('width'), 'initial bar size is correct'); 84 | equal(123, d3.select(target).select('.mg-barplot .mg-bar-prediction').attr('width'), 'initial predictor size is correct'); 85 | equal(160, d3.select(target).select('.mg-barplot .mg-bar-baseline').attr('x1'), 'initial baseline position is correct'); 86 | 87 | params.data[0][0].value = 50; 88 | params.data[0][0].predictor = 100; 89 | params.data[0][0].baseline = 75; 90 | 91 | MG.data_graphic(params); 92 | equal(82, d3.select(target).select('.mg-barplot .mg-bar').attr('width'), 'the bars are redrawn with correct sizes'); 93 | equal(164, d3.select(target).select('.mg-barplot .mg-bar-prediction').attr('width'), 'the predictors are redrawn with correct sizes'); 94 | equal(201, d3.select(target).select('.mg-barplot .mg-bar-baseline').attr('x1'), 'the baseline is redrawn in the correct position'); 95 | }); 96 | -------------------------------------------------------------------------------- /tests/charts/missing_test.js: -------------------------------------------------------------------------------- 1 | module('missing'); 2 | 3 | test('Missing chart\'s text matches specified missing_text', function() { 4 | var params = { 5 | target: '#qunit-fixture', 6 | chart_type: 'missing-data', 7 | missing_text: 'In an astral plane that was never meant to fly...' 8 | }; 9 | 10 | MG.data_graphic(params); 11 | equal(document.querySelector('.mg-missing-text').textContent, 12 | params.missing_text, 13 | 'Missing chart\'s text matches missing_text'); 14 | }); 15 | 16 | test('Only one mg-missing-pane on multiple calls to the same target element', function() { 17 | var params = { 18 | target: '#qunit-fixture', 19 | chart_type: 'missing-data', 20 | missing_text: 'In an astral plane that was never meant to fly...' 21 | }; 22 | 23 | MG.data_graphic(params); 24 | MG.data_graphic(MG.clone(params)); 25 | 26 | equal(document.querySelectorAll(params.target + ' .mg-missing-pane').length, 1, 'We only have one mg-missing-pane'); 27 | }); 28 | 29 | test('Only one mg-missing-text on multiple calls to the same target element', function() { 30 | var params = { 31 | target: '#qunit-fixture', 32 | chart_type: 'missing-data', 33 | missing_text: 'In an astral plane that was never meant to fly...' 34 | }; 35 | 36 | MG.data_graphic(params); 37 | MG.data_graphic(MG.clone(params)); 38 | 39 | equal(document.querySelectorAll(params.target + ' .mg-missing-text').length, 1, 'We only have one mg-missing-text'); 40 | }); 41 | 42 | test('missing chart obeys full_width: true', function() { 43 | var params = { 44 | target: '#qunit-fixture', 45 | chart_type: 'missing-data', 46 | full_width: true, 47 | missing_text: 'In an astral plane that was never meant to fly...' 48 | }; 49 | document.querySelector('#qunit-fixture').style.width='700px'; 50 | 51 | MG.data_graphic(params); 52 | 53 | equal(document.querySelector('#qunit-fixture svg').getAttribute('width'), 700, 'The missing chart svg has same width as parent element.'); 54 | }); 55 | 56 | test('missing chart obeys full_height: true', function() { 57 | var params = { 58 | target: '#qunit-fixture', 59 | chart_type: 'missing-data', 60 | full_height: true, 61 | missing_text: 'In an astral plane that was never meant to fly...' 62 | }; 63 | document.querySelector('#qunit-fixture').style.height='700px'; 64 | 65 | MG.data_graphic(params); 66 | 67 | equal(document.querySelector('#qunit-fixture svg').getAttribute('height'), 700, 'The missing chart svg has same width as parent element.'); 68 | }); 69 | 70 | test('Missing chart\'s width is set correctly on subsequent calls to existing chart', function() { 71 | var params_0 = { 72 | target: '#qunit-fixture', 73 | chart_type: 'missing-data', 74 | missing_text: 'In an astral plane that was never meant to fly...' 75 | }; 76 | 77 | var params = { 78 | target: '#qunit-fixture', 79 | chart_type: 'missing-data', 80 | missing_text: 'In an astral plane that was never meant to fly...', 81 | width: 200, 82 | height: 100, 83 | }; 84 | 85 | MG.data_graphic(params_0); 86 | MG.data_graphic(params); 87 | 88 | var width = document.querySelector(params.target + ' svg').offsetWidth; 89 | ok(width == 200, 'SVG\'s width matches latest specified width'); 90 | }); -------------------------------------------------------------------------------- /examples/css/highlightjs-default.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original style from softwaremaniacs.org (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #f0f0f0; 12 | -webkit-text-size-adjust: none; 13 | } 14 | 15 | .hljs, 16 | .hljs-subst, 17 | .hljs-tag .hljs-title, 18 | .nginx .hljs-title { 19 | color: black; 20 | } 21 | 22 | .hljs-string, 23 | .hljs-title, 24 | .hljs-constant, 25 | .hljs-parent, 26 | .hljs-tag .hljs-value, 27 | .hljs-rules .hljs-value, 28 | .hljs-preprocessor, 29 | .hljs-pragma, 30 | .haml .hljs-symbol, 31 | .ruby .hljs-symbol, 32 | .ruby .hljs-symbol .hljs-string, 33 | .hljs-template_tag, 34 | .django .hljs-variable, 35 | .smalltalk .hljs-class, 36 | .hljs-addition, 37 | .hljs-flow, 38 | .hljs-stream, 39 | .bash .hljs-variable, 40 | .apache .hljs-tag, 41 | .apache .hljs-cbracket, 42 | .tex .hljs-command, 43 | .tex .hljs-special, 44 | .erlang_repl .hljs-function_or_atom, 45 | .asciidoc .hljs-header, 46 | .markdown .hljs-header, 47 | .coffeescript .hljs-attribute { 48 | color: #e6337c; 49 | } 50 | 51 | .smartquote, 52 | .hljs-comment, 53 | .hljs-annotation, 54 | .hljs-template_comment, 55 | .diff .hljs-header, 56 | .hljs-chunk, 57 | .asciidoc .hljs-blockquote, 58 | .markdown .hljs-blockquote { 59 | color: #888; 60 | } 61 | 62 | .hljs-number, 63 | .hljs-date, 64 | .hljs-regexp, 65 | .hljs-literal, 66 | .hljs-hexcolor, 67 | .smalltalk .hljs-symbol, 68 | .smalltalk .hljs-char, 69 | .go .hljs-constant, 70 | .hljs-change, 71 | .lasso .hljs-variable, 72 | .makefile .hljs-variable, 73 | .asciidoc .hljs-bullet, 74 | .markdown .hljs-bullet, 75 | .asciidoc .hljs-link_url, 76 | .markdown .hljs-link_url { 77 | color: #366797; 78 | } 79 | 80 | .hljs-label, 81 | .hljs-javadoc, 82 | .ruby .hljs-string, 83 | .hljs-decorator, 84 | .hljs-filter .hljs-argument, 85 | .hljs-localvars, 86 | .hljs-array, 87 | .hljs-attr_selector, 88 | .hljs-important, 89 | .hljs-pseudo, 90 | .hljs-pi, 91 | .haml .hljs-bullet, 92 | .hljs-doctype, 93 | .hljs-deletion, 94 | .hljs-envvar, 95 | .hljs-shebang, 96 | .apache .hljs-sqbracket, 97 | .nginx .hljs-built_in, 98 | .tex .hljs-formula, 99 | .erlang_repl .hljs-reserved, 100 | .hljs-prompt, 101 | .asciidoc .hljs-link_label, 102 | .markdown .hljs-link_label, 103 | .vhdl .hljs-attribute, 104 | .clojure .hljs-attribute, 105 | .asciidoc .hljs-attribute, 106 | .lasso .hljs-attribute, 107 | .coffeescript .hljs-property, 108 | .hljs-phony { 109 | color: #88f; 110 | } 111 | 112 | .hljs-keyword, 113 | .hljs-id, 114 | .hljs-title, 115 | .hljs-built_in, 116 | .css .hljs-tag, 117 | .hljs-javadoctag, 118 | .hljs-phpdoc, 119 | .hljs-dartdoc, 120 | .hljs-yardoctag, 121 | .smalltalk .hljs-class, 122 | .hljs-winutils, 123 | .bash .hljs-variable, 124 | .apache .hljs-tag, 125 | .hljs-type, 126 | .hljs-typename, 127 | .tex .hljs-command, 128 | .asciidoc .hljs-strong, 129 | .markdown .hljs-strong, 130 | .hljs-request, 131 | .hljs-status { 132 | font-weight: bold; 133 | } 134 | 135 | .asciidoc .hljs-emphasis, 136 | .markdown .hljs-emphasis { 137 | font-style: italic; 138 | } 139 | 140 | .nginx .hljs-built_in { 141 | font-weight: normal; 142 | } 143 | 144 | .coffeescript .javascript, 145 | .javascript .xml, 146 | .lasso .markup, 147 | .tex .hljs-formula, 148 | .xml .javascript, 149 | .xml .vbscript, 150 | .xml .css, 151 | .xml .hljs-cdata { 152 | opacity: 0.5; 153 | } 154 | -------------------------------------------------------------------------------- /examples/css/railscasts.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Railscasts-like style (c) Visoft, Inc. (Damien White) 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #232323; 12 | color: #e6e1dc; 13 | -webkit-text-size-adjust: none; 14 | } 15 | 16 | .hljs-comment, 17 | .hljs-javadoc, 18 | .hljs-shebang { 19 | color: #bc9458; 20 | font-style: italic; 21 | } 22 | 23 | .hljs-keyword, 24 | .ruby .hljs-function .hljs-keyword, 25 | .hljs-request, 26 | .hljs-status, 27 | .nginx .hljs-title, 28 | .method, 29 | .hljs-list .hljs-title { 30 | color: #c26230; 31 | } 32 | 33 | .hljs-string, 34 | .hljs-number, 35 | .hljs-regexp, 36 | .hljs-tag .hljs-value, 37 | .hljs-cdata, 38 | .hljs-filter .hljs-argument, 39 | .hljs-attr_selector, 40 | .apache .hljs-cbracket, 41 | .hljs-date, 42 | .tex .hljs-command, 43 | .asciidoc .hljs-link_label, 44 | .markdown .hljs-link_label { 45 | color: #a5c261; 46 | } 47 | 48 | .hljs-subst { 49 | color: #519f50; 50 | } 51 | 52 | .hljs-tag, 53 | .hljs-tag .hljs-keyword, 54 | .hljs-tag .hljs-title, 55 | .hljs-doctype, 56 | .hljs-sub .hljs-identifier, 57 | .hljs-pi, 58 | .input_number { 59 | color: #e8bf6a; 60 | } 61 | 62 | .hljs-identifier { 63 | color: #d0d0ff; 64 | } 65 | 66 | .hljs-class .hljs-title, 67 | .hljs-type, 68 | .smalltalk .hljs-class, 69 | .hljs-javadoctag, 70 | .hljs-yardoctag, 71 | .hljs-phpdoc, 72 | .hljs-dartdoc { 73 | text-decoration: none; 74 | } 75 | 76 | .hljs-constant { 77 | color: #da4939; 78 | } 79 | 80 | 81 | .hljs-symbol, 82 | .hljs-built_in, 83 | .ruby .hljs-symbol .hljs-string, 84 | .ruby .hljs-symbol .hljs-identifier, 85 | .asciidoc .hljs-link_url, 86 | .markdown .hljs-link_url, 87 | .hljs-attribute { 88 | color: #6d9cbe; 89 | } 90 | 91 | .asciidoc .hljs-link_url, 92 | .markdown .hljs-link_url { 93 | text-decoration: underline; 94 | } 95 | 96 | 97 | 98 | .hljs-params, 99 | .hljs-variable, 100 | .clojure .hljs-attribute { 101 | color: #d0d0ff; 102 | } 103 | 104 | .css .hljs-tag, 105 | .hljs-rules .hljs-property, 106 | .hljs-pseudo, 107 | .tex .hljs-special { 108 | color: #cda869; 109 | } 110 | 111 | .css .hljs-class { 112 | color: #9b703f; 113 | } 114 | 115 | .hljs-rules .hljs-keyword { 116 | color: #c5af75; 117 | } 118 | 119 | .hljs-rules .hljs-value { 120 | color: #cf6a4c; 121 | } 122 | 123 | .css .hljs-id { 124 | color: #8b98ab; 125 | } 126 | 127 | .hljs-annotation, 128 | .apache .hljs-sqbracket, 129 | .nginx .hljs-built_in { 130 | color: #9b859d; 131 | } 132 | 133 | .hljs-preprocessor, 134 | .hljs-preprocessor *, 135 | .hljs-pragma { 136 | color: #8996a8 !important; 137 | } 138 | 139 | .hljs-hexcolor, 140 | .css .hljs-value .hljs-number { 141 | color: #a5c261; 142 | } 143 | 144 | .hljs-title, 145 | .hljs-decorator, 146 | .css .hljs-function { 147 | color: #ffc66d; 148 | } 149 | 150 | .diff .hljs-header, 151 | .hljs-chunk { 152 | background-color: #2f33ab; 153 | color: #e6e1dc; 154 | display: inline-block; 155 | width: 100%; 156 | } 157 | 158 | .diff .hljs-change { 159 | background-color: #4a410d; 160 | color: #f8f8f8; 161 | display: inline-block; 162 | width: 100%; 163 | } 164 | 165 | .hljs-addition { 166 | background-color: #144212; 167 | color: #e6e1dc; 168 | display: inline-block; 169 | width: 100%; 170 | } 171 | 172 | .hljs-deletion { 173 | background-color: #600; 174 | color: #e6e1dc; 175 | display: inline-block; 176 | width: 100%; 177 | } 178 | 179 | .coffeescript .javascript, 180 | .javascript .xml, 181 | .tex .hljs-formula, 182 | .xml .javascript, 183 | .xml .vbscript, 184 | .xml .css, 185 | .xml .hljs-cdata { 186 | opacity: 0.7; 187 | } 188 | -------------------------------------------------------------------------------- /src/js/common/markers.js: -------------------------------------------------------------------------------- 1 | function mg_return_label (d) { 2 | return d.label; 3 | } 4 | 5 | function mg_remove_existing_markers (svg) { 6 | svg.selectAll('.mg-markers').remove(); 7 | svg.selectAll('.mg-baselines').remove(); 8 | } 9 | 10 | function mg_in_range (args) { 11 | return function (d) { 12 | return (args.scales.X(d[args.x_accessor]) > mg_get_plot_left(args)) 13 | && (args.scales.X(d[args.x_accessor]) < mg_get_plot_right(args)); 14 | }; 15 | } 16 | 17 | function mg_x_position (args) { 18 | return function (d) { 19 | return args.scales.X(d[args.x_accessor]); 20 | }; 21 | } 22 | 23 | function mg_x_position_fixed (args) { 24 | var _mg_x_pos = mg_x_position(args); 25 | return function (d) { 26 | return _mg_x_pos(d).toFixed(2); 27 | }; 28 | } 29 | 30 | function mg_y_position_fixed (args) { 31 | var _mg_y_pos = args.scales.Y; 32 | return function (d) { 33 | return _mg_y_pos(d.value).toFixed(2); 34 | }; 35 | } 36 | 37 | function mg_place_annotations(checker, class_name, args, svg, line_fcn, text_fcn){ 38 | var g; 39 | if (checker) { 40 | g = svg.append('g').attr('class', class_name); 41 | line_fcn(g, args); 42 | text_fcn(g, args); 43 | } 44 | } 45 | 46 | function mg_place_markers (args, svg) { 47 | mg_place_annotations(args.markers, 'mg-markers', args, svg, mg_place_marker_lines, mg_place_marker_text); 48 | } 49 | 50 | function mg_place_baselines (args, svg) { 51 | mg_place_annotations(args.baselines, 'mg-baselines', args, svg, mg_place_baseline_lines, mg_place_baseline_text); 52 | } 53 | 54 | function mg_place_marker_lines (gm, args) { 55 | var x_pos_fixed = mg_x_position_fixed(args); 56 | gm.selectAll('.mg-markers') 57 | .data(args.markers.filter(mg_in_range(args))) 58 | .enter() 59 | .append('line') 60 | .attr('x1', x_pos_fixed) 61 | .attr('x2', x_pos_fixed) 62 | .attr('y1', args.top) 63 | .attr('y2', mg_get_plot_bottom(args)) 64 | .attr('class', function (d) { 65 | return d.lineclass; 66 | }) 67 | .attr('stroke-dasharray', '3,1'); 68 | } 69 | 70 | function mg_place_marker_text (gm, args) { 71 | gm.selectAll('.mg-markers') 72 | .data(args.markers.filter(mg_in_range(args))) 73 | .enter() 74 | .append('text') 75 | .attr('class', function (d) { return d.textclass || ''; }) 76 | .classed('mg-marker-text', true) 77 | .attr('x', mg_x_position(args)) 78 | .attr('y', args.top * 0.95) 79 | .attr('text-anchor', 'middle') 80 | .text(mg_return_label) 81 | .each(function (d) { 82 | if (d.click) d3.select(this).style('cursor', 'pointer').on('click', d.click); 83 | }); 84 | mg_prevent_horizontal_overlap(gm.selectAll('.mg-marker-text')[0], args); 85 | } 86 | 87 | function mg_place_baseline_lines (gb, args) { 88 | var y_pos = mg_y_position_fixed(args); 89 | gb.selectAll('.mg-baselines') 90 | .data(args.baselines) 91 | .enter().append('line') 92 | .attr('x1', mg_get_plot_left(args)) 93 | .attr('x2', mg_get_plot_right(args)) 94 | .attr('y1', y_pos) 95 | .attr('y2', y_pos); 96 | } 97 | 98 | function mg_place_baseline_text (gb, args) { 99 | var y_pos = mg_y_position_fixed(args); 100 | gb.selectAll('.mg-baselines') 101 | .data(args.baselines) 102 | .enter().append('text') 103 | .attr('x', mg_get_plot_right(args)) 104 | .attr('y', y_pos) 105 | .attr('dy', -3) 106 | .attr('text-anchor', 'end') 107 | .text(mg_return_label); 108 | } 109 | 110 | function markers (args) { 111 | 'use strict'; 112 | var svg = mg_get_svg_child_of(args.target); 113 | mg_remove_existing_markers(svg); 114 | mg_place_markers(args, svg); 115 | mg_place_baselines(args, svg); 116 | return this; 117 | } 118 | 119 | MG.markers = markers; 120 | -------------------------------------------------------------------------------- /examples/css/metricsgraphics-demo.css: -------------------------------------------------------------------------------- 1 | a, 2 | a:active, 3 | a:visited { 4 | color: steelblue; 5 | text-decoration: none; 6 | } 7 | 8 | a:hover { 9 | color: steelblue; 10 | } 11 | 12 | a.active { 13 | background-color: steelblue; 14 | border: 0; 15 | border-radius: 24px; 16 | color: #fff; 17 | padding: 2px 8px 2px 8px; 18 | text-decoration: none; 19 | } 20 | 21 | a.examples-options.selected { 22 | font-weight: bold; 23 | } 24 | 25 | a:hover.examples-options { 26 | text-decoration: none; 27 | } 28 | 29 | body { 30 | background-color: #fff; 31 | color: #000; 32 | font-family: 'Open Sans', sans-serif, Arial; 33 | } 34 | 35 | .btn-group { 36 | display: inline-block; 37 | margin-left: 3px; 38 | margin-right: 3px; 39 | } 40 | 41 | .btn-group span.which-button { 42 | font-weight: 300; 43 | color: darkgray; 44 | } 45 | 46 | .btn-group span.title { 47 | margin-left: 8px; 48 | margin-right: 8px; 49 | } 50 | 51 | .data-column { 52 | font-size: 0.9rem; 53 | margin: 0; 54 | position: absolute; 55 | right: 0; 56 | } 57 | 58 | #description { 59 | margin: auto; 60 | margin-bottom: 50px; 61 | width: 100%; 62 | } 63 | 64 | .divider { 65 | color: #000000; 66 | opacity: 0.2; 67 | } 68 | 69 | .examples-options { 70 | font-size: 0.8rem; 71 | } 72 | 73 | .float-right { 74 | float: right; 75 | } 76 | 77 | .footer { 78 | font-size: 0.9rem; 79 | padding: 40px; 80 | text-align: center; 81 | } 82 | 83 | .head { 84 | color: black; 85 | margin: auto; 86 | margin-bottom: 20px; 87 | width: 100%; 88 | } 89 | 90 | .head h1 { 91 | font-size: 4.1rem; 92 | font-weight: 300; 93 | margin: 45px 0 0 0; 94 | } 95 | 96 | ul { 97 | clear: both; 98 | font-size: 1.2rem; 99 | padding: 10px 0 0 0; 100 | } 101 | 102 | .head ul li, 103 | ul.examples li { 104 | display: inline; 105 | list-style: none; 106 | padding-right: 15px; 107 | } 108 | 109 | ul li.no-padding { 110 | padding: 0; 111 | } 112 | 113 | ul.examples li { 114 | line-height: 30px; 115 | } 116 | 117 | html { 118 | font-size: 12px; 119 | } 120 | 121 | .legend { 122 | font-size: 0.9rem; 123 | padding: 25px 0 15px 0; 124 | } 125 | 126 | #logo { 127 | margin: 60px 0 30px 0; 128 | height: 79px; 129 | width: 400px; 130 | } 131 | 132 | p { 133 | font-family: 'PT Serif', serif; 134 | font-size: 1.3rem; 135 | line-height: 24px; 136 | } 137 | 138 | .pill { 139 | font-family: 'Open Sans', serif; 140 | font-size: 1rem; 141 | text-transform: uppercase; 142 | } 143 | 144 | .pill:hover { 145 | text-decoration: none; 146 | } 147 | 148 | pre { 149 | background-color: transparent; 150 | border: 0; 151 | border-left: 2px solid #ccc; 152 | border-radius: 0; 153 | font-size: 0.9rem; 154 | margin: 40px 0 0 10px; 155 | overflow-x: auto; 156 | padding: 0; 157 | width: 100%; 158 | word-wrap: normal; 159 | } 160 | 161 | pre code { 162 | white-space: inherit; 163 | } 164 | 165 | pre code.hljs { 166 | background-color: transparent; 167 | } 168 | 169 | pre, code, text-area { 170 | font-family: "Menlo", monospace; 171 | } 172 | 173 | svg { 174 | margin-top: 30px; 175 | } 176 | 177 | #table1 .chart_title { 178 | margin-left: 0; 179 | } 180 | 181 | #torso { 182 | margin: 0 auto; 183 | text-align: center; 184 | width: 100%; 185 | } 186 | 187 | #torso div { 188 | display: inline-block; 189 | } 190 | 191 | #trunk { 192 | margin-top: 40px; 193 | } 194 | 195 | #trunk h2 { 196 | font-size: 1.2rem; 197 | } 198 | 199 | #trunk h2.trunk-title { 200 | font-size: 2.2rem; 201 | font-weight: 300; 202 | text-transform: uppercase; 203 | } 204 | 205 | 206 | .trunk-section { 207 | border-top: 1px solid #ccc; 208 | padding-bottom: 25px; 209 | padding-top: 15px; 210 | } 211 | 212 | .wip { 213 | background-color: #f1f1f1; 214 | font-size: 1.3rem; 215 | margin-top: 60px; 216 | padding: 10px; 217 | border-top-left-radius: 6px; 218 | border-top-right-radius: 6px; 219 | opacity: 0.8; 220 | } -------------------------------------------------------------------------------- /tests/common/hooks_test.js: -------------------------------------------------------------------------------- 1 | module('hooks', { 2 | setup: function() { 3 | delete MG._hooks.test; 4 | } 5 | }); 6 | 7 | test('multiple hooks with the same name execute in order', function() { 8 | var result = ''; 9 | 10 | function hookOne() { 11 | result = result + 'one'; 12 | } 13 | 14 | function hookTwo() { 15 | result = result + 'two'; 16 | } 17 | 18 | MG.add_hook('test', hookOne); 19 | MG.add_hook('test', hookTwo); 20 | 21 | MG.call_hook('test'); 22 | 23 | equal(result, 'onetwo', 'both hooks are called'); 24 | }); 25 | 26 | test('hooks can have context', function() { 27 | var result = {}; 28 | 29 | function contextedHook() { 30 | this.foo = 'bar'; 31 | } 32 | 33 | MG.add_hook('test', contextedHook, result); 34 | 35 | MG.call_hook('test'); 36 | 37 | equal(result.foo, 'bar', 'exectued in the correct context'); 38 | }); 39 | 40 | test('hooks accept single arguments', function() { 41 | var result; 42 | 43 | function singleArgHook(arg) { 44 | result = arg; 45 | equal(typeof arg, 'string', 'correctly passed as a string') 46 | } 47 | 48 | MG.add_hook('test', singleArgHook, null); 49 | 50 | MG.call_hook('test', 'one'); 51 | 52 | equal(result, 'one', 'single argument is received'); 53 | }); 54 | 55 | 56 | test('hooks accept multiple arguments', function() { 57 | var result; 58 | 59 | function multipleArgHook(arg1, arg2, arg3) { 60 | result = [arg1, arg2, arg3].join(' '); 61 | 62 | ok([arg1, arg2, arg3].every(function(arg) { 63 | return typeof arg === 'string'; 64 | }), 'correctly passed as strings'); 65 | } 66 | 67 | MG.add_hook('test', multipleArgHook); 68 | 69 | MG.call_hook('test', 'one', 'two', 'three'); 70 | 71 | equal(result, 'one two three', 'multiple arguments are passed correctly'); 72 | }); 73 | 74 | test('hooks are chained - result from one passed into the next', function() { 75 | var initial = 2, 76 | result; 77 | 78 | function hookOne(arg) { 79 | return arg * 2; 80 | } 81 | 82 | function hookTwo(arg) { 83 | return arg - 1; 84 | } 85 | 86 | MG.add_hook('test', hookOne); 87 | MG.add_hook('test', hookTwo); 88 | 89 | result = MG.call_hook('test', initial); 90 | 91 | equal(result, 3, 'result has been chained'); 92 | }); 93 | 94 | test('hooks should return multiple inputs as an array', function() { 95 | var result; 96 | 97 | function hookOne(arg1, arg2, arg3) { 98 | return [arg1, arg2, arg3]; 99 | } 100 | 101 | function hookTwo(arg1, arg2, arg3) { 102 | return [arg3, arg2, arg1]; 103 | } 104 | 105 | MG.add_hook('test', hookOne); 106 | MG.add_hook('test', hookTwo); 107 | 108 | result = MG.call_hook('test', [1,2,3]); 109 | 110 | equal(result.join('-'), '3-2-1', 'array is passed in the result') 111 | }); 112 | 113 | test('if the result from a chained hook is undefined', function() { 114 | var initial = 2; 115 | 116 | function hookOne(arg) { 117 | // don't return anything 118 | } 119 | 120 | function hookTwo(arg) { 121 | equal(arg, initial, 'initial value is used'); 122 | } 123 | 124 | MG.add_hook('test', hookOne); 125 | MG.add_hook('test', hookTwo); 126 | result = MG.call_hook('test', initial); 127 | 128 | 129 | delete MG._hooks.test; 130 | 131 | function hookThree(arg) { 132 | return arg - 1; 133 | } 134 | 135 | function hookFour(arg) { 136 | // don't return anything 137 | } 138 | 139 | function hookFive(arg) { 140 | equal(initial, arg - 1, 'processed value is passed if it is already set'); 141 | } 142 | 143 | MG.add_hook('test', hookOne); 144 | MG.add_hook('test', hookTwo); 145 | result = MG.call_hook('test', initial); 146 | }); 147 | 148 | test('a hook can only have one registered instance of any function', function() { 149 | function hookOne() {} 150 | 151 | MG.add_hook('test', hookOne); 152 | 153 | try { 154 | MG.add_hook('test', hookOne); 155 | } 156 | catch(error) { 157 | equal(error, 'That function is already registered.', 'an exception is raised'); 158 | } 159 | }); 160 | -------------------------------------------------------------------------------- /gulp/index.js: -------------------------------------------------------------------------------- 1 | // Gulp and plugins 2 | var 3 | gulp = require('gulp'), 4 | umd = require('gulp-umd'), 5 | rimraf = require('gulp-rimraf'), 6 | uglify = require('gulp-uglify'), 7 | concat = require('gulp-concat'), 8 | rename = require('gulp-rename'), 9 | //sass = require('gulp-sass'), // for building css from scss 10 | //minifycss = require('gulp-minify-css'), // for minifiing css 11 | jshint = require('gulp-jshint'), 12 | testem = require('gulp-testem'), 13 | connect = require('gulp-connect'), 14 | es6ModuleTranspiler = require("gulp-es6-module-transpiler"); 15 | 16 | // paths 17 | var 18 | src = './src/js/', 19 | //scss = './scss/', 20 | //scssFiles = [], 21 | //scssDependencies = [], 22 | dist = './dist/', 23 | jsFiles = [ 24 | src + 'MG.js', 25 | src + 'common/register.js', 26 | src + 'common/hooks.js', 27 | src + 'common/data_graphic.js', 28 | src + 'common/bootstrap_tooltip_popover.js', 29 | src + 'common/chart_title.js', 30 | src + 'common/y_axis.js', 31 | src + 'common/x_axis.js', 32 | src + 'common/init.js', 33 | src + 'common/markers.js', 34 | src + 'common/window_listeners.js', 35 | src + 'layout/bootstrap_dropdown.js', 36 | src + 'layout/button.js', 37 | src + 'charts/line.js', 38 | src + 'charts/histogram.js', 39 | src + 'charts/point.js', 40 | src + 'charts/bar.js', 41 | src + 'charts/table.js', 42 | src + 'charts/missing.js', 43 | src + 'misc/process.js', 44 | src + 'misc/smoothers.js', 45 | src + 'misc/formatters.js', 46 | src + 'misc/transitions.js', 47 | src + 'misc/utility.js', 48 | src + 'misc/error.js' 49 | ]; 50 | 51 | 52 | gulp.task('default', ['jshint', 'test', 'build:js']); 53 | 54 | gulp.task('clean', function () { 55 | return gulp.src([dist + 'metricsgraphics.js', dist + 'metricsgraphics.min.js'], {read: false}) 56 | .pipe(rimraf()); 57 | }); 58 | 59 | // build css files from scss 60 | //gulp.task('build:css', ['clean'], function () { 61 | // return gulp.src(scssFiles) 62 | // .pipe(sass({includePaths: scssDependencies})) 63 | // .pipe(minifycss()) 64 | // .pipe(gulp.dest(dist)); 65 | //}); 66 | 67 | // create 'metricsgraphics.js' and 'metricsgraphics.min.js' from source js 68 | gulp.task('build:js', ['clean'], function () { 69 | return gulp.src(jsFiles) 70 | .pipe(concat({path: 'metricsgraphics.js'})) 71 | .pipe(umd( 72 | { 73 | dependencies:function() { 74 | return [{ 75 | name: 'd3', 76 | amd: 'd3', 77 | cjs: 'd3', 78 | global: 'd3', 79 | param: 'd3' 80 | }, 81 | { 82 | name: 'jquery', 83 | amd: 'jquery', 84 | cjs: 'jquery', 85 | global: 'jQuery', 86 | param: '$' 87 | }]; 88 | }, 89 | exports: function() { 90 | return "MG"; 91 | }, 92 | namespace: function() { 93 | return "MG"; 94 | } 95 | } 96 | )) 97 | .pipe(gulp.dest(dist)) 98 | .pipe(rename('metricsgraphics.min.js')) 99 | .pipe(uglify()) 100 | .pipe(gulp.dest(dist)); 101 | }); 102 | 103 | // Check source js files with jshint 104 | gulp.task('jshint', function () { 105 | return gulp.src(jsFiles) 106 | .pipe(jshint()) 107 | .pipe(jshint.reporter('default')); 108 | }); 109 | 110 | // Run test suite server (testem') 111 | gulp.task('test', function() { 112 | return gulp.src(['']) 113 | .pipe(testem({ 114 | configFile: 'testem.json' 115 | })); 116 | }); 117 | 118 | 119 | // Development server tasks 120 | // NOTE: these paths will need changing when the SCSS source is ready 121 | var roots = ['dist', 'examples', 'src', 'bower_components'], 122 | watchables = roots.map(function(root) { 123 | return root + '/**/*'; 124 | }); 125 | 126 | gulp.task('dev:watch', function() { return gulp.watch(watchables, ['jshint', 'dev:reload']); }); 127 | gulp.task('dev:reload', function() { return gulp.src(watchables).pipe(connect.reload()); }); 128 | gulp.task('serve', ['jshint', 'dev:serve', 'dev:watch']); 129 | 130 | gulp.task('dev:serve', function() { 131 | connect.server({ 132 | root: roots, 133 | port: 4300, 134 | livereload: true 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /examples/js/main.js: -------------------------------------------------------------------------------- 1 | var theme = 'light'; 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | //set the active pill and section on first load 7 | var section = (document.location.hash) ? document.location.hash.slice(1) : 'lines'; 8 | 9 | $('#trunk').load('charts/' + section + '.htm', function() { 10 | $('pre code').each(function(i, block) { 11 | hljs.highlightBlock(block); 12 | }); 13 | }); 14 | 15 | $('.examples li a#goto-' + section).addClass('active'); 16 | 17 | //handle mouse clicks and so on 18 | assignEventListeners(); 19 | 20 | function assignEventListeners() { 21 | $('ul.examples li a.pill').on('click', function(event) { 22 | event.preventDefault(); 23 | $('ul.examples li a.pill').removeClass('active'); 24 | $(this).addClass('active'); 25 | 26 | var section = $(this).attr('id').slice(5); 27 | $('#trunk').load('charts/' + section + '.htm', function() { 28 | $('pre code').each(function(i, block) { 29 | hljs.highlightBlock(block); 30 | }); 31 | }); 32 | 33 | document.location.hash = section; 34 | 35 | return false; 36 | }) 37 | 38 | $('#dark-css').on('click', function () { 39 | theme = 'dark'; 40 | 41 | $('.missing') 42 | .css('background-image', 'url(images/missing-data-dark.png)'); 43 | 44 | $('.wip') 45 | .css('background-color', '#3b3b3b'); 46 | 47 | $('.trunk-section') 48 | .css('border-top-color', '#5e5e5e'); 49 | 50 | $('.mg-missing-background') 51 | .css('stroke', '#ccc'); 52 | 53 | $('.head ul li a.pill').removeClass('active'); 54 | $(this).toggleClass('active'); 55 | $('#dark').attr({href : 'css/metricsgraphics-demo-dark.css'}); 56 | $('#dark-code').attr({href : 'css/railscasts.css'}); 57 | $('#accessible').attr({href : ''}); 58 | 59 | return false; 60 | }); 61 | 62 | $('#light-css').on('click', function () { 63 | theme = 'light'; 64 | 65 | $('.missing') 66 | .css('background-image', 'url(images/missing-data.png)'); 67 | 68 | $('.wip') 69 | .css('background-color', '#f1f1f1'); 70 | 71 | $('.trunk-section') 72 | .css('border-top-color', '#ccc'); 73 | 74 | $('.mg-missing-background') 75 | .css('stroke', 'blue'); 76 | 77 | $('.head ul li a.pill').removeClass('active'); 78 | $(this).toggleClass('active'); 79 | $('#dark').attr({href : ''}); 80 | $('#dark-code').attr({href : ''}); 81 | $('#accessible').attr({href : ''}); 82 | 83 | return false; 84 | }); 85 | 86 | $('#accessible-css').on('click', function () { 87 | $('.head ul li a.pill').removeClass('active'); 88 | $(this).toggleClass('active'); 89 | $('#accessible').attr({href : 'css/metricsgraphics-demo-accessible.css'}); 90 | 91 | return false; 92 | }); 93 | } 94 | 95 | // replace all SVG images with inline SVG 96 | // http://stackoverflow.com/questions/11978995/how-to-change-color-of-svg 97 | // -image-using-css-jquery-svg-image-replacement 98 | $('img.svg').each(function() { 99 | var $img = jQuery(this); 100 | var imgID = $img.attr('id'); 101 | var imgClass = $img.attr('class'); 102 | var imgURL = $img.attr('src'); 103 | 104 | $.get(imgURL, function(data) { 105 | // Get the SVG tag, ignore the rest 106 | var $svg = jQuery(data).find('svg'); 107 | 108 | // Add replaced image's ID to the new SVG 109 | if (typeof imgID !== 'undefined') { 110 | $svg = $svg.attr('id', imgID); 111 | } 112 | // Add replaced image's classes to the new SVG 113 | if (typeof imgClass !== 'undefined') { 114 | $svg = $svg.attr('class', imgClass + ' replaced-svg'); 115 | } 116 | 117 | // Remove any invalid XML tags as per http://validator.w3.org 118 | $svg = $svg.removeAttr('xmlns:a'); 119 | 120 | // Replace image with new SVG 121 | $img.replaceWith($svg); 122 | 123 | }, 'xml'); 124 | }); 125 | })(); 126 | -------------------------------------------------------------------------------- /examples/charts/other.htm: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 |
d3.json('data/fake_users3.json', function(data) {
  7 |     for(var i = 0; i < data.length; i++) {
  8 |         data[i] = MG.convert.date(data[i], 'date');
  9 |     }
 10 | 
 11 |     MG.data_graphic({
 12 |         title: "Preserving the aspect ratio",
 13 |         description: "You can automatically set the width or height of a data graphic to fit its parent element. When done the graphic will rescale to fit the size of the parent element while preserving its aspect ratio.",
 14 |         data: data,
 15 |         full_width: true,
 16 |         height: 300,
 17 |         right: 40,
 18 |         x_extended_ticks: true,
 19 |         target: '#aspect1',
 20 |         x_accessor: 'date',
 21 |         y_accessor: 'value'
 22 |     });
 23 | });
24 | 25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 | 36 |
d3.json('data/brief-1.json', function(data) {
 37 |     data = MG.convert.date(data, 'date');
 38 |     MG.data_graphic({
 39 |         title: "Small Text Inferred By Size",
 40 |         description: "If args.width - args.left - args.right is smaller than args.small_width_threshold, and the similarly for the height, the text size automatically scales so that it is slightly smaller.",
 41 |         data: data,
 42 |         width: 240,
 43 |         height: 140,
 44 |         right: 20,
 45 |         xax_count: 4,
 46 |         target: '#small1'
 47 |     });
 48 | });
 49 | 
 50 | d3.json('data/brief-2.json', function(data) {
 51 |     data = MG.convert.date(data, 'date');
 52 |     MG.data_graphic({
 53 |         title: "Small Text",
 54 |         description: "By adding small_text to true, we can force the use of smaller axis text regardless of the width or height.",
 55 |         data: data,
 56 |         width: 295,
 57 |         height: 150,
 58 |         right: 10,
 59 |         small_text: true,
 60 |         xax_count: 4,
 61 |         target: '#small2'
 62 |     });
 63 | });
64 | 65 |
66 |
67 | 68 | -------------------------------------------------------------------------------- /tests/charts/point_test.js: -------------------------------------------------------------------------------- 1 | module('point'); 2 | 3 | test('A solitary active datapoint exists', function() { 4 | var params = { 5 | target: '#qunit-fixture', 6 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 7 | {'date': new Date('2014-03-01'), 'value': 18}], 8 | chart_type: 'point' 9 | }; 10 | 11 | MG.data_graphic(params); 12 | equal(document.querySelectorAll('.mg-active-datapoint').length, 1, 'One active datapoint exists'); 13 | }); 14 | 15 | test('Rollovers exist', function() { 16 | var params = { 17 | target: '#qunit-fixture', 18 | data: [[{'date': new Date('2014-01-01'), 'value': 12}, 19 | {'date': new Date('2014-03-01'), 'value': 18}], 20 | [{'date': new Date('2014-01-01'), 'value': 120}, 21 | {'date': new Date('2014-03-01'), 'value': 180}]], 22 | chart_type: 'point' 23 | }; 24 | 25 | MG.data_graphic(params); 26 | ok(document.querySelector('.mg-voronoi'), 'Rollovers exist'); 27 | }); 28 | 29 | test('We have only one set of rollovers', function() { 30 | var params = { 31 | target: '#qunit-fixture', 32 | data: [[{'date': new Date('2014-01-01'), 'value': 12}, 33 | {'date': new Date('2014-03-01'), 'value': 18}], 34 | [{'date': new Date('2014-01-01'), 'value': 120}, 35 | {'date': new Date('2014-03-01'), 'value': 180}]], 36 | chart_type: 'point' 37 | }; 38 | 39 | MG.data_graphic(params); 40 | equal(document.querySelectorAll('.mg-voronoi').length, 1, 'One set of rollovers exists'); 41 | }); 42 | 43 | test('args.x_rug', function() { 44 | var params = { 45 | target: '#qunit-fixture', 46 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 47 | {'date': new Date('2014-03-01'), 'value': 18}], 48 | x_rug: true, 49 | chart_type: 'point' 50 | }; 51 | 52 | MG.data_graphic(params); 53 | ok(document.querySelector('.mg-x-rug'), 'X-axis rugplot exists'); 54 | }); 55 | 56 | test('Only one rugplot is added on multiple calls to the same target element', function() { 57 | var params = { 58 | target: '#qunit-fixture', 59 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 60 | {'date': new Date('2014-03-01'), 'value': 18}], 61 | x_rug: true, 62 | chart_type: 'point' 63 | }; 64 | 65 | MG.data_graphic(params); 66 | MG.data_graphic(MG.clone(params)); 67 | 68 | equal(document.querySelectorAll('.mg-x-rug').length, 2, 'We only have one rugplot (two ticks) on the x-axis'); 69 | }); 70 | 71 | test('args.y_rug', function() { 72 | var params = { 73 | target: '#qunit-fixture', 74 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 75 | {'date': new Date('2014-03-01'), 'value': 18}], 76 | y_rug: true, 77 | chart_type: 'point' 78 | }; 79 | 80 | MG.data_graphic(params); 81 | ok(document.querySelector('.mg-y-rug'), 'Y-axis rugplot exists'); 82 | }); 83 | 84 | test('Only one rugplot is added on multiple calls to the same target element', function() { 85 | var params = { 86 | target: '#qunit-fixture', 87 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 88 | {'date': new Date('2014-03-01'), 'value': 18}], 89 | y_rug: true, 90 | chart_type: 'point' 91 | }; 92 | 93 | MG.data_graphic(params); 94 | MG.data_graphic(MG.clone(params)); 95 | equal(document.querySelectorAll('.mg-y-rug').length, 2, 'We only have one rugplot (two ticks) on the y-axis'); 96 | }); 97 | 98 | test('args.least_squares', function() { 99 | var params = { 100 | target: '#qunit-fixture', 101 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 102 | {'date': new Date('2014-03-01'), 'value': 18}], 103 | chart_type: 'point', 104 | least_squares: true 105 | }; 106 | 107 | MG.data_graphic(params); 108 | ok(document.querySelector('.mg-least-squares-line'), 'Least-squares line exists'); 109 | }); 110 | 111 | test('Only one least-squares line is added on multiple calls to the same target element', function() { 112 | var params = { 113 | target: '#qunit-fixture', 114 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 115 | {'date': new Date('2014-03-01'), 'value': 18}], 116 | chart_type: 'point', 117 | least_squares: true 118 | }; 119 | 120 | MG.data_graphic(params); 121 | MG.data_graphic(MG.clone(params)); 122 | equal(document.querySelectorAll('.mg-least-squares-line').length, 1, 'We only have one least-squares line'); 123 | }); -------------------------------------------------------------------------------- /src/js/charts/missing.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | function mg_missing_add_text (svg, args) { 5 | svg.selectAll('.mg-missing-text').data([args.missing_text]) 6 | .enter().append('text') 7 | .attr('class', 'mg-missing-text') 8 | .attr('x', args.width / 2) 9 | .attr('y', args.height / 2) 10 | .attr('dy', '.50em') 11 | .attr('text-anchor', 'middle') 12 | .text(args.missing_text); 13 | } 14 | 15 | function mg_missing_x_scale (args) { 16 | args.scales.X = d3.scale.linear() 17 | .domain([0, args.data.length]) 18 | .range([mg_get_plot_left(args), mg_get_plot_right(args)]); 19 | args.scalefns.yf = function (di) { return args.scales.Y(di.y); }; 20 | } 21 | 22 | function mg_missing_y_scale (args) { 23 | args.scales.Y = d3.scale.linear() 24 | .domain([-2, 2]) 25 | .range([args.height - args.bottom - args.buffer * 2, args.top]); 26 | args.scalefns.xf = function (di) { return args.scales.X(di.x); }; 27 | } 28 | 29 | function mg_make_fake_data (args) { 30 | var data = []; 31 | for (var x = 1; x <= 50; x++) { 32 | data.push({'x': x, 'y': Math.random() - (x * 0.03)}); 33 | } 34 | args.data = data; 35 | } 36 | 37 | 38 | 39 | function mg_add_missing_background_rect (g, args) { 40 | g.append('svg:rect') 41 | .classed('mg-missing-background', true) 42 | .attr('x', args.buffer) 43 | .attr('y', args.buffer) 44 | .attr('width', args.width - args.buffer * 2) 45 | .attr('height', args.height - args.buffer * 2) 46 | .attr('rx', 15) 47 | .attr('ry', 15); 48 | } 49 | 50 | function mg_missing_add_line (g, args) { 51 | var line = d3.svg.line() 52 | .x(args.scalefns.xf) 53 | .y(args.scalefns.yf) 54 | .interpolate(args.interpolate); 55 | g.append('path') 56 | .attr('class', 'mg-main-line mg-line1-color') 57 | .attr('d', line(args.data)); 58 | } 59 | 60 | function mg_missing_add_area (g, args) { 61 | var area = d3.svg.area() 62 | .x(args.scalefns.xf) 63 | .y0(args.scales.Y.range()[0]) 64 | .y1(args.scalefns.yf) 65 | .interpolate(args.interpolate); 66 | g.append('path') 67 | .attr('class', 'mg-main-area mg-area1-color') 68 | .attr('d', area(args.data)); 69 | } 70 | 71 | function mg_remove_all_children (args) { 72 | d3.select(args.target).selectAll('svg *').remove(); 73 | } 74 | 75 | function mg_missing_remove_legend (args) { 76 | if (args.legend_target) { 77 | d3.select(args.legend_target).html(''); 78 | } 79 | } 80 | 81 | function missingData (args) { 82 | this.init = function (args) { 83 | this.args = args; 84 | 85 | mg_init_compute_width(args); 86 | mg_init_compute_height(args); 87 | 88 | chart_title(args); 89 | 90 | // create svg if one doesn't exist 91 | 92 | var container = d3.select(args.target); 93 | mg_raise_container_error(container, args); 94 | var svg = container.selectAll('svg'); 95 | mg_remove_svg_if_chart_type_has_changed(svg, args); 96 | svg = mg_add_svg_if_it_doesnt_exist(svg, args); 97 | mg_adjust_width_and_height_if_changed(svg, args); 98 | mg_set_viewbox_for_scaling(svg, args); 99 | mg_remove_all_children(args); 100 | 101 | svg.classed('mg-missing', true); 102 | mg_missing_remove_legend(args); 103 | 104 | // are we adding a background placeholder 105 | if (args.show_missing_background) { 106 | mg_make_fake_data(args); 107 | mg_missing_x_scale(args); 108 | mg_missing_y_scale(args); 109 | var g = mg_add_g(svg, 'mg-missing-pane'); 110 | 111 | mg_add_missing_background_rect(g, args); 112 | mg_missing_add_line(g, args); 113 | mg_missing_add_area(g, args); 114 | } 115 | 116 | mg_missing_add_text(svg, args); 117 | 118 | this.windowListeners(); 119 | 120 | return this; 121 | }; 122 | 123 | this.windowListeners = function () { 124 | mg_window_listeners(this.args); 125 | return this; 126 | }; 127 | 128 | this.init(args); 129 | } 130 | 131 | var defaults = { 132 | top: 40, // the size of the top margin 133 | bottom: 30, // the size of the bottom margin 134 | right: 10, // size of the right margin 135 | left: 10, // size of the left margin 136 | buffer: 8, // the buffer between the actual chart area and the margins 137 | legend_target: '', 138 | width: 350, 139 | height: 220, 140 | missing_text: 'Data currently missing or unavailable', 141 | scalefns: {}, 142 | scales: {}, 143 | show_tooltips: true, 144 | show_missing_background: true, 145 | interpolate: 'cardinal' 146 | }; 147 | 148 | MG.register('missing-data', missingData, defaults); 149 | }).call(this); 150 | -------------------------------------------------------------------------------- /src/js/layout/button.js: -------------------------------------------------------------------------------- 1 | MG.button_layout = function(target) { 2 | 'use strict'; 3 | this.target = target; 4 | this.feature_set = {}; 5 | this.public_name = {}; 6 | this.sorters = {}; 7 | this.manual = []; 8 | this.manual_map = {}; 9 | this.manual_callback = {}; 10 | 11 | this._strip_punctuation = function(s) { 12 | var punctuationless = s.replace(/[^a-zA-Z0-9 _]+/g, ''); 13 | var finalString = punctuationless.replace(/ +?/g, ''); 14 | return finalString; 15 | }; 16 | 17 | this.data = function(data) { 18 | this._data = data; 19 | return this; 20 | }; 21 | 22 | this.manual_button = function(feature, feature_set, callback) { 23 | this.feature_set[feature]=feature_set; 24 | this.manual_map[this._strip_punctuation(feature)] = feature; 25 | this.manual_callback[feature]=callback;// the default is going to be the first feature. 26 | return this; 27 | }; 28 | 29 | this.button = function(feature) { 30 | if (arguments.length > 1) { 31 | this.public_name[feature] = arguments[1]; 32 | } 33 | 34 | if (arguments.length > 2) { 35 | this.sorters[feature] = arguments[2]; 36 | } 37 | 38 | this.feature_set[feature] = []; 39 | return this; 40 | }; 41 | 42 | this.callback = function(callback) { 43 | this._callback = callback; 44 | return this; 45 | }; 46 | 47 | this.display = function() { 48 | var callback = this._callback; 49 | var manual_callback = this.manual_callback; 50 | var manual_map = this.manual_map; 51 | 52 | var d,f, features, feat; 53 | features = Object.keys(this.feature_set); 54 | 55 | var mapDtoF = function(f) { return d[f]; }; 56 | 57 | var i; 58 | 59 | // build out this.feature_set with this.data 60 | for (i = 0; i < this._data.length; i++) { 61 | d = this._data[i]; 62 | f = features.map(mapDtoF); 63 | for (var j = 0; j < features.length; j++) { 64 | feat = features[j]; 65 | if (this.feature_set[feat].indexOf(f[j]) === -1) { 66 | this.feature_set[feat].push(f[j]); 67 | } 68 | } 69 | } 70 | 71 | for (feat in this.feature_set) { 72 | if (this.sorters.hasOwnProperty(feat)) { 73 | this.feature_set[feat].sort(this.sorters[feat]); 74 | } 75 | } 76 | 77 | $(this.target).empty(); 78 | 79 | $(this.target).append("
"); 80 | 81 | var dropdownLiAClick = function() { 82 | var k = $(this).data('key'); 83 | var feature = $(this).data('feature'); 84 | var manual_feature; 85 | $('.' + feature + '-btns button.btn span.title').html(k); 86 | if (!manual_map.hasOwnProperty(feature)) { 87 | callback(feature, k); 88 | } else { 89 | manual_feature = manual_map[feature]; 90 | manual_callback[manual_feature](k); 91 | } 92 | 93 | return false; 94 | }; 95 | 96 | for (var feature in this.feature_set) { 97 | features = this.feature_set[feature]; 98 | $(this.target + ' div.segments').append( 99 | '
' + // This never changes. 100 | '' + 105 | '' 109 | + '
'); 110 | 111 | for (i = 0; i < features.length; i++) { 112 | if (features[i] !== 'all' && features[i] !== undefined) { // strange bug with undefined being added to manual buttons. 113 | $(this.target + ' div.' + this._strip_punctuation(feature) + '-btns ul.dropdown-menu').append( 114 | '
  • ' 115 | + features[i] + '
  • ' 116 | ); 117 | } 118 | } 119 | 120 | $('.' + this._strip_punctuation(feature) + '-btns .dropdown-menu li a').on('click', dropdownLiAClick); 121 | } 122 | 123 | return this; 124 | }; 125 | 126 | return this; 127 | }; 128 | -------------------------------------------------------------------------------- /examples/charts/auto-time-formatting.htm: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    6 |
    7 |
    8 |
    9 |
    10 |
    11 | 12 |
    function fake_data(length, seconds) {
     13 |     var d = new Date();
     14 |     var v = 100000;
     15 |     var data=[];
     16 | 
     17 |     for (var i = 0; i < length; i++) {
     18 |         v += (Math.random() - 0.5) * 10000;
     19 |         data.push({date: MG.clone(d), value: v});
     20 |         d = new Date(d.getTime() + seconds * 1000);
     21 |     }
     22 |     return data;
     23 | }
     24 | 
     25 | function fake_days(length) {
     26 |     var d = new Date();
     27 |     var v = 100000;
     28 | 
     29 |     var data = [];
     30 |     for (var i = 0; i < length; i++) {
     31 |         v += (Math.random() - 0.5) * 10000;
     32 |         data.push({date: MG.clone(d), value: v});
     33 |         d.setDate(d.getDate() + 1);
     34 |     }
     35 |     return data;
     36 | }
     37 | 
     38 | var less_than_a_minute = fake_data(25, 1);
     39 | var less_than_a_day = fake_data(25,60 * 20);
     40 | var a_few_days = fake_data(75,60 * 60);
     41 | var many_days = fake_days(25);
     42 | 
     43 | MG.data_graphic({
     44 |     title: "Less Than A Minute",
     45 |     data: less_than_a_minute,
     46 |     target: '#time1',
     47 |     width: 600,
     48 |     height: 200,
     49 |     right: 40
     50 | });
     51 | 
     52 | MG.data_graphic({
     53 |     title: "Less Than A Day",
     54 |     data: less_than_a_day,
     55 |     target: '#time2',
     56 |     width: 600,
     57 |     height: 200,
     58 |     right: 40
     59 | });
     60 | 
     61 | MG.data_graphic({
     62 |     title: "A Few Days",
     63 |     data: a_few_days,
     64 |     target: '#time3',
     65 |     width: 600,
     66 |     height: 200,
     67 |     right: 40
     68 | });
     69 | 
     70 | MG.data_graphic({
     71 |     title: "Over A Large Span of Days",
     72 |     data: many_days,
     73 |     target: '#time4',
     74 |     width: 600,
     75 |     height: 200,
     76 |     right: 40
     77 | });
    78 | 79 |
    80 |
    81 | 82 | 83 |
    84 |
    85 |
    86 |
    87 |
    88 |
    89 |
    90 | 91 |
    MG.data_graphic({
     92 |     title: "European Clock",
     93 |     description: 'By setting european_clock to true, you can default to European-style time. This is at the moment experimental, and the formatting may change.',
     94 |     data: less_than_a_minute,
     95 |     target: '#european',
     96 |     european_clock: true,
     97 |     width: 600,
     98 |     height: 200,
     99 |     right: 40
    100 | });
    101 |                 
    102 | 103 |
    104 |
    105 | 106 | -------------------------------------------------------------------------------- /examples/data/ufo-sightings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "year": "1945", 4 | "sightings": 6 5 | }, 6 | { 7 | "year": "1946", 8 | "sightings": 8 9 | }, 10 | { 11 | "year": "1947", 12 | "sightings": 34 13 | }, 14 | { 15 | "year": "1948", 16 | "sightings": 8 17 | }, 18 | { 19 | "year": "1949", 20 | "sightings": 15 21 | }, 22 | { 23 | "year": "1950", 24 | "sightings": 25 25 | }, 26 | { 27 | "year": "1951", 28 | "sightings": 20 29 | }, 30 | { 31 | "year": "1952", 32 | "sightings": 48 33 | }, 34 | { 35 | "year": "1953", 36 | "sightings": 34 37 | }, 38 | { 39 | "year": "1954", 40 | "sightings": 50 41 | }, 42 | { 43 | "year": "1955", 44 | "sightings": 31 45 | }, 46 | { 47 | "year": "1956", 48 | "sightings": 38 49 | }, 50 | { 51 | "year": "1957", 52 | "sightings": 67 53 | }, 54 | { 55 | "year": "1958", 56 | "sightings": 40 57 | }, 58 | { 59 | "year": "1959", 60 | "sightings": 47 61 | }, 62 | { 63 | "year": "1960", 64 | "sightings": 64 65 | }, 66 | { 67 | "year": "1961", 68 | "sightings": 39 69 | }, 70 | { 71 | "year": "1962", 72 | "sightings": 55 73 | }, 74 | { 75 | "year": "1963", 76 | "sightings": 75 77 | }, 78 | { 79 | "year": "1964", 80 | "sightings": 77 81 | }, 82 | { 83 | "year": "1965", 84 | "sightings": 167 85 | }, 86 | { 87 | "year": "1966", 88 | "sightings": 169 89 | }, 90 | { 91 | "year": "1967", 92 | "sightings": 178 93 | }, 94 | { 95 | "year": "1968", 96 | "sightings": 183 97 | }, 98 | { 99 | "year": "1969", 100 | "sightings": 138 101 | }, 102 | { 103 | "year": "1970", 104 | "sightings": 126 105 | }, 106 | { 107 | "year": "1971", 108 | "sightings": 110 109 | }, 110 | { 111 | "year": "1972", 112 | "sightings": 146 113 | }, 114 | { 115 | "year": "1973", 116 | "sightings": 209 117 | }, 118 | { 119 | "year": "1974", 120 | "sightings": 241 121 | }, 122 | { 123 | "year": "1975", 124 | "sightings": 279 125 | }, 126 | { 127 | "year": "1976", 128 | "sightings": 246 129 | }, 130 | { 131 | "year": "1977", 132 | "sightings": 239 133 | }, 134 | { 135 | "year": "1978", 136 | "sightings": 301 137 | }, 138 | { 139 | "year": "1979", 140 | "sightings": 221 141 | }, 142 | { 143 | "year": "1980", 144 | "sightings": 211 145 | }, 146 | { 147 | "year": "1981", 148 | "sightings": 146 149 | }, 150 | { 151 | "year": "1982", 152 | "sightings": 182 153 | }, 154 | { 155 | "year": "1983", 156 | "sightings": 132 157 | }, 158 | { 159 | "year": "1984", 160 | "sightings": 172 161 | }, 162 | { 163 | "year": "1985", 164 | "sightings": 192 165 | }, 166 | { 167 | "year": "1986", 168 | "sightings": 173 169 | }, 170 | { 171 | "year": "1987", 172 | "sightings": 193 173 | }, 174 | { 175 | "year": "1988", 176 | "sightings": 203 177 | }, 178 | { 179 | "year": "1989", 180 | "sightings": 220 181 | }, 182 | { 183 | "year": "1990", 184 | "sightings": 217 185 | }, 186 | { 187 | "year": "1991", 188 | "sightings": 210 189 | }, 190 | { 191 | "year": "1992", 192 | "sightings": 228 193 | }, 194 | { 195 | "year": "1993", 196 | "sightings": 285 197 | }, 198 | { 199 | "year": "1994", 200 | "sightings": 381 201 | }, 202 | { 203 | "year": "1995", 204 | "sightings": 1336 205 | }, 206 | { 207 | "year": "1996", 208 | "sightings": 862 209 | }, 210 | { 211 | "year": "1997", 212 | "sightings": 1248 213 | }, 214 | { 215 | "year": "1998", 216 | "sightings": 1812 217 | }, 218 | { 219 | "year": "1999", 220 | "sightings": 2906 221 | }, 222 | { 223 | "year": "2000", 224 | "sightings": 2780 225 | }, 226 | { 227 | "year": "2001", 228 | "sightings": 3105 229 | }, 230 | { 231 | "year": "2002", 232 | "sightings": 3176 233 | }, 234 | { 235 | "year": "2003", 236 | "sightings": 3896 237 | }, 238 | { 239 | "year": "2004", 240 | "sightings": 4208 241 | }, 242 | { 243 | "year": "2005", 244 | "sightings": 3996 245 | }, 246 | { 247 | "year": "2006", 248 | "sightings": 3590 249 | }, 250 | { 251 | "year": "2007", 252 | "sightings": 4195 253 | }, 254 | { 255 | "year": "2008", 256 | "sightings": 4705 257 | }, 258 | { 259 | "year": "2009", 260 | "sightings": 4297 261 | }, 262 | { 263 | "year": "2010", 264 | "sightings": 2531 265 | } 266 | ] -------------------------------------------------------------------------------- /tests/common/markers_test.js: -------------------------------------------------------------------------------- 1 | module('markers'); 2 | 3 | test('All markers are added if they lie within the visible range', function() { 4 | var markers = [{ 5 | 'date': new Date('2014-02-01'), 6 | 'label': '1st Milestone' 7 | }, { 8 | 'date': new Date('2014-02-02'), 9 | 'label': '2nd Milestone' 10 | }]; 11 | 12 | var params = { 13 | target: '#qunit-fixture', 14 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 15 | {'date': new Date('2014-03-01'), 'value': 18}], 16 | markers: markers 17 | }; 18 | 19 | MG.data_graphic(params); 20 | equal(document.querySelectorAll(params.target + ' .mg-markers line').length, markers.length, 'Two markers added'); 21 | }); 22 | 23 | test('Markers that lie outside the visible range are excluded', function() { 24 | var markers = [{ 25 | 'date': new Date('2014-02-01'), 26 | 'label': '1st Milestone' 27 | }, { 28 | 'date': new Date('2014-02-02'), 29 | 'label': '2nd Milestone' 30 | }]; 31 | 32 | var params = { 33 | target: '#qunit-fixture', 34 | data: [{'date': new Date('2014-02-01'), 'value': 12}, 35 | {'date': new Date('2014-03-01'), 'value': 18}], 36 | markers: markers 37 | }; 38 | 39 | MG.data_graphic(params); 40 | equal(document.querySelectorAll(params.target + ' .mg-markers line').length, 1, 'One marker added'); 41 | }); 42 | 43 | test('All baselines are added', function() { 44 | var baselines = [{value:50, label:'a baseline'}]; 45 | 46 | var params = { 47 | target: '#qunit-fixture', 48 | data: [{'date': new Date('2014-01-01'), 'value': 100}, 49 | {'date': new Date('2014-03-01'), 'value': 10}], 50 | baselines: baselines 51 | }; 52 | 53 | MG.data_graphic(params); 54 | equal(document.querySelectorAll(params.target + ' .mg-baselines line').length, markers.length, 'One baseline added'); 55 | }); 56 | 57 | test('Markers\' texts are correctly added', function() { 58 | var markers = [{ 59 | 'date': new Date('2014-02-01'), 60 | 'label': '1st Milestone' 61 | }, { 62 | 'date': new Date('2014-02-02'), 63 | 'label': '2nd Milestone' 64 | }]; 65 | 66 | var params = { 67 | target: '#qunit-fixture', 68 | data: [{'date': new Date('2014-01-01'), 'value': 100}, 69 | {'date': new Date('2014-03-01'), 'value': 10}], 70 | markers: markers 71 | }; 72 | 73 | MG.data_graphic(params); 74 | equal(document.querySelectorAll(params.target + ' .mg-markers text')[0].textContent, markers[0].label, 'First marker\'s text matches specified one'); 75 | equal(document.querySelectorAll(params.target + ' .mg-markers text')[1].textContent, markers[1].label, 'Second marker\'s text matches specified one'); 76 | }); 77 | 78 | test('Baseline text is correctly added', function() { 79 | var baselines = [{value:50, label:'a baseline'}]; 80 | 81 | var params = { 82 | target: '#qunit-fixture', 83 | data: [{'date': new Date('2014-01-01'), 'value': 100}, 84 | {'date': new Date('2014-03-01'), 'value': 10}], 85 | baselines: baselines 86 | }; 87 | 88 | MG.data_graphic(params); 89 | equal(document.querySelectorAll(params.target + ' .mg-baselines text')[0].textContent, baselines[0].label, 'Baseline text matches specified one'); 90 | }); 91 | 92 | test('When an existing chart is updated with no markers, existing markers are cleared', function() { 93 | var markers = [{ 94 | 'date': new Date('2014-11-02'), 95 | 'label': 'Lorem Ipsum' 96 | }]; 97 | 98 | var params_0 = { 99 | target: '#qunit-fixture', 100 | data: [{'date': new Date('2014-11-01'), 'value': 12}, 101 | {'date': new Date('2014-11-03'), 'value': 18}], 102 | markers: markers 103 | }; 104 | 105 | var params = { 106 | target: '#qunit-fixture', 107 | data: [{'date': new Date('2014-11-01'), 'value': 14}, 108 | {'date': new Date('2014-11-03'), 'value': 20}], 109 | }; 110 | 111 | MG.data_graphic(params_0); 112 | MG.data_graphic(params); 113 | 114 | equal(document.querySelectorAll('.mg-markers').length, 0, 'Old markers were cleared'); 115 | }); 116 | 117 | test('When an existing chart is updated with no baselines, existing baselines are cleared', function() { 118 | var baselines = [{ 119 | 'value': 10, 120 | 'label': 'Lorem Ipsum' 121 | }]; 122 | 123 | var params_0 = { 124 | target: '#qunit-fixture', 125 | data: [{'date': new Date('2014-11-01'), 'value': 12}, 126 | {'date': new Date('2014-11-03'), 'value': 18}], 127 | baselines: baselines 128 | }; 129 | 130 | var params = { 131 | target: '#qunit-fixture', 132 | data: [{'date': new Date('2014-11-01'), 'value': 14}, 133 | {'date': new Date('2014-11-03'), 'value': 20}], 134 | }; 135 | 136 | MG.data_graphic(params_0); 137 | MG.data_graphic(params); 138 | 139 | equal(document.querySelectorAll('.mg-baselines').length, 0, 'Old baselines were cleared'); 140 | }); 141 | 142 | // test('args.small_text', function() { 143 | // var baselines = [{value:50, label:'a baseline'}]; 144 | 145 | // var params = { 146 | // target: '#qunit-fixture', 147 | // data: [{'date': new Date('2014-01-01'), 'value': 100}, 148 | // {'date': new Date('2014-03-01'), 'value': 10}], 149 | // baselines: baselines, 150 | // small_text: true 151 | // }; 152 | 153 | // MG.data_graphic(params); 154 | // ok(document.querySelector('.mg-baselines-small'), 'Small baselines is set'); 155 | // }); 156 | -------------------------------------------------------------------------------- /examples/charts/updating.htm: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 | 7 | 9 | 11 |
    12 |
    13 |
    15 | 17 | 19 | 21 |
    22 |
    23 |
    24 | 25 | 26 |
    var globals = {};
     27 | 
     28 | var split_by_params = {
     29 |     title: "Downloads by Channel",
     30 |     description: "We sometimes have the need to split the data and then gracefully update the graphic with the newly selected subset of data.",
     31 |     width: 600,
     32 |     height: 200,
     33 |     right: 40,
     34 |     xax_count: 4,
     35 |     target: '#split_by',
     36 |     x_accessor: 'date',
     37 |     y_accessor: 'release'
     38 | };
     39 | 
     40 | var modify_time_period_params = {
     41 |     title: "Beta Downloads",
     42 |     description: "We sometimes have the need to view data for just the past n days. Here, the transition_on_update option is set to false.",
     43 |     width: 600,
     44 |     height: 200,
     45 |     right: 40,
     46 |     show_secondary_x_label: false,
     47 |     xax_count: 4,
     48 |     target: '#modify_time_period',
     49 |     x_accessor: 'date',
     50 |     y_accessor: 'beta'
     51 | }
     52 | 
     53 | d3.json('data/split_by.json', function(data) {
     54 |     data = MG.convert.date(data, 'date');
     55 |     globals.data = data;
     56 | 
     57 |     split_by_params.data = data;
     58 |     MG.data_graphic(split_by_params);
     59 | 
     60 |     modify_time_period_params.data = data;
     61 |     MG.data_graphic(modify_time_period_params);
     62 | });
     63 | 
     64 | $('.split-by-controls button').click(function() {
     65 |     var new_y_accessor = $(this).data('y_accessor');
     66 |     split_by_params.y_accessor = new_y_accessor;
     67 | 
     68 |     // change button state
     69 |     $(this).addClass('active').siblings().removeClass('active');
     70 | 
     71 |     // update data
     72 |     delete split_by_params.xax_format;
     73 |     MG.data_graphic(split_by_params);
     74 | });
     75 | 
     76 | $('.modify-time-period-controls button').click(function() {
     77 |     var past_n_days = $(this).data('time_period');
     78 |     var data = modify_time_period(globals.data, past_n_days);
     79 | 
     80 |     // change button state
     81 |     $(this).addClass('active').siblings().removeClass('active');
     82 | 
     83 |     delete modify_time_period_params.xax_format;
     84 |     modify_time_period_params.data = data;
     85 |     MG.data_graphic(modify_time_period_params);
     86 | });
     87 | 
     88 | function modify_time_period(data, past_n_days) {
     89 |     if (past_n_days !== '') {
     90 |         return MG.clone(data).slice(past_n_days * -1);
     91 |     }
     92 | 
     93 |     return data;
     94 | }
    95 | 96 |
    97 |
    98 | 99 | -------------------------------------------------------------------------------- /src/js/common/init.js: -------------------------------------------------------------------------------- 1 | function mg_merge_args_with_defaults (args) { 2 | var defaults = { 3 | target: null, 4 | title: null, 5 | description: null 6 | }; 7 | if (!args) { args = {}; } 8 | 9 | if (!args.processed) { 10 | args.processed = {}; 11 | } 12 | 13 | args = merge_with_defaults(args, defaults); 14 | return args; 15 | } 16 | 17 | function mg_is_time_series (args) { 18 | var first_elem = mg_flatten_array(args.processed.original_data || args.data)[0]; 19 | args.time_series = first_elem[args.processed.original_x_accessor || args.x_accessor] instanceof Date; 20 | } 21 | 22 | function mg_init_compute_width (args) { 23 | var svg_width = args.width; 24 | // are we setting the aspect ratio? 25 | if (args.full_width) { 26 | // get parent element 27 | svg_width = get_width(args.target); 28 | } 29 | args.width = svg_width; 30 | } 31 | 32 | function mg_init_compute_height (args) { 33 | var svg_height = args.height; 34 | if (args.full_height) { 35 | svg_height = get_height(args.target); 36 | } 37 | 38 | if (args.chart_type === 'bar' && svg_height === null) { 39 | svg_height = args.height = args.data[0].length * args.bar_height + args.top + args.bottom; 40 | } 41 | 42 | args.height = svg_height; 43 | } 44 | 45 | function mg_remove_svg_if_chart_type_has_changed (svg, args) { 46 | if ((!svg.selectAll('.mg-main-line').empty() && args.chart_type !== 'line') || 47 | (!svg.selectAll('.mg-points').empty() && args.chart_type !== 'point') || 48 | (!svg.selectAll('.mg-histogram').empty() && args.chart_type !== 'histogram') || 49 | (!svg.selectAll('.mg-barplot').empty() && args.chart_type !== 'bar') 50 | ) { 51 | svg.remove(); 52 | } 53 | } 54 | 55 | function mg_add_svg_if_it_doesnt_exist (svg, args) { 56 | if (mg_get_svg_child_of(args.target).empty()) { 57 | svg = d3.select(args.target) 58 | .append('svg') 59 | .classed('linked', args.linked) 60 | .attr('width', args.width) 61 | .attr('height', args.height); 62 | } 63 | return svg; 64 | } 65 | 66 | function mg_add_clip_path_for_plot_area (svg, args) { 67 | svg.selectAll('.mg-clip-path').remove(); 68 | svg.append('defs') 69 | .attr('class', 'mg-clip-path') 70 | .append('clipPath') 71 | .attr('id', 'mg-plot-window-' + mg_target_ref(args.target)) 72 | .append('svg:rect') 73 | .attr('x', args.left) 74 | .attr('y', args.top) 75 | .attr('width', args.width - args.left - args.right - args.buffer) 76 | .attr('height', args.height - args.top - args.bottom - args.buffer + 1); 77 | } 78 | 79 | function mg_adjust_width_and_height_if_changed (svg, args) { 80 | if (args.width !== Number(svg.attr('width'))) { 81 | svg.attr('width', args.width); 82 | } 83 | if (args.height !== Number(svg.attr('height'))) { 84 | svg.attr('height', args.height); 85 | } 86 | } 87 | 88 | function mg_set_viewbox_for_scaling (svg, args) { 89 | // we need to reconsider how we handle automatic scaling 90 | svg.attr('viewBox', '0 0 ' + args.width + ' ' + args.height); 91 | if (args.full_width || args.full_height) { 92 | svg.attr('preserveAspectRatio', 'xMinYMin meet'); 93 | } 94 | } 95 | 96 | function mg_remove_missing_classes_and_text (svg) { 97 | // remove missing class 98 | svg.classed('mg-missing', false); 99 | 100 | // remove missing text 101 | svg.selectAll('.mg-missing-text').remove(); 102 | svg.selectAll('.mg-missing-pane').remove(); 103 | } 104 | 105 | function mg_remove_outdated_lines (svg, args) { 106 | // if we're updating an existing chart and we have fewer lines than 107 | // before, remove the outdated lines, e.g. if we had 3 lines, and we're calling 108 | // data_graphic() on the same target with 2 lines, remove the 3rd line 109 | 110 | var i = 0; 111 | if (svg.selectAll('.mg-main-line')[0].length >= args.data.length) { 112 | // now, the thing is we can't just remove, say, line3 if we have a custom 113 | // line-color map, instead, see which are the lines to be removed, and delete those 114 | if (args.custom_line_color_map.length > 0) { 115 | var array_full_series = function (len) { 116 | var arr = new Array(len); 117 | for (var i = 0; i < arr.length; i++) { arr[i] = i + 1; } 118 | return arr; 119 | }; 120 | 121 | // get an array of lines ids to remove 122 | var lines_to_remove = arr_diff( 123 | array_full_series(args.max_data_size), 124 | args.custom_line_color_map); 125 | 126 | for (i = 0; i < lines_to_remove.length; i++) { 127 | svg.selectAll('.mg-main-line.mg-line' + lines_to_remove[i] + '-color') 128 | .remove(); 129 | } 130 | } else { 131 | // if we don't have a custom line-color map, just remove the lines from the end 132 | 133 | var num_of_new = args.data.length; 134 | var num_of_existing = svg.selectAll('.mg-main-line')[0].length; 135 | 136 | for (i = num_of_existing; i > num_of_new; i--) { 137 | svg.selectAll('.mg-main-line.mg-line' + i + '-color') 138 | .remove(); 139 | } 140 | } 141 | } 142 | } 143 | 144 | function mg_raise_container_error(container, args){ 145 | if (container.empty()) { 146 | console.warn('The specified target element "' + args.target + '" could not be found in the page. The chart will not be rendered.'); 147 | return; 148 | } 149 | } 150 | 151 | function init (args) { 152 | 'use strict'; 153 | args = arguments[0]; 154 | args = mg_merge_args_with_defaults(args); 155 | // If you pass in a dom element for args.target, the expectation 156 | // of a string elsewhere will break. 157 | var container = d3.select(args.target); 158 | mg_raise_container_error(container, args); 159 | 160 | var svg = container.selectAll('svg'); 161 | 162 | mg_is_time_series(args); 163 | mg_init_compute_width(args); 164 | mg_init_compute_height(args); 165 | 166 | mg_remove_svg_if_chart_type_has_changed(svg, args); 167 | svg = mg_add_svg_if_it_doesnt_exist(svg, args); 168 | 169 | mg_add_clip_path_for_plot_area(svg, args); 170 | mg_adjust_width_and_height_if_changed(svg, args); 171 | mg_set_viewbox_for_scaling(svg, args); 172 | mg_remove_missing_classes_and_text(svg); 173 | chart_title(args); 174 | mg_remove_outdated_lines(svg, args); 175 | 176 | return this; 177 | } 178 | 179 | MG.init = init; 180 | -------------------------------------------------------------------------------- /tests/common/y_axis_test.js: -------------------------------------------------------------------------------- 1 | module('y_axis'); 2 | 3 | test('Y-axis is added', function() { 4 | var params = { 5 | target: '#qunit-fixture', 6 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 7 | {'date': new Date('2014-03-01'), 'value': 18}] 8 | }; 9 | 10 | MG.data_graphic(params); 11 | ok(document.querySelector('.mg-y-axis'), 'Y-axis is added'); 12 | }); 13 | 14 | test('args.y_axis set to false', function() { 15 | var params = { 16 | target: '#qunit-fixture', 17 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 18 | {'date': new Date('2014-03-01'), 'value': 18}], 19 | y_axis: false 20 | }; 21 | 22 | MG.data_graphic(params); 23 | equal(document.querySelector('.mg-y-axis'), null, 'Y-axis is not added'); 24 | }); 25 | 26 | test('Only one y-axis is added on multiple calls to the same target element', function() { 27 | var params = { 28 | target: '#qunit-fixture', 29 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 30 | {'date': new Date('2014-03-01'), 'value': 18}] 31 | }; 32 | 33 | MG.data_graphic(params); 34 | MG.data_graphic(MG.clone(params)); 35 | 36 | equal(document.querySelectorAll(params.target + ' .mg-y-axis').length, 1, 'We only have one y-axis'); 37 | }); 38 | 39 | test('args.y_label', function() { 40 | var params = { 41 | target: '#qunit-fixture', 42 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 43 | {'date': new Date('2014-03-01'), 'value': 18}], 44 | y_label: 'foo bar' 45 | }; 46 | 47 | MG.data_graphic(params); 48 | ok(document.querySelector('.mg-y-axis .label'), 'Y-axis label exists'); 49 | }); 50 | 51 | test('Y-axis doesn\'t break when data object is of length 1', function() { 52 | var params = { 53 | target: '#qunit-fixture', 54 | data: [{'date': new Date('2014-01-01'), 'value': 12}] 55 | }; 56 | 57 | MG.data_graphic(params); 58 | ok(document.querySelector('.mg-y-axis'), 'Y-axis exists'); 59 | }); 60 | 61 | // test('args.small_text', function() { 62 | // var params = { 63 | // target: '#qunit-fixture', 64 | // data: [{'date': new Date('2014-01-01'), 'value': 12}], 65 | // small_text: true, 66 | // }; 67 | 68 | // MG.data_graphic(params); 69 | // ok(document.querySelector('.mg-y-axis-small'), 'Small y-axis is set'); 70 | // }); 71 | 72 | test('args.y_rug', function() { 73 | var params = { 74 | target: '#qunit-fixture', 75 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 76 | {'date': new Date('2014-03-01'), 'value': 18}], 77 | y_rug: true 78 | }; 79 | 80 | MG.data_graphic(params); 81 | ok(document.querySelector('.mg-y-rug'), 'Y-axis rugplot exists'); 82 | }); 83 | 84 | test('Only one rugplot is added on multiple calls to the same target element', function() { 85 | var params = { 86 | target: '#qunit-fixture', 87 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 88 | {'date': new Date('2014-03-01'), 'value': 18}], 89 | y_rug: true 90 | }; 91 | 92 | MG.data_graphic(params); 93 | MG.data_graphic(MG.clone(params)); 94 | equal(document.querySelectorAll('.mg-y-rug').length, 2, 'We only have one rugplot on the y-axis'); 95 | }); 96 | 97 | test('Default min_y is 0', function() { 98 | var params = { 99 | target: '#qunit-fixture', 100 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 101 | {'date': new Date('2014-03-01'), 'value': 18}] 102 | }; 103 | 104 | MG.data_graphic(params); 105 | equal(document.querySelector('.mg-y-axis text').textContent, 0, 'Y-axis starts at 0'); 106 | }); 107 | 108 | test('args.min_y_from_data', function() { 109 | var params = { 110 | target: '#qunit-fixture', 111 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 112 | {'date': new Date('2014-03-01'), 'value': 18}], 113 | min_y_from_data: true 114 | }; 115 | 116 | MG.data_graphic(params); 117 | equal(document.querySelector('.mg-y-axis text').textContent, 12, 'Y-axis starts at 12'); 118 | }); 119 | 120 | test('args.min_y set to arbitrary value', function() { 121 | var params = { 122 | target: '#qunit-fixture', 123 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 124 | {'date': new Date('2014-03-01'), 'value': 18}], 125 | min_y: 5 126 | }; 127 | 128 | MG.data_graphic(params); 129 | equal(document.querySelector('.mg-y-axis text').textContent, 5, 'Y-axis starts at 5'); 130 | }); 131 | 132 | test('args.y_extended_ticks', function() { 133 | var params = { 134 | target: '#qunit-fixture', 135 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 136 | {'date': new Date('2014-03-01'), 'value': 18}], 137 | y_extended_ticks: true 138 | }; 139 | 140 | MG.data_graphic(params); 141 | ok(document.querySelector('.mg-extended-y-ticks'), 'Y-axis extended ticks exist'); 142 | }); 143 | 144 | test('args.format is set to percentage', function() { 145 | var params = { 146 | target: '#qunit-fixture', 147 | data: [{'date': new Date('2014-01-01'), 'value': 0.12}, 148 | {'date': new Date('2014-03-01'), 'value': 0.18}], 149 | format: 'percentage' 150 | }; 151 | 152 | MG.data_graphic(params); 153 | equal(document.querySelector('.mg-y-axis text').textContent.slice(-1), '%', 'Y-axis units are %'); 154 | }); 155 | 156 | test('args.yax_units', function() { 157 | var params = { 158 | target: '#qunit-fixture', 159 | data: [{'date': new Date('2014-01-01'), 'value': 2.12}, 160 | {'date': new Date('2014-03-01'), 'value': 4.18}], 161 | yax_units: '$', 162 | }; 163 | 164 | MG.data_graphic(params); 165 | equal(document.querySelector('.mg-y-axis text').textContent[0], '$', 'Y-axis units are $'); 166 | }); 167 | 168 | test('When args.max_y is set, ignore inflator', function() { 169 | var params = { 170 | target: '#qunit-fixture', 171 | data: [{'date': new Date('2014-01-01'), 'value': 12}, 172 | {'date': new Date('2014-03-01'), 'value': 18}], 173 | max_y: 50, 174 | }; 175 | 176 | MG.data_graphic(params); 177 | var nodes = document.querySelectorAll('.mg-y-axis text'); 178 | equal(nodes[nodes.length - 1].textContent, 50, 'Maximum y-axis value is 50'); 179 | }); 180 | -------------------------------------------------------------------------------- /src/js/layout/bootstrap_dropdown.js: -------------------------------------------------------------------------------- 1 | if (typeof jQuery !== 'undefined') { 2 | /*! 3 | * Bootstrap v3.3.1 (http://getbootstrap.com) 4 | * Copyright 2011-2014 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | */ 7 | 8 | /*! 9 | * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=c3834cc5b59ef727da53) 10 | * Config saved to config.json and https://gist.github.com/c3834cc5b59ef727da53 11 | */ 12 | 13 | /* ======================================================================== 14 | * Bootstrap: dropdown.js v3.3.1 15 | * http://getbootstrap.com/javascript/#dropdowns 16 | * ======================================================================== 17 | * Copyright 2011-2014 Twitter, Inc. 18 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 19 | * ======================================================================== */ 20 | 21 | 22 | +function ($) { 23 | 'use strict'; 24 | 25 | if(typeof $().dropdown == 'function') 26 | return true; 27 | 28 | // DROPDOWN CLASS DEFINITION 29 | // ========================= 30 | 31 | var backdrop = '.dropdown-backdrop'; 32 | var toggle = '[data-toggle="dropdown"]'; 33 | var Dropdown = function (element) { 34 | $(element).on('click.bs.dropdown', this.toggle); 35 | }; 36 | 37 | Dropdown.VERSION = '3.3.1'; 38 | 39 | Dropdown.prototype.toggle = function (e) { 40 | var $this = $(this); 41 | 42 | if ($this.is('.disabled, :disabled')) return; 43 | 44 | var $parent = getParent($this); 45 | var isActive = $parent.hasClass('open'); 46 | 47 | clearMenus(); 48 | 49 | if (!isActive) { 50 | if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { 51 | // if mobile we use a backdrop because click events don't delegate 52 | $('