├── .gitignore ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docker-compose.yml ├── jest.config.js ├── package.json ├── src ├── editor.html ├── img │ ├── piechart-donut.png │ ├── piechart-legend-on-graph.png │ ├── piechart-legend-rhs.png │ ├── piechart-legend-under.png │ ├── piechart-options.png │ ├── piechart_logo_large.png │ ├── piechart_logo_large.svg │ ├── piechart_logo_small.png │ └── piechart_logo_small.svg ├── legend.ts ├── lib │ ├── jquery.flot.js │ ├── jquery.flot.pie.js │ ├── jquery.flot.time.js │ └── perfect-scrollbar.min.js ├── module.html ├── module.ts ├── piechart_ctrl.test.ts ├── piechart_ctrl.ts ├── plugin.json ├── rendering.ts └── styles │ ├── dark.scss │ ├── light.scss │ └── piechart.scss ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist/ 4 | coverage/ 5 | .aws-config.json 6 | awsconfig 7 | /emails/dist 8 | /public_gen 9 | /tmp 10 | vendor/phantomjs/phantomjs 11 | 12 | docs/AWS_S3_BUCKET 13 | docs/GIT_BRANCH 14 | docs/VERSION 15 | docs/GITCOMMIT 16 | docs/changed-files 17 | docs/changed-files 18 | 19 | # locally required config files 20 | public/css/*.min.css 21 | 22 | # Editor junk 23 | *.sublime-workspace 24 | *.swp 25 | .idea/ 26 | *.iml 27 | 28 | /data/* 29 | /bin/* 30 | 31 | conf/custom.ini 32 | fig.yml 33 | profile.cov 34 | grafana 35 | .notouch 36 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('@grafana/toolkit/src/config/prettier.plugin.config.json'), 3 | }; 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.6.4] - 2022-11-04 4 | 5 | - Fix readme 6 | 7 | ## [1.6.3] - 2022-11-02 8 | 9 | - Bump version for publishing 10 | 11 | ## [1.6.2] - 2022-11-02 12 | 13 | - Bump version for deprecation notice 14 | - Update packages 15 | - Switch to drone vs circleci 16 | 17 | ## [1.6.1] - 2020-09-14 18 | 19 | - Fixed issue with legend color picker [#248](https://github.com/grafana/piechart-panel/issues/248) 20 | 21 | ## [1.5.0] - 2020-05-15 22 | 23 | - Updated logo 24 | - Sanitize legend header. Thanks rotemreiss [#230](https://github.com/grafana/piechart-panel/pull/230) 25 | - Signed for Grafana 7.0 26 | 27 | ## [1.4.0] - 2020-02-04 28 | 29 | - Added combine option for legend 30 | 31 | ## [1.3.9] - 2019-09-05 32 | 33 | - Fixed dark/light mode text font colors: [#189](https://github.com/grafana/piechart-panel/issues/189). 34 | 35 | ## [1.3.8] - 2019-07-22 36 | 37 | - Added hotfix for older versions of grafana 38 | 39 | ## [1.3.7] - 2019-07-22 40 | 41 | - Converted to typescript. 42 | - Fixed several bugs. 43 | - Fixed 6.3 compatibility. 44 | - Used toolkit for building. 45 | - Fixed for issue #154. 46 | 47 | ## 1.3.3 48 | 49 | - Fixed legend sorting: [#145](https://github.com/grafana/piechart-panel/issues/145) 50 | 51 | ## 1.3.2 52 | 53 | - Automatically set legend width if Internet Explorer 11 and positioned to the right: [#148](https://github.com/grafana/piechart-panel/issues/148) 54 | 55 | ## 1.3.1 56 | 57 | - Fixed scrolling and legend issues in Internet Explorer 11: [#143](https://github.com/grafana/piechart-panel/issues/143) 58 | 59 | ## 1.3.0 60 | 61 | - Fixed legend and piechart rendering and sorting: [#138](https://github.com/grafana/piechart-panel/pull/138), [#136](https://github.com/grafana/piechart-panel/pull/136) 62 | - Fixed decimal field for percentages [#108](https://github.com/grafana/piechart-panel/pull/108) 63 | 64 | ## 1.1.5 65 | 66 | - Fixed color picker in legend 67 | - Fixed - [Values in legend are displayed raw, not with the correct unit](https://github.com/grafana/piechart-panel/issues/51). Thanks, [@conet](https://github.com/conet) 68 | - Fixed - [Legend overlaps with graphs](https://github.com/grafana/piechart-panel/issues/34). Thanks, [@smalik03](https://github.com/smalik03) 69 | 70 | ## 1.1.4 71 | 72 | - Added support for combining small slices (https://github.com/grafana/piechart-panel/pull/43) 73 | - Added option to show percentage in legend https://github.com/grafana/piechart-panel/pull/41 74 | 75 | ## 1.0.2 76 | 77 | - Added piechart piece divider setting 78 | - Removed unused code 79 | - Added fontsize option for labels on graph 80 | - Only show the displayed piechart value in legend 81 | - Added possibility to pick stat to use for piechart 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Grafana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Notice:** This plugin is deprecated 2 | 3 | [![Marketplace](https://img.shields.io/badge/dynamic/json?logo=grafana&color=F47A20&label=marketplace&prefix=v&query=%24.items%5B%3F%28%40.slug%20%3D%3D%20%22grafana-piechart-panel%22%29%5D.version&url=https%3A%2F%2Fgrafana.com%2Fapi%2Fplugins)](https://grafana.com/grafana/plugins/grafana-piechart-panel) 4 | [![Downloads](https://img.shields.io/badge/dynamic/json?logo=grafana&color=F47A20&label=downloads&query=%24.items%5B%3F%28%40.slug%20%3D%3D%20%22grafana-piechart-panel%22%29%5D.downloads&url=https%3A%2F%2Fgrafana.com%2Fapi%2Fplugins)](https://grafana.com/grafana/plugins/grafana-piechart-panel) 5 | [![License](https://img.shields.io/github/license/grafana/piechart-panel)](LICENSE) 6 | [![Known Vulnerabilities](https://snyk.io/test/github/grafana/piechart-panel/badge.svg)](https://snyk.io/test/github/grafana/piechart-panel) 7 | [![Build Status](https://drone.grafana.net/api/badges/grafana/piechart-panel/status.svg)](https://drone.grafana.net/grafana/piechart-panel) 8 | 9 | As of [Grafana 8.0](https://grafana.com/docs/grafana/latest/whatsnew/whats-new-in-v8-0/#whats-new-in-grafana-v80) Pie chart panel is included with Grafana. Please refer to the [Pie chart panel](https://grafana.com/docs/grafana/latest/visualizations/pie-chart-panel/) documentation for more information. 10 | 11 | ## Installation 12 | 13 | Use the new grafana-cli tool to install piechart-panel from the commandline: 14 | 15 | ```SHELL 16 | grafana-cli plugins install grafana-piechart-panel 17 | ``` 18 | 19 | The plugin will be installed into your Grafana plugins directory; the default is /var/lib/grafana/plugins if you installed the Grafana package. 20 | 21 | More instructions on the Grafana CLI tool can be found in [Grafana CLI](https://grafana.com/docs/grafana/latest/administration/cli/). 22 | 23 | You must have Grafana 4.6.5 or newer installed to run this plugin. You can get it here: 24 | 25 | ## Alternative installation methods 26 | 27 | ### Download latest zip 28 | 29 | ```BASH 30 | wget -nv https://grafana.com/api/plugins/grafana-piechart-panel/versions/latest/download -O /tmp/grafana-piechart-panel.zip 31 | ``` 32 | 33 | Extract and move into place 34 | 35 | ```BASH 36 | unzip -q /tmp/grafana-piechart-panel.zip -d /tmp 37 | mv /tmp/grafana-piechart-panel-* /var/lib/grafana/plugins/grafana-piechart-panel 38 | sudo service grafana-server restart 39 | ``` 40 | 41 | ### Git clone 42 | 43 | You can clone this repo directly into your plugins directory. 44 | 45 | Afterwards restart grafana-server and the plugin should be automatically detected and used. 46 | 47 | ```SHELL 48 | git clone https://github.com/grafana/piechart-panel.git --branch release-1.6.2 49 | sudo service grafana-server restart 50 | ``` 51 | 52 | ### Clone into a directory of your choice 53 | 54 | If the plugin is cloned to a directory that is not the default plugins directory, then you need to edit your grafana.ini config file (default location is at /etc/grafana/grafana.ini) and add this: 55 | 56 | ```ini 57 | [plugin.piechart] 58 | path = /home/your/clone/dir/piechart-panel 59 | ``` 60 | 61 | Note that if you clone it into the Grafana plugins directory, then you do not need to add the above config option. That is only 62 | if you want to place the plugin in a directory outside the standard plugins directory. Make sure that grafana-server 63 | has read access to the directory. 64 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | grafana: 4 | # plugin verified to run on versions up to 9.2.3 5 | image: grafana/grafana:9.2.3 6 | #image: grafana/grafana:8.0.6 7 | #image: grafana/grafana:7.5.17 8 | #image: grafana/grafana:7.0.6 9 | #image: grafana/grafana:6.3.7 10 | #image: grafana/grafana:6.0.2 11 | #image: grafana/grafana:5.4.5 12 | # absolute minimum version is 4.6.5 13 | #image: grafana/grafana:4.6.5 14 | ports: 15 | - "3000:3000" 16 | volumes: 17 | - ./dist:/var/lib/grafana/plugins/grafana-piechart-panel 18 | - ./provisioning:/etc/grafana/provisioning 19 | environment: 20 | TERM: linux 21 | GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: grafana-piechart-panel 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // This file is needed because it is used by vscode and other tools that 2 | // call `jest` directly. However, unless you are doing anything special 3 | // do not edit this file 4 | 5 | const standard = require('@grafana/toolkit/src/config/jest.plugin.config'); 6 | 7 | // This process will use the same config that `yarn test` is using 8 | module.exports = standard.jestConfig(); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "piechart-panel", 3 | "version": "1.6.4", 4 | "description": "Pie chart panel for Grafana", 5 | "scripts": { 6 | "build": "grafana-toolkit plugin:build", 7 | "test": "grafana-toolkit plugin:test", 8 | "dev": "grafana-toolkit plugin:dev", 9 | "watch": "grafana-toolkit plugin:dev --watch" 10 | }, 11 | "repository": "github:grafana/piechart-panel.git", 12 | "author": "Grafana Labs", 13 | "license": "MIT", 14 | "private": true, 15 | "dependencies": { 16 | "angular": "1.7.9", 17 | "flot": "4.2.3", 18 | "jquery": "3.6.1", 19 | "jquery.flot": "0.8.3", 20 | "lodash": "4.17.21" 21 | }, 22 | "devDependencies": { 23 | "@grafana/data": "7.0.6", 24 | "@grafana/toolkit": "8.4.11", 25 | "@grafana/ui": "7.0.6", 26 | "@types/angular": "1.7.4", 27 | "@types/flot": "^0.0.32", 28 | "@types/grafana": "github:CorpGlory/types-grafana", 29 | "@types/lodash": "4.14.187" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/editor.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
New pie chart!
4 |

5 | There is a new pie chart visualization built into Grafana! 6 |

7 |

8 | 11 |

12 |
13 |
14 |
General
15 |
16 | Type 17 |
18 | 19 |
20 |
21 |
22 | Unit 23 |
24 |
25 |
26 |
27 | Value 28 |
29 | 31 |
32 |
33 |
34 | Divider width 35 | 36 |
37 |
38 | 39 |
40 |
Legend
41 | 42 |
43 | Position 44 |
45 | 47 |
48 |
49 |
50 | 51 | 53 |
54 |
55 | Legend Breakpoint 56 |
57 | 59 |
60 |
61 |
62 | Font size 63 |
64 | 66 |
67 |
68 | 69 |
70 | Values Header 71 | 73 |
74 |
75 | Values Decimals 76 | 78 |
79 | 80 |
81 | Percentage Decimals 82 | 83 |
84 |
85 | 86 |
87 |
Combine (only for percentages)
88 |
89 | Threshold: 90 | 91 | Combines all slices that are smaller than the specified percentage (ranging from 0 to 1) i.e. a value of '0.03' will 92 | combine all slices 3% or less into one slice). 93 |
94 |
95 | Label 96 | 97 | Label text for the combined slice. 98 |
99 |
100 |
101 | -------------------------------------------------------------------------------- /src/img/piechart-donut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/piechart-panel/4eff3329acac076f56b19375ac52fa8228ddbbba/src/img/piechart-donut.png -------------------------------------------------------------------------------- /src/img/piechart-legend-on-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/piechart-panel/4eff3329acac076f56b19375ac52fa8228ddbbba/src/img/piechart-legend-on-graph.png -------------------------------------------------------------------------------- /src/img/piechart-legend-rhs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/piechart-panel/4eff3329acac076f56b19375ac52fa8228ddbbba/src/img/piechart-legend-rhs.png -------------------------------------------------------------------------------- /src/img/piechart-legend-under.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/piechart-panel/4eff3329acac076f56b19375ac52fa8228ddbbba/src/img/piechart-legend-under.png -------------------------------------------------------------------------------- /src/img/piechart-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/piechart-panel/4eff3329acac076f56b19375ac52fa8228ddbbba/src/img/piechart-options.png -------------------------------------------------------------------------------- /src/img/piechart_logo_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/piechart-panel/4eff3329acac076f56b19375ac52fa8228ddbbba/src/img/piechart_logo_large.png -------------------------------------------------------------------------------- /src/img/piechart_logo_large.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/piechart_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/piechart-panel/4eff3329acac076f56b19375ac52fa8228ddbbba/src/img/piechart_logo_small.png -------------------------------------------------------------------------------- /src/img/piechart_logo_small.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/legend.ts: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | // @ts-ignore 3 | import $ from 'jquery'; 4 | //import './lib/jquery.flot.pie'; 5 | 6 | //import 'jquery.flot'; 7 | 8 | import './lib/jquery.flot.time'; 9 | 10 | import _ from 'lodash'; 11 | 12 | // @ts-ignore 13 | import PerfectScrollbar from './lib/perfect-scrollbar.min'; 14 | 15 | angular.module('grafana.directives').directive('piechartLegend', (popoverSrv: any, $timeout: any) => { 16 | return { 17 | link: (scope: any, elem: any) => { 18 | const $container = $('
'); 19 | let firstRender = true; 20 | const ctrl = scope.ctrl; 21 | const panel = ctrl.panel; 22 | let data: any; 23 | let seriesList: any; 24 | let dataList: any; 25 | let i; 26 | let legendScrollbar: any; 27 | 28 | scope.$on('$destroy', () => { 29 | if (legendScrollbar) { 30 | legendScrollbar.destroy(); 31 | } 32 | }); 33 | 34 | ctrl.events.on('render', () => { 35 | data = ctrl.series; 36 | if (data) { 37 | for (const i in data) { 38 | data[i].color = ctrl.data[i].color; 39 | } 40 | render(); 41 | } 42 | }); 43 | 44 | function getSeriesIndexForElement(el: any) { 45 | return el.parents('[data-series-index]').data('series-index'); 46 | } 47 | 48 | function toggleSeries(e: any) { 49 | const el = $(e.currentTarget); 50 | // Consider Combine entry as special case (not clickable) 51 | if (el && el.text() !== panel.combine.label) { 52 | const index = getSeriesIndexForElement(el); 53 | const seriesInfo = dataList[index]; 54 | const scrollPosition = $($container.children('tbody')).scrollTop(); 55 | ctrl.toggleSeries(seriesInfo); 56 | if (typeof scrollPosition !== 'undefined') { 57 | $($container.children('tbody')).scrollTop(scrollPosition); 58 | } 59 | } 60 | } 61 | 62 | function sortLegend(e: any) { 63 | const el = $(e.currentTarget); 64 | const stat = el.data('stat'); 65 | 66 | if (stat !== panel.legend.sort) { 67 | panel.legend.sortDesc = null; 68 | } 69 | 70 | // if already sort ascending, disable sorting 71 | if (panel.legend.sortDesc === false) { 72 | panel.legend.sort = null; 73 | panel.legend.sortDesc = null; 74 | ctrl.render(); 75 | return; 76 | } 77 | 78 | panel.legend.sortDesc = !panel.legend.sortDesc; 79 | panel.legend.sort = stat; 80 | ctrl.render(); 81 | } 82 | 83 | function getLegendHeaderHtml(statName: any) { 84 | let name = statName; 85 | 86 | if (panel.legend.header) { 87 | name = panel.legend.header; 88 | } 89 | 90 | let html = '' + _.escape(name); 91 | 92 | if (panel.legend.sort === statName) { 93 | const cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up'; 94 | html += ' '; 95 | } 96 | 97 | return html + ''; 98 | } 99 | 100 | function getLegendPercentageHtml(statName: any) { 101 | const name = 'percentage'; 102 | let html = '' + name; 103 | 104 | if (panel.legend.sort === statName) { 105 | const cssClass = panel.legend.sortDesc ? 'fa fa-caret-down' : 'fa fa-caret-up'; 106 | html += ' '; 107 | } 108 | 109 | return html + ''; 110 | } 111 | 112 | function openColorSelector(e: any) { 113 | // if we clicked inside poup container ignore click 114 | if ($(e.target).parents('.popover').length) { 115 | return; 116 | } 117 | 118 | const el = $(e.currentTarget).find('.fa-minus'); 119 | const index = getSeriesIndexForElement(el); 120 | const series = seriesList[index]; 121 | 122 | $timeout(() => { 123 | popoverSrv.show({ 124 | element: el[0], 125 | position: 'right center', 126 | template: 127 | '' + 128 | '', 129 | openOn: 'hover', 130 | classNames: 'drop-popover drop-popover--transparent', 131 | model: { 132 | autoClose: true, 133 | series: series, 134 | toggleAxis: () => {}, 135 | colorSelected: (color: any) => { 136 | ctrl.changeSeriesColor(series, color); 137 | }, 138 | }, 139 | }); 140 | }); 141 | } 142 | 143 | function render() { 144 | if (panel.legendType === 'On graph' || !panel.legend.show) { 145 | $container.empty(); 146 | elem.find('.piechart-legend').css('padding-top', 0); 147 | return; 148 | } else { 149 | elem.find('.piechart-legend').css('padding-top', 6); 150 | } 151 | 152 | if (firstRender) { 153 | elem.append($container); 154 | $container.on('click', '.piechart-legend-icon', openColorSelector); 155 | $container.on('click', '.piechart-legend-alias', toggleSeries); 156 | $container.on('click', 'th', sortLegend); 157 | firstRender = false; 158 | } 159 | 160 | seriesList = data; 161 | dataList = ctrl.data; 162 | 163 | $container.empty(); 164 | 165 | const width = panel.legendType === 'Right side' && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : ''; 166 | const ieWidth = 167 | panel.legendType === 'Right side' && panel.legend.sideWidth ? panel.legend.sideWidth - 1 + 'px' : ''; 168 | elem.css('min-width', width); 169 | elem.css('width', ieWidth); 170 | 171 | const showValues = panel.legend.values || panel.legend.percentage; 172 | const tableLayout = (panel.legendType === 'Under graph' || panel.legendType === 'Right side') && showValues; 173 | 174 | $container.toggleClass('piechart-legend-table', tableLayout); 175 | 176 | let legendHeader; 177 | if (tableLayout) { 178 | let header = ''; 179 | if (panel.legend.values) { 180 | header += getLegendHeaderHtml(ctrl.panel.valueName); 181 | } 182 | if (panel.legend.percentage) { 183 | header += getLegendPercentageHtml(ctrl.panel.valueName); 184 | } 185 | header += ''; 186 | legendHeader = $(header); 187 | } 188 | 189 | let total = 0; 190 | if (panel.legend.percentage) { 191 | for (i = 0; i < seriesList.length; i++) { 192 | if (!ctrl.hiddenSeries[seriesList[i].label]) { 193 | total += seriesList[i].stats[ctrl.panel.valueName]; 194 | } 195 | } 196 | } 197 | 198 | let combineNum = 0; 199 | const combineVal = { 200 | label: panel.combine.label, 201 | color: '', 202 | legendData: 0, 203 | }; 204 | const seriesElements = []; 205 | 206 | for (i = 0; i < seriesList.length; i++) { 207 | const series = seriesList[i]; 208 | const seriesData = dataList[i]; 209 | // combine lower values than threshold and not include into legent 210 | if (seriesData.data / total < panel.combine.threshold) { 211 | // Jump hidden series 212 | if (!ctrl.hiddenSeries[seriesData.label]) { 213 | combineNum++; 214 | combineVal.legendData += seriesData.data; 215 | } 216 | } else { 217 | // ignore empty series 218 | if (panel.legend.hideEmpty && series.allIsNull) { 219 | continue; 220 | } 221 | // ignore series excluded via override 222 | if (!series.legend) { 223 | continue; 224 | } 225 | 226 | seriesElements.push($(generateLegendItem(seriesData, i, total, showValues, tableLayout))); 227 | } 228 | } 229 | 230 | // Add combine to legend 231 | if (combineNum > 0) { 232 | // Define color according to hiddenSeries and combineNum 233 | if (typeof panel.legend.sortDesc === 'undefined' || panel.legend.sortDesc === null || panel.legend.sortDesc) { 234 | if (Object.keys(ctrl.hiddenSeries).length > 0) { 235 | let _el, _max; 236 | for (const _key in ctrl.hiddenSeries) { 237 | _el = dataList.find((x: any) => x.label === _key); 238 | if (typeof _max === 'undefined') { 239 | _max = _el.legendData; 240 | combineVal.color = _el.color; 241 | } else { 242 | if (_el.legendData > _max) { 243 | _max = _el.legendData; 244 | combineVal.color = _el.color; 245 | } 246 | } 247 | } 248 | } else { 249 | combineVal.color = seriesList[seriesList.length - combineNum].color; 250 | } 251 | } else { 252 | combineVal.color = seriesList[0].color; 253 | } 254 | 255 | seriesElements.push( 256 | $(generateLegendItem(combineVal, dataList.length - combineNum, total, showValues, tableLayout)) 257 | ); 258 | } 259 | 260 | if (tableLayout) { 261 | // const topPadding = 6; 262 | const tbodyElem = $(''); 263 | // tbodyElem.css("max-height", maxHeight - topPadding); 264 | if (typeof legendHeader !== 'undefined') { 265 | tbodyElem.append(legendHeader); 266 | } 267 | tbodyElem.append(seriesElements); 268 | $container.append(tbodyElem); 269 | } else { 270 | $container.append(seriesElements); 271 | } 272 | 273 | if (panel.legendType === 'Under graph') { 274 | addScrollbar(); 275 | } else { 276 | destroyScrollbar(); 277 | } 278 | } 279 | 280 | function generateLegendItem(data: any, index: any, total: any, showValues: boolean, tableLayout: boolean) { 281 | let html = '
'; 286 | html += ''; 287 | html += ''; 288 | html += ''; 289 | 290 | html += '' + _.escape(data.label) + ''; 291 | let decimal = 0; 292 | if (ctrl.panel.legend.percentageDecimals) { 293 | decimal = ctrl.panel.legend.percentageDecimals; 294 | } 295 | 296 | if (showValues && tableLayout) { 297 | const value = data.legendData; 298 | if (panel.legend.values) { 299 | html += '
' + ctrl.formatValue(value) + '
'; 300 | } 301 | if (total) { 302 | const pvalue = ((value / total) * 100).toFixed(decimal) + '%'; 303 | html += '
' + pvalue + '
'; 304 | } 305 | } 306 | html += '
'; 307 | 308 | return html; 309 | } 310 | 311 | function addScrollbar() { 312 | const scrollbarOptions = { 313 | // Number of pixels the content height can surpass the container height without enabling the scroll bar. 314 | scrollYMarginOffset: 2, 315 | suppressScrollX: true, 316 | }; 317 | 318 | if (!legendScrollbar) { 319 | legendScrollbar = new PerfectScrollbar(elem[0], scrollbarOptions); 320 | } else { 321 | legendScrollbar.update(); 322 | } 323 | } 324 | 325 | function destroyScrollbar() { 326 | if (legendScrollbar) { 327 | legendScrollbar.destroy(); 328 | legendScrollbar = null; 329 | } 330 | } 331 | }, 332 | }; 333 | }); 334 | -------------------------------------------------------------------------------- /src/lib/jquery.flot.pie.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for rendering pie charts. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | The plugin assumes that each series has a single data value, and that each 7 | value is a positive integer or zero. Negative numbers don't make sense for a 8 | pie chart, and have unpredictable results. The values do NOT need to be 9 | passed in as percentages; the plugin will calculate the total and per-slice 10 | percentages internally. 11 | 12 | * Created by Brian Medendorp 13 | 14 | * Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars 15 | 16 | The plugin supports these options: 17 | 18 | series: { 19 | pie: { 20 | show: true/false 21 | radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' 22 | innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect 23 | startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result 24 | tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) 25 | offset: { 26 | top: integer value to move the pie up or down 27 | left: integer value to move the pie left or right, or 'auto' 28 | }, 29 | stroke: { 30 | color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF') 31 | width: integer pixel width of the stroke 32 | }, 33 | label: { 34 | show: true/false, or 'auto' 35 | formatter: a user-defined function that modifies the text/style of the label text 36 | radius: 0-1 for percentage of fullsize, or a specified pixel length 37 | background: { 38 | color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000') 39 | opacity: 0-1 40 | }, 41 | threshold: 0-1 for the percentage value at which to hide labels (if they're too small) 42 | }, 43 | combine: { 44 | threshold: 0-1 for the percentage value at which to combine slices (if they're too small) 45 | color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined 46 | label: any text value of what the combined slice should be labeled 47 | } 48 | highlight: { 49 | opacity: 0-1 50 | } 51 | } 52 | } 53 | 54 | More detail and specific examples can be found in the included HTML file. 55 | 56 | */ 57 | 58 | (function($) { 59 | 60 | // Maximum redraw attempts when fitting labels within the plot 61 | 62 | var REDRAW_ATTEMPTS = 10; 63 | 64 | // Factor by which to shrink the pie when fitting labels within the plot 65 | 66 | var REDRAW_SHRINK = 0.95; 67 | 68 | function init(plot) { 69 | 70 | var canvas = null, 71 | target = null, 72 | options = null, 73 | maxRadius = null, 74 | centerLeft = null, 75 | centerTop = null, 76 | processed = false, 77 | ctx = null; 78 | 79 | // interactive variables 80 | 81 | var highlights = []; 82 | 83 | // add hook to determine if pie plugin in enabled, and then perform necessary operations 84 | 85 | plot.hooks.processOptions.push(function(plot, options) { 86 | if (options.series.pie.show) { 87 | 88 | options.grid.show = false; 89 | 90 | // set labels.show 91 | 92 | if (options.series.pie.label.show == "auto") { 93 | if (options.legend.show) { 94 | options.series.pie.label.show = false; 95 | } else { 96 | options.series.pie.label.show = true; 97 | } 98 | } 99 | 100 | // set radius 101 | 102 | if (options.series.pie.radius == "auto") { 103 | if (options.series.pie.label.show) { 104 | options.series.pie.radius = 3/4; 105 | } else { 106 | options.series.pie.radius = 1; 107 | } 108 | } 109 | 110 | // ensure sane tilt 111 | 112 | if (options.series.pie.tilt > 1) { 113 | options.series.pie.tilt = 1; 114 | } else if (options.series.pie.tilt < 0) { 115 | options.series.pie.tilt = 0; 116 | } 117 | } 118 | }); 119 | 120 | plot.hooks.bindEvents.push(function(plot, eventHolder) { 121 | var options = plot.getOptions(); 122 | if (options.series.pie.show) { 123 | if (options.grid.hoverable) { 124 | eventHolder.unbind("mousemove").mousemove(onMouseMove); 125 | } 126 | if (options.grid.clickable) { 127 | eventHolder.unbind("click").click(onClick); 128 | } 129 | } 130 | }); 131 | 132 | plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { 133 | var options = plot.getOptions(); 134 | if (options.series.pie.show) { 135 | processDatapoints(plot, series, data, datapoints); 136 | } 137 | }); 138 | 139 | plot.hooks.drawOverlay.push(function(plot, octx) { 140 | var options = plot.getOptions(); 141 | if (options.series.pie.show) { 142 | drawOverlay(plot, octx); 143 | } 144 | }); 145 | 146 | plot.hooks.draw.push(function(plot, newCtx) { 147 | var options = plot.getOptions(); 148 | if (options.series.pie.show) { 149 | draw(plot, newCtx); 150 | } 151 | }); 152 | 153 | function processDatapoints(plot, series, datapoints) { 154 | if (!processed) { 155 | processed = true; 156 | canvas = plot.getCanvas(); 157 | target = $(canvas).parent(); 158 | options = plot.getOptions(); 159 | plot.setData(combine(plot.getData())); 160 | } 161 | } 162 | 163 | function combine(data) { 164 | 165 | var total = 0, 166 | combined = 0, 167 | numCombined = 0, 168 | color = options.series.pie.combine.color, 169 | newdata = []; 170 | 171 | // Fix up the raw data from Flot, ensuring the data is numeric 172 | 173 | for (var i = 0; i < data.length; ++i) { 174 | 175 | var value = data[i].data; 176 | 177 | // If the data is an array, we'll assume that it's a standard 178 | // Flot x-y pair, and are concerned only with the second value. 179 | 180 | // Note how we use the original array, rather than creating a 181 | // new one; this is more efficient and preserves any extra data 182 | // that the user may have stored in higher indexes. 183 | 184 | if ($.isArray(value) && value.length == 1) { 185 | value = value[0]; 186 | } 187 | 188 | if ($.isArray(value)) { 189 | // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 190 | if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { 191 | value[1] = +value[1]; 192 | } else { 193 | value[1] = 0; 194 | } 195 | } else if (!isNaN(parseFloat(value)) && isFinite(value)) { 196 | value = [1, +value]; 197 | } else { 198 | value = [1, 0]; 199 | } 200 | 201 | data[i].data = [value]; 202 | } 203 | 204 | // Sum up all the slices, so we can calculate percentages for each 205 | 206 | for (var i = 0; i < data.length; ++i) { 207 | total += data[i].data[0][1]; 208 | } 209 | 210 | // Count the number of slices with percentages below the combine 211 | // threshold; if it turns out to be just one, we won't combine. 212 | 213 | for (var i = 0; i < data.length; ++i) { 214 | var value = data[i].data[0][1]; 215 | if (value / total <= options.series.pie.combine.threshold) { 216 | combined += value; 217 | numCombined++; 218 | if (!color) { 219 | color = data[i].color; 220 | } 221 | } 222 | } 223 | 224 | for (var i = 0; i < data.length; ++i) { 225 | var value = data[i].data[0][1]; 226 | if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { 227 | newdata.push( 228 | $.extend(data[i], { /* extend to allow keeping all other original data values 229 | and using them e.g. in labelFormatter. */ 230 | data: [[1, value]], 231 | color: data[i].color, 232 | label: data[i].label, 233 | angle: value * Math.PI * 2 / total, 234 | percent: value / (total / 100) 235 | }) 236 | ); 237 | } 238 | } 239 | 240 | if (numCombined > 1) { 241 | newdata.push({ 242 | data: [[1, combined]], 243 | color: color, 244 | label: options.series.pie.combine.label, 245 | angle: combined * Math.PI * 2 / total, 246 | percent: combined / (total / 100) 247 | }); 248 | } 249 | 250 | return newdata; 251 | } 252 | 253 | function draw(plot, newCtx) { 254 | 255 | if (!target) { 256 | return; // if no series were passed 257 | } 258 | 259 | var canvasWidth = plot.getPlaceholder().width(), 260 | canvasHeight = plot.getPlaceholder().height(), 261 | legendWidth = target.children().filter(".legend").children().width() || 0; 262 | 263 | ctx = newCtx; 264 | 265 | // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! 266 | 267 | // When combining smaller slices into an 'other' slice, we need to 268 | // add a new series. Since Flot gives plugins no way to modify the 269 | // list of series, the pie plugin uses a hack where the first call 270 | // to processDatapoints results in a call to setData with the new 271 | // list of series, then subsequent processDatapoints do nothing. 272 | 273 | // The plugin-global 'processed' flag is used to control this hack; 274 | // it starts out false, and is set to true after the first call to 275 | // processDatapoints. 276 | 277 | // Unfortunately this turns future setData calls into no-ops; they 278 | // call processDatapoints, the flag is true, and nothing happens. 279 | 280 | // To fix this we'll set the flag back to false here in draw, when 281 | // all series have been processed, so the next sequence of calls to 282 | // processDatapoints once again starts out with a slice-combine. 283 | // This is really a hack; in 0.9 we need to give plugins a proper 284 | // way to modify series before any processing begins. 285 | 286 | processed = false; 287 | 288 | // calculate maximum radius and center point 289 | 290 | maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; 291 | centerTop = canvasHeight / 2 + options.series.pie.offset.top; 292 | centerLeft = canvasWidth / 2; 293 | 294 | if (options.series.pie.offset.left == "auto") { 295 | if (options.legend.position.match("w")) { 296 | centerLeft += legendWidth / 2; 297 | } else { 298 | centerLeft -= legendWidth / 2; 299 | } 300 | if (centerLeft < maxRadius) { 301 | centerLeft = maxRadius; 302 | } else if (centerLeft > canvasWidth - maxRadius) { 303 | centerLeft = canvasWidth - maxRadius; 304 | } 305 | } else { 306 | centerLeft += options.series.pie.offset.left; 307 | } 308 | 309 | var slices = plot.getData(), 310 | attempts = 0; 311 | 312 | // Keep shrinking the pie's radius until drawPie returns true, 313 | // indicating that all the labels fit, or we try too many times. 314 | 315 | do { 316 | if (attempts > 0) { 317 | maxRadius *= REDRAW_SHRINK; 318 | } 319 | attempts += 1; 320 | clear(); 321 | if (options.series.pie.tilt <= 0.8) { 322 | drawShadow(); 323 | } 324 | } while (!drawPie() && attempts < REDRAW_ATTEMPTS) 325 | 326 | if (attempts >= REDRAW_ATTEMPTS) { 327 | clear(); 328 | target.prepend("
Could not draw pie with labels contained inside canvas
"); 329 | } 330 | 331 | if (plot.setSeries && plot.insertLegend) { 332 | plot.setSeries(slices); 333 | plot.insertLegend(); 334 | } 335 | 336 | // we're actually done at this point, just defining internal functions at this point 337 | 338 | function clear() { 339 | ctx.clearRect(0, 0, canvasWidth, canvasHeight); 340 | target.children().filter(".pieLabel, .pieLabelBackground").remove(); 341 | } 342 | 343 | function drawShadow() { 344 | 345 | var shadowLeft = options.series.pie.shadow.left; 346 | var shadowTop = options.series.pie.shadow.top; 347 | var edge = 10; 348 | var alpha = options.series.pie.shadow.alpha; 349 | var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; 350 | 351 | if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { 352 | return; // shadow would be outside canvas, so don't draw it 353 | } 354 | 355 | ctx.save(); 356 | ctx.translate(shadowLeft,shadowTop); 357 | ctx.globalAlpha = alpha; 358 | ctx.fillStyle = "#000"; 359 | 360 | // center and rotate to starting position 361 | 362 | ctx.translate(centerLeft,centerTop); 363 | ctx.scale(1, options.series.pie.tilt); 364 | 365 | //radius -= edge; 366 | 367 | for (var i = 1; i <= edge; i++) { 368 | ctx.beginPath(); 369 | ctx.arc(0, 0, radius, 0, Math.PI * 2, false); 370 | ctx.fill(); 371 | radius -= i; 372 | } 373 | 374 | ctx.restore(); 375 | } 376 | 377 | function drawPie() { 378 | 379 | var startAngle = Math.PI * options.series.pie.startAngle; 380 | var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; 381 | 382 | // center and rotate to starting position 383 | 384 | ctx.save(); 385 | ctx.translate(centerLeft,centerTop); 386 | ctx.scale(1, options.series.pie.tilt); 387 | //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera 388 | 389 | // draw slices 390 | 391 | ctx.save(); 392 | var currentAngle = startAngle; 393 | for (var i = 0; i < slices.length; ++i) { 394 | slices[i].startAngle = currentAngle; 395 | drawSlice(slices[i].angle, slices[i].color, true); 396 | } 397 | ctx.restore(); 398 | 399 | // draw slice outlines 400 | 401 | if (options.series.pie.stroke.width > 0) { 402 | ctx.save(); 403 | ctx.lineWidth = options.series.pie.stroke.width; 404 | currentAngle = startAngle; 405 | for (var i = 0; i < slices.length; ++i) { 406 | drawSlice(slices[i].angle, options.series.pie.stroke.color, false); 407 | } 408 | ctx.restore(); 409 | } 410 | 411 | // draw donut hole 412 | 413 | drawDonutHole(ctx); 414 | 415 | ctx.restore(); 416 | 417 | // Draw the labels, returning true if they fit within the plot 418 | 419 | if (options.series.pie.label.show) { 420 | return drawLabels(); 421 | } else return true; 422 | 423 | function drawSlice(angle, color, fill) { 424 | 425 | if (angle <= 0 || isNaN(angle)) { 426 | return; 427 | } 428 | 429 | if (fill) { 430 | ctx.fillStyle = color; 431 | } else { 432 | ctx.strokeStyle = color; 433 | ctx.lineJoin = "round"; 434 | } 435 | 436 | ctx.beginPath(); 437 | if (Math.abs(angle - Math.PI * 2) > 0.000000001) { 438 | ctx.moveTo(0, 0); // Center of the pie 439 | } 440 | 441 | //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera 442 | ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); 443 | ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false); 444 | ctx.closePath(); 445 | //ctx.rotate(angle); // This doesn't work properly in Opera 446 | currentAngle += angle; 447 | 448 | if (fill) { 449 | ctx.fill(); 450 | } else { 451 | ctx.stroke(); 452 | } 453 | } 454 | 455 | function drawLabels() { 456 | 457 | var currentAngle = startAngle; 458 | var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; 459 | 460 | for (var i = 0; i < slices.length; ++i) { 461 | if (slices[i].percent >= options.series.pie.label.threshold * 100) { 462 | if (!drawLabel(slices[i], currentAngle, i)) { 463 | return false; 464 | } 465 | } 466 | currentAngle += slices[i].angle; 467 | } 468 | 469 | return true; 470 | 471 | function drawLabel(slice, startAngle, index) { 472 | 473 | if (slice.data[0][1] == 0) { 474 | return true; 475 | } 476 | 477 | // format label text 478 | 479 | var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; 480 | 481 | if (lf) { 482 | text = lf(slice.label, slice); 483 | } else { 484 | text = slice.label; 485 | } 486 | 487 | if (plf) { 488 | text = plf(text, slice); 489 | } 490 | 491 | var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; 492 | var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); 493 | var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; 494 | 495 | var html = "" + text + ""; 496 | target.append(html); 497 | 498 | var label = target.children("#pieLabel" + index); 499 | var labelTop = (y - label.height() / 2); 500 | var labelLeft = (x - label.width() / 2); 501 | 502 | label.css("top", labelTop); 503 | label.css("left", labelLeft); 504 | 505 | // check to make sure that the label is not outside the canvas 506 | 507 | if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { 508 | return false; 509 | } 510 | 511 | if (options.series.pie.label.background.opacity != 0) { 512 | 513 | // put in the transparent background separately to avoid blended labels and label boxes 514 | 515 | var c = options.series.pie.label.background.color; 516 | 517 | if (c == null) { 518 | c = slice.color; 519 | } 520 | 521 | var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; 522 | $("
") 523 | .css("opacity", options.series.pie.label.background.opacity) 524 | .insertBefore(label); 525 | } 526 | 527 | return true; 528 | } // end individual label function 529 | } // end drawLabels function 530 | } // end drawPie function 531 | } // end draw function 532 | 533 | // Placed here because it needs to be accessed from multiple locations 534 | 535 | function drawDonutHole(layer) { 536 | if (options.series.pie.innerRadius > 0) { 537 | 538 | // subtract the center 539 | 540 | layer.save(); 541 | var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; 542 | layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color 543 | layer.beginPath(); 544 | layer.fillStyle = options.series.pie.stroke.color; 545 | layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); 546 | layer.fill(); 547 | layer.closePath(); 548 | layer.restore(); 549 | 550 | // add inner stroke 551 | 552 | layer.save(); 553 | layer.beginPath(); 554 | layer.strokeStyle = options.series.pie.stroke.color; 555 | layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); 556 | layer.stroke(); 557 | layer.closePath(); 558 | layer.restore(); 559 | 560 | // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. 561 | } 562 | } 563 | 564 | //-- Additional Interactive related functions -- 565 | 566 | function isPointInPoly(poly, pt) { 567 | for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) 568 | ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) 569 | && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) 570 | && (c = !c); 571 | return c; 572 | } 573 | 574 | function findNearbySlice(mouseX, mouseY) { 575 | 576 | var slices = plot.getData(), 577 | options = plot.getOptions(), 578 | radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, 579 | x, y; 580 | 581 | for (var i = 0; i < slices.length; ++i) { 582 | 583 | var s = slices[i]; 584 | 585 | if (s.pie.show) { 586 | 587 | ctx.save(); 588 | ctx.beginPath(); 589 | ctx.moveTo(0, 0); // Center of the pie 590 | //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. 591 | ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); 592 | ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); 593 | ctx.closePath(); 594 | x = mouseX - centerLeft; 595 | y = mouseY - centerTop; 596 | 597 | if (ctx.isPointInPath) { 598 | if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { 599 | ctx.restore(); 600 | return { 601 | datapoint: [s.percent, s.data], 602 | dataIndex: 0, 603 | series: s, 604 | seriesIndex: i 605 | }; 606 | } 607 | } else { 608 | 609 | // excanvas for IE doesn;t support isPointInPath, this is a workaround. 610 | 611 | var p1X = radius * Math.cos(s.startAngle), 612 | p1Y = radius * Math.sin(s.startAngle), 613 | p2X = radius * Math.cos(s.startAngle + s.angle / 4), 614 | p2Y = radius * Math.sin(s.startAngle + s.angle / 4), 615 | p3X = radius * Math.cos(s.startAngle + s.angle / 2), 616 | p3Y = radius * Math.sin(s.startAngle + s.angle / 2), 617 | p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), 618 | p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), 619 | p5X = radius * Math.cos(s.startAngle + s.angle), 620 | p5Y = radius * Math.sin(s.startAngle + s.angle), 621 | arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], 622 | arrPoint = [x, y]; 623 | 624 | // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt? 625 | 626 | if (isPointInPoly(arrPoly, arrPoint)) { 627 | ctx.restore(); 628 | return { 629 | datapoint: [s.percent, s.data], 630 | dataIndex: 0, 631 | series: s, 632 | seriesIndex: i 633 | }; 634 | } 635 | } 636 | 637 | ctx.restore(); 638 | } 639 | } 640 | 641 | return null; 642 | } 643 | 644 | function onMouseMove(e) { 645 | triggerClickHoverEvent("plothover", e); 646 | } 647 | 648 | function onClick(e) { 649 | triggerClickHoverEvent("plotclick", e); 650 | } 651 | 652 | // trigger click or hover event (they send the same parameters so we share their code) 653 | 654 | function triggerClickHoverEvent(eventname, e) { 655 | 656 | var offset = plot.offset(); 657 | var canvasX = parseInt(e.pageX - offset.left); 658 | var canvasY = parseInt(e.pageY - offset.top); 659 | var item = findNearbySlice(canvasX, canvasY); 660 | 661 | if (options.grid.autoHighlight) { 662 | 663 | // clear auto-highlights 664 | 665 | for (var i = 0; i < highlights.length; ++i) { 666 | var h = highlights[i]; 667 | if (h.auto == eventname && !(item && h.series == item.series)) { 668 | unhighlight(h.series); 669 | } 670 | } 671 | } 672 | 673 | // highlight the slice 674 | 675 | if (item) { 676 | highlight(item.series, eventname); 677 | } 678 | 679 | // trigger any hover bind events 680 | 681 | var pos = { pageX: e.pageX, pageY: e.pageY }; 682 | target.trigger(eventname, [pos, item]); 683 | } 684 | 685 | function highlight(s, auto) { 686 | //if (typeof s == "number") { 687 | // s = series[s]; 688 | //} 689 | 690 | var i = indexOfHighlight(s); 691 | 692 | if (i == -1) { 693 | highlights.push({ series: s, auto: auto }); 694 | plot.triggerRedrawOverlay(); 695 | } else if (!auto) { 696 | highlights[i].auto = false; 697 | } 698 | } 699 | 700 | function unhighlight(s) { 701 | if (s == null) { 702 | highlights = []; 703 | plot.triggerRedrawOverlay(); 704 | } 705 | 706 | //if (typeof s == "number") { 707 | // s = series[s]; 708 | //} 709 | 710 | var i = indexOfHighlight(s); 711 | 712 | if (i != -1) { 713 | highlights.splice(i, 1); 714 | plot.triggerRedrawOverlay(); 715 | } 716 | } 717 | 718 | function indexOfHighlight(s) { 719 | for (var i = 0; i < highlights.length; ++i) { 720 | var h = highlights[i]; 721 | if (h.series == s) 722 | return i; 723 | } 724 | return -1; 725 | } 726 | 727 | function drawOverlay(plot, octx) { 728 | 729 | var options = plot.getOptions(); 730 | 731 | var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; 732 | 733 | octx.save(); 734 | octx.translate(centerLeft, centerTop); 735 | octx.scale(1, options.series.pie.tilt); 736 | 737 | for (var i = 0; i < highlights.length; ++i) { 738 | drawHighlight(highlights[i].series); 739 | } 740 | 741 | drawDonutHole(octx); 742 | 743 | octx.restore(); 744 | 745 | function drawHighlight(series) { 746 | 747 | if (series.angle <= 0 || isNaN(series.angle)) { 748 | return; 749 | } 750 | 751 | //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); 752 | octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor 753 | octx.beginPath(); 754 | if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { 755 | octx.moveTo(0, 0); // Center of the pie 756 | } 757 | octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); 758 | octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); 759 | octx.closePath(); 760 | octx.fill(); 761 | } 762 | } 763 | } // end init (plugin body) 764 | 765 | // define pie specific options and their default values 766 | 767 | var options = { 768 | series: { 769 | pie: { 770 | show: false, 771 | radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) 772 | innerRadius: 0, /* for donut */ 773 | startAngle: 3/2, 774 | tilt: 1, 775 | shadow: { 776 | left: 5, // shadow left offset 777 | top: 15, // shadow top offset 778 | alpha: 0.02 // shadow alpha 779 | }, 780 | offset: { 781 | top: 0, 782 | left: "auto" 783 | }, 784 | stroke: { 785 | color: "#fff", 786 | width: 1 787 | }, 788 | label: { 789 | show: "auto", 790 | formatter: function(label, slice) { 791 | return "
" + label + "
" + Math.round(slice.percent) + "%
"; 792 | }, // formatter function 793 | radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) 794 | background: { 795 | color: null, 796 | opacity: 0 797 | }, 798 | threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) 799 | }, 800 | combine: { 801 | threshold: -1, // percentage at which to combine little slices into one larger slice 802 | color: null, // color to give the new slice (auto-generated if null) 803 | label: "Other" // label to give the new slice 804 | }, 805 | highlight: { 806 | //color: "#fff", // will add this functionality once parseColor is available 807 | opacity: 0.5 808 | } 809 | } 810 | } 811 | }; 812 | 813 | $.plot.plugins.push({ 814 | init: init, 815 | options: options, 816 | name: "pie", 817 | version: "1.1" 818 | }); 819 | 820 | })(jQuery); 821 | -------------------------------------------------------------------------------- /src/lib/jquery.flot.time.js: -------------------------------------------------------------------------------- 1 | /* Pretty handling of time axes. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | Set axis.mode to "time" to enable. See the section "Time series data" in 7 | API.txt for details. 8 | 9 | */ 10 | 11 | (function($) { 12 | 13 | var options = { 14 | xaxis: { 15 | timezone: null, // "browser" for local to the client or timezone for timezone-js 16 | timeformat: null, // format string to use 17 | twelveHourClock: false, // 12 or 24 time in time mode 18 | monthNames: null // list of names of months 19 | } 20 | }; 21 | 22 | // round to nearby lower multiple of base 23 | 24 | function floorInBase(n, base) { 25 | return base * Math.floor(n / base); 26 | } 27 | 28 | // Returns a string with the date d formatted according to fmt. 29 | // A subset of the Open Group's strftime format is supported. 30 | 31 | function formatDate(d, fmt, monthNames, dayNames) { 32 | 33 | if (typeof d.strftime == "function") { 34 | return d.strftime(fmt); 35 | } 36 | 37 | var leftPad = function(n, pad) { 38 | n = "" + n; 39 | pad = "" + (pad == null ? "0" : pad); 40 | return n.length == 1 ? pad + n : n; 41 | }; 42 | 43 | var r = []; 44 | var escape = false; 45 | var hours = d.getHours(); 46 | var isAM = hours < 12; 47 | 48 | if (monthNames == null) { 49 | monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 50 | } 51 | 52 | if (dayNames == null) { 53 | dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 54 | } 55 | 56 | var hours12; 57 | 58 | if (hours > 12) { 59 | hours12 = hours - 12; 60 | } else if (hours == 0) { 61 | hours12 = 12; 62 | } else { 63 | hours12 = hours; 64 | } 65 | 66 | for (var i = 0; i < fmt.length; ++i) { 67 | 68 | var c = fmt.charAt(i); 69 | 70 | if (escape) { 71 | switch (c) { 72 | case 'a': c = "" + dayNames[d.getDay()]; break; 73 | case 'b': c = "" + monthNames[d.getMonth()]; break; 74 | case 'd': c = leftPad(d.getDate()); break; 75 | case 'e': c = leftPad(d.getDate(), " "); break; 76 | case 'h': // For back-compat with 0.7; remove in 1.0 77 | case 'H': c = leftPad(hours); break; 78 | case 'I': c = leftPad(hours12); break; 79 | case 'l': c = leftPad(hours12, " "); break; 80 | case 'm': c = leftPad(d.getMonth() + 1); break; 81 | case 'M': c = leftPad(d.getMinutes()); break; 82 | // quarters not in Open Group's strftime specification 83 | case 'q': 84 | c = "" + (Math.floor(d.getMonth() / 3) + 1); break; 85 | case 'S': c = leftPad(d.getSeconds()); break; 86 | case 'y': c = leftPad(d.getFullYear() % 100); break; 87 | case 'Y': c = "" + d.getFullYear(); break; 88 | case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; 89 | case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; 90 | case 'w': c = "" + d.getDay(); break; 91 | } 92 | r.push(c); 93 | escape = false; 94 | } else { 95 | if (c == "%") { 96 | escape = true; 97 | } else { 98 | r.push(c); 99 | } 100 | } 101 | } 102 | 103 | return r.join(""); 104 | } 105 | 106 | // To have a consistent view of time-based data independent of which time 107 | // zone the client happens to be in we need a date-like object independent 108 | // of time zones. This is done through a wrapper that only calls the UTC 109 | // versions of the accessor methods. 110 | 111 | function makeUtcWrapper(d) { 112 | 113 | function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { 114 | sourceObj[sourceMethod] = function() { 115 | return targetObj[targetMethod].apply(targetObj, arguments); 116 | }; 117 | }; 118 | 119 | var utc = { 120 | date: d 121 | }; 122 | 123 | // support strftime, if found 124 | 125 | if (d.strftime != undefined) { 126 | addProxyMethod(utc, "strftime", d, "strftime"); 127 | } 128 | 129 | addProxyMethod(utc, "getTime", d, "getTime"); 130 | addProxyMethod(utc, "setTime", d, "setTime"); 131 | 132 | var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; 133 | 134 | for (var p = 0; p < props.length; p++) { 135 | addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); 136 | addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); 137 | } 138 | 139 | return utc; 140 | }; 141 | 142 | // select time zone strategy. This returns a date-like object tied to the 143 | // desired timezone 144 | 145 | function dateGenerator(ts, opts) { 146 | if (opts.timezone == "browser") { 147 | return new Date(ts); 148 | } else if (!opts.timezone || opts.timezone == "utc") { 149 | return makeUtcWrapper(new Date(ts)); 150 | } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { 151 | var d = new timezoneJS.Date(); 152 | // timezone-js is fickle, so be sure to set the time zone before 153 | // setting the time. 154 | d.setTimezone(opts.timezone); 155 | d.setTime(ts); 156 | return d; 157 | } else { 158 | return makeUtcWrapper(new Date(ts)); 159 | } 160 | } 161 | 162 | // map of app. size of time units in milliseconds 163 | 164 | var timeUnitSize = { 165 | "second": 1000, 166 | "minute": 60 * 1000, 167 | "hour": 60 * 60 * 1000, 168 | "day": 24 * 60 * 60 * 1000, 169 | "month": 30 * 24 * 60 * 60 * 1000, 170 | "quarter": 3 * 30 * 24 * 60 * 60 * 1000, 171 | "year": 365.2425 * 24 * 60 * 60 * 1000 172 | }; 173 | 174 | // the allowed tick sizes, after 1 year we use 175 | // an integer algorithm 176 | 177 | var baseSpec = [ 178 | [1, "second"], [2, "second"], [5, "second"], [10, "second"], 179 | [30, "second"], 180 | [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], 181 | [30, "minute"], 182 | [1, "hour"], [2, "hour"], [4, "hour"], 183 | [8, "hour"], [12, "hour"], 184 | [1, "day"], [2, "day"], [3, "day"], 185 | [0.25, "month"], [0.5, "month"], [1, "month"], 186 | [2, "month"] 187 | ]; 188 | 189 | // we don't know which variant(s) we'll need yet, but generating both is 190 | // cheap 191 | 192 | var specMonths = baseSpec.concat([[3, "month"], [6, "month"], 193 | [1, "year"]]); 194 | var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], 195 | [1, "year"]]); 196 | 197 | function init(plot) { 198 | plot.hooks.processOptions.push(function (plot, options) { 199 | $.each(plot.getAxes(), function(axisName, axis) { 200 | 201 | var opts = axis.options; 202 | 203 | if (opts.mode == "time") { 204 | axis.tickGenerator = function(axis) { 205 | 206 | var ticks = []; 207 | var d = dateGenerator(axis.min, opts); 208 | var minSize = 0; 209 | 210 | // make quarter use a possibility if quarters are 211 | // mentioned in either of these options 212 | 213 | var spec = (opts.tickSize && opts.tickSize[1] === 214 | "quarter") || 215 | (opts.minTickSize && opts.minTickSize[1] === 216 | "quarter") ? specQuarters : specMonths; 217 | 218 | if (opts.minTickSize != null) { 219 | if (typeof opts.tickSize == "number") { 220 | minSize = opts.tickSize; 221 | } else { 222 | minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; 223 | } 224 | } 225 | 226 | for (var i = 0; i < spec.length - 1; ++i) { 227 | if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] 228 | + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 229 | && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { 230 | break; 231 | } 232 | } 233 | 234 | var size = spec[i][0]; 235 | var unit = spec[i][1]; 236 | 237 | // special-case the possibility of several years 238 | 239 | if (unit == "year") { 240 | 241 | // if given a minTickSize in years, just use it, 242 | // ensuring that it's an integer 243 | 244 | if (opts.minTickSize != null && opts.minTickSize[1] == "year") { 245 | size = Math.floor(opts.minTickSize[0]); 246 | } else { 247 | 248 | var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); 249 | var norm = (axis.delta / timeUnitSize.year) / magn; 250 | 251 | if (norm < 1.5) { 252 | size = 1; 253 | } else if (norm < 3) { 254 | size = 2; 255 | } else if (norm < 7.5) { 256 | size = 5; 257 | } else { 258 | size = 10; 259 | } 260 | 261 | size *= magn; 262 | } 263 | 264 | // minimum size for years is 1 265 | 266 | if (size < 1) { 267 | size = 1; 268 | } 269 | } 270 | 271 | axis.tickSize = opts.tickSize || [size, unit]; 272 | var tickSize = axis.tickSize[0]; 273 | unit = axis.tickSize[1]; 274 | 275 | var step = tickSize * timeUnitSize[unit]; 276 | 277 | if (unit == "second") { 278 | d.setSeconds(floorInBase(d.getSeconds(), tickSize)); 279 | } else if (unit == "minute") { 280 | d.setMinutes(floorInBase(d.getMinutes(), tickSize)); 281 | } else if (unit == "hour") { 282 | d.setHours(floorInBase(d.getHours(), tickSize)); 283 | } else if (unit == "month") { 284 | d.setMonth(floorInBase(d.getMonth(), tickSize)); 285 | } else if (unit == "quarter") { 286 | d.setMonth(3 * floorInBase(d.getMonth() / 3, 287 | tickSize)); 288 | } else if (unit == "year") { 289 | d.setFullYear(floorInBase(d.getFullYear(), tickSize)); 290 | } 291 | 292 | // reset smaller components 293 | 294 | d.setMilliseconds(0); 295 | 296 | if (step >= timeUnitSize.minute) { 297 | d.setSeconds(0); 298 | } 299 | if (step >= timeUnitSize.hour) { 300 | d.setMinutes(0); 301 | } 302 | if (step >= timeUnitSize.day) { 303 | d.setHours(0); 304 | } 305 | if (step >= timeUnitSize.day * 4) { 306 | d.setDate(1); 307 | } 308 | if (step >= timeUnitSize.month * 2) { 309 | d.setMonth(floorInBase(d.getMonth(), 3)); 310 | } 311 | if (step >= timeUnitSize.quarter * 2) { 312 | d.setMonth(floorInBase(d.getMonth(), 6)); 313 | } 314 | if (step >= timeUnitSize.year) { 315 | d.setMonth(0); 316 | } 317 | 318 | var carry = 0; 319 | var v = Number.NaN; 320 | var prev; 321 | 322 | do { 323 | 324 | prev = v; 325 | v = d.getTime(); 326 | ticks.push(v); 327 | 328 | if (unit == "month" || unit == "quarter") { 329 | if (tickSize < 1) { 330 | 331 | // a bit complicated - we'll divide the 332 | // month/quarter up but we need to take 333 | // care of fractions so we don't end up in 334 | // the middle of a day 335 | 336 | d.setDate(1); 337 | var start = d.getTime(); 338 | d.setMonth(d.getMonth() + 339 | (unit == "quarter" ? 3 : 1)); 340 | var end = d.getTime(); 341 | d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); 342 | carry = d.getHours(); 343 | d.setHours(0); 344 | } else { 345 | d.setMonth(d.getMonth() + 346 | tickSize * (unit == "quarter" ? 3 : 1)); 347 | } 348 | } else if (unit == "year") { 349 | d.setFullYear(d.getFullYear() + tickSize); 350 | } else { 351 | d.setTime(v + step); 352 | } 353 | } while (v < axis.max && v != prev); 354 | 355 | return ticks; 356 | }; 357 | 358 | axis.tickFormatter = function (v, axis) { 359 | 360 | var d = dateGenerator(v, axis.options); 361 | 362 | // first check global format 363 | 364 | if (opts.timeformat != null) { 365 | return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); 366 | } 367 | 368 | // possibly use quarters if quarters are mentioned in 369 | // any of these places 370 | 371 | var useQuarters = (axis.options.tickSize && 372 | axis.options.tickSize[1] == "quarter") || 373 | (axis.options.minTickSize && 374 | axis.options.minTickSize[1] == "quarter"); 375 | 376 | var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; 377 | var span = axis.max - axis.min; 378 | var suffix = (opts.twelveHourClock) ? " %p" : ""; 379 | var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; 380 | var fmt; 381 | 382 | if (t < timeUnitSize.minute) { 383 | fmt = hourCode + ":%M:%S" + suffix; 384 | } else if (t < timeUnitSize.day) { 385 | if (span < 2 * timeUnitSize.day) { 386 | fmt = hourCode + ":%M" + suffix; 387 | } else { 388 | fmt = "%b %d " + hourCode + ":%M" + suffix; 389 | } 390 | } else if (t < timeUnitSize.month) { 391 | fmt = "%b %d"; 392 | } else if ((useQuarters && t < timeUnitSize.quarter) || 393 | (!useQuarters && t < timeUnitSize.year)) { 394 | if (span < timeUnitSize.year) { 395 | fmt = "%b"; 396 | } else { 397 | fmt = "%b %Y"; 398 | } 399 | } else if (useQuarters && t < timeUnitSize.year) { 400 | if (span < timeUnitSize.year) { 401 | fmt = "Q%q"; 402 | } else { 403 | fmt = "Q%q %Y"; 404 | } 405 | } else { 406 | fmt = "%Y"; 407 | } 408 | 409 | var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); 410 | 411 | return rt; 412 | }; 413 | } 414 | }); 415 | }); 416 | } 417 | 418 | $.plot.plugins.push({ 419 | init: init, 420 | options: options, 421 | name: 'time', 422 | version: '1.0' 423 | }); 424 | 425 | // Time-axis support used to be in Flot core, which exposed the 426 | // formatDate function on the plot object. Various plugins depend 427 | // on the function, so we need to re-expose it here. 428 | 429 | $.plot.formatDate = formatDate; 430 | $.plot.dateGenerator = dateGenerator; 431 | 432 | })(jQuery); 433 | -------------------------------------------------------------------------------- /src/lib/perfect-scrollbar.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * perfect-scrollbar v1.2.0 3 | * (c) 2017 Hyunje Jun 4 | * @license MIT 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.PerfectScrollbar=e()}(this,function(){"use strict";function t(t){return getComputedStyle(t)}function e(t,e){for(var i in e){var n=e[i];"number"==typeof n&&(n+="px"),t.style[i]=n}return t}function i(t){var e=document.createElement("div");return e.className=t,e}function n(t,e){if(!v)throw new Error("No element matching method supported");return v.call(t,e)}function r(t){t.remove?t.remove():t.parentNode&&t.parentNode.removeChild(t)}function l(t,e){return Array.prototype.filter.call(t.children,function(t){return n(t,e)})}function o(t,e){var i=t.element.classList,n=m.state.scrolling(e);i.contains(n)?clearTimeout(w[e]):i.add(n)}function s(t,e){w[e]=setTimeout(function(){return t.isAlive&&t.element.classList.remove(m.state.scrolling(e))},t.settings.scrollingThreshold)}function a(t,e){o(t,e),s(t,e)}function c(t){if("function"==typeof window.CustomEvent)return new CustomEvent(t);var e=document.createEvent("CustomEvent");return e.initCustomEvent(t,!1,!1,void 0),e}function h(t,e,i,n,r){var l=i[0],o=i[1],s=i[2],h=i[3],u=i[4],d=i[5];void 0===n&&(n=!0),void 0===r&&(r=!1);var p=t.element;t.reach[h]=null,p[s]<1&&(t.reach[h]="start"),p[s]>t[l]-t[o]-1&&(t.reach[h]="end"),e&&(p.dispatchEvent(c("ps-scroll-"+h)),e<0?p.dispatchEvent(c("ps-scroll-"+u)):e>0&&p.dispatchEvent(c("ps-scroll-"+d)),n&&a(t,h)),t.reach[h]&&(e||r)&&p.dispatchEvent(c("ps-"+h+"-reach-"+t.reach[h]))}function u(t){return parseInt(t,10)||0}function d(t){return n(t,"input,[contenteditable]")||n(t,"select,[contenteditable]")||n(t,"textarea,[contenteditable]")||n(t,"button,[contenteditable]")}function p(e){var i=t(e);return u(i.width)+u(i.paddingLeft)+u(i.paddingRight)+u(i.borderLeftWidth)+u(i.borderRightWidth)}function f(t,e){return t.settings.minScrollbarLength&&(e=Math.max(e,t.settings.minScrollbarLength)),t.settings.maxScrollbarLength&&(e=Math.min(e,t.settings.maxScrollbarLength)),e}function b(t,i){var n={width:i.railXWidth};i.isRtl?n.left=i.negativeScrollAdjustment+t.scrollLeft+i.containerWidth-i.contentWidth:n.left=t.scrollLeft,i.isScrollbarXUsingBottom?n.bottom=i.scrollbarXBottom-t.scrollTop:n.top=i.scrollbarXTop+t.scrollTop,e(i.scrollbarXRail,n);var r={top:t.scrollTop,height:i.railYHeight};i.isScrollbarYUsingRight?i.isRtl?r.right=i.contentWidth-(i.negativeScrollAdjustment+t.scrollLeft)-i.scrollbarYRight-i.scrollbarYOuterWidth:r.right=i.scrollbarYRight-t.scrollLeft:i.isRtl?r.left=i.negativeScrollAdjustment+t.scrollLeft+2*i.containerWidth-i.contentWidth-i.scrollbarYLeft-i.scrollbarYOuterWidth:r.left=i.scrollbarYLeft+t.scrollLeft,e(i.scrollbarYRail,r),e(i.scrollbarX,{left:i.scrollbarXLeft,width:i.scrollbarXWidth-i.railBorderXWidth}),e(i.scrollbarY,{top:i.scrollbarYTop,height:i.scrollbarYHeight-i.railBorderYWidth})}function g(t,e){function i(e){f[d]=b+v*(e[a]-g),o(t,p),R(t),e.stopPropagation(),e.preventDefault()}function n(){s(t,p),t.event.unbind(t.ownerDocument,"mousemove",i)}var r=e[0],l=e[1],a=e[2],c=e[3],h=e[4],u=e[5],d=e[6],p=e[7],f=t.element,b=null,g=null,v=null;t.event.bind(t[h],"mousedown",function(e){b=f[d],g=e[a],v=(t[l]-t[r])/(t[c]-t[u]),t.event.bind(t.ownerDocument,"mousemove",i),t.event.once(t.ownerDocument,"mouseup",n),e.stopPropagation(),e.preventDefault()})}var v=Element.prototype.matches||Element.prototype.webkitMatchesSelector||Element.prototype.msMatchesSelector,m={main:"ps",element:{thumb:function(t){return"ps__thumb-"+t},rail:function(t){return"ps__rail-"+t},consuming:"ps__child--consume"},state:{focus:"ps--focus",active:function(t){return"ps--active-"+t},scrolling:function(t){return"ps--scrolling-"+t}}},w={x:null,y:null},Y=function(t){this.element=t,this.handlers={}},X={isEmpty:{configurable:!0}};Y.prototype.bind=function(t,e){void 0===this.handlers[t]&&(this.handlers[t]=[]),this.handlers[t].push(e),this.element.addEventListener(t,e,!1)},Y.prototype.unbind=function(t,e){var i=this;this.handlers[t]=this.handlers[t].filter(function(n){return!(!e||n===e)||(i.element.removeEventListener(t,n,!1),!1)})},Y.prototype.unbindAll=function(){var t=this;for(var e in t.handlers)t.unbind(e)},X.isEmpty.get=function(){var t=this;return Object.keys(this.handlers).every(function(e){return 0===t.handlers[e].length})},Object.defineProperties(Y.prototype,X);var W=function(){this.eventElements=[]};W.prototype.eventElement=function(t){var e=this.eventElements.filter(function(e){return e.element===t})[0];return e||(e=new Y(t),this.eventElements.push(e)),e},W.prototype.bind=function(t,e,i){this.eventElement(t).bind(e,i)},W.prototype.unbind=function(t,e,i){var n=this.eventElement(t);n.unbind(e,i),n.isEmpty&&this.eventElements.splice(this.eventElements.indexOf(n),1)},W.prototype.unbindAll=function(){this.eventElements.forEach(function(t){return t.unbindAll()}),this.eventElements=[]},W.prototype.once=function(t,e,i){var n=this.eventElement(t),r=function(t){n.unbind(e,r),i(t)};n.bind(e,r)};var y=function(t,e,i,n,r){void 0===n&&(n=!0),void 0===r&&(r=!1);var l;if("top"===e)l=["contentHeight","containerHeight","scrollTop","y","up","down"];else{if("left"!==e)throw new Error("A proper axis should be provided");l=["contentWidth","containerWidth","scrollLeft","x","left","right"]}h(t,i,l,n,r)},L={isWebKit:document&&"WebkitAppearance"in document.documentElement.style,supportsTouch:window&&("ontouchstart"in window||window.DocumentTouch&&document instanceof window.DocumentTouch),supportsIePointer:navigator&&navigator.msMaxTouchPoints},R=function(t){var e=t.element;t.containerWidth=e.clientWidth,t.containerHeight=e.clientHeight,t.contentWidth=e.scrollWidth,t.contentHeight=e.scrollHeight,e.contains(t.scrollbarXRail)||(l(e,m.element.rail("x")).forEach(function(t){return r(t)}),e.appendChild(t.scrollbarXRail)),e.contains(t.scrollbarYRail)||(l(e,m.element.rail("y")).forEach(function(t){return r(t)}),e.appendChild(t.scrollbarYRail)),!t.settings.suppressScrollX&&t.containerWidth+t.settings.scrollXMarginOffset=t.railXWidth-t.scrollbarXWidth&&(t.scrollbarXLeft=t.railXWidth-t.scrollbarXWidth),t.scrollbarYTop>=t.railYHeight-t.scrollbarYHeight&&(t.scrollbarYTop=t.railYHeight-t.scrollbarYHeight),b(e,t),t.scrollbarXActive?e.classList.add(m.state.active("x")):(e.classList.remove(m.state.active("x")),t.scrollbarXWidth=0,t.scrollbarXLeft=0,e.scrollLeft=0),t.scrollbarYActive?e.classList.add(m.state.active("y")):(e.classList.remove(m.state.active("y")),t.scrollbarYHeight=0,t.scrollbarYTop=0,e.scrollTop=0)},T={"click-rail":function(t){t.event.bind(t.scrollbarY,"mousedown",function(t){return t.stopPropagation()}),t.event.bind(t.scrollbarYRail,"mousedown",function(e){var i=e.pageY-window.pageYOffset-t.scrollbarYRail.getBoundingClientRect().top>t.scrollbarYTop?1:-1;t.element.scrollTop+=i*t.containerHeight,R(t),e.stopPropagation()}),t.event.bind(t.scrollbarX,"mousedown",function(t){return t.stopPropagation()}),t.event.bind(t.scrollbarXRail,"mousedown",function(e){var i=e.pageX-window.pageXOffset-t.scrollbarXRail.getBoundingClientRect().left>t.scrollbarXLeft?1:-1;t.element.scrollLeft+=i*t.containerWidth,R(t),e.stopPropagation()})},"drag-thumb":function(t){g(t,["containerWidth","contentWidth","pageX","railXWidth","scrollbarX","scrollbarXWidth","scrollLeft","x"]),g(t,["containerHeight","contentHeight","pageY","railYHeight","scrollbarY","scrollbarYHeight","scrollTop","y"])},keyboard:function(t){function e(e,n){var r=i.scrollTop;if(0===e){if(!t.scrollbarYActive)return!1;if(0===r&&n>0||r>=t.contentHeight-t.containerHeight&&n<0)return!t.settings.wheelPropagation}var l=i.scrollLeft;if(0===n){if(!t.scrollbarXActive)return!1;if(0===l&&e<0||l>=t.contentWidth-t.containerWidth&&e>0)return!t.settings.wheelPropagation}return!0}var i=t.element,r=function(){return n(i,":hover")},l=function(){return n(t.scrollbarX,":focus")||n(t.scrollbarY,":focus")};t.event.bind(t.ownerDocument,"keydown",function(n){if(!(n.isDefaultPrevented&&n.isDefaultPrevented()||n.defaultPrevented)&&(r()||l())){var o=document.activeElement?document.activeElement:t.ownerDocument.activeElement;if(o){if("IFRAME"===o.tagName)o=o.contentDocument.activeElement;else for(;o.shadowRoot;)o=o.shadowRoot.activeElement;if(d(o))return}var s=0,a=0;switch(n.which){case 37:s=n.metaKey?-t.contentWidth:n.altKey?-t.containerWidth:-30;break;case 38:a=n.metaKey?t.contentHeight:n.altKey?t.containerHeight:30;break;case 39:s=n.metaKey?t.contentWidth:n.altKey?t.containerWidth:30;break;case 40:a=n.metaKey?-t.contentHeight:n.altKey?-t.containerHeight:-30;break;case 32:a=n.shiftKey?t.containerHeight:-t.containerHeight;break;case 33:a=t.containerHeight;break;case 34:a=-t.containerHeight;break;case 36:a=t.contentHeight;break;case 35:a=-t.contentHeight;break;default:return}t.settings.suppressScrollX&&0!==s||t.settings.suppressScrollY&&0!==a||(i.scrollTop-=a,i.scrollLeft+=s,R(t),e(s,a)&&n.preventDefault())}})},wheel:function(e){function i(t,i){var n=o.scrollTop;if(0===t){if(!e.scrollbarYActive)return!1;if(0===n&&i>0||n>=e.contentHeight-e.containerHeight&&i<0)return!e.settings.wheelPropagation}var r=o.scrollLeft;if(0===i){if(!e.scrollbarXActive)return!1;if(0===r&&t<0||r>=e.contentWidth-e.containerWidth&&t>0)return!e.settings.wheelPropagation}return!0}function n(t){var e=t.deltaX,i=-1*t.deltaY;return void 0!==e&&void 0!==i||(e=-1*t.wheelDeltaX/6,i=t.wheelDeltaY/6),t.deltaMode&&1===t.deltaMode&&(e*=10,i*=10),e!==e&&i!==i&&(e=0,i=t.wheelDelta),t.shiftKey?[-i,-e]:[e,i]}function r(e,i,n){if(!L.isWebKit&&o.querySelector("select:focus"))return!0;if(!o.contains(e))return!1;for(var r=e;r&&r!==o;){if(r.classList.contains(m.element.consuming))return!0;var l=t(r);if([l.overflow,l.overflowX,l.overflowY].join("").match(/(scroll|auto)/)){var s=r.scrollHeight-r.clientHeight;if(s>0&&!(0===r.scrollTop&&n>0||r.scrollTop===s&&n<0))return!0;var a=r.scrollLeft-r.clientWidth;if(a>0&&!(0===r.scrollLeft&&i<0||r.scrollLeft===a&&i>0))return!0}r=r.parentNode}return!1}function l(t){var l=n(t),s=l[0],a=l[1];if(!r(t.target,s,a)){var c=!1;e.settings.useBothWheelAxes?e.scrollbarYActive&&!e.scrollbarXActive?(a?o.scrollTop-=a*e.settings.wheelSpeed:o.scrollTop+=s*e.settings.wheelSpeed,c=!0):e.scrollbarXActive&&!e.scrollbarYActive&&(s?o.scrollLeft+=s*e.settings.wheelSpeed:o.scrollLeft-=a*e.settings.wheelSpeed,c=!0):(o.scrollTop-=a*e.settings.wheelSpeed,o.scrollLeft+=s*e.settings.wheelSpeed),R(e),(c=c||i(s,a))&&!t.ctrlKey&&(t.stopPropagation(),t.preventDefault())}}var o=e.element;void 0!==window.onwheel?e.event.bind(o,"wheel",l):void 0!==window.onmousewheel&&e.event.bind(o,"mousewheel",l)},touch:function(t){function e(e,i){var n=h.scrollTop,r=h.scrollLeft,l=Math.abs(e),o=Math.abs(i);if(o>l){if(i<0&&n===t.contentHeight-t.containerHeight||i>0&&0===n)return{stop:!t.settings.swipePropagation,prevent:0===window.scrollY}}else if(l>o&&(e<0&&r===t.contentWidth-t.containerWidth||e>0&&0===r))return{stop:!t.settings.swipePropagation,prevent:!0};return{stop:!0,prevent:!0}}function i(e,i){h.scrollTop-=i,h.scrollLeft-=e,R(t)}function n(){b=!0}function r(){b=!1}function l(t){return t.targetTouches?t.targetTouches[0]:t}function o(t){return!(t.pointerType&&"pen"===t.pointerType&&0===t.buttons||(!t.targetTouches||1!==t.targetTouches.length)&&(!t.pointerType||"mouse"===t.pointerType||t.pointerType===t.MSPOINTER_TYPE_MOUSE))}function s(t){if(o(t)){g=!0;var e=l(t);u.pageX=e.pageX,u.pageY=e.pageY,d=(new Date).getTime(),null!==f&&clearInterval(f),t.stopPropagation()}}function a(n){if(!g&&t.settings.swipePropagation&&s(n),!b&&g&&o(n)){var r=l(n),a={pageX:r.pageX,pageY:r.pageY},c=a.pageX-u.pageX,h=a.pageY-u.pageY;i(c,h),u=a;var f=(new Date).getTime(),v=f-d;v>0&&(p.x=c/v,p.y=h/v,d=f);var m=e(c,h),w=m.stop,Y=m.prevent;w&&n.stopPropagation(),Y&&n.preventDefault()}}function c(){!b&&g&&(g=!1,t.settings.swipeEasing&&(clearInterval(f),f=setInterval(function(){t.isInitialized?clearInterval(f):p.x||p.y?Math.abs(p.x)<.01&&Math.abs(p.y)<.01?clearInterval(f):(i(30*p.x,30*p.y),p.x*=.8,p.y*=.8):clearInterval(f)},10)))}if(L.supportsTouch||L.supportsIePointer){var h=t.element,u={},d=0,p={},f=null,b=!1,g=!1;L.supportsTouch?(t.event.bind(window,"touchstart",n),t.event.bind(window,"touchend",r),t.event.bind(h,"touchstart",s),t.event.bind(h,"touchmove",a),t.event.bind(h,"touchend",c)):L.supportsIePointer&&(window.PointerEvent?(t.event.bind(window,"pointerdown",n),t.event.bind(window,"pointerup",r),t.event.bind(h,"pointerdown",s),t.event.bind(h,"pointermove",a),t.event.bind(h,"pointerup",c)):window.MSPointerEvent&&(t.event.bind(window,"MSPointerDown",n),t.event.bind(window,"MSPointerUp",r),t.event.bind(h,"MSPointerDown",s),t.event.bind(h,"MSPointerMove",a),t.event.bind(h,"MSPointerUp",c)))}}},H=function(n,r){var l=this;if(void 0===r&&(r={}),"string"==typeof n&&(n=document.querySelector(n)),!n||!n.nodeName)throw new Error("no element is specified to initialize PerfectScrollbar");this.element=n,n.classList.add(m.main),this.settings={handlers:["click-rail","drag-thumb","keyboard","wheel","touch"],maxScrollbarLength:null,minScrollbarLength:null,scrollingThreshold:1e3,scrollXMarginOffset:0,scrollYMarginOffset:0,suppressScrollX:!1,suppressScrollY:!1,swipePropagation:!0,swipeEasing:!0,useBothWheelAxes:!1,wheelPropagation:!1,wheelSpeed:1};for(var o in r)l.settings[o]=r[o];this.containerWidth=null,this.containerHeight=null,this.contentWidth=null,this.contentHeight=null;var s=function(){return n.classList.add(m.state.focus)},a=function(){return n.classList.remove(m.state.focus)};this.isRtl="rtl"===t(n).direction,this.isNegativeScroll=function(){var t=n.scrollLeft,e=null;return n.scrollLeft=-1,e=n.scrollLeft<0,n.scrollLeft=t,e}(),this.negativeScrollAdjustment=this.isNegativeScroll?n.scrollWidth-n.clientWidth:0,this.event=new W,this.ownerDocument=n.ownerDocument||document,this.scrollbarXRail=i(m.element.rail("x")),n.appendChild(this.scrollbarXRail),this.scrollbarX=i(m.element.thumb("x")),this.scrollbarXRail.appendChild(this.scrollbarX),this.scrollbarX.setAttribute("tabindex",0),this.event.bind(this.scrollbarX,"focus",s),this.event.bind(this.scrollbarX,"blur",a),this.scrollbarXActive=null,this.scrollbarXWidth=null,this.scrollbarXLeft=null;var c=t(this.scrollbarXRail);this.scrollbarXBottom=parseInt(c.bottom,10),isNaN(this.scrollbarXBottom)?(this.isScrollbarXUsingBottom=!1,this.scrollbarXTop=u(c.top)):this.isScrollbarXUsingBottom=!0,this.railBorderXWidth=u(c.borderLeftWidth)+u(c.borderRightWidth),e(this.scrollbarXRail,{display:"block"}),this.railXMarginWidth=u(c.marginLeft)+u(c.marginRight),e(this.scrollbarXRail,{display:""}),this.railXWidth=null,this.railXRatio=null,this.scrollbarYRail=i(m.element.rail("y")),n.appendChild(this.scrollbarYRail),this.scrollbarY=i(m.element.thumb("y")),this.scrollbarYRail.appendChild(this.scrollbarY),this.scrollbarY.setAttribute("tabindex",0),this.event.bind(this.scrollbarY,"focus",s),this.event.bind(this.scrollbarY,"blur",a),this.scrollbarYActive=null,this.scrollbarYHeight=null,this.scrollbarYTop=null;var h=t(this.scrollbarYRail);this.scrollbarYRight=parseInt(h.right,10),isNaN(this.scrollbarYRight)?(this.isScrollbarYUsingRight=!1,this.scrollbarYLeft=u(h.left)):this.isScrollbarYUsingRight=!0,this.scrollbarYOuterWidth=this.isRtl?p(this.scrollbarY):null,this.railBorderYWidth=u(h.borderTopWidth)+u(h.borderBottomWidth),e(this.scrollbarYRail,{display:"block"}),this.railYMarginHeight=u(h.marginTop)+u(h.marginBottom),e(this.scrollbarYRail,{display:""}),this.railYHeight=null,this.railYRatio=null,this.reach={x:n.scrollLeft<=0?"start":n.scrollLeft>=this.contentWidth-this.containerWidth?"end":null,y:n.scrollTop<=0?"start":n.scrollTop>=this.contentHeight-this.containerHeight?"end":null},this.isAlive=!0,this.settings.handlers.forEach(function(t){return T[t](l)}),this.lastScrollTop=n.scrollTop,this.lastScrollLeft=n.scrollLeft,this.event.bind(this.element,"scroll",function(t){return l.onScroll(t)}),R(this)};return H.prototype.update=function(){this.isAlive&&(this.negativeScrollAdjustment=this.isNegativeScroll?this.element.scrollWidth-this.element.clientWidth:0,e(this.scrollbarXRail,{display:"block"}),e(this.scrollbarYRail,{display:"block"}),this.railXMarginWidth=u(t(this.scrollbarXRail).marginLeft)+u(t(this.scrollbarXRail).marginRight),this.railYMarginHeight=u(t(this.scrollbarYRail).marginTop)+u(t(this.scrollbarYRail).marginBottom),e(this.scrollbarXRail,{display:"none"}),e(this.scrollbarYRail,{display:"none"}),R(this),y(this,"top",0,!1,!0),y(this,"left",0,!1,!0),e(this.scrollbarXRail,{display:""}),e(this.scrollbarYRail,{display:""}))},H.prototype.onScroll=function(t){this.isAlive&&(R(this),y(this,"top",this.element.scrollTop-this.lastScrollTop),y(this,"left",this.element.scrollLeft-this.lastScrollLeft),this.lastScrollTop=this.element.scrollTop,this.lastScrollLeft=this.element.scrollLeft)},H.prototype.destroy=function(){this.isAlive&&(this.event.unbindAll(),r(this.scrollbarX),r(this.scrollbarY),r(this.scrollbarXRail),r(this.scrollbarYRail),this.removePsClasses(),this.element=null,this.scrollbarX=null,this.scrollbarY=null,this.scrollbarXRail=null,this.scrollbarYRail=null,this.isAlive=!1)},H.prototype.removePsClasses=function(){this.element.className=this.element.className.split(" ").filter(function(t){return!t.match(/^ps([-_].+|)$/)}).join(" ")},H}); -------------------------------------------------------------------------------- /src/module.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { PieChartCtrl } from './piechart_ctrl'; 2 | import { loadPluginCss } from 'grafana/app/plugins/sdk'; 3 | 4 | loadPluginCss({ 5 | dark: 'plugins/grafana-piechart-panel/styles/dark.css', 6 | light: 'plugins/grafana-piechart-panel/styles/light.css', 7 | }); 8 | 9 | export { PieChartCtrl as PanelCtrl }; 10 | -------------------------------------------------------------------------------- /src/piechart_ctrl.test.ts: -------------------------------------------------------------------------------- 1 | describe('Basic test', () => { 2 | const text = 'hello'; 3 | 4 | it('should work', () => { 5 | expect(text).toBe('hello'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/piechart_ctrl.ts: -------------------------------------------------------------------------------- 1 | import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk'; 2 | import _ from 'lodash'; 3 | import kbn from 'grafana/app/core/utils/kbn'; 4 | import config from 'grafana/app/core/config'; 5 | // @ts-ignore 6 | import TimeSeries from 'grafana/app/core/time_series'; 7 | import rendering from './rendering'; 8 | import './legend'; 9 | 10 | class PieChartCtrl extends MetricsPanelCtrl { 11 | static templateUrl = 'module.html'; 12 | $rootScope: any; 13 | hiddenSeries: any; 14 | unitFormats: any; 15 | series: any; 16 | data: any; 17 | 18 | /** @ngInject */ 19 | constructor($scope: any, $injector: any, $rootScope: any) { 20 | super($scope, $injector); 21 | this.$rootScope = $rootScope; 22 | this.hiddenSeries = {}; 23 | 24 | const panelDefaults = { 25 | pieType: 'pie', 26 | legend: { 27 | show: true, // disable/enable legend 28 | values: true, 29 | }, 30 | links: [], 31 | datasource: null, 32 | interval: null, 33 | targets: [{}], 34 | cacheTimeout: null, 35 | nullPointMode: 'connected', 36 | legendType: 'Under graph', 37 | breakPoint: '50%', 38 | aliasColors: {}, 39 | format: 'short', 40 | valueName: 'current', 41 | strokeWidth: 1, 42 | fontSize: '80%', 43 | combine: { 44 | threshold: 0.0, 45 | label: 'Others', 46 | }, 47 | }; 48 | 49 | _.defaults(this.panel, panelDefaults); 50 | _.defaults(this.panel.legend, panelDefaults.legend); 51 | 52 | this.events.on('render', this.onRender.bind(this)); 53 | this.events.on('data-received', this.onDataReceived.bind(this)); 54 | this.events.on('data-error', this.onDataError.bind(this)); 55 | this.events.on('data-snapshot-load', this.onDataReceived.bind(this)); 56 | this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); 57 | 58 | this.setLegendWidthForLegacyBrowser(); 59 | } 60 | 61 | onInitEditMode() { 62 | this.addEditorTab('Options', 'public/plugins/grafana-piechart-panel/editor.html', 2); 63 | this.unitFormats = kbn.getUnitFormats(); 64 | } 65 | 66 | setUnitFormat(subItem: any) { 67 | this.panel.format = subItem.value; 68 | this.render(); 69 | } 70 | 71 | onDataError() { 72 | this.series = []; 73 | this.render(); 74 | } 75 | 76 | changeSeriesColor(series: any, color: any) { 77 | series.color = color; 78 | this.panel.aliasColors[series.alias] = series.color; 79 | this.render(); 80 | } 81 | 82 | migrateToPanel(type: string) { 83 | this.onPluginTypeChange(config.panels[type]); 84 | } 85 | 86 | onRender() { 87 | this.data = this.parseSeries(this.series); 88 | } 89 | 90 | parseSeries(series: any) { 91 | return _.map(this.series, (serie, i) => { 92 | return { 93 | label: serie.alias, 94 | data: serie.stats[this.panel.valueName], 95 | color: this.panel.aliasColors[serie.alias] || this.$rootScope.colors[i], 96 | legendData: serie.stats[this.panel.valueName], 97 | }; 98 | }); 99 | } 100 | 101 | onDataReceived(dataList: any) { 102 | this.series = dataList.map(this.seriesHandler.bind(this)); 103 | this.data = this.parseSeries(this.series); 104 | this.render(this.data); 105 | } 106 | 107 | seriesHandler(seriesData: any) { 108 | const series = new TimeSeries({ 109 | datapoints: seriesData.datapoints, 110 | alias: seriesData.target, 111 | }); 112 | 113 | series.flotpairs = series.getFlotPairs(this.panel.nullPointMode); 114 | return series; 115 | } 116 | 117 | getDecimalsForValue(value: any) { 118 | if (_.isNumber(this.panel.decimals)) { 119 | return { decimals: this.panel.decimals, scaledDecimals: null }; 120 | } 121 | 122 | const delta = value / 2; 123 | let dec = -Math.floor(Math.log(delta) / Math.LN10); 124 | 125 | const magn = Math.pow(10, -dec); 126 | const norm = delta / magn; // norm is between 1.0 and 10.0 127 | let size; 128 | 129 | if (norm < 1.5) { 130 | size = 1; 131 | } else if (norm < 3) { 132 | size = 2; 133 | // special case for 2.5, requires an extra decimal 134 | if (norm > 2.25) { 135 | size = 2.5; 136 | ++dec; 137 | } 138 | } else if (norm < 7.5) { 139 | size = 5; 140 | } else { 141 | size = 10; 142 | } 143 | 144 | size *= magn; 145 | 146 | // reduce starting decimals if not needed 147 | if (Math.floor(value) === value) { 148 | dec = 0; 149 | } 150 | 151 | const result = { 152 | decimals: 0, 153 | scaledDecimals: 0, 154 | }; 155 | result.decimals = Math.max(0, dec); 156 | result.scaledDecimals = result.decimals - Math.floor(Math.log(size) / Math.LN10) + 2; 157 | 158 | return result; 159 | } 160 | 161 | formatValue(value: any) { 162 | const decimalInfo = this.getDecimalsForValue(value); 163 | const formatFunc = kbn.valueFormats[this.panel.format]; 164 | if (formatFunc) { 165 | return formatFunc(value, decimalInfo.decimals, decimalInfo.scaledDecimals); 166 | } 167 | return value; 168 | } 169 | 170 | link(scope: any, elem: any, attrs: any, ctrl: any) { 171 | rendering(scope, elem, attrs, ctrl); 172 | } 173 | 174 | toggleSeries(serie: any) { 175 | if (this.hiddenSeries[serie.label]) { 176 | delete this.hiddenSeries[serie.label]; 177 | } else { 178 | this.hiddenSeries[serie.label] = true; 179 | } 180 | this.render(); 181 | } 182 | 183 | onLegendTypeChanged() { 184 | this.setLegendWidthForLegacyBrowser(); 185 | this.render(); 186 | } 187 | 188 | setLegendWidthForLegacyBrowser() { 189 | // @ts-ignore 190 | const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; 191 | if (isIE11 && this.panel.legendType === 'Right side' && !this.panel.legend.sideWidth) { 192 | this.panel.legend.sideWidth = 150; 193 | } 194 | } 195 | } 196 | 197 | export { PieChartCtrl, PieChartCtrl as MetricsPanelCtrl }; 198 | -------------------------------------------------------------------------------- /src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "name": "Pie Chart", 4 | "id": "grafana-piechart-panel", 5 | "info": { 6 | "description": "Pie chart panel for grafana", 7 | "author": { 8 | "name": "Grafana Labs", 9 | "url": "http://grafana.com" 10 | }, 11 | "keywords": ["piechart", "panel"], 12 | "logos": { 13 | "small": "img/piechart_logo_small.svg", 14 | "large": "img/piechart_logo_large.svg" 15 | }, 16 | "links": [ 17 | { "name": "Project site", "url": "https://github.com/grafana/piechart-panel" }, 18 | { "name": "Blog Post", "url": "https://blog.raintank.io/friends-dont-let-friends-abuse-pie-charts/" }, 19 | { "name": "MIT License", "url": "https://github.com/grafana/piechart-panel/blob/master/LICENSE" } 20 | ], 21 | "screenshots": [ 22 | { "name": "Donut!", "path": "img/piechart-donut.png" }, 23 | { "name": "Legend on the graph", "path": "img/piechart-legend-on-graph.png" }, 24 | { "name": "Legend to the right", "path": "img/piechart-legend-rhs.png" }, 25 | { "name": "Legend underneath", "path": "img/piechart-legend-under.png" }, 26 | { "name": "Piechart options", "path": "img/piechart-options.png" } 27 | ], 28 | "version": "%VERSION%", 29 | "updated": "%TODAY%" 30 | }, 31 | 32 | "dependencies": { 33 | "grafanaVersion": "4.6.5", 34 | "grafanaDependency": ">=4.6.5", 35 | "plugins": [] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/rendering.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import './lib/jquery.flot.pie'; 3 | import $ from 'jquery'; 4 | //import './lib/jquery.flot'; 5 | 6 | export default function link(scope: any, elem: any, attrs: any, ctrl: any) { 7 | let data; 8 | const panel = ctrl.panel; 9 | elem = elem.find('.piechart-panel__chart'); 10 | const $tooltip = $('
') as any; 11 | 12 | ctrl.events.on('render', () => { 13 | if (panel.legendType === 'Right side') { 14 | render(false); 15 | setTimeout(() => { 16 | render(true); 17 | }, 50); 18 | } else { 19 | render(true); 20 | } 21 | }); 22 | 23 | function getLegendHeight(panelHeight: any) { 24 | if (!ctrl.panel.legend.show || ctrl.panel.legendType === 'Right side' || ctrl.panel.legendType === 'On graph') { 25 | return 20; 26 | } 27 | 28 | if ((ctrl.panel.legendType === 'Under graph' && ctrl.panel.legend.percentage) || ctrl.panel.legend.values) { 29 | const breakPoint = parseInt(ctrl.panel.breakPoint, 10) / 100; 30 | const total = 23 + 20 * data.length; 31 | return Math.min(total, Math.floor(panelHeight * breakPoint)); 32 | } 33 | 34 | return 0; 35 | } 36 | 37 | function formatter(label: any, slice: any) { 38 | const sliceData = slice.data[0][slice.data[0].length - 1]; 39 | let decimal = 2; 40 | const start = `
${label}
`; 41 | 42 | if (ctrl.panel.legend.percentageDecimals) { 43 | decimal = ctrl.panel.legend.percentageDecimals; 44 | } 45 | if (ctrl.panel.legend.values && ctrl.panel.legend.percentage) { 46 | return start + ctrl.formatValue(sliceData) + '
' + slice.percent.toFixed(decimal) + '%
'; 47 | } else if (ctrl.panel.legend.values) { 48 | return start + ctrl.formatValue(sliceData) + '
'; 49 | } else if (ctrl.panel.legend.percentage) { 50 | return start + slice.percent.toFixed(decimal) + '%'; 51 | } else { 52 | return start + ''; 53 | } 54 | } 55 | 56 | function noDataPoints() { 57 | const html = '
No data points
'; 58 | elem.html(html); 59 | } 60 | 61 | function addPieChart() { 62 | const width = elem.width(); 63 | const height = ctrl.height - getLegendHeight(ctrl.height); 64 | 65 | const size = Math.min(width, height); 66 | 67 | const plotCanvas = $('
'); 68 | const plotCss = { 69 | margin: 'auto', 70 | position: 'relative', 71 | paddingBottom: 20 + 'px', 72 | height: size + 'px', 73 | }; 74 | 75 | plotCanvas.css(plotCss); 76 | 77 | const backgroundColor = $('body').css('background-color'); 78 | 79 | const options = { 80 | legend: { 81 | show: false, 82 | }, 83 | series: { 84 | pie: { 85 | radius: 1, 86 | innerRadius: 0, 87 | show: true, 88 | stroke: { 89 | color: backgroundColor, 90 | width: parseFloat(ctrl.panel.strokeWidth).toFixed(1), 91 | }, 92 | label: { 93 | show: ctrl.panel.legend.show && ctrl.panel.legendType === 'On graph', 94 | formatter: formatter, 95 | }, 96 | highlight: { 97 | opacity: 0.0, 98 | }, 99 | combine: { 100 | threshold: ctrl.panel.combine.threshold, 101 | label: ctrl.panel.combine.label, 102 | }, 103 | }, 104 | }, 105 | grid: { 106 | hoverable: true, 107 | clickable: false, 108 | }, 109 | }; 110 | 111 | if (panel.pieType === 'donut') { 112 | options.series.pie.innerRadius = 0.5; 113 | } 114 | 115 | data = ctrl.data; 116 | 117 | for (let i = 0; i < data.length; i++) { 118 | const series = data[i]; 119 | 120 | // if hidden remove points 121 | if (ctrl.hiddenSeries[series.label]) { 122 | series.data = {}; 123 | } 124 | } 125 | 126 | if (panel.legend.sort) { 127 | if (ctrl.panel.valueName !== panel.legend.sort) { 128 | panel.legend.sort = ctrl.panel.valueName; 129 | } 130 | if (panel.legend.sortDesc === true) { 131 | data.sort((a: any, b: any) => { 132 | return b.legendData - a.legendData; 133 | }); 134 | } else { 135 | data.sort((a: any, b: any) => { 136 | return a.legendData - b.legendData; 137 | }); 138 | } 139 | } 140 | 141 | elem.html(plotCanvas); 142 | 143 | // @ts-ignore 144 | $.plot(plotCanvas, data, options); 145 | plotCanvas.bind('plothover', (event: any, pos: any, item: any) => { 146 | if (!item) { 147 | $tooltip.detach(); 148 | return; 149 | } 150 | 151 | let body; 152 | const percent = parseFloat(item.series.percent).toFixed(2); 153 | const formatted = ctrl.formatValue(item.series.data[0][1]); 154 | 155 | body = '
'; 156 | body += '
' + _.escape(item.series.label) + ': ' + formatted; 157 | body += ' (' + percent + '%)' + '
'; 158 | body += '
'; 159 | 160 | $tooltip.html(body).place_tt(pos.pageX + 20, pos.pageY); 161 | }); 162 | } 163 | 164 | function render(incrementRenderCounter: any) { 165 | if (!ctrl.data) { 166 | return; 167 | } 168 | 169 | data = ctrl.data; 170 | 171 | if (0 === ctrl.data.length) { 172 | noDataPoints(); 173 | } else { 174 | addPieChart(); 175 | } 176 | 177 | if (incrementRenderCounter) { 178 | ctrl.renderingCompleted(); 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/styles/dark.scss: -------------------------------------------------------------------------------- 1 | @import '../../node_modules/@grafana/toolkit/sass/_variables.dark.scss'; 2 | @import 'piechart'; 3 | 4 | .pieLabel > div { 5 | color: $white; 6 | } 7 | 8 | .piechart-panel { 9 | display: flex; 10 | flex-direction: column; 11 | height: 100%; 12 | } 13 | 14 | .piechart-panel--legend-right { 15 | flex-direction: row; 16 | } 17 | 18 | .piechart-panel--legend-right .piechart-legend { 19 | flex: 0 1 10px; 20 | height: 100%; 21 | } 22 | 23 | .piechart-panel--legend-right .piechart-legend-series { 24 | display: block; 25 | padding-left: 0px; 26 | } 27 | 28 | .piechart-panel--legend-right .piechart-legend-table .piechart-legend-series { 29 | display: table-row; 30 | } 31 | 32 | .piechart-legend__container { 33 | height: 100%; 34 | } 35 | 36 | .piechart-panel__chart { 37 | position: relative; 38 | cursor: crosshair; 39 | flex-grow: 1; 40 | top: 10px; 41 | } 42 | 43 | .datapoints-warning { 44 | pointer: none; 45 | position: absolute; 46 | top: 50%; 47 | left: 50%; 48 | z-index: 10; 49 | margin-top: -50px; 50 | margin-left: -100px; 51 | width: 200px; 52 | text-align: center; 53 | cursor: auto; 54 | padding: 10px; 55 | } 56 | 57 | .piechart-legend { 58 | flex: 0 1 auto; 59 | margin: 0 1rem; 60 | text-align: center; 61 | padding-top: 6px; 62 | position: relative; 63 | } 64 | 65 | .piechart-legend .popover-content { 66 | padding: 0; 67 | } 68 | 69 | .piechart-legend-icon { 70 | position: relative; 71 | padding-right: 4px; 72 | top: 1px; 73 | } 74 | 75 | .piechart-legend-icon, 76 | .piechart-legend-alias, 77 | .piechart-legend-value { 78 | cursor: pointer; 79 | float: left; 80 | white-space: nowrap; 81 | font-size: 85%; 82 | text-align: left; 83 | color: #d8d9da; 84 | } 85 | 86 | .piechart-legend-icon.current::before { 87 | content: 'Current: '; 88 | } 89 | 90 | .piechart-legend-icon.max::before { 91 | content: 'Max: '; 92 | } 93 | 94 | .piechart-legend-icon.min::before { 95 | content: 'Min: '; 96 | } 97 | 98 | .piechart-legend-icon.total::before { 99 | content: 'Total: '; 100 | } 101 | 102 | .piechart-legend-icon.avg::before { 103 | content: 'Avg: '; 104 | } 105 | 106 | .piechart-legend-alias.current::before { 107 | content: 'Current: '; 108 | } 109 | 110 | .piechart-legend-alias.max::before { 111 | content: 'Max: '; 112 | } 113 | 114 | .piechart-legend-alias.min::before { 115 | content: 'Min: '; 116 | } 117 | 118 | .piechart-legend-alias.total::before { 119 | content: 'Total: '; 120 | } 121 | 122 | .piechart-legend-alias.avg::before { 123 | content: 'Avg: '; 124 | } 125 | 126 | .piechart-legend-value.current::before { 127 | content: 'Current: '; 128 | } 129 | 130 | .piechart-legend-value.max::before { 131 | content: 'Max: '; 132 | } 133 | 134 | .piechart-legend-value.min::before { 135 | content: 'Min: '; 136 | } 137 | 138 | .piechart-legend-value.total::before { 139 | content: 'Total: '; 140 | } 141 | 142 | .piechart-legend-value.avg::before { 143 | content: 'Avg: '; 144 | } 145 | 146 | .piechart-legend-icon .fa { 147 | font-size: 135%; 148 | position: relative; 149 | top: 1px; 150 | } 151 | 152 | .piechart-legend-series { 153 | float: left; 154 | white-space: nowrap; 155 | padding-left: 10px; 156 | } 157 | 158 | .piechart-legend-series--right-y { 159 | float: right; 160 | } 161 | 162 | .piechart-legend-value { 163 | padding-left: 6px; 164 | } 165 | 166 | .piechart-legend-table tbody { 167 | display: block; 168 | overflow-y: auto; 169 | overflow-x: hidden; 170 | height: 100%; 171 | padding-bottom: 1px; 172 | padding-right: 5px; 173 | } 174 | 175 | .piechart-legend-table .piechart-legend-series { 176 | display: table-row; 177 | float: none; 178 | padding-left: 0; 179 | } 180 | 181 | .piechart-legend-table .piechart-legend-series--right-y { 182 | float: none; 183 | } 184 | 185 | .piechart-legend-table .piechart-legend-series--right-y .piechart-legend-alias::after { 186 | content: '(right-y)'; 187 | padding: 0 5px; 188 | color: #8e8e8e; 189 | } 190 | 191 | .piechart-legend-table td, 192 | .piechart-legend-alias, 193 | .piechart-legend-icon, 194 | .piechart-legend-value { 195 | float: none; 196 | display: table-cell; 197 | white-space: nowrap; 198 | padding: 2px 10px; 199 | text-align: right; 200 | } 201 | 202 | .piechart-legend-table .piechart-legend-icon { 203 | width: 5px; 204 | padding: 0; 205 | top: 0; 206 | } 207 | 208 | .piechart-legend-table .piechart-legend-icon .fa { 209 | top: 4px; 210 | } 211 | 212 | .piechart-legend-table .piechart-legend-value { 213 | padding-left: 15px; 214 | } 215 | 216 | .piechart-legend-table .piechart-legend-alias { 217 | padding-left: 7px; 218 | text-align: left; 219 | width: 95%; 220 | max-width: 650px; 221 | text-overflow: ellipsis; 222 | overflow: hidden; 223 | } 224 | 225 | .piechart-legend-table .piechart-legend-series:nth-child(odd) { 226 | background: #262628; 227 | } 228 | 229 | .piechart-legend-table .piechart-legend-value.current::before { 230 | content: ''; 231 | } 232 | 233 | .piechart-legend-table .piechart-legend-value.max::before { 234 | content: ''; 235 | } 236 | 237 | .piechart-legend-table .piechart-legend-value.min::before { 238 | content: ''; 239 | } 240 | 241 | .piechart-legend-table .piechart-legend-value.total::before { 242 | content: ''; 243 | } 244 | 245 | .piechart-legend-table .piechart-legend-value.avg::before { 246 | content: ''; 247 | } 248 | 249 | .piechart-legend-table th { 250 | text-align: right; 251 | padding: 0px 10px 1px 0; 252 | font-weight: bold; 253 | color: #33b5e5; 254 | font-size: 85%; 255 | white-space: nowrap; 256 | } 257 | 258 | .piechart-legend-series-hidden .piechart-legend-value, 259 | .piechart-legend-series-hidden .piechart-legend-alias { 260 | color: #8e8e8e; 261 | } 262 | 263 | .piechart-tooltip { 264 | white-space: nowrap; 265 | font-size: 12px; 266 | background-color: #141414; 267 | color: #d8d9da; 268 | } 269 | 270 | .piechart-tooltip .piechart-tooltip-time { 271 | text-align: center; 272 | position: relative; 273 | top: -3px; 274 | padding: 0.2rem; 275 | font-weight: bold; 276 | color: #d8d9da; 277 | } 278 | 279 | .piechart-tooltip .piechart-tooltip-value { 280 | display: table-cell; 281 | font-weight: bold; 282 | padding-left: 15px; 283 | text-align: right; 284 | } 285 | 286 | .migrate-button { 287 | height: auto; 288 | min-height: 32px; 289 | } -------------------------------------------------------------------------------- /src/styles/light.scss: -------------------------------------------------------------------------------- 1 | @import '../../node_modules/@grafana/toolkit/sass/_variables.light.scss'; 2 | @import 'piechart'; 3 | 4 | .pieLabel > div { 5 | color: $black; 6 | } 7 | 8 | .piechart-panel { 9 | display: flex; 10 | flex-direction: column; 11 | height: 100%; 12 | } 13 | 14 | .piechart-panel--legend-right { 15 | flex-direction: row; 16 | } 17 | 18 | .piechart-panel--legend-right .piechart-legend { 19 | flex: 0 1 10px; 20 | height: 100%; 21 | } 22 | 23 | _:-ms-fullscreen, 24 | :root .piechart-panel--legend-right .piechart-legend { 25 | flex: 0 1 200px; 26 | } 27 | 28 | .piechart-panel--legend-right .piechart-legend-series { 29 | display: block; 30 | padding-left: 0px; 31 | } 32 | 33 | .piechart-panel--legend-right .piechart-legend-table .piechart-legend-series { 34 | display: table-row; 35 | } 36 | 37 | .piechart-legend__container { 38 | height: 100%; 39 | } 40 | 41 | .piechart-panel__chart { 42 | position: relative; 43 | cursor: crosshair; 44 | flex-grow: 1; 45 | top: 10px; 46 | } 47 | 48 | .datapoints-warning { 49 | pointer: none; 50 | position: absolute; 51 | top: 50%; 52 | left: 50%; 53 | z-index: 10; 54 | margin-top: -50px; 55 | margin-left: -100px; 56 | width: 200px; 57 | text-align: center; 58 | cursor: auto; 59 | padding: 10px; 60 | } 61 | 62 | .piechart-legend { 63 | flex: 0 1 auto; 64 | margin: 0 1rem; 65 | text-align: center; 66 | padding-top: 6px; 67 | position: relative; 68 | } 69 | 70 | .piechart-legend .popover-content { 71 | padding: 0; 72 | } 73 | 74 | .piechart-legend-icon { 75 | position: relative; 76 | padding-right: 4px; 77 | top: 1px; 78 | } 79 | 80 | .piechart-legend-icon, 81 | .piechart-legend-alias, 82 | .piechart-legend-value { 83 | cursor: pointer; 84 | float: left; 85 | white-space: nowrap; 86 | font-size: 85%; 87 | text-align: left; 88 | color: 52545c; 89 | } 90 | 91 | .piechart-legend-icon.current::before { 92 | content: 'Current: '; 93 | } 94 | 95 | .piechart-legend-icon.max::before { 96 | content: 'Max: '; 97 | } 98 | 99 | .piechart-legend-icon.min::before { 100 | content: 'Min: '; 101 | } 102 | 103 | .piechart-legend-icon.total::before { 104 | content: 'Total: '; 105 | } 106 | 107 | .piechart-legend-icon.avg::before { 108 | content: 'Avg: '; 109 | } 110 | 111 | .piechart-legend-alias.current::before { 112 | content: 'Current: '; 113 | } 114 | 115 | .piechart-legend-alias.max::before { 116 | content: 'Max: '; 117 | } 118 | 119 | .piechart-legend-alias.min::before { 120 | content: 'Min: '; 121 | } 122 | 123 | .piechart-legend-alias.total::before { 124 | content: 'Total: '; 125 | } 126 | 127 | .piechart-legend-alias.avg::before { 128 | content: 'Avg: '; 129 | } 130 | 131 | .piechart-legend-value.current::before { 132 | content: 'Current: '; 133 | } 134 | 135 | .piechart-legend-value.max::before { 136 | content: 'Max: '; 137 | } 138 | 139 | .piechart-legend-value.min::before { 140 | content: 'Min: '; 141 | } 142 | 143 | .piechart-legend-value.total::before { 144 | content: 'Total: '; 145 | } 146 | 147 | .piechart-legend-value.avg::before { 148 | content: 'Avg: '; 149 | } 150 | 151 | .piechart-legend-icon .fa { 152 | font-size: 135%; 153 | position: relative; 154 | top: 1px; 155 | } 156 | 157 | .piechart-legend-series { 158 | float: left; 159 | white-space: nowrap; 160 | padding-left: 10px; 161 | } 162 | 163 | .piechart-legend-series--right-y { 164 | float: right; 165 | } 166 | 167 | .piechart-legend-value { 168 | padding-left: 6px; 169 | } 170 | 171 | .piechart-legend-table tbody { 172 | display: block; 173 | overflow-y: auto; 174 | overflow-x: hidden; 175 | height: 100%; 176 | padding-bottom: 1px; 177 | padding-right: 5px; 178 | } 179 | 180 | .piechart-legend-table .piechart-legend-series { 181 | display: table-row; 182 | float: none; 183 | padding-left: 0; 184 | } 185 | 186 | .piechart-legend-table .piechart-legend-series--right-y { 187 | float: none; 188 | } 189 | 190 | .piechart-legend-table .piechart-legend-series--right-y .piechart-legend-alias::after { 191 | content: '(right-y)'; 192 | padding: 0 5px; 193 | color: #767980; 194 | } 195 | 196 | .piechart-legend-table td, 197 | .piechart-legend-alias, 198 | .piechart-legend-icon, 199 | .piechart-legend-value { 200 | float: none; 201 | display: table-cell; 202 | white-space: nowrap; 203 | padding: 2px 10px; 204 | text-align: right; 205 | } 206 | 207 | .piechart-legend-table .piechart-legend-icon { 208 | width: 5px; 209 | padding: 0; 210 | top: 0; 211 | } 212 | 213 | .piechart-legend-table .piechart-legend-icon .fa { 214 | top: 4px; 215 | } 216 | 217 | .piechart-legend-table .piechart-legend-value { 218 | padding-left: 15px; 219 | } 220 | 221 | .piechart-legend-table .piechart-legend-alias { 222 | padding-left: 7px; 223 | text-align: left; 224 | width: 95%; 225 | max-width: 650px; 226 | text-overflow: ellipsis; 227 | overflow: hidden; 228 | } 229 | 230 | .piechart-legend-table .piechart-legend-series:nth-child(odd) { 231 | background: #dde4ed; 232 | } 233 | 234 | .piechart-legend-table .piechart-legend-value.current::before { 235 | content: ''; 236 | } 237 | 238 | .piechart-legend-table .piechart-legend-value.max::before { 239 | content: ''; 240 | } 241 | 242 | .piechart-legend-table .piechart-legend-value.min::before { 243 | content: ''; 244 | } 245 | 246 | .piechart-legend-table .piechart-legend-value.total::before { 247 | content: ''; 248 | } 249 | 250 | .piechart-legend-table .piechart-legend-value.avg::before { 251 | content: ''; 252 | } 253 | 254 | .piechart-legend-table th { 255 | text-align: right; 256 | padding: 0px 10px 1px 0; 257 | font-weight: bold; 258 | color: #61c2f2; 259 | font-size: 85%; 260 | white-space: nowrap; 261 | } 262 | 263 | .piechart-legend-series-hidden .piechart-legend-value, 264 | .piechart-legend-series-hidden .piechart-legend-alias { 265 | color: #acb6bf; 266 | } 267 | 268 | .piechart-tooltip { 269 | white-space: nowrap; 270 | font-size: 12px; 271 | background-color: #dde4ed; 272 | color: #35373f; 273 | } 274 | 275 | .piechart-tooltip .piechart-tooltip-time { 276 | text-align: center; 277 | position: relative; 278 | top: -3px; 279 | padding: 0.2rem; 280 | font-weight: bold; 281 | color: #35373f; 282 | } 283 | 284 | .piechart-tooltip .piechart-tooltip-value { 285 | display: table-cell; 286 | font-weight: bold; 287 | padding-left: 15px; 288 | text-align: right; 289 | } 290 | 291 | .migrate-button { 292 | height: auto; 293 | min-height: 32px; 294 | } -------------------------------------------------------------------------------- /src/styles/piechart.scss: -------------------------------------------------------------------------------- 1 | @import '../../node_modules/@grafana/toolkit/sass/_variables.scss'; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@grafana/toolkit/src/config/tsconfig.plugin.json", 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "rootDir": "./src", 6 | "baseUrl": "./src", 7 | "typeRoots": ["./node_modules/@types"] 8 | } 9 | } 10 | --------------------------------------------------------------------------------