From 06afdc1ee8f23de211054b12b7c2ddf97dffa9ad Mon Sep 17 00:00:00 2001 From: Ivan Mikheykin Date: Thu, 16 Aug 2018 16:17:39 +0300 Subject: [PATCH] status-heatmap-plugin for Grafana - fixes for 5.1.3 - nulls as empty --- .gitignore | 2 + Gruntfile.js | 66 ++ README.md | 1 + package.json | 39 ++ src/color_legend.js | 321 +++++++++ src/css/status-heatmap.scss | 94 +++ src/img/logo.svg | 88 +++ .../d3-scale-chromatic/categorical/Accent.js | 3 + .../d3-scale-chromatic/categorical/Dark2.js | 3 + .../d3-scale-chromatic/categorical/Paired.js | 3 + .../d3-scale-chromatic/categorical/Pastel1.js | 3 + .../d3-scale-chromatic/categorical/Pastel2.js | 3 + .../d3-scale-chromatic/categorical/Set1.js | 3 + .../d3-scale-chromatic/categorical/Set2.js | 3 + .../d3-scale-chromatic/categorical/Set3.js | 3 + src/libs/d3-scale-chromatic/colors.js | 5 + src/libs/d3-scale-chromatic/diverging/BrBG.js | 16 + .../d3-scale-chromatic/diverging/GnYlRd.js | 16 + src/libs/d3-scale-chromatic/diverging/PRGn.js | 16 + src/libs/d3-scale-chromatic/diverging/PiYG.js | 16 + src/libs/d3-scale-chromatic/diverging/PuOr.js | 16 + src/libs/d3-scale-chromatic/diverging/RdBu.js | 16 + src/libs/d3-scale-chromatic/diverging/RdGy.js | 16 + .../d3-scale-chromatic/diverging/RdYlBu.js | 16 + .../d3-scale-chromatic/diverging/RdYlGn.js | 16 + .../d3-scale-chromatic/diverging/Spectral.js | 16 + src/libs/d3-scale-chromatic/index.js | 36 + src/libs/d3-scale-chromatic/ramp.js | 5 + .../sequential-multi/BuGn.js | 14 + .../sequential-multi/BuPu.js | 14 + .../sequential-multi/GnBu.js | 14 + .../sequential-multi/OrRd.js | 14 + .../sequential-multi/PuBu.js | 14 + .../sequential-multi/PuBuGn.js | 14 + .../sequential-multi/PuRd.js | 14 + .../sequential-multi/RdPu.js | 14 + .../sequential-multi/YlGn.js | 14 + .../sequential-multi/YlGnBu.js | 14 + .../sequential-multi/YlOrBr.js | 14 + .../sequential-multi/YlOrRd.js | 14 + .../sequential-single/Blues.js | 14 + .../sequential-single/Greens.js | 14 + .../sequential-single/Greys.js | 14 + .../sequential-single/Oranges.js | 14 + .../sequential-single/Purples.js | 14 + .../sequential-single/Reds.js | 14 + src/module.html | 14 + src/module.js | 6 + src/options_editor.js | 27 + src/partials/options_editor.html | 99 +++ src/plugin.json | 25 + src/rendering.js | 620 ++++++++++++++++++ src/status_heatmap_ctrl.js | 182 +++++ src/tooltip.js | 108 +++ 54 files changed, 2174 insertions(+) create mode 100644 .gitignore create mode 100644 Gruntfile.js create mode 100644 README.md create mode 100644 package.json create mode 100644 src/color_legend.js create mode 100644 src/css/status-heatmap.scss create mode 100644 src/img/logo.svg create mode 100644 src/libs/d3-scale-chromatic/categorical/Accent.js create mode 100644 src/libs/d3-scale-chromatic/categorical/Dark2.js create mode 100644 src/libs/d3-scale-chromatic/categorical/Paired.js create mode 100644 src/libs/d3-scale-chromatic/categorical/Pastel1.js create mode 100644 src/libs/d3-scale-chromatic/categorical/Pastel2.js create mode 100644 src/libs/d3-scale-chromatic/categorical/Set1.js create mode 100644 src/libs/d3-scale-chromatic/categorical/Set2.js create mode 100644 src/libs/d3-scale-chromatic/categorical/Set3.js create mode 100644 src/libs/d3-scale-chromatic/colors.js create mode 100644 src/libs/d3-scale-chromatic/diverging/BrBG.js create mode 100644 src/libs/d3-scale-chromatic/diverging/GnYlRd.js create mode 100644 src/libs/d3-scale-chromatic/diverging/PRGn.js create mode 100644 src/libs/d3-scale-chromatic/diverging/PiYG.js create mode 100644 src/libs/d3-scale-chromatic/diverging/PuOr.js create mode 100644 src/libs/d3-scale-chromatic/diverging/RdBu.js create mode 100644 src/libs/d3-scale-chromatic/diverging/RdGy.js create mode 100644 src/libs/d3-scale-chromatic/diverging/RdYlBu.js create mode 100644 src/libs/d3-scale-chromatic/diverging/RdYlGn.js create mode 100644 src/libs/d3-scale-chromatic/diverging/Spectral.js create mode 100644 src/libs/d3-scale-chromatic/index.js create mode 100644 src/libs/d3-scale-chromatic/ramp.js create mode 100644 src/libs/d3-scale-chromatic/sequential-multi/BuGn.js create mode 100644 src/libs/d3-scale-chromatic/sequential-multi/BuPu.js create mode 100644 src/libs/d3-scale-chromatic/sequential-multi/GnBu.js create mode 100644 src/libs/d3-scale-chromatic/sequential-multi/OrRd.js create mode 100644 src/libs/d3-scale-chromatic/sequential-multi/PuBu.js create mode 100644 src/libs/d3-scale-chromatic/sequential-multi/PuBuGn.js create mode 100644 src/libs/d3-scale-chromatic/sequential-multi/PuRd.js create mode 100644 src/libs/d3-scale-chromatic/sequential-multi/RdPu.js create mode 100644 src/libs/d3-scale-chromatic/sequential-multi/YlGn.js create mode 100644 src/libs/d3-scale-chromatic/sequential-multi/YlGnBu.js create mode 100644 src/libs/d3-scale-chromatic/sequential-multi/YlOrBr.js create mode 100644 src/libs/d3-scale-chromatic/sequential-multi/YlOrRd.js create mode 100644 src/libs/d3-scale-chromatic/sequential-single/Blues.js create mode 100644 src/libs/d3-scale-chromatic/sequential-single/Greens.js create mode 100644 src/libs/d3-scale-chromatic/sequential-single/Greys.js create mode 100644 src/libs/d3-scale-chromatic/sequential-single/Oranges.js create mode 100644 src/libs/d3-scale-chromatic/sequential-single/Purples.js create mode 100644 src/libs/d3-scale-chromatic/sequential-single/Reds.js create mode 100644 src/module.html create mode 100644 src/module.js create mode 100644 src/options_editor.js create mode 100644 src/partials/options_editor.html create mode 100644 src/plugin.json create mode 100644 src/rendering.js create mode 100644 src/status_heatmap_ctrl.js create mode 100644 src/tooltip.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de81777 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +.jshintrc diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..59e95b4 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,66 @@ +module.exports = (grunt) => { + require('load-grunt-tasks')(grunt); + + grunt.loadNpmTasks('grunt-execute'); + grunt.loadNpmTasks('grunt-contrib-clean'); + + grunt.initConfig({ + + clean: ['dist'], + + copy: { + src_to_dist: { + cwd: 'src', + expand: true, + src: ['**/*', '!**/*.js', '!**/*.scss'], + dest: 'dist' + }, + pluginDef: { + expand: true, + src: ['plugin.json', 'README.md'], + dest: 'dist' + } + }, + + watch: { + rebuild_all: { + files: ['src/**/*', 'plugin.json'], + tasks: ['default'], + options: { + spawn: false, + livereload: true + } + } + }, + + babel: { + options: { + sourceMap: true, + presets: ['es2015'], + plugins: ['transform-es2015-modules-systemjs', 'transform-es2015-for-of', 'transform-class-properties', 'transform-object-rest-spread'], + }, + dist: { + files: [{ + cwd: 'src', + expand: true, + src: ['*.js', '**/*.js'], + dest: 'dist', + ext: '.js' + }] + } + }, + + sass: { + dist: { + files: { + 'dist/css/status-heatmap.css': 'src/css/status-heatmap.scss' + } + } + } + + }); + + grunt.registerTask('default', ['clean', 'copy:src_to_dist', 'copy:pluginDef', 'babel', 'sass']); + // grunt.registerTask('clean', ['clean']); + // grunt.registerTask('watch', ['watch']); +}; diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ + diff --git a/package.json b/package.json new file mode 100644 index 0000000..c9ae7a7 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "status-hetamap", + "version": "0.0.1", + "description": "Status Heatmap panel plugin for grafana", + "main": "README.md", + "scripts": { + "build": "grunt" + }, + "author": "Flant JSC", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/flant/grafana-status-heatmap.git" + }, + "devDependencies": { + "babel": "^6.23.0", + "babel-eslint": "^7.2.3", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.23.0", + "babel-preset-es2015": "^6.24.1", + "grunt": "^1.0.1", + "grunt-babel": "^6.0.0", + "grunt-contrib-clean": "^1.1.0", + "grunt-contrib-copy": "^1.0.0", + "grunt-contrib-sass": "^1.0.0", + "grunt-contrib-uglify": "^3.0.1", + "grunt-contrib-watch": "^1.0.0", + "grunt-execute": "^0.2.2", + "grunt-systemjs-builder": "^1.0.0", + "load-grunt-tasks": "^3.5.2" + }, + "dependencies": { + "d3": "^4.11.0", + "d3-scale-chromatic": "^1.1.1", + "lodash": "^4.17.4", + "moment": "^2.18.1" + } +} diff --git a/src/color_legend.js b/src/color_legend.js new file mode 100644 index 0000000..ed77896 --- /dev/null +++ b/src/color_legend.js @@ -0,0 +1,321 @@ +import angular from 'angular'; +import _ from 'lodash'; +import $ from 'jquery'; +import d3 from 'd3'; +import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index'; +import {contextSrv} from 'app/core/core'; +import {tickStep} from 'app/core/utils/ticks'; + +let mod = angular.module('grafana.directives'); + +const MIN_LEGEND_STEPS = 10; + +/** + * Color legend for heatmap editor. + */ +mod.directive('optionsColorLegend', function() { + return { + restrict: 'E', + template: '
', + link: function(scope, elem, attrs) { + let ctrl = scope.ctrl; + let panel = scope.ctrl.panel; + + render(); + + ctrl.events.on('render', function() { + render(); + }); + + function render() { + let legendElem = $(elem).find('svg'); + let legendWidth = Math.floor(legendElem.outerWidth()); + + if (panel.color.mode === 'spectrum') { + let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme}); + let colorScale = getColorScale(colorScheme, legendWidth); + drawSimpleColorLegend(elem, colorScale); + } else if (panel.color.mode === 'opacity') { + let colorOptions = panel.color; + drawSimpleOpacityLegend(elem, colorOptions); + } + } + } + }; +}); + +/** + * Heatmap legend with scale values. + */ +mod.directive('statusHeatmapLegend', function() { + return { + restrict: 'E', + template: '
', + link: function(scope, elem, attrs) { + let ctrl = scope.ctrl; + let panel = scope.ctrl.panel; + + render(); + ctrl.events.on('render', function() { + render(); + }); + + function render() { + clearLegend(elem); + if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) { + let rangeFrom = ctrl.cardsData.minValue; + let rangeTo = ctrl.cardsData.maxValue; + let maxValue = panel.color.max || rangeTo; + let minValue = panel.color.min || rangeFrom; + + if (panel.color.mode === 'spectrum') { + let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme}); + drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue); + } else if (panel.color.mode === 'opacity') { + let colorOptions = panel.color; + drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue); + } + } + } + } + }; +}); + +function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) { + let legendElem = $(elem).find('svg'); + let legend = d3.select(legendElem.get(0)); + clearLegend(elem); + + let legendWidth = Math.floor(legendElem.outerWidth()) - 30; + let legendHeight = legendElem.attr("height"); + + let rangeStep = 1; + if (rangeTo - rangeFrom > legendWidth) { + rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth); + } + + if (rangeStep * MIN_LEGEND_STEPS > rangeTo) { + rangeStep = rangeTo / MIN_LEGEND_STEPS; + } + + let widthFactor = legendWidth / (rangeTo - rangeFrom); + let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); + + let colorScale = getColorScale(colorScheme, maxValue, minValue); + legend.selectAll(".status-heatmap-color-legend-rect") + .data(valuesRange) + .enter().append("rect") + .attr("x", d => d * widthFactor) + .attr("y", 0) + .attr("width", rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps + .attr("height", legendHeight) + .attr("stroke-width", 0) + .attr("fill", d => colorScale(d)); + + drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth); +} + +function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue) { + let legendElem = $(elem).find('svg'); + let legend = d3.select(legendElem.get(0)); + clearLegend(elem); + + let legendWidth = Math.floor(legendElem.outerWidth()) - 30; + let legendHeight = legendElem.attr("height"); + + let rangeStep = 10; + let widthFactor = legendWidth / (rangeTo - rangeFrom); + + if (rangeStep * MIN_LEGEND_STEPS > rangeTo) { + rangeStep = rangeTo / MIN_LEGEND_STEPS; + } + + let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep); + + let opacityScale = getOpacityScale(options, maxValue, minValue); + legend.selectAll(".status-heatmap-opacity-legend-rect") + .data(valuesRange) + .enter().append("rect") + .attr("x", d => d * widthFactor) + .attr("y", 0) + .attr("width", rangeStep * widthFactor) + .attr("height", legendHeight) + .attr("stroke-width", 0) + .attr("fill", options.cardColor) + .style("opacity", d => opacityScale(d)); + + drawLegendValues(elem, opacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth); +} + +function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) { + let legendElem = $(elem).find('svg'); + let legend = d3.select(legendElem.get(0)); + + if (legendWidth <= 0 || legendElem.get(0).childNodes.length === 0) { + return; + } + + let legendValueScale = d3.scaleLinear() + .domain([0, rangeTo]) + .range([0, legendWidth]); + + let ticks = buildLegendTicks(0, rangeTo, maxValue, minValue); + let xAxis = d3.axisBottom(legendValueScale) + .tickValues(ticks) + .tickSize(2); + + let colorRect = legendElem.find(":first-child"); + let posY = getSvgElemHeight(legendElem) + 2; + let posX = getSvgElemX(colorRect); + + d3.select(legendElem.get(0)).append("g") + .attr("class", "axis") + .attr("transform", "translate(" + posX + "," + posY + ")") + .call(xAxis); + + legend.select(".axis").select(".domain").remove(); +} + +function drawSimpleColorLegend(elem, colorScale) { + let legendElem = $(elem).find('svg'); + clearLegend(elem); + + let legendWidth = Math.floor(legendElem.outerWidth()); + let legendHeight = legendElem.attr("height"); + + if (legendWidth) { + let valuesNumber = Math.floor(legendWidth / 2); + let rangeStep = Math.floor(legendWidth / valuesNumber); + let valuesRange = d3.range(0, legendWidth, rangeStep); + + let legend = d3.select(legendElem.get(0)); + var legendRects = legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange); + + legendRects.enter().append("rect") + .attr("x", d => d) + .attr("y", 0) + .attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps + .attr("height", legendHeight) + .attr("stroke-width", 0) + .attr("fill", d => colorScale(d)); + } +} + +function drawSimpleOpacityLegend(elem, options) { + let legendElem = $(elem).find('svg'); + clearLegend(elem); + + let legend = d3.select(legendElem.get(0)); + let legendWidth = Math.floor(legendElem.outerWidth()); + let legendHeight = legendElem.attr("height"); + + if (legendWidth) { + let legendOpacityScale; + if (options.colorScale === 'linear') { + legendOpacityScale = d3.scaleLinear() + .domain([0, legendWidth]) + .range([0, 1]); + } else if (options.colorScale === 'sqrt') { + legendOpacityScale = d3.scalePow().exponent(options.exponent) + .domain([0, legendWidth]) + .range([0, 1]); + } + + let rangeStep = 10; + let valuesRange = d3.range(0, legendWidth, rangeStep); + var legendRects = legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange); + + legendRects.enter().append("rect") + .attr("x", d => d) + .attr("y", 0) + .attr("width", rangeStep) + .attr("height", legendHeight) + .attr("stroke-width", 0) + .attr("fill", options.cardColor) + .style("opacity", d => legendOpacityScale(d)); + } +} + +function clearLegend(elem) { + let legendElem = $(elem).find('svg'); + legendElem.empty(); +} + +function getColorScale(colorScheme, maxValue, minValue = 0) { + let colorInterpolator = d3ScaleChromatic[colorScheme.value]; + let colorScaleInverted = colorScheme.invert === 'always' || + (colorScheme.invert === 'dark' && !contextSrv.user.lightTheme); + + let start = colorScaleInverted ? maxValue : minValue; + let end = colorScaleInverted ? minValue : maxValue; + + return d3.scaleSequential(colorInterpolator).domain([start, end]); +} + +function getOpacityScale(options, maxValue, minValue = 0) { + let legendOpacityScale; + if (options.colorScale === 'linear') { + legendOpacityScale = d3.scaleLinear() + .domain([minValue, maxValue]) + .range([0, 1]); + } else if (options.colorScale === 'sqrt') { + legendOpacityScale = d3.scalePow().exponent(options.exponent) + .domain([minValue, maxValue]) + .range([0, 1]); + } + return legendOpacityScale; +} + +function getSvgElemX(elem) { + let svgElem = elem.get(0); + if (svgElem && svgElem.x && svgElem.x.baseVal) { + return svgElem.x.baseVal.value; + } else { + return 0; + } +} + +function getSvgElemHeight(elem) { + let svgElem = elem.get(0); + if (svgElem && svgElem.height && svgElem.height.baseVal) { + return svgElem.height.baseVal.value; + } else { + return 0; + } +} + +function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) { + let range = rangeTo - rangeFrom; + let tickStepSize = tickStep(rangeFrom, rangeTo, 3); + let ticksNum = Math.round(range / tickStepSize); + let ticks = []; + + for (let i = 0; i < ticksNum; i++) { + let current = tickStepSize * i; + // Add user-defined min and max if it had been set + if (isValueCloseTo(minValue, current, tickStepSize)) { + ticks.push(minValue); + continue; + } else if (minValue < current) { + ticks.push(minValue); + } + if (isValueCloseTo(maxValue, current, tickStepSize)) { + ticks.push(maxValue); + continue; + } else if (maxValue < current) { + ticks.push(maxValue); + } + ticks.push(tickStepSize * i); + } + if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) { + ticks.push(maxValue); + } + ticks.push(rangeTo); + ticks = _.sortBy(_.uniq(ticks)); + return ticks; +} + +function isValueCloseTo(val, valueTo, step) { + let diff = Math.abs(val - valueTo); + return diff < step * 0.3; +} diff --git a/src/css/status-heatmap.scss b/src/css/status-heatmap.scss new file mode 100644 index 0000000..0c22f3f --- /dev/null +++ b/src/css/status-heatmap.scss @@ -0,0 +1,94 @@ +$font-size-sm: 12px !default; + + +.status-heatmap-canvas-wrapper { + // position: relative; + cursor: crosshair; +} + +.status-heatmap-panel { + position: relative; + + .axis .tick { + text { + fill: #52545c; + color: #52545c; + font-size: 11px; + } + + line { + opacity: 0.4; + stroke: #767980; + } + } + + // This hack prevents mouseenter/mouseleave events get fired too often + svg { + pointer-events: none; + + rect { + pointer-events: visiblePainted; + } + } +} + +.status-heatmap-tooltip { + white-space: nowrap; + font-size: $font-size-sm; + background-color: #dde4ed; + color: #52545c; +} + +.status-heatmap-histogram rect { + fill: #767980; +} + +.status-heatmap-crosshair { + line { + stroke: darken(red,15%); + stroke-width: 1; + } +} + +.status-heatmap-selection { + stroke-width: 1; + fill: rgba(102, 102, 102, 0.4); + stroke: rgba(102, 102, 102, 0.8); +} + +.status-heatmap-legend-wrapper { + // @include clearfix(); + margin: 0 10px; + //padding-top: 10px; + + svg { + width: 100%; + max-width: 300px; + height: 24px; + float: left; + white-space: nowrap; + padding-left: 10px; + } + + .status-heatmap-legend-values { + display: inline-block; + } + + .axis .tick { + text { + fill: #52545c; + color: #52545c; + font-size: 11px; + } + + line { + opacity: 0.4; + stroke: #767980; + } + + .domain { + opacity: 0.4; + stroke: #767980; + } + } +} diff --git a/src/img/logo.svg b/src/img/logo.svg new file mode 100644 index 0000000..54e5f5d --- /dev/null +++ b/src/img/logo.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libs/d3-scale-chromatic/categorical/Accent.js b/src/libs/d3-scale-chromatic/categorical/Accent.js new file mode 100644 index 0000000..07ab707 --- /dev/null +++ b/src/libs/d3-scale-chromatic/categorical/Accent.js @@ -0,0 +1,3 @@ +import colors from "../colors"; + +export default colors("7fc97fbeaed4fdc086ffff99386cb0f0027fbf5b17666666"); diff --git a/src/libs/d3-scale-chromatic/categorical/Dark2.js b/src/libs/d3-scale-chromatic/categorical/Dark2.js new file mode 100644 index 0000000..13fc291 --- /dev/null +++ b/src/libs/d3-scale-chromatic/categorical/Dark2.js @@ -0,0 +1,3 @@ +import colors from "../colors"; + +export default colors("1b9e77d95f027570b3e7298a66a61ee6ab02a6761d666666"); diff --git a/src/libs/d3-scale-chromatic/categorical/Paired.js b/src/libs/d3-scale-chromatic/categorical/Paired.js new file mode 100644 index 0000000..30094a0 --- /dev/null +++ b/src/libs/d3-scale-chromatic/categorical/Paired.js @@ -0,0 +1,3 @@ +import colors from "../colors"; + +export default colors("a6cee31f78b4b2df8a33a02cfb9a99e31a1cfdbf6fff7f00cab2d66a3d9affff99b15928"); diff --git a/src/libs/d3-scale-chromatic/categorical/Pastel1.js b/src/libs/d3-scale-chromatic/categorical/Pastel1.js new file mode 100644 index 0000000..4b0e093 --- /dev/null +++ b/src/libs/d3-scale-chromatic/categorical/Pastel1.js @@ -0,0 +1,3 @@ +import colors from "../colors"; + +export default colors("fbb4aeb3cde3ccebc5decbe4fed9a6ffffcce5d8bdfddaecf2f2f2"); diff --git a/src/libs/d3-scale-chromatic/categorical/Pastel2.js b/src/libs/d3-scale-chromatic/categorical/Pastel2.js new file mode 100644 index 0000000..520a5a3 --- /dev/null +++ b/src/libs/d3-scale-chromatic/categorical/Pastel2.js @@ -0,0 +1,3 @@ +import colors from "../colors"; + +export default colors("b3e2cdfdcdaccbd5e8f4cae4e6f5c9fff2aef1e2cccccccc"); diff --git a/src/libs/d3-scale-chromatic/categorical/Set1.js b/src/libs/d3-scale-chromatic/categorical/Set1.js new file mode 100644 index 0000000..8c34c1b --- /dev/null +++ b/src/libs/d3-scale-chromatic/categorical/Set1.js @@ -0,0 +1,3 @@ +import colors from "../colors"; + +export default colors("e41a1c377eb84daf4a984ea3ff7f00ffff33a65628f781bf999999"); diff --git a/src/libs/d3-scale-chromatic/categorical/Set2.js b/src/libs/d3-scale-chromatic/categorical/Set2.js new file mode 100644 index 0000000..e6bad53 --- /dev/null +++ b/src/libs/d3-scale-chromatic/categorical/Set2.js @@ -0,0 +1,3 @@ +import colors from "../colors"; + +export default colors("66c2a5fc8d628da0cbe78ac3a6d854ffd92fe5c494b3b3b3"); diff --git a/src/libs/d3-scale-chromatic/categorical/Set3.js b/src/libs/d3-scale-chromatic/categorical/Set3.js new file mode 100644 index 0000000..8eeeb04 --- /dev/null +++ b/src/libs/d3-scale-chromatic/categorical/Set3.js @@ -0,0 +1,3 @@ +import colors from "../colors"; + +export default colors("8dd3c7ffffb3bebadafb807280b1d3fdb462b3de69fccde5d9d9d9bc80bdccebc5ffed6f"); diff --git a/src/libs/d3-scale-chromatic/colors.js b/src/libs/d3-scale-chromatic/colors.js new file mode 100644 index 0000000..aeedad5 --- /dev/null +++ b/src/libs/d3-scale-chromatic/colors.js @@ -0,0 +1,5 @@ +export default function(specifier) { + var n = specifier.length / 6 | 0, colors = new Array(n), i = 0; + while (i < n) colors[i] = "#" + specifier.slice(i * 6, ++i * 6); + return colors; +} diff --git a/src/libs/d3-scale-chromatic/diverging/BrBG.js b/src/libs/d3-scale-chromatic/diverging/BrBG.js new file mode 100644 index 0000000..5e27ee6 --- /dev/null +++ b/src/libs/d3-scale-chromatic/diverging/BrBG.js @@ -0,0 +1,16 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "d8b365f5f5f55ab4ac", + "a6611adfc27d80cdc1018571", + "a6611adfc27df5f5f580cdc1018571", + "8c510ad8b365f6e8c3c7eae55ab4ac01665e", + "8c510ad8b365f6e8c3f5f5f5c7eae55ab4ac01665e", + "8c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e", + "8c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e", + "5430058c510abf812ddfc27df6e8c3c7eae580cdc135978f01665e003c30", + "5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/diverging/GnYlRd.js b/src/libs/d3-scale-chromatic/diverging/GnYlRd.js new file mode 100644 index 0000000..2857b2b --- /dev/null +++ b/src/libs/d3-scale-chromatic/diverging/GnYlRd.js @@ -0,0 +1,16 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "91cf60ffffbffc8d59", + "1a9641a6d96afdae61d7191c", + "1a9641a6d96affffbffdae61d7191c", + "1a985091cf60d9ef8bfee08bfc8d59d73027", + "1a985091cf60d9ef8bffffbffee08bfc8d59d73027", + "1a985066bd63a6d96ad9ef8bfee08bfdae61f46d43d73027", + "1a985066bd63a6d96ad9ef8bffffbffee08bfdae61f46d43d73027", + "0068371a985066bd63a6d96ad9ef8bfee08bfdae61f46d43d73027a50026", + "0068371a985066bd63a6d96ad9ef8bffffbffee08bfdae61f46d43d73027a50026" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/diverging/PRGn.js b/src/libs/d3-scale-chromatic/diverging/PRGn.js new file mode 100644 index 0000000..3be94f3 --- /dev/null +++ b/src/libs/d3-scale-chromatic/diverging/PRGn.js @@ -0,0 +1,16 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "af8dc3f7f7f77fbf7b", + "7b3294c2a5cfa6dba0008837", + "7b3294c2a5cff7f7f7a6dba0008837", + "762a83af8dc3e7d4e8d9f0d37fbf7b1b7837", + "762a83af8dc3e7d4e8f7f7f7d9f0d37fbf7b1b7837", + "762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b7837", + "762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b7837", + "40004b762a839970abc2a5cfe7d4e8d9f0d3a6dba05aae611b783700441b", + "40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/diverging/PiYG.js b/src/libs/d3-scale-chromatic/diverging/PiYG.js new file mode 100644 index 0000000..1d57c4f --- /dev/null +++ b/src/libs/d3-scale-chromatic/diverging/PiYG.js @@ -0,0 +1,16 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "e9a3c9f7f7f7a1d76a", + "d01c8bf1b6dab8e1864dac26", + "d01c8bf1b6daf7f7f7b8e1864dac26", + "c51b7de9a3c9fde0efe6f5d0a1d76a4d9221", + "c51b7de9a3c9fde0eff7f7f7e6f5d0a1d76a4d9221", + "c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221", + "c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221", + "8e0152c51b7dde77aef1b6dafde0efe6f5d0b8e1867fbc414d9221276419", + "8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/diverging/PuOr.js b/src/libs/d3-scale-chromatic/diverging/PuOr.js new file mode 100644 index 0000000..924f5cb --- /dev/null +++ b/src/libs/d3-scale-chromatic/diverging/PuOr.js @@ -0,0 +1,16 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "998ec3f7f7f7f1a340", + "5e3c99b2abd2fdb863e66101", + "5e3c99b2abd2f7f7f7fdb863e66101", + "542788998ec3d8daebfee0b6f1a340b35806", + "542788998ec3d8daebf7f7f7fee0b6f1a340b35806", + "5427888073acb2abd2d8daebfee0b6fdb863e08214b35806", + "5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b35806", + "2d004b5427888073acb2abd2d8daebfee0b6fdb863e08214b358067f3b08", + "2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/diverging/RdBu.js b/src/libs/d3-scale-chromatic/diverging/RdBu.js new file mode 100644 index 0000000..f9ba196 --- /dev/null +++ b/src/libs/d3-scale-chromatic/diverging/RdBu.js @@ -0,0 +1,16 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "ef8a62f7f7f767a9cf", + "ca0020f4a58292c5de0571b0", + "ca0020f4a582f7f7f792c5de0571b0", + "b2182bef8a62fddbc7d1e5f067a9cf2166ac", + "b2182bef8a62fddbc7f7f7f7d1e5f067a9cf2166ac", + "b2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac", + "b2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac", + "67001fb2182bd6604df4a582fddbc7d1e5f092c5de4393c32166ac053061", + "67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/diverging/RdGy.js b/src/libs/d3-scale-chromatic/diverging/RdGy.js new file mode 100644 index 0000000..9d1a4a1 --- /dev/null +++ b/src/libs/d3-scale-chromatic/diverging/RdGy.js @@ -0,0 +1,16 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "ef8a62ffffff999999", + "ca0020f4a582bababa404040", + "ca0020f4a582ffffffbababa404040", + "b2182bef8a62fddbc7e0e0e09999994d4d4d", + "b2182bef8a62fddbc7ffffffe0e0e09999994d4d4d", + "b2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d", + "b2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d", + "67001fb2182bd6604df4a582fddbc7e0e0e0bababa8787874d4d4d1a1a1a", + "67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/diverging/RdYlBu.js b/src/libs/d3-scale-chromatic/diverging/RdYlBu.js new file mode 100644 index 0000000..ef93e9d --- /dev/null +++ b/src/libs/d3-scale-chromatic/diverging/RdYlBu.js @@ -0,0 +1,16 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "fc8d59ffffbf91bfdb", + "d7191cfdae61abd9e92c7bb6", + "d7191cfdae61ffffbfabd9e92c7bb6", + "d73027fc8d59fee090e0f3f891bfdb4575b4", + "d73027fc8d59fee090ffffbfe0f3f891bfdb4575b4", + "d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4", + "d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4", + "a50026d73027f46d43fdae61fee090e0f3f8abd9e974add14575b4313695", + "a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/diverging/RdYlGn.js b/src/libs/d3-scale-chromatic/diverging/RdYlGn.js new file mode 100644 index 0000000..2e3964d --- /dev/null +++ b/src/libs/d3-scale-chromatic/diverging/RdYlGn.js @@ -0,0 +1,16 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "fc8d59ffffbf91cf60", + "d7191cfdae61a6d96a1a9641", + "d7191cfdae61ffffbfa6d96a1a9641", + "d73027fc8d59fee08bd9ef8b91cf601a9850", + "d73027fc8d59fee08bffffbfd9ef8b91cf601a9850", + "d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850", + "d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850", + "a50026d73027f46d43fdae61fee08bd9ef8ba6d96a66bd631a9850006837", + "a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/diverging/Spectral.js b/src/libs/d3-scale-chromatic/diverging/Spectral.js new file mode 100644 index 0000000..0133e0a --- /dev/null +++ b/src/libs/d3-scale-chromatic/diverging/Spectral.js @@ -0,0 +1,16 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "fc8d59ffffbf99d594", + "d7191cfdae61abdda42b83ba", + "d7191cfdae61ffffbfabdda42b83ba", + "d53e4ffc8d59fee08be6f59899d5943288bd", + "d53e4ffc8d59fee08bffffbfe6f59899d5943288bd", + "d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd", + "d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd", + "9e0142d53e4ff46d43fdae61fee08be6f598abdda466c2a53288bd5e4fa2", + "9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/index.js b/src/libs/d3-scale-chromatic/index.js new file mode 100644 index 0000000..e752dc4 --- /dev/null +++ b/src/libs/d3-scale-chromatic/index.js @@ -0,0 +1,36 @@ +export {default as schemeAccent} from "./categorical/Accent"; +export {default as schemeDark2} from "./categorical/Dark2"; +export {default as schemePaired} from "./categorical/Paired"; +export {default as schemePastel1} from "./categorical/Pastel1"; +export {default as schemePastel2} from "./categorical/Pastel2"; +export {default as schemeSet1} from "./categorical/Set1"; +export {default as schemeSet2} from "./categorical/Set2"; +export {default as schemeSet3} from "./categorical/Set3"; +export {default as interpolateBrBG, scheme as schemeBrBG} from "./diverging/BrBG"; +export {default as interpolatePRGn, scheme as schemePRGn} from "./diverging/PRGn"; +export {default as interpolatePiYG, scheme as schemePiYG} from "./diverging/PiYG"; +export {default as interpolatePuOr, scheme as schemePuOr} from "./diverging/PuOr"; +export {default as interpolateRdBu, scheme as schemeRdBu} from "./diverging/RdBu"; +export {default as interpolateRdGy, scheme as schemeRdGy} from "./diverging/RdGy"; +export {default as interpolateRdYlBu, scheme as schemeRdYlBu} from "./diverging/RdYlBu"; +export {default as interpolateRdYlGn, scheme as schemeRdYlGn} from "./diverging/RdYlGn"; +export {default as interpolateGnYlRd, scheme as schemeGnYlRd} from "./diverging/GnYlRd"; +export {default as interpolateSpectral, scheme as schemeSpectral} from "./diverging/Spectral"; +export {default as interpolateBuGn, scheme as schemeBuGn} from "./sequential-multi/BuGn"; +export {default as interpolateBuPu, scheme as schemeBuPu} from "./sequential-multi/BuPu"; +export {default as interpolateGnBu, scheme as schemeGnBu} from "./sequential-multi/GnBu"; +export {default as interpolateOrRd, scheme as schemeOrRd} from "./sequential-multi/OrRd"; +export {default as interpolatePuBuGn, scheme as schemePuBuGn} from "./sequential-multi/PuBuGn"; +export {default as interpolatePuBu, scheme as schemePuBu} from "./sequential-multi/PuBu"; +export {default as interpolatePuRd, scheme as schemePuRd} from "./sequential-multi/PuRd"; +export {default as interpolateRdPu, scheme as schemeRdPu} from "./sequential-multi/RdPu"; +export {default as interpolateYlGnBu, scheme as schemeYlGnBu} from "./sequential-multi/YlGnBu"; +export {default as interpolateYlGn, scheme as schemeYlGn} from "./sequential-multi/YlGn"; +export {default as interpolateYlOrBr, scheme as schemeYlOrBr} from "./sequential-multi/YlOrBr"; +export {default as interpolateYlOrRd, scheme as schemeYlOrRd} from "./sequential-multi/YlOrRd"; +export {default as interpolateBlues, scheme as schemeBlues} from "./sequential-single/Blues"; +export {default as interpolateGreens, scheme as schemeGreens} from "./sequential-single/Greens"; +export {default as interpolateGreys, scheme as schemeGreys} from "./sequential-single/Greys"; +export {default as interpolatePurples, scheme as schemePurples} from "./sequential-single/Purples"; +export {default as interpolateReds, scheme as schemeReds} from "./sequential-single/Reds"; +export {default as interpolateOranges, scheme as schemeOranges} from "./sequential-single/Oranges"; diff --git a/src/libs/d3-scale-chromatic/ramp.js b/src/libs/d3-scale-chromatic/ramp.js new file mode 100644 index 0000000..f138a9b --- /dev/null +++ b/src/libs/d3-scale-chromatic/ramp.js @@ -0,0 +1,5 @@ +import d3 from 'd3'; + +export default function(scheme) { + return d3.interpolateRgbBasis(scheme[scheme.length - 1]); +} diff --git a/src/libs/d3-scale-chromatic/sequential-multi/BuGn.js b/src/libs/d3-scale-chromatic/sequential-multi/BuGn.js new file mode 100644 index 0000000..e7dade7 --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-multi/BuGn.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "e5f5f999d8c92ca25f", + "edf8fbb2e2e266c2a4238b45", + "edf8fbb2e2e266c2a42ca25f006d2c", + "edf8fbccece699d8c966c2a42ca25f006d2c", + "edf8fbccece699d8c966c2a441ae76238b45005824", + "f7fcfde5f5f9ccece699d8c966c2a441ae76238b45005824", + "f7fcfde5f5f9ccece699d8c966c2a441ae76238b45006d2c00441b" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-multi/BuPu.js b/src/libs/d3-scale-chromatic/sequential-multi/BuPu.js new file mode 100644 index 0000000..8aa9232 --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-multi/BuPu.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "e0ecf49ebcda8856a7", + "edf8fbb3cde38c96c688419d", + "edf8fbb3cde38c96c68856a7810f7c", + "edf8fbbfd3e69ebcda8c96c68856a7810f7c", + "edf8fbbfd3e69ebcda8c96c68c6bb188419d6e016b", + "f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d6e016b", + "f7fcfde0ecf4bfd3e69ebcda8c96c68c6bb188419d810f7c4d004b" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-multi/GnBu.js b/src/libs/d3-scale-chromatic/sequential-multi/GnBu.js new file mode 100644 index 0000000..c8dae65 --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-multi/GnBu.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "e0f3dba8ddb543a2ca", + "f0f9e8bae4bc7bccc42b8cbe", + "f0f9e8bae4bc7bccc443a2ca0868ac", + "f0f9e8ccebc5a8ddb57bccc443a2ca0868ac", + "f0f9e8ccebc5a8ddb57bccc44eb3d32b8cbe08589e", + "f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe08589e", + "f7fcf0e0f3dbccebc5a8ddb57bccc44eb3d32b8cbe0868ac084081" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-multi/OrRd.js b/src/libs/d3-scale-chromatic/sequential-multi/OrRd.js new file mode 100644 index 0000000..3f96f7d --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-multi/OrRd.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "fee8c8fdbb84e34a33", + "fef0d9fdcc8afc8d59d7301f", + "fef0d9fdcc8afc8d59e34a33b30000", + "fef0d9fdd49efdbb84fc8d59e34a33b30000", + "fef0d9fdd49efdbb84fc8d59ef6548d7301f990000", + "fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301f990000", + "fff7ecfee8c8fdd49efdbb84fc8d59ef6548d7301fb300007f0000" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-multi/PuBu.js b/src/libs/d3-scale-chromatic/sequential-multi/PuBu.js new file mode 100644 index 0000000..c279e48 --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-multi/PuBu.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "ece7f2a6bddb2b8cbe", + "f1eef6bdc9e174a9cf0570b0", + "f1eef6bdc9e174a9cf2b8cbe045a8d", + "f1eef6d0d1e6a6bddb74a9cf2b8cbe045a8d", + "f1eef6d0d1e6a6bddb74a9cf3690c00570b0034e7b", + "fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0034e7b", + "fff7fbece7f2d0d1e6a6bddb74a9cf3690c00570b0045a8d023858" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-multi/PuBuGn.js b/src/libs/d3-scale-chromatic/sequential-multi/PuBuGn.js new file mode 100644 index 0000000..f6d8fe0 --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-multi/PuBuGn.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "ece2f0a6bddb1c9099", + "f6eff7bdc9e167a9cf02818a", + "f6eff7bdc9e167a9cf1c9099016c59", + "f6eff7d0d1e6a6bddb67a9cf1c9099016c59", + "f6eff7d0d1e6a6bddb67a9cf3690c002818a016450", + "fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016450", + "fff7fbece2f0d0d1e6a6bddb67a9cf3690c002818a016c59014636" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-multi/PuRd.js b/src/libs/d3-scale-chromatic/sequential-multi/PuRd.js new file mode 100644 index 0000000..fe762d2 --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-multi/PuRd.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "e7e1efc994c7dd1c77", + "f1eef6d7b5d8df65b0ce1256", + "f1eef6d7b5d8df65b0dd1c77980043", + "f1eef6d4b9dac994c7df65b0dd1c77980043", + "f1eef6d4b9dac994c7df65b0e7298ace125691003f", + "f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125691003f", + "f7f4f9e7e1efd4b9dac994c7df65b0e7298ace125698004367001f" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-multi/RdPu.js b/src/libs/d3-scale-chromatic/sequential-multi/RdPu.js new file mode 100644 index 0000000..748939c --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-multi/RdPu.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "fde0ddfa9fb5c51b8a", + "feebe2fbb4b9f768a1ae017e", + "feebe2fbb4b9f768a1c51b8a7a0177", + "feebe2fcc5c0fa9fb5f768a1c51b8a7a0177", + "feebe2fcc5c0fa9fb5f768a1dd3497ae017e7a0177", + "fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a0177", + "fff7f3fde0ddfcc5c0fa9fb5f768a1dd3497ae017e7a017749006a" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-multi/YlGn.js b/src/libs/d3-scale-chromatic/sequential-multi/YlGn.js new file mode 100644 index 0000000..aaacd6c --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-multi/YlGn.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "f7fcb9addd8e31a354", + "ffffccc2e69978c679238443", + "ffffccc2e69978c67931a354006837", + "ffffccd9f0a3addd8e78c67931a354006837", + "ffffccd9f0a3addd8e78c67941ab5d238443005a32", + "ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443005a32", + "ffffe5f7fcb9d9f0a3addd8e78c67941ab5d238443006837004529" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-multi/YlGnBu.js b/src/libs/d3-scale-chromatic/sequential-multi/YlGnBu.js new file mode 100644 index 0000000..eb009ef --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-multi/YlGnBu.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "edf8b17fcdbb2c7fb8", + "ffffcca1dab441b6c4225ea8", + "ffffcca1dab441b6c42c7fb8253494", + "ffffccc7e9b47fcdbb41b6c42c7fb8253494", + "ffffccc7e9b47fcdbb41b6c41d91c0225ea80c2c84", + "ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea80c2c84", + "ffffd9edf8b1c7e9b47fcdbb41b6c41d91c0225ea8253494081d58" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-multi/YlOrBr.js b/src/libs/d3-scale-chromatic/sequential-multi/YlOrBr.js new file mode 100644 index 0000000..2fc1aa1 --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-multi/YlOrBr.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "fff7bcfec44fd95f0e", + "ffffd4fed98efe9929cc4c02", + "ffffd4fed98efe9929d95f0e993404", + "ffffd4fee391fec44ffe9929d95f0e993404", + "ffffd4fee391fec44ffe9929ec7014cc4c028c2d04", + "ffffe5fff7bcfee391fec44ffe9929ec7014cc4c028c2d04", + "ffffe5fff7bcfee391fec44ffe9929ec7014cc4c02993404662506" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-multi/YlOrRd.js b/src/libs/d3-scale-chromatic/sequential-multi/YlOrRd.js new file mode 100644 index 0000000..d34cca6 --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-multi/YlOrRd.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "ffeda0feb24cf03b20", + "ffffb2fecc5cfd8d3ce31a1c", + "ffffb2fecc5cfd8d3cf03b20bd0026", + "ffffb2fed976feb24cfd8d3cf03b20bd0026", + "ffffb2fed976feb24cfd8d3cfc4e2ae31a1cb10026", + "ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cb10026", + "ffffccffeda0fed976feb24cfd8d3cfc4e2ae31a1cbd0026800026" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-single/Blues.js b/src/libs/d3-scale-chromatic/sequential-single/Blues.js new file mode 100644 index 0000000..cb36bf7 --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-single/Blues.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "deebf79ecae13182bd", + "eff3ffbdd7e76baed62171b5", + "eff3ffbdd7e76baed63182bd08519c", + "eff3ffc6dbef9ecae16baed63182bd08519c", + "eff3ffc6dbef9ecae16baed64292c62171b5084594", + "f7fbffdeebf7c6dbef9ecae16baed64292c62171b5084594", + "f7fbffdeebf7c6dbef9ecae16baed64292c62171b508519c08306b" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-single/Greens.js b/src/libs/d3-scale-chromatic/sequential-single/Greens.js new file mode 100644 index 0000000..074f5bf --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-single/Greens.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "e5f5e0a1d99b31a354", + "edf8e9bae4b374c476238b45", + "edf8e9bae4b374c47631a354006d2c", + "edf8e9c7e9c0a1d99b74c47631a354006d2c", + "edf8e9c7e9c0a1d99b74c47641ab5d238b45005a32", + "f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45005a32", + "f7fcf5e5f5e0c7e9c0a1d99b74c47641ab5d238b45006d2c00441b" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-single/Greys.js b/src/libs/d3-scale-chromatic/sequential-single/Greys.js new file mode 100644 index 0000000..ae5a0b4 --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-single/Greys.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "f0f0f0bdbdbd636363", + "f7f7f7cccccc969696525252", + "f7f7f7cccccc969696636363252525", + "f7f7f7d9d9d9bdbdbd969696636363252525", + "f7f7f7d9d9d9bdbdbd969696737373525252252525", + "fffffff0f0f0d9d9d9bdbdbd969696737373525252252525", + "fffffff0f0f0d9d9d9bdbdbd969696737373525252252525000000" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-single/Oranges.js b/src/libs/d3-scale-chromatic/sequential-single/Oranges.js new file mode 100644 index 0000000..a4babe1 --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-single/Oranges.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "fee6cefdae6be6550d", + "feeddefdbe85fd8d3cd94701", + "feeddefdbe85fd8d3ce6550da63603", + "feeddefdd0a2fdae6bfd8d3ce6550da63603", + "feeddefdd0a2fdae6bfd8d3cf16913d948018c2d04", + "fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d948018c2d04", + "fff5ebfee6cefdd0a2fdae6bfd8d3cf16913d94801a636037f2704" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-single/Purples.js b/src/libs/d3-scale-chromatic/sequential-single/Purples.js new file mode 100644 index 0000000..d9810e1 --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-single/Purples.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "efedf5bcbddc756bb1", + "f2f0f7cbc9e29e9ac86a51a3", + "f2f0f7cbc9e29e9ac8756bb154278f", + "f2f0f7dadaebbcbddc9e9ac8756bb154278f", + "f2f0f7dadaebbcbddc9e9ac8807dba6a51a34a1486", + "fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a34a1486", + "fcfbfdefedf5dadaebbcbddc9e9ac8807dba6a51a354278f3f007d" +).map(colors); + +export default ramp(scheme); diff --git a/src/libs/d3-scale-chromatic/sequential-single/Reds.js b/src/libs/d3-scale-chromatic/sequential-single/Reds.js new file mode 100644 index 0000000..447084c --- /dev/null +++ b/src/libs/d3-scale-chromatic/sequential-single/Reds.js @@ -0,0 +1,14 @@ +import colors from "../colors"; +import ramp from "../ramp"; + +export var scheme = new Array(3).concat( + "fee0d2fc9272de2d26", + "fee5d9fcae91fb6a4acb181d", + "fee5d9fcae91fb6a4ade2d26a50f15", + "fee5d9fcbba1fc9272fb6a4ade2d26a50f15", + "fee5d9fcbba1fc9272fb6a4aef3b2ccb181d99000d", + "fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181d99000d", + "fff5f0fee0d2fcbba1fc9272fb6a4aef3b2ccb181da50f1567000d" +).map(colors); + +export default ramp(scheme); diff --git a/src/module.html b/src/module.html new file mode 100644 index 0000000..8b2c23c --- /dev/null +++ b/src/module.html @@ -0,0 +1,14 @@ +
+
+ +
+ {{ctrl.dataWarning.title}} +
+ +
+
+
+ +
+
+
diff --git a/src/module.js b/src/module.js new file mode 100644 index 0000000..9b47ac0 --- /dev/null +++ b/src/module.js @@ -0,0 +1,6 @@ +import './color_legend'; +import { StatusHeatmapCtrl } from './status_heatmap_ctrl'; + +export { + StatusHeatmapCtrl as PanelCtrl +}; diff --git a/src/options_editor.js b/src/options_editor.js new file mode 100644 index 0000000..c3d6128 --- /dev/null +++ b/src/options_editor.js @@ -0,0 +1,27 @@ +import kbn from 'app/core/utils/kbn'; + +export class StatusHeatmapOptionsEditorCtrl { + constructor($scope) { + $scope.editor = this; + this.panelCtrl = $scope.ctrl; + this.panel = this.panelCtrl.panel; + this.unitFormats = kbn.getUnitFormats(); + + this.panelCtrl.render(); + } + + setUnitFormat(subItem) { + this.panel.data.unitFormat = subItem.value; + this.panelCtrl.render(); + } +} + +export function statusHeatmapOptionsEditor() { + 'use strict'; + return { + restrict: 'E', + scope: true, + templateUrl: 'public/plugins/status-heatmap-panel/partials/options_editor.html', + controller: StatusHeatmapOptionsEditorCtrl, + }; +} diff --git a/src/partials/options_editor.html b/src/partials/options_editor.html new file mode 100644 index 0000000..b9ea9e3 --- /dev/null +++ b/src/partials/options_editor.html @@ -0,0 +1,99 @@ +
+
+
Colors
+
+ +
+ +
+
+ +
+
+ + + + +
+
+ +
+ +
+
+
+ + +
+
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
Color scale
+
+ + +
+
+ + +
+
+ +
+
Legend
+ + +
+ +
+
Buckets
+
+ + +
+
+ + +
+
+ +
+
Tooltip
+ + +
+ + +
+
+ +
+
Null value
+
+ +
+ +
+
+
+
diff --git a/src/plugin.json b/src/plugin.json new file mode 100644 index 0000000..91a8b88 --- /dev/null +++ b/src/plugin.json @@ -0,0 +1,25 @@ +{ + "type": "panel", + "name": "Status Heatmap", + "id": "status-heatmap-panel", + + "info": { + "description": "Status Heatmap panel plugin for grafana", + "author": { + "name": "Flant JSC", + "url": "http://flant.com" + }, + "keywords": ["status", "heatmap", "panel"], + "version": "0.0.1", + "updated": "2017-11-01", + "logos": { + "small": "img/logo.svg", + "large": "img/logo.svg" + } + }, + + "dependencies": { + "grafanaVersion": "5.1.x", + "plugins": [ ] + } +} diff --git a/src/rendering.js b/src/rendering.js new file mode 100644 index 0000000..bbb8d8b --- /dev/null +++ b/src/rendering.js @@ -0,0 +1,620 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import moment from 'moment'; +import kbn from 'app/core/utils/kbn'; +import {appEvents, contextSrv} from 'app/core/core'; +import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks'; +import d3 from 'd3'; +import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index'; +import {StatusHeatmapTooltip} from './tooltip'; + +let MIN_CARD_SIZE = 1, + CARD_PADDING = 1, + CARD_ROUND = 0, + DATA_RANGE_WIDING_FACTOR = 1.2, + DEFAULT_X_TICK_SIZE_PX = 100, + DEFAULT_Y_TICK_SIZE_PX = 50, + X_AXIS_TICK_PADDING = 10, + Y_AXIS_TICK_PADDING = 5, + MIN_SELECTION_WIDTH = 2; + +export default function link(scope, elem, attrs, ctrl) { + let data, cardsData, timeRange, panel, heatmap; + + // $heatmap is JQuery object, but heatmap is D3 + let $heatmap = elem.find('.status-heatmap-panel'); + let tooltip = new StatusHeatmapTooltip($heatmap, scope); + + let width, height, + yScale, xScale, + chartWidth, chartHeight, + chartTop, chartBottom, + yAxisWidth, xAxisHeight, + cardPadding, cardRound, + cardWidth, cardHeight, + colorScale, opacityScale, + mouseUpHandler; + + let yOffset = 0; + + let selection = { + active: false, + x1: -1, + x2: -1 + }; + + let padding = {left: 0, right: 0, top: 0, bottom: 0}, + margin = {left: 25, right: 15, top: 10, bottom: 20}, + dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR; + + ctrl.events.on('render', () => { + render(); + // ctrl.renderingCompleted(); + }); + + function setElementHeight() { + try { + var height = ctrl.height || panel.height || ctrl.row.height; + if (_.isString(height)) { + height = parseInt(height.replace('px', ''), 10); + } + + height -= panel.legend.show ? 28 : 11; // bottom padding and space for legend + + $heatmap.css('height', height + 'px'); + + return true; + } catch (e) { // IE throws errors sometimes + return false; + } + } + + function getYAxisWidth(elem) { + let axis_text = elem.selectAll(".axis-y text").nodes(); + let max_text_width = _.max(_.map(axis_text, text => { + // Use SVG getBBox method + return text.getBBox().width; + })); + + return max_text_width; + } + + function getXAxisHeight(elem) { + let axis_line = elem.select(".axis-x line"); + if (!axis_line.empty()) { + let axis_line_position = parseFloat(elem.select(".axis-x line").attr("y2")); + let canvas_width = parseFloat(elem.attr("height")); + return canvas_width - axis_line_position; + } else { + // Default height + return 30; + } + } + + function addXAxis() { + scope.xScale = xScale = d3.scaleTime() + .domain([timeRange.from, timeRange.to]) + .range([0, chartWidth]); + + let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX; + let grafanaTimeFormatter = grafanaTimeFormat(ticks, timeRange.from, timeRange.to); + let timeFormat; + let dashboardTimeZone = ctrl.dashboard.getTimezone(); + if (dashboardTimeZone === 'utc') { + timeFormat = d3.utcFormat(grafanaTimeFormatter); + } else { + timeFormat = d3.timeFormat(grafanaTimeFormatter); + } + + let xAxis = d3.axisBottom(xScale) + .ticks(ticks) + .tickFormat(timeFormat) + .tickPadding(X_AXIS_TICK_PADDING) + .tickSize(chartHeight); + + let posY = chartTop; + let posX = yAxisWidth; + + heatmap.append("g") + .attr("class", "axis axis-x") + .attr("transform", "translate(" + posX + "," + posY + ")") + .call(xAxis); + + // Remove horizontal line in the top of axis labels (called domain in d3) + heatmap.select(".axis-x").select(".domain").remove(); + } + + function addYAxis() { + let ticks = _.map(data, d => d.target); + + // Set default Y min and max if no data + if (_.isEmpty(data)) { + ticks = ['']; + } + + let range = []; + let step = chartHeight / ticks.length; + range.push(chartHeight - yOffset); + for (let i = 1; i < ticks.length; i++) { + range.push(chartHeight - step * i - yOffset); + } + + console.log('yRange', range, yOffset); + + scope.yScale = yScale = d3.scaleOrdinal() + .domain(ticks) + .range(range); + + let yAxis = d3.axisLeft(yScale) + .tickValues(ticks) + .tickSizeInner(0 - width) + .tickPadding(Y_AXIS_TICK_PADDING); + + heatmap.append("g") + .attr("class", "axis axis-y") + .call(yAxis); + + // Calculate Y axis width first, then move axis into visible area + let posY = margin.top; + let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; + heatmap.select(".axis-y").attr("transform", "translate(" + posX + "," + posY + ")"); + + // Remove vertical line in the right of axis labels (called domain in d3) + heatmap.select(".axis-y").select(".domain").remove(); + heatmap.select(".axis-y").selectAll(".tick line").remove(); + } + + // Wide Y values range and anjust to bucket size + function wideYAxisRange(min, max, tickInterval) { + let y_widing = (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) / 2; + let y_min, y_max; + + if (tickInterval === 0) { + y_max = max * dataRangeWidingFactor; + y_min = min - min * (dataRangeWidingFactor - 1); + tickInterval = (y_max - y_min) / 2; + } else { + y_max = Math.ceil((max + y_widing) / tickInterval) * tickInterval; + y_min = Math.floor((min - y_widing) / tickInterval) * tickInterval; + } + + // Don't wide axis below 0 if all values are positive + if (min >= 0 && y_min < 0) { + y_min = 0; + } + + return {y_min, y_max}; + } + + function tickValueFormatter(decimals, scaledDecimals = null) { + let format = panel.yAxis.format; + return function(value) { + return kbn.valueFormats[format](value, decimals, scaledDecimals); + }; + } + + function fixYAxisTickSize() { + heatmap.select(".axis-y") + .selectAll(".tick line") + .attr("x2", chartWidth); + } + + function addAxes() { + addYAxis(); + + yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING; + chartWidth = width - yAxisWidth - margin.right; + // fixYAxisTickSize(); + // + addXAxis(); + xAxisHeight = getXAxisHeight(heatmap); + + if (!panel.yAxis.show) { + heatmap.select(".axis-y").selectAll("line").style("opacity", 0); + } + + if (!panel.xAxis.show) { + heatmap.select(".axis-x").selectAll("line").style("opacity", 0); + } + } + + function addHeatmapCanvas() { + let heatmap_elem = $heatmap[0]; + + width = Math.floor($heatmap.width()) - padding.right; + height = Math.floor($heatmap.height()) - padding.bottom; + + chartHeight = height - margin.top - margin.bottom - yOffset; + chartTop = margin.top; + chartBottom = chartTop + chartHeight; + + cardPadding = panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING; + cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND; + + if (heatmap) { + heatmap.remove(); + } + + heatmap = d3.select(heatmap_elem) + .append("svg") + .attr("width", width) + .attr("height", height); + } + + function addHeatmap() { + addHeatmapCanvas(); + + let maxValue = panel.color.max || cardsData.maxValue; + let minValue = panel.color.min || cardsData.minValue; + + colorScale = getColorScale(maxValue, minValue); + setOpacityScale(maxValue); + setCardSize(); + + addAxes(); + + let cards = heatmap.selectAll(".status-heatmap-card").data(cardsData.cards); + cards.append("title"); + cards = cards.enter().append("rect") + .attr("value", c => c.value) + .attr("xVal", c => c.x) + .attr("x", getCardX) + .attr("width", getCardWidth) + .attr("yVal", c => c.y) + .attr("y", getCardY) + .attr("height", getCardHeight) + .attr("rx", cardRound) + .attr("ry", cardRound) + .attr("class", "bordered status-heatmap-card") + .style("fill", getCardColor) + .style("stroke", getCardColor) + .style("stroke-width", 0) + .style("opacity", getCardOpacity); + + let $cards = $heatmap.find(".status-heatmap-card"); + $cards.on("mouseenter", (event) => { + tooltip.mouseOverBucket = true; + highlightCard(event); + let current_card = d3.select(event.target); + tooltip.show(event, current_card.attr('xVal'), current_card.attr('yVal'), current_card.attr('value')); + }) + .on("mouseleave", (event) => { + tooltip.mouseOverBucket = false; + resetCardHighLight(event); + }); + } + + function highlightCard(event) { + let color = d3.select(event.target).style("fill"); + let highlightColor = d3.color(color).darker(2); + let strokeColor = d3.color(color).brighter(4); + let current_card = d3.select(event.target); + tooltip.originalFillColor = color; + current_card.style("fill", highlightColor) + .style("stroke", strokeColor) + .style("stroke-width", 1); + } + + function resetCardHighLight(event) { + d3.select(event.target).style("fill", tooltip.originalFillColor) + .style("stroke", tooltip.originalFillColor) + .style("stroke-width", 0); + } + + function getColorScale(maxValue, minValue = 0) { + let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme}); + let colorInterpolator = d3ScaleChromatic[colorScheme.value]; + let colorScaleInverted = colorScheme.invert === 'always' || + (colorScheme.invert === 'dark' && !contextSrv.user.lightTheme); + + if (maxValue == minValue) + maxValue = minValue + 1; + + let start = colorScaleInverted ? maxValue : minValue; + let end = colorScaleInverted ? minValue : maxValue; + + return d3.scaleSequential(colorInterpolator).domain([start, end]); + } + + function setOpacityScale(maxValue) { + if (panel.color.colorScale === 'linear') { + opacityScale = d3.scaleLinear() + .domain([0, maxValue]) + .range([0, 1]); + } else if (panel.color.colorScale === 'sqrt') { + opacityScale = d3.scalePow().exponent(panel.color.exponent) + .domain([0, maxValue]) + .range([0, 1]); + } + } + + function setCardSize() { + let xGridSize = Math.floor(chartWidth / cardsData.xBucketSize); + let yGridSize = Math.floor(chartHeight / cardsData.yBucketSize); + + cardWidth = xGridSize - cardPadding * 2; + cardHeight = yGridSize ? yGridSize - cardPadding * 2 : 0; + + yOffset = cardHeight / 2; + } + + function getCardX(d) { + let x; + if (xScale(d.x) < 0) { + // Cut card left to prevent overlay + x = yAxisWidth + cardPadding; + } else { + x = xScale(d.x) + yAxisWidth + cardPadding; + } + + return x; + } + + function getCardWidth(d) { + let w; + if (xScale(d.x) < 0) { + // Cut card left to prevent overlay + let cutted_width = xScale(d.x) + cardWidth; + w = cutted_width > 0 ? cutted_width : 0; + } else if (xScale(d.x) + cardWidth > chartWidth) { + // Cut card right to prevent overlay + w = chartWidth - xScale(d.x) - cardPadding; + } else { + w = cardWidth; + } + + // Card width should be MIN_CARD_SIZE at least + w = Math.max(w, MIN_CARD_SIZE); + return w; + } + + function getCardY(d) { + let y = yScale(d.y); + + y = y + chartTop - cardHeight - cardPadding + yOffset; + + return y; + } + + function getCardHeight(d) { + let y = yScale(d.y) + chartTop - cardHeight - cardPadding; + let h = cardHeight; + + // Cut card height to prevent overlay + // if (y < chartTop) { + // h = yScale(d.y) - cardPadding; + // } else if (yScale(d.y) > chartBottom) { + // h = chartBottom - y; + // } else if (y + cardHeight > chartBottom) { + // h = chartBottom - y; + // } + + // Height can't be more than chart height + h = Math.min(h, chartHeight); + // Card height should be MIN_CARD_SIZE at least + h = Math.max(h, MIN_CARD_SIZE); + + return h; + } + + function getCardColor(d) { + if (panel.color.mode === 'opacity') { + return panel.color.cardColor; + } else { + return colorScale(d.value); + } + } + + function getCardOpacity(d) { + if (panel.nullPointMode === 'null' && d.value == null ) { + return 0; + } + if (panel.color.mode === 'opacity') { + return opacityScale(d.value); + } else { + return 1; + } + } + + ///////////////////////////// + // Selection and crosshair // + ///////////////////////////// + + // Shared crosshair and tooltip + appEvents.on('graph-hover', event => { + drawSharedCrosshair(event.pos); + }, scope); + + appEvents.on('graph-hover-clear', () => { + clearCrosshair(); + }, scope); + + function onMouseDown(event) { + selection.active = true; + selection.x1 = event.offsetX; + + mouseUpHandler = function() { + onMouseUp(); + }; + + $(document).one("mouseup", mouseUpHandler); + } + + function onMouseUp() { + $(document).unbind("mouseup", mouseUpHandler); + mouseUpHandler = null; + selection.active = false; + + let selectionRange = Math.abs(selection.x2 - selection.x1); + if (selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) { + let timeFrom = xScale.invert(Math.min(selection.x1, selection.x2) - yAxisWidth); + let timeTo = xScale.invert(Math.max(selection.x1, selection.x2) - yAxisWidth); + + ctrl.timeSrv.setTime({ + from: moment.utc(timeFrom), + to: moment.utc(timeTo) + }); + } + + clearSelection(); + } + + function onMouseLeave() { + appEvents.emit('graph-hover-clear'); + clearCrosshair(); + } + + function onMouseMove(event) { + if (!heatmap) { return; } + + if (selection.active) { + // Clear crosshair and tooltip + clearCrosshair(); + tooltip.destroy(); + + selection.x2 = limitSelection(event.offsetX); + drawSelection(selection.x1, selection.x2); + } else { + emitGraphHoverEvet(event); + drawCrosshair(event.offsetX); + } + } + + function emitGraphHoverEvet(event) { + let x = xScale.invert(event.offsetX - yAxisWidth).valueOf(); + let y = yScale(event.offsetY); + let pos = { + pageX: event.pageX, + pageY: event.pageY, + x: x, x1: x, + y: y, y1: y, + panelRelY: null + }; + + // Set minimum offset to prevent showing legend from another panel + pos.panelRelY = Math.max(event.offsetY / height, 0.001); + + // broadcast to other graph panels that we are hovering + appEvents.emit('graph-hover', {pos: pos, panel: panel}); + } + + function limitSelection(x2) { + x2 = Math.max(x2, yAxisWidth); + x2 = Math.min(x2, chartWidth + yAxisWidth); + return x2; + } + + function drawSelection(posX1, posX2) { + if (heatmap) { + heatmap.selectAll(".status-heatmap-selection").remove(); + let selectionX = Math.min(posX1, posX2); + let selectionWidth = Math.abs(posX1 - posX2); + + if (selectionWidth > MIN_SELECTION_WIDTH) { + heatmap.append("rect") + .attr("class", "status-heatmap-selection") + .attr("x", selectionX) + .attr("width", selectionWidth) + .attr("y", chartTop) + .attr("height", chartHeight); + } + } + } + + function clearSelection() { + selection.x1 = -1; + selection.x2 = -1; + + if (heatmap) { + heatmap.selectAll(".status-heatmap-selection").remove(); + } + } + + function drawCrosshair(position) { + if (heatmap) { + heatmap.selectAll(".status-heatmap-crosshair").remove(); + + let posX = position; + posX = Math.max(posX, yAxisWidth); + posX = Math.min(posX, chartWidth + yAxisWidth); + + heatmap.append("g") + .attr("class", "status-heatmap-crosshair") + .attr("transform", "translate(" + posX + ",0)") + .append("line") + .attr("x1", 1) + .attr("y1", chartTop) + .attr("x2", 1) + .attr("y2", chartBottom) + .attr("stroke-width", 1); + } + } + + function drawSharedCrosshair(pos) { + if (heatmap && ctrl.dashboard.graphTooltip !== 0) { + let posX = xScale(pos.x) + yAxisWidth; + drawCrosshair(posX); + } + } + + function clearCrosshair() { + if (heatmap) { + heatmap.selectAll(".status-heatmap-crosshair").remove(); + } + } + + function render() { + data = ctrl.data; + panel = ctrl.panel; + timeRange = ctrl.range; + cardsData = ctrl.cardsData; + + if (!setElementHeight() || !data) { + return; + } + + // Draw default axes and return if no data + if (_.isEmpty(cardsData.cards)) { + addHeatmapCanvas(); + addAxes(); + return; + } + + addHeatmap(); + scope.yAxisWidth = yAxisWidth; + scope.xAxisHeight = xAxisHeight; + scope.chartHeight = chartHeight; + scope.chartWidth = chartWidth; + scope.chartTop = chartTop; + } + + // Register selection listeners + $heatmap.on("mousedown", onMouseDown); + $heatmap.on("mousemove", onMouseMove); + $heatmap.on("mouseleave", onMouseLeave); +} + +function grafanaTimeFormat(ticks, min, max) { + if (min && max && ticks) { + let range = max - min; + let secPerTick = (range/ticks) / 1000; + let oneDay = 86400000; + let oneYear = 31536000000; + + if (secPerTick <= 45) { + return "%H:%M:%S"; + } + if (secPerTick <= 7200 || range <= oneDay) { + return "%H:%M"; + } + if (secPerTick <= 80000) { + return "%m/%d %H:%M"; + } + if (secPerTick <= 2419200 || range <= oneYear) { + return "%m/%d"; + } + return "%Y-%m"; + } + + return "%H:%M"; +} diff --git a/src/status_heatmap_ctrl.js b/src/status_heatmap_ctrl.js new file mode 100644 index 0000000..cf93aed --- /dev/null +++ b/src/status_heatmap_ctrl.js @@ -0,0 +1,182 @@ +import { MetricsPanelCtrl } from 'app/plugins/sdk'; +import _ from 'lodash'; +import { contextSrv } from 'app/core/core'; +import kbn from 'app/core/utils/kbn'; + +import rendering from './rendering'; +// import aggregates, { aggregatesMap } from './aggregates'; +// import fragments, { fragmentsMap } from './fragments'; +// import { labelFormats } from './xAxisLabelFormats'; +// import canvasRendering from './canvas/rendering'; +import {statusHeatmapOptionsEditor} from './options_editor'; +import './css/status-heatmap.css!'; + +const CANVAS = 'CANVAS'; +const SVG = 'SVG'; +const VALUE_INDEX = 0, + TIME_INDEX = 1; + +const panelDefaults = { + // aggregate: aggregates.AVG, + // fragment: fragments.HOUR, + color: { + mode: 'spectrum', + cardColor: '#b4ff00', + colorScale: 'sqrt', + exponent: 0.5, + colorScheme: 'interpolateGnYlRd' + }, + cards: { + cardPadding: null, + cardRound: null + }, + xAxis: { + show: true, + showWeekends: true, + minBucketWidthToShowWeekends: 4, + showCrosshair: true, + labelFormat: '%a %m/%d' + }, + yAxis: { + show: true, + showCrosshair: false + }, + tooltip: { + show: true + }, + legend: { + show: true + }, + data: { + unitFormat: 'short', + decimals: null + }, + // how null points should be handled + nullPointMode: 'null', + highlightCards: true +}; + +const renderer = CANVAS; + +const colorSchemes = [ + // Diverging + { name: 'Spectral', value: 'interpolateSpectral', invert: 'always' }, + { name: 'RdYlGn', value: 'interpolateRdYlGn', invert: 'always' }, + { name: 'GnYlRd', value: 'interpolateGnYlRd', invert: 'always' }, + + // Sequential (Single Hue) + { name: 'Blues', value: 'interpolateBlues', invert: 'dark' }, + { name: 'Greens', value: 'interpolateGreens', invert: 'dark' }, + { name: 'Greys', value: 'interpolateGreys', invert: 'dark' }, + { name: 'Oranges', value: 'interpolateOranges', invert: 'dark' }, + { name: 'Purples', value: 'interpolatePurples', invert: 'dark' }, + { name: 'Reds', value: 'interpolateReds', invert: 'dark' }, + + // Sequential (Multi-Hue) + { name: 'BuGn', value: 'interpolateBuGn', invert: 'dark' }, + { name: 'BuPu', value: 'interpolateBuPu', invert: 'dark' }, + { name: 'GnBu', value: 'interpolateGnBu', invert: 'dark' }, + { name: 'OrRd', value: 'interpolateOrRd', invert: 'dark' }, + { name: 'PuBuGn', value: 'interpolatePuBuGn', invert: 'dark' }, + { name: 'PuBu', value: 'interpolatePuBu', invert: 'dark' }, + { name: 'PuRd', value: 'interpolatePuRd', invert: 'dark' }, + { name: 'RdPu', value: 'interpolateRdPu', invert: 'dark' }, + { name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' }, + { name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' }, + { name: 'YlOrBr', value: 'interpolateYlOrBr', invert: 'dark' }, + { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' } +]; + +let colorModes = ['opacity', 'spectrum']; +let opacityScales = ['linear', 'sqrt']; + +export class StatusHeatmapCtrl extends MetricsPanelCtrl { + static templateUrl = 'module.html'; + + constructor($scope, $injector, $rootScope, timeSrv) { + super($scope, $injector); + _.defaultsDeep(this.panel, panelDefaults); + + this.opacityScales = opacityScales; + this.colorModes = colorModes; + this.colorSchemes = colorSchemes; + + this.events.on('data-received', this.onDataReceived); + this.events.on('data-snapshot-load', this.onDataReceived); + this.events.on('data-error', this.onDataError); + this.events.on('init-edit-mode', this.onInitEditMode); + this.events.on('render', this.onRender); + } + + onDataReceived = (dataList) => { + this.data = dataList; + this.cardsData = this.convertToCards(this.data); + this.render(); + }; + + onInitEditMode = () => { + this.addEditorTab('Options', statusHeatmapOptionsEditor, 2); + this.unitFormats = kbn.getUnitFormats(); + }; + + onRender = () => { + if (!this.data) { return; } + }; + + onCardColorChange = (newColor) => { + this.panel.color.cardColor = newColor; + this.render(); + }; + + onDataError = () => { + this.data = []; + this.render(); + }; + + link = (scope, elem, attrs, ctrl) => { + console.log('LINK'); + rendering(scope, elem, attrs, ctrl); + // switch (renderer) { + // case CANVAS: { + // canvasRendering(scope, elem, attrs, ctrl); + // break; + // } + // case SVG: { + // svgRendering(scope, elem, attrs, ctrl); + // break; + // } + // } + }; + + convertToCards = (data) => { + let cardsData = { cards: [], xBucketSize: 0, yBucketSize: 0, maxValue: 0, minValue: 0 }; + + if (!data || data.length == 0) { return cardsData;} + + cardsData.yBucketSize = data.length; + cardsData.xBucketSize = _.min(_.map(data, d => d.datapoints.length)); + + for(let i = 0; i < cardsData.yBucketSize; i++) { + let s = data[i]; + + for (let j = 0; j < cardsData.xBucketSize; j++) { + let card = {}; + let v = s.datapoints[j]; + + card.x = v[TIME_INDEX]; + card.y = s.target; + card.value = v[VALUE_INDEX]; + + if (cardsData.maxValue < card.value) + cardsData.maxValue = card.value; + + if (cardsData.minValue > card.value) + cardsData.minValue = card.value; + + cardsData.cards.push(card); + } + } + + return cardsData; + }; +} diff --git a/src/tooltip.js b/src/tooltip.js new file mode 100644 index 0000000..ccb853b --- /dev/null +++ b/src/tooltip.js @@ -0,0 +1,108 @@ +import d3 from 'd3'; +import $ from 'jquery'; +import _ from 'lodash'; +import kbn from 'app/core/utils/kbn'; + +let TOOLTIP_PADDING_X = 30; +let TOOLTIP_PADDING_Y = 5; +let HISTOGRAM_WIDTH = 160; +let HISTOGRAM_HEIGHT = 40; + +export class StatusHeatmapTooltip { + constructor(elem, scope) { + this.scope = scope; + this.dashboard = scope.ctrl.dashboard; + this.panelCtrl = scope.ctrl; + this.panel = scope.ctrl.panel; + this.heatmapPanel = elem; + this.mouseOverBucket = false; + this.originalFillColor = null; + + elem.on("mouseover", this.onMouseOver.bind(this)); + elem.on("mouseleave", this.onMouseLeave.bind(this)); + } + + onMouseOver(e) { + if (!this.panel.tooltip.show || !this.scope.ctrl.data || _.isEmpty(this.scope.ctrl.data)) { return; } + + if (!this.tooltip) { + this.add(); + this.move(e); + } + } + + onMouseLeave() { + this.destroy(); + } + + onMouseMove(e) { + if (!this.panel.tooltip.show) { return; } + + this.move(e); + } + + add() { + this.tooltip = d3.select("body") + .append("div") + .attr("class", "heatmap-tooltip graph-tooltip grafana-tooltip"); + } + + destroy() { + if (this.tooltip) { + this.tooltip.remove(); + } + + this.tooltip = null; + } + + show(pos, x, y, value) { + if (!this.panel.tooltip.show) { return; } + // shared tooltip mode + if (pos.panelRelY) { + return; + } + + if (!x || !y || !this.tooltip) { + this.destroy(); + return; + } + + let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss'; + let time = this.dashboard.formatDate(+x, tooltipTimeFormat); + + let tooltipHtml = `
${time}
+
`; + + tooltipHtml += `
+ name: ${y}
+ value: ${value}
+
`; + + this.tooltip.html(tooltipHtml); + + this.move(pos); + } + + move(pos) { + if (!this.tooltip) { return; } + + let elem = $(this.tooltip.node())[0]; + let tooltipWidth = elem.clientWidth; + let tooltipHeight = elem.clientHeight; + + let left = pos.pageX + TOOLTIP_PADDING_X; + let top = pos.pageY + TOOLTIP_PADDING_Y; + + if (pos.pageX + tooltipWidth + 40 > window.innerWidth) { + left = pos.pageX - tooltipWidth - TOOLTIP_PADDING_X; + } + + if (pos.pageY - window.pageYOffset + tooltipHeight + 20 > window.innerHeight) { + top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y; + } + + return this.tooltip + .style("left", left + "px") + .style("top", top + "px"); + } +}