├── .eslintrc.yml ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── CHANGELOG.md ├── README.md ├── barlinks.html ├── columns.html ├── css │ └── multistat-panel.css ├── grouping.html ├── img │ ├── Showcase.gif │ └── michaeldmoore-multistat-panel.svg ├── layout.html ├── linesandlimits.html ├── module.html ├── module.js ├── module.js.map ├── options.html └── plugin.json ├── gruntfile.js ├── package.json ├── src ├── barlinks.html ├── columns.html ├── css │ └── multistat-panel.css ├── grouping.html ├── img │ ├── Alarms-Alarms.gif │ ├── Alarms-Alarms.mp4 │ ├── Bar-Color-and-Padding.png │ ├── BaseLine.png │ ├── Basic-Bar-Charts.png │ ├── Calm-Alarms.gif │ ├── Calm-Alarms.mp4 │ ├── Label-Format.png │ ├── Limit-Bar-Colors.png │ ├── Limits.png │ ├── Lines-and-Limits.png │ ├── MappingFields.png │ ├── Margins.png │ ├── Max-Value.png │ ├── Multistat.png │ ├── No-data.png │ ├── Options.png │ ├── SampleQuery.png │ ├── Showcase.gif │ ├── Showcase.mp4 │ ├── Sorting.png │ └── michaeldmoore-multistat-panel.svg ├── layout.html ├── linesandlimits.html ├── module.html ├── module.js ├── options.html └── plugin.json └── yarn.lock /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es2021: true 4 | extends: 'eslint:recommended' 5 | parserOptions: 6 | ecmaVersion: 12 7 | sourceType: module 8 | rules: 9 | accessor-pairs: error 10 | array-bracket-newline: 'off' 11 | array-bracket-spacing: 12 | - error 13 | - never 14 | array-callback-return: error 15 | array-element-newline: 'off' 16 | arrow-body-style: error 17 | arrow-parens: 18 | - error 19 | - always 20 | arrow-spacing: 21 | - error 22 | - after: true 23 | before: true 24 | block-scoped-var: error 25 | block-spacing: error 26 | brace-style: error 27 | callback-return: error 28 | capitalized-comments: error 29 | class-methods-use-this: error 30 | comma-dangle: error 31 | comma-spacing: 32 | - error 33 | - after: true 34 | before: false 35 | comma-style: 36 | - error 37 | - last 38 | complexity: error 39 | computed-property-spacing: error 40 | consistent-return: error 41 | consistent-this: error 42 | curly: error 43 | default-case: error 44 | default-case-last: error 45 | default-param-last: error 46 | dot-location: error 47 | dot-notation: error 48 | eol-last: error 49 | eqeqeq: error 50 | func-call-spacing: error 51 | func-name-matching: error 52 | func-names: error 53 | func-style: error 54 | function-paren-newline: error 55 | generator-star-spacing: error 56 | global-require: 'off' 57 | grouped-accessor-pairs: error 58 | guard-for-in: error 59 | handle-callback-err: error 60 | id-blacklist: error 61 | id-denylist: error 62 | id-length: error 63 | id-match: error 64 | implicit-arrow-linebreak: error 65 | indent: 'off' 66 | indent-legacy: 'off' 67 | init-declarations: error 68 | jsx-quotes: error 69 | key-spacing: error 70 | keyword-spacing: error 71 | line-comment-position: error 72 | linebreak-style: 73 | - error 74 | - windows 75 | lines-around-comment: error 76 | lines-around-directive: error 77 | lines-between-class-members: error 78 | max-classes-per-file: error 79 | max-depth: error 80 | max-len: 'off' 81 | max-lines: error 82 | max-lines-per-function: 'off' 83 | max-nested-callbacks: error 84 | max-params: error 85 | max-statements: error 86 | max-statements-per-line: error 87 | multiline-comment-style: error 88 | multiline-ternary: error 89 | new-cap: error 90 | new-parens: error 91 | newline-after-var: error 92 | newline-before-return: error 93 | newline-per-chained-call: error 94 | no-alert: error 95 | no-array-constructor: error 96 | no-await-in-loop: error 97 | no-bitwise: error 98 | no-buffer-constructor: error 99 | no-caller: error 100 | no-catch-shadow: error 101 | no-confusing-arrow: error 102 | no-console: error 103 | no-constructor-return: error 104 | no-continue: error 105 | no-div-regex: error 106 | no-duplicate-imports: error 107 | no-else-return: error 108 | no-empty-function: error 109 | no-eq-null: error 110 | no-eval: error 111 | no-extend-native: error 112 | no-extra-bind: error 113 | no-extra-label: error 114 | no-extra-parens: error 115 | no-floating-decimal: error 116 | no-implicit-coercion: error 117 | no-implicit-globals: error 118 | no-implied-eval: error 119 | no-inline-comments: error 120 | no-invalid-this: error 121 | no-iterator: error 122 | no-label-var: error 123 | no-labels: error 124 | no-lone-blocks: error 125 | no-lonely-if: error 126 | no-loop-func: error 127 | no-loss-of-precision: error 128 | no-magic-numbers: error 129 | no-mixed-operators: error 130 | no-mixed-requires: error 131 | no-multi-assign: error 132 | no-multi-spaces: error 133 | no-multi-str: error 134 | no-multiple-empty-lines: error 135 | no-native-reassign: error 136 | no-negated-condition: error 137 | no-negated-in-lhs: error 138 | no-nested-ternary: error 139 | no-new: error 140 | no-new-func: error 141 | no-new-object: error 142 | no-new-require: error 143 | no-new-wrappers: error 144 | no-nonoctal-decimal-escape: error 145 | no-octal-escape: error 146 | no-param-reassign: error 147 | no-path-concat: error 148 | no-plusplus: error 149 | no-process-env: error 150 | no-process-exit: error 151 | no-promise-executor-return: error 152 | no-proto: error 153 | no-restricted-exports: error 154 | no-restricted-globals: error 155 | no-restricted-imports: error 156 | no-restricted-modules: error 157 | no-restricted-properties: error 158 | no-restricted-syntax: error 159 | no-return-assign: error 160 | no-return-await: error 161 | no-script-url: error 162 | no-self-compare: error 163 | no-sequences: error 164 | no-shadow: error 165 | no-spaced-func: error 166 | no-sync: error 167 | no-tabs: error 168 | no-template-curly-in-string: error 169 | no-ternary: error 170 | no-throw-literal: error 171 | no-trailing-spaces: error 172 | no-undef-init: error 173 | no-undefined: error 174 | no-underscore-dangle: error 175 | no-unmodified-loop-condition: error 176 | no-unneeded-ternary: error 177 | no-unreachable-loop: error 178 | no-unsafe-optional-chaining: error 179 | no-unused-expressions: error 180 | no-use-before-define: error 181 | no-useless-backreference: error 182 | no-useless-call: error 183 | no-useless-computed-key: error 184 | no-useless-concat: error 185 | no-useless-constructor: error 186 | no-useless-rename: error 187 | no-useless-return: error 188 | no-var: error 189 | no-void: error 190 | no-warning-comments: error 191 | no-whitespace-before-property: error 192 | nonblock-statement-body-position: error 193 | object-curly-newline: error 194 | object-curly-spacing: 195 | - error 196 | - always 197 | object-property-newline: error 198 | object-shorthand: error 199 | one-var: error 200 | one-var-declaration-per-line: error 201 | operator-assignment: error 202 | operator-linebreak: error 203 | padded-blocks: 'off' 204 | padding-line-between-statements: error 205 | prefer-arrow-callback: error 206 | prefer-const: error 207 | prefer-destructuring: error 208 | prefer-exponentiation-operator: error 209 | prefer-named-capture-group: error 210 | prefer-numeric-literals: error 211 | prefer-object-spread: error 212 | prefer-promise-reject-errors: error 213 | prefer-reflect: error 214 | prefer-regex-literals: error 215 | prefer-rest-params: error 216 | prefer-spread: error 217 | prefer-template: error 218 | quote-props: 'off' 219 | quotes: 220 | - error 221 | - single 222 | radix: error 223 | require-atomic-updates: error 224 | require-await: error 225 | require-jsdoc: error 226 | require-unicode-regexp: error 227 | rest-spread-spacing: error 228 | semi: error 229 | semi-spacing: error 230 | semi-style: 231 | - error 232 | - last 233 | sort-imports: error 234 | sort-keys: 'off' 235 | sort-vars: error 236 | space-before-blocks: error 237 | space-before-function-paren: error 238 | space-in-parens: 239 | - error 240 | - never 241 | space-infix-ops: error 242 | space-unary-ops: error 243 | spaced-comment: 244 | - error 245 | - never 246 | strict: error 247 | switch-colon-spacing: error 248 | symbol-description: error 249 | template-curly-spacing: error 250 | template-tag-spacing: error 251 | unicode-bom: 252 | - error 253 | - never 254 | valid-jsdoc: error 255 | vars-on-top: error 256 | wrap-iife: error 257 | wrap-regex: error 258 | yield-star-spacing: error 259 | yoda: error 260 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | {{!-- /* 🚨 The `${{ }}` Github workflow expressions need to be escaped so they are not being interpreted by Handlebars. (this comment is going to be removed after scaffolding) 🚨 */ --}} 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | - main 9 | pull_request: 10 | branches: 11 | - master 12 | - main 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | {{#if_eq packageManagerName "pnpm"}} 20 | # pnpm action uses the packageManager field in package.json to 21 | # understand which version to install. 22 | - uses: pnpm/action-setup@v2 23 | {{/if_eq}} 24 | - name: Setup Node.js environment 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: '16' 28 | cache: '{{ packageManagerName }}' 29 | 30 | - name: Install dependencies 31 | run: {{ packageManagerInstallCmd }} 32 | 33 | - name: Check types 34 | run: {{ packageManagerName }} run typecheck 35 | - name: Lint 36 | run: {{ packageManagerName }} run lint 37 | - name: Unit tests 38 | run: {{ packageManagerName }} run test:ci 39 | - name: Build frontend 40 | run: {{ packageManagerName }} run build 41 | 42 | - name: Check for backend 43 | id: check-for-backend 44 | run: | 45 | if [ -f "Magefile.go" ] 46 | then 47 | echo "has-backend=true" >> $GITHUB_OUTPUT 48 | fi 49 | 50 | - name: Setup Go environment 51 | if: steps.check-for-backend.outputs.has-backend == 'true' 52 | uses: actions/setup-go@v3 53 | with: 54 | go-version: '1.20' 55 | 56 | - name: Test backend 57 | if: steps.check-for-backend.outputs.has-backend == 'true' 58 | uses: magefile/mage-action@v2 59 | with: 60 | version: latest 61 | args: coverage 62 | 63 | - name: Build backend 64 | if: steps.check-for-backend.outputs.has-backend == 'true' 65 | uses: magefile/mage-action@v2 66 | with: 67 | version: latest 68 | args: buildAll 69 | 70 | - name: Check for E2E 71 | id: check-for-e2e 72 | run: | 73 | if [ -d "cypress" ] 74 | then 75 | echo "has-e2e=true" >> $GITHUB_OUTPUT 76 | fi 77 | 78 | - name: Start grafana docker 79 | if: steps.check-for-e2e.outputs.has-e2e == 'true' 80 | run: docker-compose up -d 81 | 82 | - name: Run e2e tests 83 | if: steps.check-for-e2e.outputs.has-e2e == 'true' 84 | run: {{ packageManagerName }} run e2e 85 | 86 | - name: Stop grafana docker 87 | if: steps.check-for-e2e.outputs.has-e2e == 'true' 88 | run: docker-compose down 89 | 90 | - name: Archive E2E output 91 | uses: actions/upload-artifact@v3 92 | if: steps.check-for-e2e.outputs.has-e2e == 'true' && steps.run-e2e-tests.outcome != 'success' 93 | with: 94 | name: cypress-videos 95 | path: cypress/videos 96 | retention-days: 5 97 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Run workflow on version tags, e.g. v1.0.0. 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | env: 12 | GRAFANA_API_KEY: $\{{ secrets.GRAFANA_API_KEY }} # Requires a Grafana API key from Grafana.com. 13 | steps: 14 | - uses: actions/checkout@v3 15 | {{#if_eq packageManagerName "pnpm"}} 16 | # pnpm action uses the packageManager field in package.json to 17 | # understand which version to install. 18 | - uses: pnpm/action-setup@v2 19 | {{/if_eq}} 20 | - name: Setup Node.js environment 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: '16' 24 | cache: '{{ packageManagerName }}' 25 | 26 | - name: Setup Go environment 27 | uses: actions/setup-go@v3 28 | with: 29 | go-version: '1.19' 30 | 31 | - name: Install dependencies 32 | run: {{ packageManagerInstallCmd }} 33 | 34 | - name: Build and test frontend 35 | run: {{ packageManagerName }} run build 36 | 37 | - name: Check for backend 38 | id: check-for-backend 39 | run: | 40 | if [ -f "Magefile.go" ] 41 | then 42 | echo "has-backend=true" >> $GITHUB_OUTPUT 43 | fi 44 | 45 | - name: Test backend 46 | if: steps.check-for-backend.outputs.has-backend == 'true' 47 | uses: magefile/mage-action@v2 48 | with: 49 | version: latest 50 | args: coverage 51 | 52 | - name: Build backend 53 | if: steps.check-for-backend.outputs.has-backend == 'true' 54 | uses: magefile/mage-action@v2 55 | with: 56 | version: latest 57 | args: buildAll 58 | 59 | - name: Warn missing Grafana API key 60 | run: | 61 | echo Please generate a Grafana API key: https://grafana.com/docs/grafana/latest/developers/plugins/sign-a-plugin/#generate-an-api-key 62 | echo Once done please follow the instructions found here: https://github.com/$\{{github.repository}}/blob/main/README.md#using-github-actions-release-workflow 63 | if: $\{{ env.GRAFANA_API_KEY == '' }} 64 | 65 | - name: Sign plugin 66 | run: {{ packageManagerName }} run sign 67 | if: $\{{ env.GRAFANA_API_KEY != '' }} 68 | 69 | - name: Get plugin metadata 70 | id: metadata 71 | run: | 72 | sudo apt-get install jq 73 | 74 | export GRAFANA_PLUGIN_ID=$(cat dist/plugin.json | jq -r .id) 75 | export GRAFANA_PLUGIN_VERSION=$(cat dist/plugin.json | jq -r .info.version) 76 | export GRAFANA_PLUGIN_TYPE=$(cat dist/plugin.json | jq -r .type) 77 | export GRAFANA_PLUGIN_ARTIFACT=${GRAFANA_PLUGIN_ID}-${GRAFANA_PLUGIN_VERSION}.zip 78 | export GRAFANA_PLUGIN_ARTIFACT_CHECKSUM=${GRAFANA_PLUGIN_ARTIFACT}.md5 79 | 80 | echo "plugin-id=${GRAFANA_PLUGIN_ID}" >> $GITHUB_OUTPUT 81 | echo "plugin-version=${GRAFANA_PLUGIN_VERSION}" >> $GITHUB_OUTPUT 82 | echo "plugin-type=${GRAFANA_PLUGIN_TYPE}" >> $GITHUB_OUTPUT 83 | echo "archive=${GRAFANA_PLUGIN_ARTIFACT}" >> $GITHUB_OUTPUT 84 | echo "archive-checksum=${GRAFANA_PLUGIN_ARTIFACT_CHECKSUM}" >> $GITHUB_OUTPUT 85 | 86 | echo "github-tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT 87 | 88 | - name: Read changelog 89 | id: changelog 90 | run: | 91 | awk '/^## / {s++} s == 1 {print}' CHANGELOG.md > release_notes.md 92 | echo "path=release_notes.md" >> $GITHUB_OUTPUT 93 | 94 | - name: Check package version 95 | run: if [ "v$\{{ steps.metadata.outputs.plugin-version }}" != "$\{{ steps.metadata.outputs.github-tag }}" ]; then printf "\033[0;31mPlugin version doesn't match tag name\033[0m\n"; exit 1; fi 96 | 97 | - name: Package plugin 98 | id: package-plugin 99 | run: | 100 | mv dist $\{{ steps.metadata.outputs.plugin-id }} 101 | zip $\{{ steps.metadata.outputs.archive }} $\{{ steps.metadata.outputs.plugin-id }} -r 102 | md5sum $\{{ steps.metadata.outputs.archive }} > $\{{ steps.metadata.outputs.archive-checksum }} 103 | echo "checksum=$(cat ./$\{{ steps.metadata.outputs.archive-checksum }} | cut -d' ' -f1)" >> $GITHUB_OUTPUT 104 | 105 | - name: Validate plugin 106 | run: | 107 | git clone https://github.com/grafana/plugin-validator 108 | pushd ./plugin-validator/pkg/cmd/plugincheck2 109 | go install 110 | popd 111 | plugincheck2 -config ./plugin-validator/config/default.yaml $\{{ steps.metadata.outputs.archive }} 112 | 113 | - name: Create Github release 114 | uses: softprops/action-gh-release@v1 115 | with: 116 | draft: true 117 | generate_release_notes: true 118 | files: | 119 | ./$\{{ steps.metadata.outputs.archive }} 120 | ./$\{{ steps.metadata.outputs.archive-checksum }} 121 | body: | 122 | **This Github draft release has been created for your plugin.** 123 | 124 | _Note: if this is the first release for your plugin please consult the [distributing-your-plugin section](https://github.com/$\{{github.repository}}/blob/main/README.md#distributing-your-plugin) of the README_ 125 | 126 | If you would like to submit this release to Grafana please consider the following steps: 127 | 128 | - Check the Validate plugin step in the [release workflow](https://github.com/$\{{github.repository}}/commit/$\{{github.sha}}/checks/$\{{github.run_id}}) for any warnings that need attention 129 | - Navigate to https://grafana.com/auth/sign-in/ to sign into your account 130 | - Once logged in click **My Plugins** in the admin navigation 131 | - Click the **Submit Plugin** button 132 | - Fill in the Plugin Submission form: 133 | - Paste this [.zip asset link](https://github.com/$\{{ github.repository }}/releases/download/v$\{{ steps.metadata.outputs.plugin-version }}/$\{{ steps.metadata.outputs.archive }}) in the Plugin URL field 134 | - Paste this [.zip.md5 link](https://github.com/$\{{ github.repository }}/releases/download/v$\{{ steps.metadata.outputs.plugin-version }}/$\{{ steps.metadata.outputs.archive-checksum }}) in the MD5 field 135 | 136 | Once done please remove these instructions and publish this release. 137 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | .vscode 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## v1.6.0 6 | 7 | Adding Multi-bar support 8 | 9 | ## v1.6.1 10 | 11 | Signed, as required by Grafana 7.x.x. No other changes 12 | 13 | ## v1.7.0 14 | 15 | Adding label and group reformatting rules, plus optional reformat-in-place for date/time field. 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | # michaeldmoore-multistat-panel 2 | 3 | *New Version (1.3.0) - Now with rules-based bar re-coloring!* 4 | 5 | *New Version (1.4.0) - Now with restyled tooltips and clickable per-bar links!* 6 | 7 | *New Version (1.6.0) - Now with support for multiple value columns per label!* 8 | 9 | *New Version (1.7.0) - Now with support for label/group renaming and date formatting!* 10 | 11 | *New Version (1.7.4) - Now working with Grafana 9.x.x (the version of d3.js library dropped support for the original grouping functions, causing the plugin to fail. Thisiis now fixed!* 12 | 13 | (Documentation for these changes is covered in the sections at the very end of this readme file) 14 | 15 | # NOTE - This plugin is built using the original Angular framework which is expected to be dropped in future versions of Grafana. Once this happens, the plugin will no longer be supported.. 16 | 17 | 18 | ## Custom multistat panel for Grafana, inspired by the built-in SingleStat panel 19 | 20 | This panel was developed for as a table-like panel for presenting bar charts, providing some useful additions particularly suited to process control/monitoring dashboards. As such, Multistat displays never use scroll-bars (scroll bars are useless in monitoring dashboard). 21 | 22 | SingleStat displays a single metric from time series data set, with optional threshold-related coloring etc. Multistat builds on this base, but with multi-column table data sets, displaying query data in the form of bar graphs with optional upper and lower hard limits. Plus a lot, lot more.... 23 | 24 | ![showcase](https://user-images.githubusercontent.com/3724718/30005310-38debe58-9094-11e7-9209-5aeb977c7577.gif) 25 | 26 | Data can be displayed as vertical bars... 27 | 28 | ![image](https://user-images.githubusercontent.com/3724718/38955637-1b1bd9d6-430a-11e8-9633-4e752f12d237.png) 29 | 30 | Horizontally... 31 | 32 | ![image](https://user-images.githubusercontent.com/3724718/38958080-07cd248c-4311-11e8-85c4-6c988b7ded08.png) 33 | 34 | Or grouped on an attribute, again vertically 35 | ![image](https://user-images.githubusercontent.com/3724718/51725178-7b3dbb00-2026-11e9-861f-d800897d17d3.png) 36 | 37 | ... or horizontally 38 | ![image](https://user-images.githubusercontent.com/3724718/51725236-c6f06480-2026-11e9-84f0-4d2f75a69d66.png) 39 | *(Note these last two examples are single Multistat panels. A single set of configuration settings is automatically applied to each of the grouped sub-displays)* 40 | 41 | And (just about) *everything* is configurable... 42 | Max, Min, auto-scaling, base-lines, colors, rows and columns - (just about) everything... 43 | 44 | High and Low limits too, with optional bar coloring 45 | ![image](https://user-images.githubusercontent.com/3724718/51725465-f3f14700-2027-11e9-8d61-592f644a5c20.png) 46 | 47 | All this, and optional flashing too when bars surpass these limits. 48 | 49 | ## Data 50 | 51 | Multistat accepts Grafana **table** formatted data. Note - There is **no support for time series** formatted data. 52 | 53 | As a minimum, Multistat requires table data with at least two fields per row - one, a label (string) and the other, a numeric value. These fields can be called anything. Multistat makes no assumptions regarding the names of the table data field it handles. 54 | 55 | Each distinct label will be displayed as a bar - the length being determined by it's numerical value. 56 | 57 | takes it's data from a table query - returning, at minimum, 2 fields (names can be anything) 58 | 59 | A **timestamp** field can be useful too - this can be in any commonly understood format (Multistat uses the popular **Moment.js** java script library for manipulating time/date strings - see https://momentjs.com/docs/#/displaying/ for details). 60 | 61 | A **grouping** field can be useful too, to organize large data sets into more meaningful sections. When grouping, the number of columns can be pre-configured, along with handy mechanisms for filtering and arranging the order each group is presented in. 62 | 63 | Any additional fields are retained and presented in optional tool-tip balloons. Again, (just about) everything is configurable here... 64 | ![image](https://user-images.githubusercontent.com/3724718/51726418-e8544f00-202c-11e9-89e8-2cce5c7bfb2a.png) 65 | 66 | ## Duplicate labels in table data 67 | 68 | Each distinct label in the input data results in a distinct bar in Multistat. Ideally, table data should be created by queries that return distinct data sets - that is, sets in which each label is presented in a single row. When data sets are processed with multiple rows for a given label, Multistat needs to know which value to use (and hence, which values to ignore). A configurable aggregation parameter tells Multistat how to handle this. 'Last' (and 'First') select the last (or first) row in the data for any given label, throwing out all the others. The optional date/timestamp field helps too by pre-sorting the data table before selecting the aggregation function. The tool-tip then shows the set of fields for the selected data row, as expected. 69 | 70 | Setting the aggregation parameter to 'Max' or 'Min' works in a similar way, selecting the row for each label with the corresponding value - and secondly using the last or latest value in the event that there is a tie in the value. 71 | 72 | Setting the aggregation parameter to 'Mean' results in the arithmetic mean of all duplicate values to be used, as should be expected. *A side effect of this though is that the fields presented in the tool-tip balloon in this case represent just one of the rows - actually the 'last' row, the value of which will not generally match the displayed 'mean' value for that label.* 73 | 74 | ## Data set size - a performance consideration 75 | 76 | As mentioned before, ideally the data set should contain a single row for each distinct label. This offloads the maximum amount of filtering and aggregation etc., to the database which is generally much more efficient for these tasks. 77 | In the event that the database cannot pre-filter the data in this way, the aggregation setting can still generate the required display, but at the cost of increased CPU and network load. Generally, this is not significant - Multistat can easily handle queries with a few hundred labels, each with a hundred or more rows. Note though that huge data sets - data sets with multiple megabytes of data etc. - these will negatively impact performance. Particularly as refresh rates shorten. In extreme cases, this can even make the browser become unresponsive. **Beware of enormous data sets.** 78 | 79 | Multistat has a wealth of configurable options. just about everything displayed can be adjusted and hidden using the extensive set of configuration options, described in detail below. 80 | 81 | ![image](https://user-images.githubusercontent.com/3724718/50191698-95cc9800-02f4-11e9-96cd-a0c5da672278.png) 82 | 83 | ## Features 84 | 85 | * Orientation - Horizontal or Vertical bars 86 | 87 | * Sorting (Dynamic, by Name, Value (or Update Time), ascending or descending) 88 | 89 | * Query result field name mapping - define which fields represent the labels and which represent values 90 | 91 | * Last Update Time display (Optional, if Update DateTime column included in query and column mapped). Useful to demonstrate the data source is actually updating 92 | 93 | * Optional display of values, selectable font size and color 94 | 95 | * Optional display of labels, selectable font size and color 96 | 97 | * Selectable Bar color (bar coloring is a subject in itself - see below for far more details regarding this) 98 | 99 | * Optional Left & Right value axes display (that is, upper & lower axes when in horizontal display mode) 100 | 101 | * Adjustable label margin size override 102 | 103 | * Min, Max and Baseline values (See below for discussion on baselines) 104 | 105 | * Optional High and Low value alarm limits and indicator lines 106 | 107 | * Optional setting color of bars exceeding High or Low alarm thresholds 108 | 109 | * Optional two-color alarm flashing, with settable flash rate 110 | 111 | `Note : With all these options, the author accepts no responsibility for` 112 | `inducing nausea, epilepsy or generally violating the bounds of good taste)` 113 | 114 | ## Configuration details 115 | 116 | The Grafana-standard Metrics tab allows the user to specify a query, to be issued each time the panel is refreshed. 117 | 118 | This area is under continued active development. Currently, Multistat only supports **Table** data queries. Each row returned will be displayed as a bar, auto-sized to use the available space. The panel does not provide scroll bars, so any query returning more rows than can comfortably fit in the allotted panel area will be unreadable. 119 | 120 | *Note - getting appropriately formatted data can be challenging, especially while becoming familiar with all the options offered by Multistat. For this end - and for general purpose Grafana plugin testing - I've created a simple NodeJS data source called **CSVServer**, working in conjunction with the standard **SimpleJSON** datasource to import simple CSV files which can be easily edited to generate any kind of table or time series data sets. See here for details, including set up instructions : * 121 | 122 | ## Table Queries 123 | 124 | Multistat queries are expected to be something along the lines of ''Get the working pressure of all steam boilers in building 5'' or ''Get the temperature of the 10 hottest cities in the US'' etc. Anything that can be re-queried efficiently and returns a list of results containing - at minimum - a Label (e.g. Boiler ID or City Name) and a value (e.g. the pressure or temperature). Optionally, Multistat takes advantage of a DateTime field, if present, which can be displayed alongside the panel title as an indicator as to the last time the data was updated. More details on this below. 125 | 126 | If no query is defined, or the data source is unavailable, Multistat displays a simple "No data" warning message. 127 | 128 | ![no-data](https://user-images.githubusercontent.com/3724718/30006554-7b72f5fc-90af-11e7-8b79-d331f60d0388.png) 129 | 130 | ## Queries *should* return just one value per label 131 | 132 | Multistat can only display a single bar for each label. Ideally, query results *should be written* to return a single value per label. When this is not possible, and the query returns multiple values per label, Multistat uses an aggregation operator to select one of these (first, last, mean, min or max). *one more - all - will eliminate the aggregator altogether, Be careful - This can create confusing displays as multiple values appear overlying the position of such bars. **You have been warned.** For efficiency though, it is much better to write a query that only returns the required data. 133 | 134 | For this discussion, I created a test data set in a CSV file (demo.csv) distributed with the **CSVServer** add-on to SimpleJSON data source. I highly recommend installing this so you can follow along and see how the various configuration options work before worrying about live real-world data sets. The demo CSV file contains the following data: 135 | 136 | ```bash 137 | time,sensor,area,quantity 138 | 2018-12-18 00:21:05.000,AAA,West,1.100 139 | 2018-12-18 00:21:04.000,AAA,West,1.000 140 | 2018-12-18 00:21:03.000,AAA,West,0.950 141 | 2018-12-18 00:21:02.000,AAA,West,0.900 142 | 2018-12-18 00:21:01.000,AAA,West,0.850 143 | 2018-12-18 00:21:00.000,AAA,West,0.800 144 | 2018-12-18 00:21:07.000,AAA,West,1.234 145 | 2018-12-18 00:21:07.000,BBB,East,0.662 146 | 2018-12-18 00:21:07.000,CCC,East,0.344 147 | 2018-12-18 00:21:07.000,EEE,West,0.357 148 | 2018-12-18 00:21:07.000,GGG,West,0.563 149 | 2018-12-18 00:21:07.000,HHH,West,0.234 150 | 2018-12-18 00:21:07.000,III,West,0.840 151 | 2018-12-18 00:21:07.000,JJJ,East,0.193 152 | 2018-12-18 00:21:07.000,KKK,West,0.262 153 | 2018-12-18 00:21:07.000,LLL,North,0.802 154 | 2018-12-18 00:21:07.000,MMM,East,0.211 155 | 2018-12-18 00:21:07.000,PPP,North,0.300 156 | 2018-12-18 00:21:07.000,QQQ,North,0.731 157 | 2018-12-18 00:21:07.000,RRR,North,1.101 158 | 2018-12-18 00:21:07.000,SSS,East,0.811 159 | 2018-12-18 00:21:07.000,WWW,East,0.213 160 | 2018-12-18 00:21:07.000,YYY,East,0.844 161 | 2018-12-18 00:21:07.000,ZZZ,North,0.928 162 | ``` 163 | 164 | Note, sensor AAA in this data set has multiple values, each a few hours apart. All the other sensors have a single row - this will allow the aggregation feature to be demonstrated later in this note. Each row in this data set includes a date/time, a label and a value, plus a region field (that will be useful in grouping). The field names can be anything; everything is defined in the configuration tabs. Additional fields, if any, will appear in the tool-tip pop-up display, if enabled. 165 | 166 | As you can see, Multistat is configured using a number of option tabs. Let's examine each of these in sequence. 167 | 168 | First, the data source and query is setup using the standard **Metrics** tab 169 | 170 | ![image](https://user-images.githubusercontent.com/3724718/50192332-5b182f00-02f7-11e9-805c-2137c832d7f6.png) 171 | 172 | Note the data set format is set to '**Table**' (Multistat does not support time series data sets) 173 | 174 | Note: The Query Inspector built into Grafana is a terrific resource for figuring out source data problems. Here's what we get from my demo query: 175 | 176 | ![image](https://user-images.githubusercontent.com/3724718/50192518-3e302b80-02f8-11e9-8e34-eb039eb23bce.png) 177 | etc. 178 | 179 | The data is mapped using the **Columns** configuration tab: 180 | 181 | ![image](https://user-images.githubusercontent.com/3724718/51728249-2ce3e880-2035-11e9-8ca6-5c51b97710c1.png) 182 | 183 | Here, you can see how the 4 key fields in the query result set get mapped to the Multistat fields. In this case, the **label** is associated with the query field 'sensor', **Value** as 'quantity', with 1 decimal place and no scaling (**scale factor = 1**). Note too that the default **aggregation** parameter of 'Last' is selected. This dataset contains multiple rows for some labels - this setting automatically selects the last (latest) value for each sensor. 184 | 185 | Note too, that the bars are set to be sorted in ascending 'sensor' name. 186 | 187 | The data is set to **group** on the 'area' field - this will create 3 sub-charts, for the East, North and West areas. 188 | 189 | The **DateTime col** (optional) is mapped here to the 'time' field. When set, the TZ Offset Hrs setting can be used to offset the display value to account for time-zone differences between the data source and the client. *(Note - this time offset features duplicates something similar built into recent versions of Grafana. This feature may be removed in future versions of Multistat)* 190 | 191 | The '**Show as-of Date**' setting controls whether or not the last update time is to be displayed in the top right of the panel. **Most users can ignore this setting**. When it is set, the maximum datetime value in the query record set is displayed alongside the panel title. This can be useful in process monitoring applications to provide evidence that the data is being updated in a timely manner etc. The format field controls how this time is displayed (see documentation for [moment.js](https://momentjs.com/guides/#/parsing/known-formats/) for formatting details), or use the reserved keyword 'ELAPSED' to display as a natural language string, relative to the current time. Help is available, if needed. 192 | 193 | The **Layout** tab 194 | 195 | ![image](https://user-images.githubusercontent.com/3724718/51728892-78979180-2037-11e9-8c4e-1578ca7302b5.png) 196 | 197 | The Layouts tab defines the basic settings that control how the data is arranged on the panel. 198 | The Horizontal checkbox switches the bar orientation from vertical to horizontal. As the chart(s) rotate, the axis and labels rotate with them - hence the use of neutral terms for the axis as 'High' and 'Low' rather than 'Left', 'Right', 'Top' or 'Bottom' 199 | 200 | **Label Margin** sets the area reserved for the labels can be set according to the length of the labels - or left blank, leaving the panel to calculate a reasonable value based on the actual data, orientation and chosen font size etc. **Angle** controls the rotation angle for the label text, which can help preserve screen real-estate - particularly when long labels are present. *Note - it is quite difficult for the control to predetermine the ideal centre of rotation for these labels. Depending on the data, this can make the charts hard to understand. More work in future releases should improve this feature. Still, if it helps in any specific case, feel free to use it.* 201 | 202 | **Low Side Margin** and **High Side Margin** set the width of the two axis. Set to 0 to hide one or both of them. 203 | 204 | **High Axis Color** and **Low Axis Color** set the colors for these axes, assuming they are visible 205 | 206 | **High Bar Color** sets the regular color of bars who's values are above the baseline (normally 0, see the Lines-And-Limits tab). **Low Bar Color** does the same for bar descending below (or to the left) of the base line. **Bar Padding** controls the width of the gap between bars, as a percentage of the bar width 207 | 208 | **Odd Row Color** and **Even Row Color** sets the colors of the alternating background stripes. These seem to work best when semi-transparent colors are chosen (switch the color picker to 'Custom' and slide the transparency control to the left) 209 | 210 | ![image](https://user-images.githubusercontent.com/3724718/51762923-b2e44b80-2096-11e9-821e-b4a1274e0b67.png) 211 | 212 | The **Grouping** tab 213 | ![image](https://user-images.githubusercontent.com/3724718/51763087-3605a180-2097-11e9-982c-a33b5e0466b5.png) 214 | 215 | Provided the Group Col (see the Columns Tab, above) is mapped to a field, this tab show settings for how the groups are to be displayed. (When Group Col is not defined, ALL the values appear in a single group) 216 | 217 | The **Columns Per Row** setting controls how many sub-charts appear in each row. When the data contains more groups than are defined here, additional rows of sub-charts are added, wrapping to fill the available space. *Note: If Group Col is set to something inappropriate, such as (say) the value or datetime field, Multistat can generate a ridiculous number of sub-charts - auto-scaled to fit in the available area, resulting in an unreadable mess. Don't panic - just choose a more meaningful grouping field, assuming your data has one.* 218 | 219 | The **Group Name Filter** field (*this is an advanced feature most users can ignore. If in doubt, make sure this is blank. Especially if the chart appears to be empty!).* This field should be a regular expression string which is used to filter out non-matching group names, when needed. 220 | 221 | Using the demo sample data, for example (which contains values for areas East, West and North), we could select just the East and West groups by using a value of 'East|West' (Note the Pipe character '|' separating a sequence of matching strings. Regular expressions are amazingly powerful and can be much, much more complicated than this - but a simple set of pipe-delimited strings is usually enough in this application. 222 | 223 | The **Group Sort Order** field is another regular expression string, this time used to define the order the groups are presented in (reading like a book from top left, wrapping to the bottom right). Matched group names are presented in order, followed by any remaining non-matched group names in the default (alphabetical) order. 224 | Left blank with our sample data, the groups will be arranged in alphabetical order - that is, East->North->West. Setting this field to the regular expression 'West|North|East' overrides the alphabetical ordering, resulting in a more map-meaningful displays with the West group on the left and the East group on the right *(apologies to users in the Southern Hemisphere who might have a different perspective...)* 225 | ![image](https://user-images.githubusercontent.com/3724718/51764732-841ca400-209b-11e9-8a0f-3527750d97fe.png) 226 | *Notice how Multistat intelligently adjusts the height and width of the sub-charts to keep all bars the same width, regardless of the number of bars in each group. When more than one row of sub-charts is generated, Multistat inserts blank/dummy rows in groups needed to keep the groups aligned properly.* 227 | ![image](https://user-images.githubusercontent.com/3724718/51766163-81bc4900-209f-11e9-945c-afe3bf001a88.png) 228 | 229 | **Show Group Labels** controls whether or not each group is topped with it's name, along with the **Font Size** and **Color** settings. 230 | 231 | The **Options** Tab 232 | ![image](https://user-images.githubusercontent.com/3724718/51764990-35bbd500-209c-11e9-9184-669e77eefddb.png) 233 | 234 | **Show Values** controls whether or not to display the values in text (the bar size always represents the value and may be enough for users without a readable text version). When checked, the **Font Size** and **Color** can also be defined. The **Position** setting controls where the value text will appear - either at the extreme (end) of the relevant bars, at the base of the bars or in a reserved area above (or to the right of) the chart. Choose what makes sense in your application. 235 | ![image](https://user-images.githubusercontent.com/3724718/51769330-6efa4200-20a8-11e9-90cf-c1a2397b4f89.png) 236 | *The three value positions, Bar Base, Bar End and Top* 237 | 238 | **Show Group Labels** and **Show Labels** - as before (Show Group Labels also appeared on the Grouping tab, for convenience).. Set Font Size and color etc. 239 | The **Out Of Range** label color override is an advanced feature for cases where a specific axis Max and/or Min setting is in place (see the **Lines-And-Limits** tab below) and a bar is outside one of these limits. This color overrides the standard label color for labels where this occurs. *(This is useful, for example where a non-working sensor, for example, generates a wildly out of range value)* 240 | **Label Margin**, **Angle**, **Low Side Margin** and **High Side Margin** - these too are duplicates of controls on the Layout tab, again, for convenience. 241 | 242 | **Tooltips** enables the mouse over info balloons, listing all the fields corresponding to the identified bar. **Date Format** allows the setting formatting characters for the field identified as the datetime field (if any) 243 | 244 | The **Lines and Limits** tab 245 | ![image](https://user-images.githubusercontent.com/3724718/51770407-4758a900-20ab-11e9-87b4-f7c69a679d6a.png) 246 | 247 | **Max Value** and **Min Value** These overrides default auto-scaling axis extents, and if the **Show Line** checkbox is set, control the color of the resulting reference lines. 248 | 249 | The **BaseLine** setting (default 0) differentiates between positive and negative values, each potentially having a different color. This can be useful when monitoring deviations from some non-zero set point. For example, Electrical generators (in North America, at least) operate at very close to 60Hz, with normally, only small deviations. Setting a baseline at 60.0 and a Max/Min to (say) 60.10 and 59.90 would make an easily understood display in such an application. 250 | 251 | Values above the base line are generally draw using the High Bar color (see the Layout Tab). Values below in the Low Bar color. 252 | 253 | **High Limit** and **Low Limit**, if set define additional 'warning' references. Corresponding reference lines and colors are set as before. In addition, the **Color Bar** option overrides the regular above or below base line bar colors for bars outside these warning levels. Optionally, these can be set to 'flash' - transitioning from one color to another at a controllable rate (period). *The period is measured in mS, Values between 200mS and 400mS seem to work best.* 254 | 255 | As with all these settings, the user can display a reference line on the chart and set the colors to whatever makes sense in the application. In the frequency example above, there might be high and Low Limits set at (say) 60.05 and 95.95 respectively. 256 | 257 | For example... 258 | 259 | ![image](https://user-images.githubusercontent.com/3724718/38963541-547a9560-4327-11e8-912a-5fdca266fd5f.png) 260 | 261 | Putting it all together, the displays can make a truly unforgettable and un-ignorable, experience. 262 | 263 | ![lines-and-limits](https://user-images.githubusercontent.com/3724718/30007679-5849101a-90c9-11e7-9c39-5618e8404454.png) 264 | 265 | ![alarms-alarms](https://user-images.githubusercontent.com/3724718/30007648-b45bfc6a-90c8-11e7-8ea8-5f43852ad27d.gif) 266 | 267 | (In retrospect, setting the Rate parameter to 300 or 400 works just as well, without risk of inducing epilepsy... 268 | ![calm-alarms](https://user-images.githubusercontent.com/3724718/30007967-6780c14a-90ce-11e7-809d-289d180ea310.gif) 269 | 270 | ## Recolor Rules 271 | 272 | (New feature added with version 1.3.0) 273 | 274 | Any column of data can be designated as a 'recolor column'. Once set, an extensible array of recolor rules appears, each having a pattern, a match type and an override color. 275 | In the sample below, the recolor column is set to 'room', which is also used in this example as the bar label column. 276 | ![image](https://user-images.githubusercontent.com/3724718/79685187-964da980-822e-11ea-9f7a-b1f9437138de.png) 277 | 278 | The pattern 'kitchen' is set for an exact match, so that the kitchen bar color is changed, in this case to yellow. 279 | 280 | Multiple rules can de defined, evaluated in order until a match is found. 281 | 282 | If none of the rules match the given recolor value, the bar colors are not overridden, and the colors based on the bar's numeric value are displayed. 283 | ![image](https://user-images.githubusercontent.com/3724718/79685382-6bfceb80-8230-11ea-9aed-fef873b257b4.png) 284 | 285 | In this case, the second rule 'room' with a subset match type sets all the bars containing the word 'room' blue. 286 | 287 | The 'List' rule match type uses a comma separated list of names. In this case, the 'bed room' match applies over the more general 'room' rule as it appears higher up in the list of recoloring rules 288 | ![image](https://user-images.githubusercontent.com/3724718/79685532-723f9780-8231-11ea-82ac-034cd6d67452.png) 289 | 290 | A final option - 'Reg ex' uses the rule pattern as a regular expression, for those brave enough to work out the syntax (!) 291 | 292 | ## Clickable tooltips & links 293 | 294 | Version 1.4.0 introduces an upgraded per-bar tooltip system, with improved styling and clickable per-bar url links 295 | 296 | ![image](https://user-images.githubusercontent.com/3724718/82230928-4e509e00-9924-11ea-909c-6b0bda7bb0d9.png) 297 | 298 | ## Bar-links 299 | 300 | New with this version, clickable URLs with bar-specific name/parameter substitutions - ideal for drill-downs or data look-ups etc. 301 | 302 | These bar-links are automatically appended to the tool tips (see above). Both the display names and the generated URLs can include substitution tokens, matching the names of the columns, surrounded by '{' and '}' characters. These are replaced by the values of these data elements. 303 | 304 | 305 | 306 | Any number of bar-links can be defined, using the Bar Links section of the panel editor 307 | 308 | ![image](https://user-images.githubusercontent.com/3724718/82231097-9079df80-9924-11ea-85fc-1d5d8e5e003f.png) 309 | 310 | ## Multi-Column support 311 | 312 | (New feature added with version 1.6.0) 313 | 314 | Previously, Multistat was only able to display values for one numeric field at a time. In this version, the value column selector has been changed to support any number of fields, each appearing as a differently colored bar. Consider this simple table-formatted data set as an example: 315 | 316 | `Shop Region Food Beverages Other` 317 | `Fred's Shop North 12000 3847 6363` 318 | `Mary's Shop North 6583 1466 7463` 319 | `Bob's Shop North 5343 9686 17632` 320 | `Ted's Shop South 5342 5325 7653` 321 | `Bill's Shop South 4252 2234 2426` 322 | `Jill's Shop South 5000 1600 2000` 323 | 324 | This can now be displayed in a single Multistat panel like this: 325 | 326 | ![image](https://user-images.githubusercontent.com/3724718/106139475-c5099080-616d-11eb-93e5-e1e3ce0735ef.png) 327 | 328 | Of course, all the other configuration features are still supported, such as bar orientation, grouping, threshold flashing etc. etc. 329 | 330 | The value column configuration menu has been changed to support these added fields. Pressing the plus sign next to Values Cols adds a new (empty) row to the value fields table where the bar color for positive and negative bars can be defined. The select switch reflects whether or not this field is visible (The optional legend supports mouse clicks to dynamically set or reset this visibility too, mimicking the action on Grafana graph panel) 331 | 332 | ![image](https://user-images.githubusercontent.com/3724718/106139941-64c71e80-616e-11eb-824e-befaa7e70b4a.png) 333 | 334 | 335 | 336 | 337 | 338 | ## Label/Group renaming, plus date reformatting support 339 | 340 | (New feature added with version 1.7.0) 341 | 342 | ### Label field renaming 343 | 344 | In previous versions of Multistat, the contents of the labels (and groups) were taken directly from the strings in the data received by the current query. In some cases, this results in unnecessarily long or confusing bar labels (and groups). 345 | 346 | Using the data set above as a trivial example, suppose users which to simplify the display by removing redundant "'s shop" from each of the labels. The Columns configuration section has a new items for Label Renaming Rules. pressing the '+' button opens a new renaming rule definition where we can enter the text to be replaced and the replacement. in this case, the string "'s Shop" and the empty replacement. 347 | 348 | ![image](https://user-images.githubusercontent.com/3724718/106141824-fe8fcb00-6170-11eb-8102-4fdce0de423c.png) 349 | 350 | 351 | 352 | ![image](https://user-images.githubusercontent.com/3724718/106141965-27b05b80-6171-11eb-9988-3b592da9a363.png) 353 | 354 | 355 | 356 | Technically, the selection text is treated as a regular expression, with the 'ig' options, making it case insensitive. 357 | 358 | Any number of rules can be defined, each operating sequentially, making this a very flexible way of pre-processing the data before display. Note, however, that every row of data is transformed by each and every renaming rule, so this does have the potential of slowing down the dashboard performance, especially when processing large data sets. 359 | 360 | ### Group field renaming 361 | 362 | A similar set of renaming rules is defined in the Grouping configuration setting, applying - obviously - to the grouping column, if any is defined. 363 | 364 | These renaming rule sets are applied to the data set before sorting, grouping and filtering etc. 365 | 366 | ### Date field reformatting 367 | 368 | Multistat has provided some support for an optional date/time field. Time series data sets require date fields, while the table formatted data sets such as used by Multistats do not. When present, date/time fields are natively treated as numbers or strings - as defined in the data query. That is, unix timestamps will appear as very large numbers (seconds or milliseconds since 1970-01-01), others may appear in any number of string formats, such as "2019-04-21 22:00:00" etc. Multistat does it's best to try and automatically parse any identified data/time field - most significantly, to sort data so that the 'first' and 'last' aggregation settings can select the appropriate record for display. This behaviour has been standard in Multistat since the beginning. Optionally, Multistat could be set to add a formatted date/time label to the panel's title string. 369 | 370 | In this version of Multistat, the optional date format field is used to parse and reformat the contents of such a date/time field in place, so these can be used in labels groups. This reformatting is done on the data received immediately before the earlier mentioned label and group renaming rules are applied. This has the potential of all sorts of data manipulation, pivot table-like. 371 | 372 | 373 | 374 | For example, with a simple data set like this: 375 | 376 | `Date Region Sales` 377 | 378 | `2019-01-01 Canada 134980` 379 | `2019-01-01 Mexico 138300` 380 | `2019-01-01 USA 231231` 381 | `2019-02-01 Canada 222849` 382 | `2019-02-01 Mexico 104779` 383 | `2019-02-01 USA 342929` 384 | `2019-03-01 Canada 273626` 385 | `2019-03-01 Mexico 144882` 386 | `2019-03-01 USA 348373` 387 | 388 | 389 | 390 | And setting the date time format string to `qQ-MMM` and selecting the Date column as the label field, with a 'sum' aggregation type, we can get this: 391 | 392 | ![image](https://user-images.githubusercontent.com/3724718/106149161-38b19a80-617a-11eb-82ab-fdbe33807943.png) 393 | 394 | 395 | 396 | ## Conclusion 397 | 398 | If you find this useful, and/or if you can think of additional features that you would find useful - make an entry on the project's [GitHub/issues page](https://github.com/michaeldmoore/michaeldmoore-multistat-panel/issues) 399 | -------------------------------------------------------------------------------- /dist/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## v1.6.0 6 | 7 | Adding Multi-bar support 8 | 9 | ## v1.6.1 10 | 11 | Signed, as required by Grafana 7.x.x. No other changes 12 | 13 | ## v1.7.0 14 | 15 | Adding label and group reformatting rules, plus optional reformat-in-place for date/time field. 16 | 17 | ## v1.7.4 18 | 19 | Fix panel to avoid deprecated grouping functions in d3.js. (this needed due to changes in Grafana 9.x.x). 20 | -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | # michaeldmoore-multistat-panel 2 | 3 | *New Version (1.3.0) - Now with rules-based bar re-coloring!* 4 | 5 | *New Version (1.4.0) - Now with restyled tooltips and clickable per-bar links!* 6 | 7 | *New Version (1.6.0) - Now with support for multiple value columns per label!* 8 | 9 | *New Version (1.7.0) - Now with support for label/group renaming and date formatting!* 10 | 11 | (Documentation for these changes is covered in the sections at the very end of this readme file) 12 | 13 | ## Custom multistat panel for Grafana, inspired by the built-in SingleStat panel 14 | 15 | This panel was developed for as a table-like panel for presenting bar charts, providing some useful additions particularly suited to process control/monitoring dashboards. As such, Multistat displays never use scroll-bars (scroll bars are useless in monitoring dashboard). 16 | 17 | SingleStat displays a single metric from time series data set, with optional threshold-related coloring etc. Multistat builds on this base, but with multi-column table data sets, displaying query data in the form of bar graphs with optional upper and lower hard limits. Plus a lot, lot more.... 18 | 19 | ![showcase](https://user-images.githubusercontent.com/3724718/30005310-38debe58-9094-11e7-9209-5aeb977c7577.gif) 20 | 21 | Data can be displayed as vertical bars... 22 | 23 | ![image](https://user-images.githubusercontent.com/3724718/38955637-1b1bd9d6-430a-11e8-9633-4e752f12d237.png) 24 | 25 | Horizontally... 26 | 27 | ![image](https://user-images.githubusercontent.com/3724718/38958080-07cd248c-4311-11e8-85c4-6c988b7ded08.png) 28 | 29 | Or grouped on an attribute, again vertically 30 | ![image](https://user-images.githubusercontent.com/3724718/51725178-7b3dbb00-2026-11e9-861f-d800897d17d3.png) 31 | 32 | ... or horizontally 33 | ![image](https://user-images.githubusercontent.com/3724718/51725236-c6f06480-2026-11e9-84f0-4d2f75a69d66.png) 34 | *(Note these last two examples are single Multistat panels. A single set of configuration settings is automatically applied to each of the grouped sub-displays)* 35 | 36 | And (just about) *everything* is configurable... 37 | Max, Min, auto-scaling, base-lines, colors, rows and columns - (just about) everything... 38 | 39 | High and Low limits too, with optional bar coloring 40 | ![image](https://user-images.githubusercontent.com/3724718/51725465-f3f14700-2027-11e9-8d61-592f644a5c20.png) 41 | 42 | All this, and optional flashing too when bars surpass these limits. 43 | 44 | ## Data 45 | 46 | Multistat accepts Grafana **table** formatted data. Note - There is **no support for time series** formatted data. 47 | 48 | As a minimum, Multistat requires table data with at least two fields per row - one, a label (string) and the other, a numeric value. These fields can be called anything. Multistat makes no assumptions regarding the names of the table data field it handles. 49 | 50 | Each distinct label will be displayed as a bar - the length being determined by it's numerical value. 51 | 52 | takes it's data from a table query - returning, at minimum, 2 fields (names can be anything) 53 | 54 | A **timestamp** field can be useful too - this can be in any commonly understood format (Multistat uses the popular **Moment.js** java script library for manipulating time/date strings - see https://momentjs.com/docs/#/displaying/ for details). 55 | 56 | A **grouping** field can be useful too, to organize large data sets into more meaningful sections. When grouping, the number of columns can be pre-configured, along with handy mechanisms for filtering and arranging the order each group is presented in. 57 | 58 | Any additional fields are retained and presented in optional tool-tip balloons. Again, (just about) everything is configurable here... 59 | ![image](https://user-images.githubusercontent.com/3724718/51726418-e8544f00-202c-11e9-89e8-2cce5c7bfb2a.png) 60 | 61 | ## Duplicate labels in table data 62 | 63 | Each distinct label in the input data results in a distinct bar in Multistat. Ideally, table data should be created by queries that return distinct data sets - that is, sets in which each label is presented in a single row. When data sets are processed with multiple rows for a given label, Multistat needs to know which value to use (and hence, which values to ignore). A configurable aggregation parameter tells Multistat how to handle this. 'Last' (and 'First') select the last (or first) row in the data for any given label, throwing out all the others. The optional date/timestamp field helps too by pre-sorting the data table before selecting the aggregation function. The tool-tip then shows the set of fields for the selected data row, as expected. 64 | 65 | Setting the aggregation parameter to 'Max' or 'Min' works in a similar way, selecting the row for each label with the corresponding value - and secondly using the last or latest value in the event that there is a tie in the value. 66 | 67 | Setting the aggregation parameter to 'Mean' results in the arithmetic mean of all duplicate values to be used, as should be expected. *A side effect of this though is that the fields presented in the tool-tip balloon in this case represent just one of the rows - actually the 'last' row, the value of which will not generally match the displayed 'mean' value for that label.* 68 | 69 | ## Data set size - a performance consideration 70 | 71 | As mentioned before, ideally the data set should contain a single row for each distinct label. This offloads the maximum amount of filtering and aggregation etc., to the database which is generally much more efficient for these tasks. 72 | In the event that the database cannot pre-filter the data in this way, the aggregation setting can still generate the required display, but at the cost of increased CPU and network load. Generally, this is not significant - Multistat can easily handle queries with a few hundred labels, each with a hundred or more rows. Note though that huge data sets - data sets with multiple megabytes of data etc. - these will negatively impact performance. Particularly as refresh rates shorten. In extreme cases, this can even make the browser become unresponsive. **Beware of enormous data sets.** 73 | 74 | Multistat has a wealth of configurable options. just about everything displayed can be adjusted and hidden using the extensive set of configuration options, described in detail below. 75 | 76 | ![image](https://user-images.githubusercontent.com/3724718/50191698-95cc9800-02f4-11e9-96cd-a0c5da672278.png) 77 | 78 | ## Features 79 | 80 | * Orientation - Horizontal or Vertical bars 81 | 82 | * Sorting (Dynamic, by Name, Value (or Update Time), ascending or descending) 83 | 84 | * Query result field name mapping - define which fields represent the labels and which represent values 85 | 86 | * Last Update Time display (Optional, if Update DateTime column included in query and column mapped). Useful to demonstrate the data source is actually updating 87 | 88 | * Optional display of values, selectable font size and color 89 | 90 | * Optional display of labels, selectable font size and color 91 | 92 | * Selectable Bar color (bar coloring is a subject in itself - see below for far more details regarding this) 93 | 94 | * Optional Left & Right value axes display (that is, upper & lower axes when in horizontal display mode) 95 | 96 | * Adjustable label margin size override 97 | 98 | * Min, Max and Baseline values (See below for discussion on baselines) 99 | 100 | * Optional High and Low value alarm limits and indicator lines 101 | 102 | * Optional setting color of bars exceeding High or Low alarm thresholds 103 | 104 | * Optional two-color alarm flashing, with settable flash rate 105 | 106 | `Note : With all these options, the author accepts no responsibility for` 107 | `inducing nausea, epilepsy or generally violating the bounds of good taste)` 108 | 109 | ## Configuration details 110 | 111 | The Grafana-standard Metrics tab allows the user to specify a query, to be issued each time the panel is refreshed. 112 | 113 | This area is under continued active development. Currently, Multistat only supports **Table** data queries. Each row returned will be displayed as a bar, auto-sized to use the available space. The panel does not provide scroll bars, so any query returning more rows than can comfortably fit in the allotted panel area will be unreadable. 114 | 115 | *Note - getting appropriately formatted data can be challenging, especially while becoming familiar with all the options offered by Multistat. For this end - and for general purpose Grafana plugin testing - I've created a simple NodeJS data source called **CSVServer**, working in conjunction with the standard **SimpleJSON** datasource to import simple CSV files which can be easily edited to generate any kind of table or time series data sets. See here for details, including set up instructions : * 116 | 117 | ## Table Queries 118 | 119 | Multistat queries are expected to be something along the lines of ''Get the working pressure of all steam boilers in building 5'' or ''Get the temperature of the 10 hottest cities in the US'' etc. Anything that can be re-queried efficiently and returns a list of results containing - at minimum - a Label (e.g. Boiler ID or City Name) and a value (e.g. the pressure or temperature). Optionally, Multistat takes advantage of a DateTime field, if present, which can be displayed alongside the panel title as an indicator as to the last time the data was updated. More details on this below. 120 | 121 | If no query is defined, or the data source is unavailable, Multistat displays a simple "No data" warning message. 122 | 123 | ![no-data](https://user-images.githubusercontent.com/3724718/30006554-7b72f5fc-90af-11e7-8b79-d331f60d0388.png) 124 | 125 | ## Queries *should* return just one value per label 126 | 127 | Multistat can only display a single bar for each label. Ideally, query results *should be written* to return a single value per label. When this is not possible, and the query returns multiple values per label, Multistat uses an aggregation operator to select one of these (first, last, mean, min or max). *one more - all - will eliminate the aggregator altogether, Be careful - This can create confusing displays as multiple values appear overlying the position of such bars. **You have been warned.** For efficiency though, it is much better to write a query that only returns the required data. 128 | 129 | For this discussion, I created a test data set in a CSV file (demo.csv) distributed with the **CSVServer** add-on to SimpleJSON data source. I highly recommend installing this so you can follow along and see how the various configuration options work before worrying about live real-world data sets. The demo CSV file contains the following data: 130 | 131 | ```bash 132 | time,sensor,area,quantity 133 | 2018-12-18 00:21:05.000,AAA,West,1.100 134 | 2018-12-18 00:21:04.000,AAA,West,1.000 135 | 2018-12-18 00:21:03.000,AAA,West,0.950 136 | 2018-12-18 00:21:02.000,AAA,West,0.900 137 | 2018-12-18 00:21:01.000,AAA,West,0.850 138 | 2018-12-18 00:21:00.000,AAA,West,0.800 139 | 2018-12-18 00:21:07.000,AAA,West,1.234 140 | 2018-12-18 00:21:07.000,BBB,East,0.662 141 | 2018-12-18 00:21:07.000,CCC,East,0.344 142 | 2018-12-18 00:21:07.000,EEE,West,0.357 143 | 2018-12-18 00:21:07.000,GGG,West,0.563 144 | 2018-12-18 00:21:07.000,HHH,West,0.234 145 | 2018-12-18 00:21:07.000,III,West,0.840 146 | 2018-12-18 00:21:07.000,JJJ,East,0.193 147 | 2018-12-18 00:21:07.000,KKK,West,0.262 148 | 2018-12-18 00:21:07.000,LLL,North,0.802 149 | 2018-12-18 00:21:07.000,MMM,East,0.211 150 | 2018-12-18 00:21:07.000,PPP,North,0.300 151 | 2018-12-18 00:21:07.000,QQQ,North,0.731 152 | 2018-12-18 00:21:07.000,RRR,North,1.101 153 | 2018-12-18 00:21:07.000,SSS,East,0.811 154 | 2018-12-18 00:21:07.000,WWW,East,0.213 155 | 2018-12-18 00:21:07.000,YYY,East,0.844 156 | 2018-12-18 00:21:07.000,ZZZ,North,0.928 157 | ``` 158 | 159 | Note, sensor AAA in this data set has multiple values, each a few hours apart. All the other sensors have a single row - this will allow the aggregation feature to be demonstrated later in this note. Each row in this data set includes a date/time, a label and a value, plus a region field (that will be useful in grouping). The field names can be anything; everything is defined in the configuration tabs. Additional fields, if any, will appear in the tool-tip pop-up display, if enabled. 160 | 161 | As you can see, Multistat is configured using a number of option tabs. Let's examine each of these in sequence. 162 | 163 | First, the data source and query is setup using the standard **Metrics** tab 164 | 165 | ![image](https://user-images.githubusercontent.com/3724718/50192332-5b182f00-02f7-11e9-805c-2137c832d7f6.png) 166 | 167 | Note the data set format is set to '**Table**' (Multistat does not support time series data sets) 168 | 169 | Note: The Query Inspector built into Grafana is a terrific resource for figuring out source data problems. Here's what we get from my demo query: 170 | 171 | ![image](https://user-images.githubusercontent.com/3724718/50192518-3e302b80-02f8-11e9-8e34-eb039eb23bce.png) 172 | etc. 173 | 174 | The data is mapped using the **Columns** configuration tab: 175 | 176 | ![image](https://user-images.githubusercontent.com/3724718/51728249-2ce3e880-2035-11e9-8ca6-5c51b97710c1.png) 177 | 178 | Here, you can see how the 4 key fields in the query result set get mapped to the Multistat fields. In this case, the **label** is associated with the query field 'sensor', **Value** as 'quantity', with 1 decimal place and no scaling (**scale factor = 1**). Note too that the default **aggregation** parameter of 'Last' is selected. This dataset contains multiple rows for some labels - this setting automatically selects the last (latest) value for each sensor. 179 | 180 | Note too, that the bars are set to be sorted in ascending 'sensor' name. 181 | 182 | The data is set to **group** on the 'area' field - this will create 3 sub-charts, for the East, North and West areas. 183 | 184 | The **DateTime col** (optional) is mapped here to the 'time' field. When set, the TZ Offset Hrs setting can be used to offset the display value to account for time-zone differences between the data source and the client. *(Note - this time offset features duplicates something similar built into recent versions of Grafana. This feature may be removed in future versions of Multistat)* 185 | 186 | The '**Show as-of Date**' setting controls whether or not the last update time is to be displayed in the top right of the panel. **Most users can ignore this setting**. When it is set, the maximum datetime value in the query record set is displayed alongside the panel title. This can be useful in process monitoring applications to provide evidence that the data is being updated in a timely manner etc. The format field controls how this time is displayed (see documentation for [moment.js](https://momentjs.com/guides/#/parsing/known-formats/) for formatting details), or use the reserved keyword 'ELAPSED' to display as a natural language string, relative to the current time. Help is available, if needed. 187 | 188 | The **Layout** tab 189 | 190 | ![image](https://user-images.githubusercontent.com/3724718/51728892-78979180-2037-11e9-8c4e-1578ca7302b5.png) 191 | 192 | The Layouts tab defines the basic settings that control how the data is arranged on the panel. 193 | The Horizontal checkbox switches the bar orientation from vertical to horizontal. As the chart(s) rotate, the axis and labels rotate with them - hence the use of neutral terms for the axis as 'High' and 'Low' rather than 'Left', 'Right', 'Top' or 'Bottom' 194 | 195 | **Label Margin** sets the area reserved for the labels can be set according to the length of the labels - or left blank, leaving the panel to calculate a reasonable value based on the actual data, orientation and chosen font size etc. **Angle** controls the rotation angle for the label text, which can help preserve screen real-estate - particularly when long labels are present. *Note - it is quite difficult for the control to predetermine the ideal centre of rotation for these labels. Depending on the data, this can make the charts hard to understand. More work in future releases should improve this feature. Still, if it helps in any specific case, feel free to use it.* 196 | 197 | **Low Side Margin** and **High Side Margin** set the width of the two axis. Set to 0 to hide one or both of them. 198 | 199 | **High Axis Color** and **Low Axis Color** set the colors for these axes, assuming they are visible 200 | 201 | **High Bar Color** sets the regular color of bars who's values are above the baseline (normally 0, see the Lines-And-Limits tab). **Low Bar Color** does the same for bar descending below (or to the left) of the base line. **Bar Padding** controls the width of the gap between bars, as a percentage of the bar width 202 | 203 | **Odd Row Color** and **Even Row Color** sets the colors of the alternating background stripes. These seem to work best when semi-transparent colors are chosen (switch the color picker to 'Custom' and slide the transparency control to the left) 204 | 205 | ![image](https://user-images.githubusercontent.com/3724718/51762923-b2e44b80-2096-11e9-821e-b4a1274e0b67.png) 206 | 207 | The **Grouping** tab 208 | ![image](https://user-images.githubusercontent.com/3724718/51763087-3605a180-2097-11e9-982c-a33b5e0466b5.png) 209 | 210 | Provided the Group Col (see the Columns Tab, above) is mapped to a field, this tab show settings for how the groups are to be displayed. (When Group Col is not defined, ALL the values appear in a single group) 211 | 212 | The **Columns Per Row** setting controls how many sub-charts appear in each row. When the data contains more groups than are defined here, additional rows of sub-charts are added, wrapping to fill the available space. *Note: If Group Col is set to something inappropriate, such as (say) the value or datetime field, Multistat can generate a ridiculous number of sub-charts - auto-scaled to fit in the available area, resulting in an unreadable mess. Don't panic - just choose a more meaningful grouping field, assuming your data has one.* 213 | 214 | The **Group Name Filter** field (*this is an advanced feature most users can ignore. If in doubt, make sure this is blank. Especially if the chart appears to be empty!).* This field should be a regular expression string which is used to filter out non-matching group names, when needed. 215 | 216 | Using the demo sample data, for example (which contains values for areas East, West and North), we could select just the East and West groups by using a value of 'East|West' (Note the Pipe character '|' separating a sequence of matching strings. Regular expressions are amazingly powerful and can be much, much more complicated than this - but a simple set of pipe-delimited strings is usually enough in this application. 217 | 218 | The **Group Sort Order** field is another regular expression string, this time used to define the order the groups are presented in (reading like a book from top left, wrapping to the bottom right). Matched group names are presented in order, followed by any remaining non-matched group names in the default (alphabetical) order. 219 | Left blank with our sample data, the groups will be arranged in alphabetical order - that is, East->North->West. Setting this field to the regular expression 'West|North|East' overrides the alphabetical ordering, resulting in a more map-meaningful displays with the West group on the left and the East group on the right *(apologies to users in the Southern Hemisphere who might have a different perspective...)* 220 | ![image](https://user-images.githubusercontent.com/3724718/51764732-841ca400-209b-11e9-8a0f-3527750d97fe.png) 221 | *Notice how Multistat intelligently adjusts the height and width of the sub-charts to keep all bars the same width, regardless of the number of bars in each group. When more than one row of sub-charts is generated, Multistat inserts blank/dummy rows in groups needed to keep the groups aligned properly.* 222 | ![image](https://user-images.githubusercontent.com/3724718/51766163-81bc4900-209f-11e9-945c-afe3bf001a88.png) 223 | 224 | **Show Group Labels** controls whether or not each group is topped with it's name, along with the **Font Size** and **Color** settings. 225 | 226 | The **Options** Tab 227 | ![image](https://user-images.githubusercontent.com/3724718/51764990-35bbd500-209c-11e9-9184-669e77eefddb.png) 228 | 229 | **Show Values** controls whether or not to display the values in text (the bar size always represents the value and may be enough for users without a readable text version). When checked, the **Font Size** and **Color** can also be defined. The **Position** setting controls where the value text will appear - either at the extreme (end) of the relevant bars, at the base of the bars or in a reserved area above (or to the right of) the chart. Choose what makes sense in your application. 230 | ![image](https://user-images.githubusercontent.com/3724718/51769330-6efa4200-20a8-11e9-90cf-c1a2397b4f89.png) 231 | *The three value positions, Bar Base, Bar End and Top* 232 | 233 | **Show Group Labels** and **Show Labels** - as before (Show Group Labels also appeared on the Grouping tab, for convenience).. Set Font Size and color etc. 234 | The **Out Of Range** label color override is an advanced feature for cases where a specific axis Max and/or Min setting is in place (see the **Lines-And-Limits** tab below) and a bar is outside one of these limits. This color overrides the standard label color for labels where this occurs. *(This is useful, for example where a non-working sensor, for example, generates a wildly out of range value)* 235 | **Label Margin**, **Angle**, **Low Side Margin** and **High Side Margin** - these too are duplicates of controls on the Layout tab, again, for convenience. 236 | 237 | **Tooltips** enables the mouse over info balloons, listing all the fields corresponding to the identified bar. **Date Format** allows the setting formatting characters for the field identified as the datetime field (if any) 238 | 239 | The **Lines and Limits** tab 240 | ![image](https://user-images.githubusercontent.com/3724718/51770407-4758a900-20ab-11e9-87b4-f7c69a679d6a.png) 241 | 242 | **Max Value** and **Min Value** These overrides default auto-scaling axis extents, and if the **Show Line** checkbox is set, control the color of the resulting reference lines. 243 | 244 | The **BaseLine** setting (default 0) differentiates between positive and negative values, each potentially having a different color. This can be useful when monitoring deviations from some non-zero set point. For example, Electrical generators (in North America, at least) operate at very close to 60Hz, with normally, only small deviations. Setting a baseline at 60.0 and a Max/Min to (say) 60.10 and 59.90 would make an easily understood display in such an application. 245 | 246 | Values above the base line are generally draw using the High Bar color (see the Layout Tab). Values below in the Low Bar color. 247 | 248 | **High Limit** and **Low Limit**, if set define additional 'warning' references. Corresponding reference lines and colors are set as before. In addition, the **Color Bar** option overrides the regular above or below base line bar colors for bars outside these warning levels. Optionally, these can be set to 'flash' - transitioning from one color to another at a controllable rate (period). *The period is measured in mS, Values between 200mS and 400mS seem to work best.* 249 | 250 | As with all these settings, the user can display a reference line on the chart and set the colors to whatever makes sense in the application. In the frequency example above, there might be high and Low Limits set at (say) 60.05 and 95.95 respectively. 251 | 252 | For example... 253 | 254 | ![image](https://user-images.githubusercontent.com/3724718/38963541-547a9560-4327-11e8-912a-5fdca266fd5f.png) 255 | 256 | Putting it all together, the displays can make a truly unforgettable and un-ignorable, experience. 257 | 258 | ![lines-and-limits](https://user-images.githubusercontent.com/3724718/30007679-5849101a-90c9-11e7-9c39-5618e8404454.png) 259 | 260 | ![alarms-alarms](https://user-images.githubusercontent.com/3724718/30007648-b45bfc6a-90c8-11e7-8ea8-5f43852ad27d.gif) 261 | 262 | (In retrospect, setting the Rate parameter to 300 or 400 works just as well, without risk of inducing epilepsy... 263 | ![calm-alarms](https://user-images.githubusercontent.com/3724718/30007967-6780c14a-90ce-11e7-809d-289d180ea310.gif) 264 | 265 | ## Recolor Rules 266 | 267 | (New feature added with version 1.3.0) 268 | 269 | Any column of data can be designated as a 'recolor column'. Once set, an extensible array of recolor rules appears, each having a pattern, a match type and an override color. 270 | In the sample below, the recolor column is set to 'room', which is also used in this example as the bar label column. 271 | ![image](https://user-images.githubusercontent.com/3724718/79685187-964da980-822e-11ea-9f7a-b1f9437138de.png) 272 | 273 | The pattern 'kitchen' is set for an exact match, so that the kitchen bar color is changed, in this case to yellow. 274 | 275 | Multiple rules can de defined, evaluated in order until a match is found. 276 | 277 | If none of the rules match the given recolor value, the bar colors are not overridden, and the colors based on the bar's numeric value are displayed. 278 | ![image](https://user-images.githubusercontent.com/3724718/79685382-6bfceb80-8230-11ea-9aed-fef873b257b4.png) 279 | 280 | In this case, the second rule 'room' with a subset match type sets all the bars containing the word 'room' blue. 281 | 282 | The 'List' rule match type uses a comma separated list of names. In this case, the 'bed room' match applies over the more general 'room' rule as it appears higher up in the list of recoloring rules 283 | ![image](https://user-images.githubusercontent.com/3724718/79685532-723f9780-8231-11ea-82ac-034cd6d67452.png) 284 | 285 | A final option - 'Reg ex' uses the rule pattern as a regular expression, for those brave enough to work out the syntax (!) 286 | 287 | ## Clickable tooltips & links 288 | 289 | Version 1.4.0 introduces an upgraded per-bar tooltip system, with improved styling and clickable per-bar url links 290 | 291 | ![image](https://user-images.githubusercontent.com/3724718/82230928-4e509e00-9924-11ea-909c-6b0bda7bb0d9.png) 292 | 293 | ## Bar-links 294 | 295 | New with this version, clickable URLs with bar-specific name/parameter substitutions - ideal for drill-downs or data look-ups etc. 296 | 297 | These bar-links are automatically appended to the tool tips (see above). Both the display names and the generated URLs can include substitution tokens, matching the names of the columns, surrounded by '{' and '}' characters. These are replaced by the values of these data elements. 298 | 299 | 300 | 301 | Any number of bar-links can be defined, using the Bar Links section of the panel editor 302 | 303 | ![image](https://user-images.githubusercontent.com/3724718/82231097-9079df80-9924-11ea-85fc-1d5d8e5e003f.png) 304 | 305 | ## Multi-Column support 306 | 307 | (New feature added with version 1.6.0) 308 | 309 | Previously, Multistat was only able to display values for one numeric field at a time. In this version, the value column selector has been changed to support any number of fields, each appearing as a differently colored bar. Consider this simple table-formatted data set as an example: 310 | 311 | `Shop Region Food Beverages Other` 312 | `Fred's Shop North 12000 3847 6363` 313 | `Mary's Shop North 6583 1466 7463` 314 | `Bob's Shop North 5343 9686 17632` 315 | `Ted's Shop South 5342 5325 7653` 316 | `Bill's Shop South 4252 2234 2426` 317 | `Jill's Shop South 5000 1600 2000` 318 | 319 | This can now be displayed in a single Multistat panel like this: 320 | 321 | ![image](https://user-images.githubusercontent.com/3724718/106139475-c5099080-616d-11eb-93e5-e1e3ce0735ef.png) 322 | 323 | Of course, all the other configuration features are still supported, such as bar orientation, grouping, threshold flashing etc. etc. 324 | 325 | The value column configuration menu has been changed to support these added fields. Pressing the plus sign next to Values Cols adds a new (empty) row to the value fields table where the bar color for positive and negative bars can be defined. The select switch reflects whether or not this field is visible (The optional legend supports mouse clicks to dynamically set or reset this visibility too, mimicking the action on Grafana graph panel) 326 | 327 | ![image](https://user-images.githubusercontent.com/3724718/106139941-64c71e80-616e-11eb-824e-befaa7e70b4a.png) 328 | 329 | 330 | 331 | 332 | 333 | ## Label/Group renaming, plus date reformatting support 334 | 335 | (New feature added with version 1.7.0) 336 | 337 | ### Label field renaming 338 | 339 | In previous versions of Multistat, the contents of the labels (and groups) were taken directly from the strings in the data received by the current query. In some cases, this results in unnecessarily long or confusing bar labels (and groups). 340 | 341 | Using the data set above as a trivial example, suppose users which to simplify the display by removing redundant "'s shop" from each of the labels. The Columns configuration section has a new items for Label Renaming Rules. pressing the '+' button opens a new renaming rule definition where we can enter the text to be replaced and the replacement. in this case, the string "'s Shop" and the empty replacement. 342 | 343 | ![image](https://user-images.githubusercontent.com/3724718/106141824-fe8fcb00-6170-11eb-8102-4fdce0de423c.png) 344 | 345 | 346 | 347 | ![image](https://user-images.githubusercontent.com/3724718/106141965-27b05b80-6171-11eb-9988-3b592da9a363.png) 348 | 349 | 350 | 351 | Technically, the selection text is treated as a regular expression, with the 'ig' options, making it case insensitive. 352 | 353 | Any number of rules can be defined, each operating sequentially, making this a very flexible way of pre-processing the data before display. Note, however, that every row of data is transformed by each and every renaming rule, so this does have the potential of slowing down the dashboard performance, especially when processing large data sets. 354 | 355 | ### Group field renaming 356 | 357 | A similar set of renaming rules is defined in the Grouping configuration setting, applying - obviously - to the grouping column, if any is defined. 358 | 359 | These renaming rule sets are applied to the data set before sorting, grouping and filtering etc. 360 | 361 | ### Date field reformatting 362 | 363 | Multistat has provided some support for an optional date/time field. Time series data sets require date fields, while the table formatted data sets such as used by Multistats do not. When present, date/time fields are natively treated as numbers or strings - as defined in the data query. That is, unix timestamps will appear as very large numbers (seconds or milliseconds since 1970-01-01), others may appear in any number of string formats, such as "2019-04-21 22:00:00" etc. Multistat does it's best to try and automatically parse any identified data/time field - most significantly, to sort data so that the 'first' and 'last' aggregation settings can select the appropriate record for display. This behaviour has been standard in Multistat since the beginning. Optionally, Multistat could be set to add a formatted date/time label to the panel's title string. 364 | 365 | In this version of Multistat, the optional date format field is used to parse and reformat the contents of such a date/time field in place, so these can be used in labels groups. This reformatting is done on the data received immediately before the earlier mentioned label and group renaming rules are applied. This has the potential of all sorts of data manipulation, pivot table-like. 366 | 367 | 368 | 369 | For example, with a simple data set like this: 370 | 371 | `Date Region Sales` 372 | 373 | `2019-01-01 Canada 134980` 374 | `2019-01-01 Mexico 138300` 375 | `2019-01-01 USA 231231` 376 | `2019-02-01 Canada 222849` 377 | `2019-02-01 Mexico 104779` 378 | `2019-02-01 USA 342929` 379 | `2019-03-01 Canada 273626` 380 | `2019-03-01 Mexico 144882` 381 | `2019-03-01 USA 348373` 382 | 383 | 384 | 385 | And setting the date time format string to `qQ-MMM` and selecting the Date column as the label field, with a 'sum' aggregation type, we can get this: 386 | 387 | ![image](https://user-images.githubusercontent.com/3724718/106149161-38b19a80-617a-11eb-82ab-fdbe33807943.png) 388 | 389 | 390 | 391 | ## Conclusion 392 | 393 | If you find this useful, and/or if you can think of additional features that you would find useful - make an entry on the project's [GitHub/issues page](https://github.com/michaeldmoore/michaeldmoore-multistat-panel/issues) 394 | -------------------------------------------------------------------------------- /dist/barlinks.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | 32 |
33 |
34 |
35 |
36 | 37 |
38 | 42 |
43 |
44 |
-------------------------------------------------------------------------------- /dist/columns.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 | 9 |
10 |
11 |
12 | 13 |
14 | 15 | 19 |
20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 43 |
44 |
45 | 46 |
47 | 48 | 51 |
52 |
53 |
54 | 55 | 57 | 58 | 60 | 61 |
62 | 64 |
65 |
66 |
67 | 68 |
69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 | 78 | 81 |
82 |
83 |
84 | 85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 | 100 |
101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 112 |
113 | 114 |
115 |
116 | 117 |
118 | 120 |
121 |
122 |
123 |
124 |
125 | 126 |
127 | 129 |
130 |
131 |
132 | 133 | 136 | 138 |
139 |
140 |
141 |
142 |
143 | 144 | 147 | 153 |
154 |
155 |
156 |
157 |
As-Of Date display:
158 | This option controls if, and how the latest query time is displayed in the top right hand side of the panel.
159 | Depending on the data source and query, each row in Multistat may be updated at different times.
160 | This display can be useful in showing the latest timestamp of all the data displayed in the panel - as a snapshot date/time.
161 | 
162 | Date display formatting string uses common standards. (see here)
163 | 
164 | Setting the format string to the reserved keyword "ELAPSED" displays the time as the elapsed time since the last data update (as per the latest grafana refresh operation).
165 |    	  
166 |
167 |
168 |
169 | 170 |
171 | 173 |
174 |
175 | 177 |
178 |
179 |
180 |
181 |
182 | 186 |
187 | 189 |
190 |
191 |
192 | 193 |
194 | 195 | 199 |
200 | 201 |
203 |
204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 219 | 220 |
221 | 223 |
224 | 225 | 226 | 227 | 228 |
229 |
230 | 231 |
232 | 237 |
238 |
239 |
-------------------------------------------------------------------------------- /dist/css/multistat-panel.css: -------------------------------------------------------------------------------- 1 | .michaeldmoore-multistat-panel-nodata { 2 | position: absolute; 3 | top: 50%; 4 | text-align: center; 5 | vertical-align: middle; 6 | font-size: 0.875rem; 7 | } 8 | 9 | .michaeldmoore-multistat-panel-maxDate { 10 | margin-right: 4em; 11 | float: right; 12 | padding-left: 20px; 13 | } 14 | 15 | .michaeldmoore-multistat-panel-dark-tooltip { 16 | position: absolute; 17 | width: auto; 18 | height: auto; 19 | padding: 5px; 20 | background-color: black; 21 | color: white; 22 | -webkit-border-radius: 10px; 23 | -moz-border-radius: 10px; 24 | border-radius: 10px; 25 | -webkit-box-shadow: 4px 4px 10px rgba(255, 255, 255, 0.4); 26 | -moz-box-shadow: 4px 4px 10px rgba(255, 255, 255, 0.4); 27 | box-shadow: 4px 4px 10px rgba(255, 255, 255, 0.4); 28 | } 29 | 30 | .michaeldmoore-multistat-panel-dark-tooltip th { 31 | font-weight: bold; 32 | text-align: center; 33 | border-bottom: 1px solid white; 34 | } 35 | 36 | .michaeldmoore-multistat-panel-dark-tooltip td:first-of-type { 37 | padding-right: 1em; 38 | } 39 | 40 | .michaeldmoore-multistat-panel-dark-tooltip a { 41 | color: #8080ff; 42 | text-decoration: blue underline; 43 | } 44 | 45 | .michaeldmoore-multistat-panel-light-tooltip { 46 | position: absolute; 47 | width: auto; 48 | height: auto; 49 | padding: 5px; 50 | background-color: white; 51 | color: black; 52 | -webkit-border-radius: 10px; 53 | -moz-border-radius: 10px; 54 | border-radius: 10px; 55 | -webkit-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4); 56 | -moz-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4); 57 | box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4); 58 | } 59 | 60 | .michaeldmoore-multistat-panel-light-tooltip th { 61 | font-weight: bold; 62 | text-align: center; 63 | border-bottom: 1px solid black; 64 | } 65 | 66 | .michaeldmoore-multistat-panel-light-tooltip td:first-of-type { 67 | padding-right: 1em; 68 | } 69 | 70 | .michaeldmoore-multistat-panel-light-tooltip a { 71 | color: #8080ff; 72 | text-decoration: blue underline; 73 | } 74 | 75 | 76 | 77 | 78 | 79 | .michaeldmoore-multistat-panel-statusmessage { 80 | position: absolute; 81 | top: 50%; 82 | left: 50%; 83 | margin-right: -50%; 84 | transform: translate(-50%, -50%); 85 | } 86 | 87 | 88 | .michaeldmoore-multistat-panel-w2 { 89 | width: 1.75rem; 90 | height: 37px; 91 | margin-right: 0.25rem; 92 | margin-bottom: 0px; 93 | } 94 | 95 | .michaeldmoore-multistat-panel-w3 { 96 | width: 2.75rem; 97 | height: 37px; 98 | margin-right: 0.25rem; 99 | margin-bottom: 0px; 100 | } 101 | 102 | .michaeldmoore-multistat-panel-w4 { 103 | width: 3.75rem; 104 | height: 37px; 105 | margin-right: 0.25rem; 106 | margin-bottom: 0px; 107 | } 108 | 109 | .michaeldmoore-multistat-panel-w5 { 110 | width: 4.75rem; 111 | height: 37px; 112 | margin-right: 0.25rem; 113 | margin-bottom: 0px; 114 | } 115 | 116 | .michaeldmoore-multistat-panel-w6 { 117 | width: 5.75rem; 118 | height: 37px; 119 | margin-right: 0.25rem; 120 | margin-bottom: 0px; 121 | } 122 | 123 | .michaeldmoore-multistat-panel-w7 { 124 | width: 6.75rem; 125 | height: 37px; 126 | margin-right: 0.25rem; 127 | margin-bottom: 0px; 128 | } 129 | 130 | .michaeldmoore-multistat-panel-w8 { 131 | width: 7.75rem; 132 | height: 37px; 133 | margin-right: 0.25rem; 134 | margin-bottom: 0px; 135 | } 136 | 137 | .michaeldmoore-multistat-panel-w9 { 138 | width: 8.75rem; 139 | height: 37px; 140 | margin-right: 0.25rem; 141 | margin-bottom: 0px; 142 | } 143 | 144 | .michaeldmoore-multistat-panel-w10 { 145 | width: 9.75rem; 146 | height: 37px; 147 | margin-right: 0.25rem; 148 | margin-bottom: 0px; 149 | } 150 | 151 | .michaeldmoore-multistat-panel-w16 { 152 | width: 15.75rem; 153 | height: 37px; 154 | margin-right: 0.25rem; 155 | margin-bottom: 0px; 156 | } 157 | 158 | .michaeldmoore-multistat-panel-w18 { 159 | width: 17.75rem; 160 | height: 37px; 161 | margin-right: 0.25rem; 162 | margin-bottom: 0px; 163 | } 164 | 165 | .michaeldmoore-multistat-panel-w25 { 166 | width: 24.75rem; 167 | height: 37px; 168 | margin-right: 0.25rem; 169 | margin-bottom: 0px; 170 | } 171 | 172 | pre a { 173 | display: contents; 174 | text-decoration: underline; 175 | } 176 | 177 | .michaeldmoore-multistat-panel-legend { 178 | flex-grow: 1; 179 | margin-left: 50px; 180 | } 181 | 182 | .michaeldmoore-multistat-panel-legend li { 183 | display: inline; 184 | padding: 5px 50px 5px 5px; 185 | margin-right: 20px; 186 | } 187 | 188 | .michaeldmoore-multistat-panel-legend-deselected { 189 | color:darkgray !important; 190 | background-color: lightgray !important; 191 | } 192 | -------------------------------------------------------------------------------- /dist/grouping.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 | 9 |
10 |
11 |
12 |
13 | 14 | 17 |
18 | 19 | 22 | 23 | 26 |
27 |
28 | 29 |
30 | 31 | 35 |
36 | 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 59 |
60 |
61 | 62 |
63 | 64 | 67 |
68 |
69 | 70 | 73 |
74 |
75 | 77 |
78 | 79 |
80 | 82 |
83 | 84 | 85 | 86 | 87 |
88 |
89 |
90 | 92 |
93 |
94 |
-------------------------------------------------------------------------------- /dist/img/Showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/dist/img/Showcase.gif -------------------------------------------------------------------------------- /dist/img/michaeldmoore-multistat-panel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | background 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /dist/layout.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 6 | 8 |
9 | 10 |
11 | 12 | 14 | 15 | 17 |
18 | 19 |
20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 30 |
31 | 32 |
33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 43 |
44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 | 60 |
61 | 62 | 65 |
66 |
67 |
-------------------------------------------------------------------------------- /dist/linesandlimits.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 7 | 9 | 10 | 11 | 12 |
13 | 14 | 16 |
17 |
18 |
19 | 20 | 22 | 24 | 25 | 26 | 27 |
28 | 29 | 31 |
32 |
33 |
34 | 35 | 37 | 39 | 40 | 41 | 42 |
43 | 44 | 46 |
47 |
48 |
49 | 51 |
52 |
53 | 54 | 56 | 58 | 59 | 60 | 61 |
62 | 63 | 65 |
66 |
67 |
68 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 80 |
81 |
82 | 83 | 84 | 85 |
86 | 87 |
88 |
89 | 92 |
93 |
94 |
95 |
96 | 97 | 99 | 101 | 102 | 103 | 104 |
105 | 106 | 108 |
109 |
110 |
111 | 113 | 114 | 115 | 116 | 117 | 118 |
119 | 121 |
122 |
123 | 124 | 125 | 126 |
127 | 128 |
129 |
130 | 133 |
134 |
135 |
136 |
137 | 139 |
140 | 142 |
143 |
144 |
145 | 146 | 148 | 149 | 150 | 151 | 152 | 154 | 155 | 156 | 157 |
158 |
159 |
-------------------------------------------------------------------------------- /dist/module.html: -------------------------------------------------------------------------------- 1 |
2 |
-------------------------------------------------------------------------------- /dist/options.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 6 |
7 | 8 |
9 | 11 |
12 | 13 | 14 | 15 | 16 | 17 |
18 | 20 |
21 |
22 |
23 |
24 | 26 |
27 | 28 |
29 | 31 |
32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 | 41 |
42 | 43 |
44 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 |
58 | 59 | 61 | 62 | 64 |
65 |
66 | 67 | 69 | 70 | 72 |
73 |
74 | 75 | 76 |
77 | 79 |
80 | 81 | 82 |
83 | 85 |
86 |
87 |
88 | 89 | 90 |
91 | 92 | 95 |
96 |
97 |
98 |
-------------------------------------------------------------------------------- /dist/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "name": "Multistat", 4 | "id": "michaeldmoore-multistat-panel", 5 | "info": { 6 | "description": "Enhanced version of built-in SingleStat panel, for queries involving multi-valued recordsets", 7 | "author": { 8 | "name": "Michael Moore", 9 | "url": "https://github.com/michaeldmoore" 10 | }, 11 | "keywords": [ 12 | "multistat", 13 | "singlestat", 14 | "panel" 15 | ], 16 | "version": "1.7.4", 17 | "updated": "2023-03-20", 18 | "logos": { 19 | "small": "img/michaeldmoore-multistat-panel.svg", 20 | "large": "img/michaeldmoore-multistat-panel.svg" 21 | }, 22 | "links": [ 23 | { 24 | "name": "Project site", 25 | "url": "https://github.com/michaeldmoore/michaeldmoore-multistat-panel" 26 | }, 27 | { 28 | "name": "MIT License", 29 | "url": "https://github.com/michaeldmoore/michaeldmoore-multistat-panel/LICENSE" 30 | } 31 | ], 32 | "screenshots": [ 33 | { 34 | "name": "Showcase", 35 | "path": "img/Showcase.gif" 36 | } 37 | ] 38 | }, 39 | "dependencies": { 40 | "grafanaDependency": ">=7.3.0", 41 | "grafanaVersion": "7.3.x", 42 | "plugins": [] 43 | } 44 | } -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | module.exports = (grunt) => { 3 | require('load-grunt-tasks')(grunt); 4 | 5 | grunt.loadNpmTasks('grunt-execute'); 6 | grunt.loadNpmTasks('grunt-contrib-clean'); 7 | grunt.loadNpmTasks('grunt-contrib-jshint'); 8 | 9 | grunt.initConfig({ 10 | 11 | clean: ['dist'], 12 | 13 | copy: { 14 | src_to_dist: { 15 | cwd: 'src', 16 | expand: true, 17 | src: ['**/*', '!**/external/*', '!**/*.js', '**/external/*.js', '!**/*.scss', '!img/**/*'], 18 | dest: 'dist' 19 | }, 20 | pluginDef: { 21 | expand: true, 22 | src: ['CHANGELOG.md', 'README.md'], 23 | dest: 'dist' 24 | }, 25 | img_to_dist: { 26 | cwd: 'src', 27 | expand: true, 28 | src: ['img/michaeldmoore-multistat-panel.svg', 'img/Showcase.gif'], 29 | dest: 'dist' 30 | }, 31 | css_to_dist: { 32 | cwd: 'src', 33 | expand: true, 34 | src: ['css/**/*'], 35 | dest: 'dist' 36 | } 37 | }, 38 | 39 | jshint: { 40 | all: ['Gruntfile.js', 'src/*.js'] 41 | }, 42 | 43 | babel: { 44 | options: { 45 | sourceMap: true, 46 | presets: ['es2015'], 47 | plugins: ['transform-es2015-modules-systemjs', 'transform-es2015-for-of'] 48 | }, 49 | dist: { 50 | files: [{ 51 | cwd: 'src', 52 | expand: true, 53 | src: ['*.js'], 54 | dest: 'dist', 55 | ext: '.js' 56 | }] 57 | } 58 | }, 59 | 60 | watch: { 61 | rebuild_all: { 62 | files: ['src/**/*'], 63 | tasks: ['default'], 64 | options: { spawn: false } 65 | } 66 | } 67 | 68 | }); 69 | 70 | grunt.registerTask('default', ['clean', 'jshint', 'copy:src_to_dist', 'copy:pluginDef', 'copy:img_to_dist', 'copy:css_to_dist', 'babel']); 71 | }; 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "michaeldmoore-multistat-panel", 3 | "version": "1.7.4", 4 | "description": "Multistat panel for Grafana", 5 | "main": "README.md", 6 | "scripts": { 7 | "build": "grunt" 8 | }, 9 | "author": "Michael Moore", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/michaeldmoore/multistat-panel.git" 14 | }, 15 | "keywords": [ 16 | "multistat", 17 | "grafana", 18 | "singlestat", 19 | "plugin", 20 | "panel" 21 | ], 22 | "bugs": { 23 | "url": "https://github.com/michaeldmoore/michaeldmoore-multistat-panel/issues" 24 | }, 25 | "homepage": "https://github.com/michaeldmoore/michaeldmoore-multistat-panel#readme", 26 | "engines": { 27 | "node": "16.x" 28 | }, 29 | "devDependencies": { 30 | "babel-eslint": "^10.1.0", 31 | "babel-preset-es2015": "^6.24.1", 32 | "eslint": "^7.16.0", 33 | "eslint-config-airbnb": "^18.2.1", 34 | "grunt": "^1.5.3", 35 | "grunt-babel": "^7.0.0", 36 | "grunt-cli": "^1.3.2", 37 | "grunt-contrib-clean": "~2.0.0", 38 | "grunt-contrib-copy": "~1.0.0", 39 | "grunt-contrib-jshint": "^2.1.0", 40 | "grunt-contrib-uglify": "~4.0.1", 41 | "grunt-contrib-watch": "^1.1.0", 42 | "grunt-systemjs-builder": "^1.0.0", 43 | "load-grunt-tasks": "^5.1.0" 44 | }, 45 | "dependencies": { 46 | "@grafana/data": "^7.3.6", 47 | "@grafana/runtime": "^7.3.6", 48 | "@grafana/toolkit": "^7.3.7", 49 | "d3": "^5.16.0", 50 | "eslint-plugin-jsx-a11y": "^6.4.1", 51 | "grunt-execute": "^0.2.2", 52 | "lodash": "^4.17.19" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/barlinks.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | 32 |
33 |
34 |
35 |
36 | 37 |
38 | 42 |
43 |
44 |
-------------------------------------------------------------------------------- /src/columns.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 | 9 |
10 |
11 |
12 | 13 |
14 | 15 | 19 |
20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 43 |
44 |
45 | 46 |
47 | 48 | 51 |
52 |
53 |
54 | 55 | 57 | 58 | 60 | 61 |
62 | 64 |
65 |
66 |
67 | 68 |
69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 | 78 | 81 |
82 |
83 |
84 | 85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 | 100 |
101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 112 |
113 | 114 |
115 |
116 | 117 |
118 | 120 |
121 |
122 |
123 |
124 |
125 | 126 |
127 | 129 |
130 |
131 |
132 | 133 | 136 | 138 |
139 |
140 |
141 |
142 |
143 | 144 | 147 | 153 |
154 |
155 |
156 |
157 |
As-Of Date display:
158 | This option controls if, and how the latest query time is displayed in the top right hand side of the panel.
159 | Depending on the data source and query, each row in Multistat may be updated at different times.
160 | This display can be useful in showing the latest timestamp of all the data displayed in the panel - as a snapshot date/time.
161 | 
162 | Date display formatting string uses common standards. (see here)
163 | 
164 | Setting the format string to the reserved keyword "ELAPSED" displays the time as the elapsed time since the last data update (as per the latest grafana refresh operation).
165 |    	  
166 |
167 |
168 |
169 | 170 |
171 | 173 |
174 |
175 | 177 |
178 |
179 |
180 |
181 |
182 | 186 |
187 | 189 |
190 |
191 |
192 | 193 |
194 | 195 | 199 |
200 | 201 |
203 |
204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 219 | 220 |
221 | 223 |
224 | 225 | 226 | 227 | 228 |
229 |
230 | 231 |
232 | 237 |
238 |
239 |
-------------------------------------------------------------------------------- /src/css/multistat-panel.css: -------------------------------------------------------------------------------- 1 | .michaeldmoore-multistat-panel-nodata { 2 | position: absolute; 3 | top: 50%; 4 | text-align: center; 5 | vertical-align: middle; 6 | font-size: 0.875rem; 7 | } 8 | 9 | .michaeldmoore-multistat-panel-maxDate { 10 | margin-right: 4em; 11 | float: right; 12 | padding-left: 20px; 13 | } 14 | 15 | .michaeldmoore-multistat-panel-dark-tooltip { 16 | position: absolute; 17 | width: auto; 18 | height: auto; 19 | padding: 5px; 20 | background-color: black; 21 | color: white; 22 | -webkit-border-radius: 10px; 23 | -moz-border-radius: 10px; 24 | border-radius: 10px; 25 | -webkit-box-shadow: 4px 4px 10px rgba(255, 255, 255, 0.4); 26 | -moz-box-shadow: 4px 4px 10px rgba(255, 255, 255, 0.4); 27 | box-shadow: 4px 4px 10px rgba(255, 255, 255, 0.4); 28 | } 29 | 30 | .michaeldmoore-multistat-panel-dark-tooltip th { 31 | font-weight: bold; 32 | text-align: center; 33 | border-bottom: 1px solid white; 34 | } 35 | 36 | .michaeldmoore-multistat-panel-dark-tooltip td:first-of-type { 37 | padding-right: 1em; 38 | } 39 | 40 | .michaeldmoore-multistat-panel-dark-tooltip a { 41 | color: #8080ff; 42 | text-decoration: blue underline; 43 | } 44 | 45 | .michaeldmoore-multistat-panel-light-tooltip { 46 | position: absolute; 47 | width: auto; 48 | height: auto; 49 | padding: 5px; 50 | background-color: white; 51 | color: black; 52 | -webkit-border-radius: 10px; 53 | -moz-border-radius: 10px; 54 | border-radius: 10px; 55 | -webkit-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4); 56 | -moz-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4); 57 | box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4); 58 | } 59 | 60 | .michaeldmoore-multistat-panel-light-tooltip th { 61 | font-weight: bold; 62 | text-align: center; 63 | border-bottom: 1px solid black; 64 | } 65 | 66 | .michaeldmoore-multistat-panel-light-tooltip td:first-of-type { 67 | padding-right: 1em; 68 | } 69 | 70 | .michaeldmoore-multistat-panel-light-tooltip a { 71 | color: #8080ff; 72 | text-decoration: blue underline; 73 | } 74 | 75 | 76 | 77 | 78 | 79 | .michaeldmoore-multistat-panel-statusmessage { 80 | position: absolute; 81 | top: 50%; 82 | left: 50%; 83 | margin-right: -50%; 84 | transform: translate(-50%, -50%); 85 | } 86 | 87 | 88 | .michaeldmoore-multistat-panel-w2 { 89 | width: 1.75rem; 90 | height: 37px; 91 | margin-right: 0.25rem; 92 | margin-bottom: 0px; 93 | } 94 | 95 | .michaeldmoore-multistat-panel-w3 { 96 | width: 2.75rem; 97 | height: 37px; 98 | margin-right: 0.25rem; 99 | margin-bottom: 0px; 100 | } 101 | 102 | .michaeldmoore-multistat-panel-w4 { 103 | width: 3.75rem; 104 | height: 37px; 105 | margin-right: 0.25rem; 106 | margin-bottom: 0px; 107 | } 108 | 109 | .michaeldmoore-multistat-panel-w5 { 110 | width: 4.75rem; 111 | height: 37px; 112 | margin-right: 0.25rem; 113 | margin-bottom: 0px; 114 | } 115 | 116 | .michaeldmoore-multistat-panel-w6 { 117 | width: 5.75rem; 118 | height: 37px; 119 | margin-right: 0.25rem; 120 | margin-bottom: 0px; 121 | } 122 | 123 | .michaeldmoore-multistat-panel-w7 { 124 | width: 6.75rem; 125 | height: 37px; 126 | margin-right: 0.25rem; 127 | margin-bottom: 0px; 128 | } 129 | 130 | .michaeldmoore-multistat-panel-w8 { 131 | width: 7.75rem; 132 | height: 37px; 133 | margin-right: 0.25rem; 134 | margin-bottom: 0px; 135 | } 136 | 137 | .michaeldmoore-multistat-panel-w9 { 138 | width: 8.75rem; 139 | height: 37px; 140 | margin-right: 0.25rem; 141 | margin-bottom: 0px; 142 | } 143 | 144 | .michaeldmoore-multistat-panel-w10 { 145 | width: 9.75rem; 146 | height: 37px; 147 | margin-right: 0.25rem; 148 | margin-bottom: 0px; 149 | } 150 | 151 | .michaeldmoore-multistat-panel-w16 { 152 | width: 15.75rem; 153 | height: 37px; 154 | margin-right: 0.25rem; 155 | margin-bottom: 0px; 156 | } 157 | 158 | .michaeldmoore-multistat-panel-w18 { 159 | width: 17.75rem; 160 | height: 37px; 161 | margin-right: 0.25rem; 162 | margin-bottom: 0px; 163 | } 164 | 165 | .michaeldmoore-multistat-panel-w25 { 166 | width: 24.75rem; 167 | height: 37px; 168 | margin-right: 0.25rem; 169 | margin-bottom: 0px; 170 | } 171 | 172 | pre a { 173 | display: contents; 174 | text-decoration: underline; 175 | } 176 | 177 | .michaeldmoore-multistat-panel-legend { 178 | flex-grow: 1; 179 | margin-left: 50px; 180 | } 181 | 182 | .michaeldmoore-multistat-panel-legend li { 183 | display: inline; 184 | padding: 5px 50px 5px 5px; 185 | margin-right: 20px; 186 | } 187 | 188 | .michaeldmoore-multistat-panel-legend-deselected { 189 | color:darkgray !important; 190 | background-color: lightgray !important; 191 | } 192 | -------------------------------------------------------------------------------- /src/grouping.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 | 9 |
10 |
11 |
12 |
13 | 14 | 17 |
18 | 19 | 22 | 23 | 26 |
27 |
28 | 29 |
30 | 31 | 35 |
36 | 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 59 |
60 |
61 | 62 |
63 | 64 | 67 |
68 |
69 | 70 | 73 |
74 |
75 | 77 |
78 | 79 |
80 | 82 |
83 | 84 | 85 | 86 | 87 |
88 |
89 |
90 | 92 |
93 |
94 |
-------------------------------------------------------------------------------- /src/img/Alarms-Alarms.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Alarms-Alarms.gif -------------------------------------------------------------------------------- /src/img/Alarms-Alarms.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Alarms-Alarms.mp4 -------------------------------------------------------------------------------- /src/img/Bar-Color-and-Padding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Bar-Color-and-Padding.png -------------------------------------------------------------------------------- /src/img/BaseLine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/BaseLine.png -------------------------------------------------------------------------------- /src/img/Basic-Bar-Charts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Basic-Bar-Charts.png -------------------------------------------------------------------------------- /src/img/Calm-Alarms.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Calm-Alarms.gif -------------------------------------------------------------------------------- /src/img/Calm-Alarms.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Calm-Alarms.mp4 -------------------------------------------------------------------------------- /src/img/Label-Format.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Label-Format.png -------------------------------------------------------------------------------- /src/img/Limit-Bar-Colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Limit-Bar-Colors.png -------------------------------------------------------------------------------- /src/img/Limits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Limits.png -------------------------------------------------------------------------------- /src/img/Lines-and-Limits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Lines-and-Limits.png -------------------------------------------------------------------------------- /src/img/MappingFields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/MappingFields.png -------------------------------------------------------------------------------- /src/img/Margins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Margins.png -------------------------------------------------------------------------------- /src/img/Max-Value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Max-Value.png -------------------------------------------------------------------------------- /src/img/Multistat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Multistat.png -------------------------------------------------------------------------------- /src/img/No-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/No-data.png -------------------------------------------------------------------------------- /src/img/Options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Options.png -------------------------------------------------------------------------------- /src/img/SampleQuery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/SampleQuery.png -------------------------------------------------------------------------------- /src/img/Showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Showcase.gif -------------------------------------------------------------------------------- /src/img/Showcase.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Showcase.mp4 -------------------------------------------------------------------------------- /src/img/Sorting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaeldmoore/michaeldmoore-multistat-panel/290277d6cb12ec2ac3de13e03aaa84f84af4c283/src/img/Sorting.png -------------------------------------------------------------------------------- /src/img/michaeldmoore-multistat-panel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | background 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/layout.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 6 | 8 |
9 | 10 |
11 | 12 | 14 | 15 | 17 |
18 | 19 |
20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 30 |
31 | 32 |
33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 43 |
44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 | 60 |
61 | 62 | 65 |
66 |
67 |
-------------------------------------------------------------------------------- /src/linesandlimits.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 7 | 9 | 10 | 11 | 12 |
13 | 14 | 16 |
17 |
18 |
19 | 20 | 22 | 24 | 25 | 26 | 27 |
28 | 29 | 31 |
32 |
33 |
34 | 35 | 37 | 39 | 40 | 41 | 42 |
43 | 44 | 46 |
47 |
48 |
49 | 51 |
52 |
53 | 54 | 56 | 58 | 59 | 60 | 61 |
62 | 63 | 65 |
66 |
67 |
68 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 80 |
81 |
82 | 83 | 84 | 85 |
86 | 87 |
88 |
89 | 92 |
93 |
94 |
95 |
96 | 97 | 99 | 101 | 102 | 103 | 104 |
105 | 106 | 108 |
109 |
110 |
111 | 113 | 114 | 115 | 116 | 117 | 118 |
119 | 121 |
122 |
123 | 124 | 125 | 126 |
127 | 128 |
129 |
130 | 133 |
134 |
135 |
136 |
137 | 139 |
140 | 142 |
143 |
144 |
145 | 146 | 148 | 149 | 150 | 151 | 152 | 154 | 155 | 156 | 157 |
158 |
159 |
-------------------------------------------------------------------------------- /src/module.html: -------------------------------------------------------------------------------- 1 |
2 |
-------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 6 |
7 | 8 |
9 | 11 |
12 | 13 | 14 | 15 | 16 | 17 |
18 | 20 |
21 |
22 |
23 |
24 | 26 |
27 | 28 |
29 | 31 |
32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 | 41 |
42 | 43 |
44 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 |
58 | 59 | 61 | 62 | 64 |
65 |
66 | 67 | 69 | 70 | 72 |
73 |
74 | 75 | 76 |
77 | 79 |
80 | 81 | 82 |
83 | 85 |
86 |
87 |
88 | 89 | 90 |
91 | 92 | 95 |
96 |
97 |
98 |
-------------------------------------------------------------------------------- /src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "name": "Multistat", 4 | "id": "michaeldmoore-multistat-panel", 5 | "info": { 6 | "description": "Enhanced version of built-in SingleStat panel, for queries involving multi-valued recordsets", 7 | "author": { 8 | "name": "Michael Moore", 9 | "url": "https://github.com/michaeldmoore" 10 | }, 11 | "keywords": [ 12 | "multistat", 13 | "singlestat", 14 | "panel" 15 | ], 16 | "version": "1.7.4", 17 | "updated": "2023-03-20", 18 | "logos": { 19 | "small": "img/michaeldmoore-multistat-panel.svg", 20 | "large": "img/michaeldmoore-multistat-panel.svg" 21 | }, 22 | "links": [ 23 | { 24 | "name": "Project site", 25 | "url": "https://github.com/michaeldmoore/michaeldmoore-multistat-panel" 26 | }, 27 | { 28 | "name": "MIT License", 29 | "url": "https://github.com/michaeldmoore/michaeldmoore-multistat-panel/LICENSE" 30 | } 31 | ], 32 | "screenshots": [ 33 | { 34 | "name": "Showcase", 35 | "path": "img/Showcase.gif" 36 | } 37 | ] 38 | }, 39 | "dependencies": { 40 | "grafanaDependency": ">=7.3.0", 41 | "grafanaVersion": "7.3.x", 42 | "plugins": [] 43 | } 44 | } --------------------------------------------------------------------------------