From 8526910c5d3771c12f844495347b6a99d0c51bca Mon Sep 17 00:00:00 2001 From: sluger Date: Mon, 6 Aug 2018 16:09:44 +0200 Subject: [PATCH] prototyping error bars for vertical (hierarchcal) bar and line charts --- .babelrc | 13 +++ .editorconfig | 18 +++ .eslintrc | 223 +++++++++++++++++++++++++++++++++++++ .gitignore | 45 ++++++++ package.json | 59 ++++++++++ rollup.config.js | 21 ++++ samples/line.html | 159 ++++++++++++++++++++++++++ samples/utils.js | 135 ++++++++++++++++++++++ samples/vertical-bar.html | 159 ++++++++++++++++++++++++++ samples/vertical-tree.html | 116 +++++++++++++++++++ src/plugin.js | 83 ++++++++++++++ 11 files changed, 1031 insertions(+) create mode 100644 .babelrc create mode 100644 .editorconfig create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 package.json create mode 100644 rollup.config.js create mode 100644 samples/line.html create mode 100644 samples/utils.js create mode 100644 samples/vertical-bar.html create mode 100644 samples/vertical-tree.html create mode 100644 src/plugin.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..9772b93 --- /dev/null +++ b/.babelrc @@ -0,0 +1,13 @@ +{ + "presets": [ + [ + "env", + { + "modules": false + } + ] + ], + "plugins": [ + "external-helpers" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5b6e75d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[gulpfile.js] +indent_style = space +indent_size = 2 + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..f340857 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,223 @@ +env: + amd: true + browser: true + es6: true + jquery: true + node: true + +parserOptions: + sourceType: module + +# http://eslint.org/docs/rules/ +rules: + # Possible Errors + no-cond-assign: 2 + no-console: [2, {allow: [warn, error]}] + no-constant-condition: 2 + no-control-regex: 2 + no-debugger: 2 + no-dupe-args: 2 + no-dupe-keys: 2 + no-duplicate-case: 2 + no-empty: 2 + no-empty-character-class: 2 + no-ex-assign: 2 + no-extra-boolean-cast: 2 + no-extra-parens: [2, functions] + no-extra-semi: 2 + no-func-assign: 2 + no-inner-declarations: [2, functions] + no-invalid-regexp: 2 + no-irregular-whitespace: 2 + no-negated-in-lhs: 2 + no-obj-calls: 2 + no-regex-spaces: 2 + no-sparse-arrays: 2 + no-unexpected-multiline: 2 + no-unreachable: 2 + use-isnan: 2 + valid-jsdoc: 0 + valid-typeof: 2 + + # Best Practices + accessor-pairs: 2 + array-callback-return: 0 + block-scoped-var: 0 + complexity: [2, 15] + consistent-return: 0 + curly: [2, all] + default-case: 2 + dot-location: 0 + dot-notation: 2 + eqeqeq: ["error", "always", {"null": "ignore"}] + guard-for-in: 2 + no-alert: 2 + no-caller: 2 + no-case-declarations: 2 + no-div-regex: 2 + no-else-return: 2 + no-empty-pattern: 2 + no-eq-null: 0 + no-eval: 2 + no-extend-native: 2 + no-extra-bind: 2 + no-fallthrough: 2 + no-floating-decimal: 2 + no-implicit-coercion: 0 + no-implied-eval: 2 + no-invalid-this: 0 + no-iterator: 2 + no-labels: 2 + no-lone-blocks: 2 + no-loop-func: 2 + no-magic-number: 0 + no-multi-spaces: 2 + no-multi-str: 2 + no-native-reassign: 2 + no-new-func: 2 + no-new-wrappers: 2 + no-new: 2 + no-octal-escape: 2 + no-octal: 2 + no-proto: 2 + no-redeclare: 2 + no-return-assign: 2 + no-script-url: 2 + no-self-compare: 2 + no-sequences: 2 + no-throw-literal: 0 + no-unused-expressions: 2 + no-useless-call: 2 + no-useless-concat: 2 + no-void: 2 + no-warning-comments: 0 + no-with: 2 + radix: 2 + vars-on-top: 0 + wrap-iife: 2 + yoda: [1, never] + + # Strict + strict: 0 + + # Variables + init-declarations: 0 + no-catch-shadow: 2 + no-delete-var: 2 + no-label-var: 2 + no-shadow-restricted-names: 2 + no-shadow: 2 + no-undef-init: 2 + no-undef: 2 + no-undefined: 0 + no-unused-vars: 2 + no-use-before-define: 2 + + # Node.js and CommonJS + callback-return: 2 + global-require: 2 + handle-callback-err: 2 + no-mixed-requires: 0 + no-new-require: 0 + no-path-concat: 2 + no-process-exit: 2 + no-restricted-modules: 0 + no-sync: 0 + + # Stylistic Issues + array-bracket-spacing: [2, never] + block-spacing: 0 + brace-style: [2, 1tbs] + camelcase: 2 + comma-dangle: [2, only-multiline] + comma-spacing: 2 + comma-style: [2, last] + computed-property-spacing: [2, never] + consistent-this: [2, me] + eol-last: 2 + func-call-spacing: 0 + func-names: [2, never] + func-style: 0 + id-length: 0 + id-match: 0 + indent: [2, 2] + jsx-quotes: 0 + key-spacing: 2 + keyword-spacing: 2 + linebreak-style: 0 + lines-around-comment: 0 + max-depth: 0 + max-len: 0 + max-lines: 0 + max-nested-callbacks: 0 + max-params: 0 + max-statements-per-line: 0 + max-statements: [2, 40] + multiline-ternary: 0 + new-cap: 0 + new-parens: 2 + newline-after-var: 0 + newline-before-return: 0 + newline-per-chained-call: 0 + no-array-constructor: 0 + no-bitwise: 0 + no-continue: 0 + no-inline-comments: 0 + no-lonely-if: 2 + no-mixed-operators: 0 + no-mixed-spaces-and-tabs: 2 + no-multiple-empty-lines: [2, {max: 2}] + no-negated-condition: 0 + no-nested-ternary: 0 + no-new-object: 0 + no-plusplus: 0 + no-restricted-syntax: 0 + no-spaced-func: 0 + no-ternary: 0 + no-trailing-spaces: 2 + no-underscore-dangle: 0 + no-unneeded-ternary: 0 + no-whitespace-before-property: 2 + object-curly-newline: 0 + object-curly-spacing: [2, never] + object-property-newline: 0 + one-var-declaration-per-line: 2 + one-var: [2, {initialized: never}] + operator-assignment: 0 + operator-linebreak: 0 + padded-blocks: 0 + quote-props: [2, as-needed] + quotes: [2, single, {avoidEscape: true}] + require-jsdoc: 0 + semi-spacing: 2 + semi: [2, always] + sort-keys: 0 + sort-vars: 0 + space-before-blocks: [2, always] + space-before-function-paren: [2, never] + space-in-parens: [2, never] + space-infix-ops: 2 + space-unary-ops: [2, {words: true, nonwords: false}] + spaced-comment: [2, always] + unicode-bom: 0 + wrap-regex: 2 + + # ECMAScript 6 + arrow-body-style: 0 + arrow-parens: 0 + arrow-spacing: 0 + constructor-super: 0 + generator-star-spacing: 0 + no-arrow-condition: 0 + no-class-assign: 0 + no-const-assign: 0 + no-dupe-class-members: 0 + no-this-before-super: 0 + no-var: 0 + object-shorthand: 0 + prefer-arrow-callback: 0 + prefer-const: 0 + prefer-reflect: 0 + prefer-spread: 0 + prefer-template: 0 + require-yield: 0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd52be6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Visual Studio +.vs + +# Idea +.idea + +/build diff --git a/package.json b/package.json new file mode 100644 index 0000000..c6a12c3 --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "chartjs-plugin-error-bars", + "version": "1.0.0", + "description": "Error Bars Chart.js Plugin", + "main": "build/Plugin.Errorbars.js", + "repository": { + "type": "git", + "url": "git+https://github.com/datavisyn/chartjs-plugin-error-bars.git" + }, + "keywords": [ + "chartjs", + "chartjs-plugin", + "chartjs-error-bars", + "error-bars", + "javascript" + ], + "author": "stefan.luger@datavisyn.io", + "license": "MIT", + "bugs": { + "url": "https://github.com/datavisyn/chartjs-plugin-error-bars/issues" + }, + "homepage": "https://github.com/datavisyn/chartjs-plugin-error-bars#readme", + "files": [ + "build", + "src/**/*.js" + ], + "dependencies": { + "chart.js": "^2.7.2" + }, + "devDependencies": { + "babel-core": "^6.26.0", + "babel-plugin-external-helpers": "^6.22.0", + "babel-preset-env": "^1.6.1", + "eslint": "^3.19.0", + "package-preamble": "0.0", + "rimraf": "^2.6.2", + "rollup": "^0.55.3", + "rollup-plugin-babel": "^3.0.3", + "rollup-plugin-commonjs": "^8.3.0", + "rollup-plugin-node-resolve": "^3.0.2", + "uglify-es": "^3.3.9" + }, + "scripts": { + "clean": "rimraf build", + "watch": "rollup -c -w -i src/plugin.js", + "test": "eslint src", + "build:dev": "rollup -c -i src/plugin.js", + "build:prod": "npm run build:dev && uglifyjs build/Plugin.Errorbars.js -c -m -o build/Plugin.Errorbars.min.js", + "prebuild": "npm run clean && npm test", + "build": "npm run build:prod", + "preversion": "npm run test", + "prepare": "npm run build:dev", + "prepublishOnly": "npm run build:prod", + "release:major": "npm version major && npm publish && git push --follow-tags", + "release:minor": "npm version minor && npm publish && git push --follow-tags", + "release:patch": "npm version patch && npm publish && git push --follow-tags", + "release:pre": "npm version prerelease && npm publish --tag=next && git push --follow-tags" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..352d82d --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,21 @@ +// rollup.config.js +import resolve from 'rollup-plugin-node-resolve'; +import commonjs from 'rollup-plugin-commonjs'; +import babel from 'rollup-plugin-babel'; + +export default { + external: ['chart.js'], + output: { + file: 'build/Plugin.Errorbars.js', + format: 'umd', + globals: { + 'chart.js': 'Chart' + }, + name: 'PluginErrorbars' + }, + plugins: [ + resolve(), + commonjs(), + babel() + ] +}; diff --git a/samples/line.html b/samples/line.html new file mode 100644 index 0000000..8633df3 --- /dev/null +++ b/samples/line.html @@ -0,0 +1,159 @@ + + + + + Bar Chart + + + + + + + +
+ +
+ + + + + + + + + diff --git a/samples/utils.js b/samples/utils.js new file mode 100644 index 0000000..172a727 --- /dev/null +++ b/samples/utils.js @@ -0,0 +1,135 @@ +'use strict'; + +window.chartColors = { + red: 'rgb(255, 99, 132)', + orange: 'rgb(255, 159, 64)', + yellow: 'rgb(255, 205, 86)', + green: 'rgb(75, 192, 192)', + blue: 'rgb(54, 162, 235)', + purple: 'rgb(153, 102, 255)', + grey: 'rgb(201, 203, 207)' +}; + +(function(global) { + var Months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; + + var COLORS = [ + '#4dc9f6', + '#f67019', + '#f53794', + '#537bc4', + '#acc236', + '#166a8f', + '#00a950', + '#58595b', + '#8549ba' + ]; + + var Samples = global.Samples || (global.Samples = {}); + var Color = global.Color; + + Samples.utils = { + // Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ + srand: function(seed) { + this._seed = seed; + }, + + rand: function(min, max) { + var seed = this._seed; + min = min === undefined ? 0 : min; + max = max === undefined ? 1 : max; + this._seed = (seed * 9301 + 49297) % 233280; + return min + (this._seed / 233280) * (max - min); + }, + + numbers: function(config) { + var cfg = config || {}; + var min = cfg.min || 0; + var max = cfg.max || 1; + var from = cfg.from || []; + var count = cfg.count || 8; + var decimals = cfg.decimals || 8; + var continuity = cfg.continuity || 1; + var dfactor = Math.pow(10, decimals) || 0; + var data = []; + var i, value; + + for (i = 0; i < count; ++i) { + value = (from[i] || 0) + this.rand(min, max); + if (this.rand() <= continuity) { + data.push(Math.round(dfactor * value) / dfactor); + } else { + data.push(null); + } + } + + return data; + }, + + labels: function(config) { + var cfg = config || {}; + var min = cfg.min || 0; + var max = cfg.max || 100; + var count = cfg.count || 8; + var step = (max - min) / count; + var decimals = cfg.decimals || 8; + var dfactor = Math.pow(10, decimals) || 0; + var prefix = cfg.prefix || ''; + var values = []; + var i; + + for (i = min; i < max; i += step) { + values.push(prefix + Math.round(dfactor * i) / dfactor); + } + + return values; + }, + + months: function(config) { + var cfg = config || {}; + var count = cfg.count || 12; + var section = cfg.section; + var values = []; + var i, value; + + for (i = 0; i < count; ++i) { + value = Months[Math.ceil(i) % 12]; + values.push(value.substring(0, section)); + } + + return values; + }, + + color: function(index) { + return COLORS[index % COLORS.length]; + }, + + transparentize: function(color, opacity) { + var alpha = opacity === undefined ? 0.5 : 1 - opacity; + return Color(color).alpha(alpha).rgbString(); + } + }; + + // DEPRECATED + window.randomScalingFactor = function() { + return Math.round(Samples.utils.rand(-100, 100)); + }; + + // INITIALIZATION + + Samples.utils.srand(Date.now()); + +}(this)); diff --git a/samples/vertical-bar.html b/samples/vertical-bar.html new file mode 100644 index 0000000..300937f --- /dev/null +++ b/samples/vertical-bar.html @@ -0,0 +1,159 @@ + + + + + Bar Chart + + + + + + + +
+ +
+ + + + + + + + + diff --git a/samples/vertical-tree.html b/samples/vertical-tree.html new file mode 100644 index 0000000..664d7f1 --- /dev/null +++ b/samples/vertical-tree.html @@ -0,0 +1,116 @@ + + + + + Hierarchical Bar Chart + + + + + + + +
+ +
+ + + + diff --git a/src/plugin.js b/src/plugin.js new file mode 100644 index 0000000..5dfb5f4 --- /dev/null +++ b/src/plugin.js @@ -0,0 +1,83 @@ +'use strict'; + +import Chart from 'chart.js'; + +// TODO: options: bar color, bar width + +const ErrorBarsPlugin = { + id: 'chartJsPluginErrorBars', + + _drawErrorBar(ctx, model, value) { + ctx.save(); + ctx.strokeStyle = model.color; + ctx.beginPath(); + ctx.moveTo(model.x, model.y); + ctx.lineTo(model.x, model.y - value); + ctx.moveTo(model.x - 5, model.y - value); + ctx.lineTo(model.x + 5, model.y - value); + ctx.stroke(); + ctx.restore(); + }, + + beforeUpdate(chart) { + + }, + + afterDatasetsDraw(chart, easing) { + var yScale = chart.scales['y-axis-0']; + var ctx = chart.ctx; + ctx.save(); + + var errorBars = chart.data.datasets.map((d) => d.errorBars); + var barCoords = []; + chart.data.datasets.forEach((d, i) => { + var bars = chart.getDatasetMeta(i).data; + barCoords.push(bars.map((b, j) => { + let barLabel = ''; + + // line charts do not have labels so the error bars cant be mapped + if (!b._model.label) { + barLabel = chart.data.labels[j]; + } else { + barLabel = b._model.label; + } + return { + label: barLabel, + x: b._model.x, + y: b._model.y, + color: b._model.borderColor + } + })); + }); + + barCoords.forEach((dataset, i) => { + dataset.forEach((b) => { + // is not hierarchical + let hasErrorBar = errorBars[i].hasOwnProperty(b.label); + + let errorBarData = null; + if (hasErrorBar) { + errorBarData = errorBars[i][b.label]; + } + // is hierarchical + if (!hasErrorBar && b.label && b.label.label && errorBars[i].hasOwnProperty(b.label.label)) { + errorBarData = errorBars[i][b.label.label]; + } + + if (errorBarData) { + var plus = yScale.getRightValue(errorBarData.plus); + var minus = yScale.getRightValue(errorBarData.minus); + + this._drawErrorBar(ctx, b, plus); + this._drawErrorBar(ctx, b, minus); + } + }); + }); + + ctx.restore(); + } +}; + +Chart.pluginService.register(ErrorBarsPlugin); + +export default ErrorBarsPlugin;