├── .editorconfig ├── .eslintrc ├── .gitignore ├── .node-version ├── LICENSE.md ├── README.md ├── build.gradle ├── gradle.properties ├── gradle ├── utils │ ├── loadPackageJson.gradle │ └── repository.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gulpfile.js ├── img ├── heroes-params.png ├── heroes-vis.png ├── kibana.png └── radar.png ├── index.js ├── package.json └── public ├── __tests__ └── index.js ├── kibi_radar_vis.html ├── kibi_radar_vis.js ├── kibi_radar_vis.less ├── kibi_radar_vis_controller.js └── kibi_radar_vis_params.html /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | insert_final_newline = false 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | parser: babel-eslint 3 | 4 | plugins: 5 | - mocha 6 | 7 | env: 8 | es6: true 9 | amd: true 10 | node: true 11 | mocha: true 12 | browser: true 13 | 14 | 15 | rules: 16 | block-scoped-var: 2 17 | camelcase: [ 2, { properties: never } ] 18 | comma-dangle: 0 19 | comma-style: [ 2, last ] 20 | consistent-return: 0 21 | curly: [ 2, multi-line ] 22 | dot-location: [ 2, property ] 23 | dot-notation: [ 2, { allowKeywords: true } ] 24 | eqeqeq: [ 2, allow-null ] 25 | guard-for-in: 2 26 | indent: [ 2, 2, { SwitchCase: 1 } ] 27 | key-spacing: [ 0, { align: value } ] 28 | max-len: [ 2, 140, 2, { ignoreComments: true, ignoreUrls: true } ] 29 | new-cap: [ 2, { capIsNewExceptions: [ Private ] } ] 30 | no-bitwise: 0 31 | no-caller: 2 32 | no-cond-assign: 0 33 | no-const-assign: 2 34 | no-debugger: 2 35 | no-empty: 2 36 | no-eval: 2 37 | no-extend-native: 2 38 | no-extra-parens: 0 39 | no-irregular-whitespace: 2 40 | no-iterator: 2 41 | no-loop-func: 2 42 | no-multi-spaces: 0 43 | no-multi-str: 2 44 | no-nested-ternary: 2 45 | no-new: 0 46 | no-path-concat: 0 47 | no-proto: 2 48 | no-return-assign: 0 49 | no-script-url: 2 50 | no-sequences: 2 51 | no-shadow: 0 52 | no-trailing-spaces: 2 53 | no-undef: 2 54 | no-underscore-dangle: 0 55 | no-unused-expressions: 0 56 | no-unused-vars: 0 57 | no-use-before-define: [ 2, nofunc ] 58 | no-var: 1 59 | no-with: 2 60 | one-var: [ 2, never ] 61 | prefer-const: 1 62 | quotes: [ 2, single ] 63 | semi-spacing: [ 2, { before: false, after: true } ] 64 | semi: [ 2, always ] 65 | space-after-keywords: [ 2, always ] 66 | space-before-blocks: [ 2, always ] 67 | space-before-function-paren: [ 2, { anonymous: always, named: never } ] 68 | space-in-parens: [ 2, never ] 69 | space-infix-ops: [ 2, { int32Hint: false } ] 70 | space-return-throw-case: [ 2 ] 71 | space-unary-ops: [ 2 ] 72 | strict: [ 2, never ] 73 | valid-typeof: 2 74 | wrap-iife: [ 2, outside ] 75 | yoda: 0 76 | 77 | mocha/no-exclusive-tests: 2 78 | mocha/handle-done-callback: 2 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | build/ 3 | node_modules/ 4 | npm-debug.log 5 | package-lock.json 6 | .gradle 7 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 8.11.4 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2015 - 2017 SIREn Solutions 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this product except in compliance with the License. You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kibi/Kibana Radar Chart Plugin 2 | 3 | This is a plugin for [Kibana](https://www.elastic.co/products/kibana) and [Kibi](http://siren.solutions/kibi) (our [extention of Kibana for Relational Data](https://www.linkedin.com/pulse/extending-elasticsearch-kibana-do-data-intelligence-kibi-tummarello)) 4 | 5 | A radar chart is a graphical method of displaying multivariate data in the form of a two-dimensional chart of three or more quantitative variables represented on axes starting from the same point. The relative position and angle of the axes is typically uninformative. 6 | 7 | ![image](img/radar.png) 8 | ![image](img/kibana.png) 9 | 10 | 11 | ## Compatibility 12 | 13 | This plugin can be installed in both: 14 | 15 | * [Kibana: 4.3+](https://www.elastic.co/downloads/past-releases/kibana-4-3-0) 16 | * [Kibi: 0.3+](https://siren.solutions/kibi) 17 | 18 | The following table shows the compatibility between releases of Kibi/Kibana and Radar Chart Plugin plugin 19 | 20 | Kibi/Kibana|Radar Chart Plugin 21 | -----|----- 22 | 5.6.4|master 23 | 5.2.2|branch-5.2.2 24 | 5.2.1|branch-5.2.1 25 | 5.2.0|branch-5.2.0 26 | 5.1.2|branch-5.1.2 27 | 5.1.1|branch-5.1.1 28 | 5.0.2|branch-5.0.2 29 | 5.0.1|branch-5.0.1 30 | 5.0.0|branch-5.0.0 31 | 4.6.4|branch-4.6.4 32 | 4.6.3|4.5.3 33 | 4.5.x|4.5.3 34 | 4.4.x|4.4.2 35 | 4.3.x|0.1.0 36 | 37 | ## Installation 38 | 39 | ### Automatic 40 | 41 | For kibi/kibana 4.x 42 | 43 | ```sh 44 | $ # for kibi 45 | $ ./bin/kibi plugin -i kibi_radar_vis -u https://github.com/sirensolutions/kibi_radar_vis/archive/4.5.3.zip 46 | $ # for kibana 47 | $ ./bin/kibana plugin -i kibi_radar_vis -u https://github.com/sirensolutions/kibi_radar_vis/archive/4.5.3.zip 48 | ``` 49 | 50 | For kibi/kibana 5.x.x (replace the 5.x.x with correct version number) 51 | 52 | ```sh 53 | $ # for kibi 54 | $ ./bin/kibi-plugin install https://github.com/sirensolutions/kibi_radar_vis/releases/download/5.x.x/kibi_radar_vis-5.x.x.zip 55 | $ # for kibana 56 | $ ./bin/kibana-plugin install https://github.com/sirensolutions/kibi_radar_vis/releases/download/5.x.x/kibi_radar_vis-5.x.x.zip 57 | ``` 58 | 59 | ### Manual 60 | 61 | For kibi/kibana 4.x 62 | 63 | ```sh 64 | $ git clone https://github.com/sirensolutions/kibi_radar_vis.git 65 | $ cd kibi_radar_vis 66 | $ npm install 67 | $ npm run build 68 | $ cp -R build/kibi_radar_vis KIBANA_FOLDER_PATH/installedPlugins/ 69 | ``` 70 | 71 | For kibi/kibana 5.x 72 | 73 | ```sh 74 | $ git clone https://github.com/sirensolutions/kibi_radar_vis.git 75 | $ cd kibi_radar_vis 76 | $ git checkout branch-5.x.x 77 | $ npm install 78 | $ npm run build 79 | $ cp -R build/kibana/kibi_radar_vis KIBANA_FOLDER_PATH/plugins/ 80 | ``` 81 | 82 | ## Uninstall 83 | 84 | For kibi/kibana 4.x 85 | 86 | ```sh 87 | $ # for kibi 88 | $ bin/kibi plugin --remove kibi_radar_vis 89 | $ # for kibana 90 | $ bin/kibana plugin --remove kibi_radar_vis 91 | ``` 92 | 93 | For kibi/kibana 5.x 94 | 95 | ```sh 96 | $ # for kibi 97 | $ bin/kibi-plugin remove kibi_radar_vis 98 | $ # for kibana 99 | $ bin/kibana-plugin remove kibi_radar_vis 100 | ``` 101 | 102 | ## Development 103 | 104 | - Clone the repository at the same level as you've cloned Kibana (>=4.6.4) 105 | - Switch to the same node version as Kibana using nvm 106 | (currently `nvm use 6.9.0`) 107 | - Install dependencies with `npm install` 108 | - Install the plugin to Kibana and start watching for changes by running 109 | `npm start` 110 | 111 | ## Demo 112 | 113 | ![heroes](img/heroes-vis.png) 114 | 115 | Create a sample index with the commands below and then create a new radar visualization. 116 | 117 | ```sh 118 | $ curl -XPUT 'http://localhost:9200/hero' -d ' 119 | { 120 | "mappings": { 121 | "Hero": { 122 | "properties": { 123 | "name": { 124 | "type": "string", 125 | "index": "not_analyzed" 126 | } 127 | } 128 | } 129 | } 130 | } 131 | ' 132 | 133 | $ curl 'http://localhost:9200/hero/Hero' -d ' 134 | { 135 | "name": "Thor", 136 | "intelligence": 2, 137 | "strength": 7, 138 | "speed": 7, 139 | "durability": 6, 140 | "energy": 6, 141 | "fighting": 4, 142 | "description": "god-like durability" 143 | } 144 | ' 145 | 146 | $ curl 'http://localhost:9200/hero/Hero' -d ' 147 | { 148 | "name": "Iron Man", 149 | "intelligence": 6, 150 | "strength": 6, 151 | "speed": 5, 152 | "durability": 6, 153 | "energy": 6, 154 | "fighting": 4, 155 | "description": "smart entreprenuer" 156 | } 157 | ' 158 | 159 | $ curl 'http://localhost:9200/hero/Hero' -d ' 160 | { 161 | "name": "Captain America", 162 | "intelligence": 3, 163 | "strength": 3, 164 | "speed": 2, 165 | "durability": 3, 166 | "energy": 1, 167 | "fighting": 6, 168 | "description": "only human" 169 | } 170 | ' 171 | 172 | $ curl 'http://localhost:9200/hero/Hero' -d ' 173 | { 174 | "name": "Hulk", 175 | "intelligence": 6, 176 | "strength": 7, 177 | "speed": 3, 178 | "durability": 7, 179 | "energy": 1, 180 | "fighting": 4, 181 | "description": "brilliant scientist" 182 | } 183 | ' 184 | ``` 185 | 186 | The metrics define the dimensions of the chart, and should be at least three. 187 | Each colored area is defined in the bucket section, e.g., a hero's name. 188 | 189 | ![heroes params](img/heroes-params.png) 190 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////// 2 | // // 3 | // Dependency Details // 4 | // // 5 | /////////////////////////////////////////////////// 6 | 7 | buildscript { 8 | 9 | repositories { 10 | maven { 11 | url "https://plugins.gradle.org/m2/" 12 | } 13 | } 14 | 15 | dependencies { 16 | classpath "com.moowork.gradle:gradle-node-plugin:1.1.1" 17 | classpath 'org.ajoberstar:gradle-git:1.6.0' 18 | } 19 | 20 | } 21 | 22 | apply plugin: "idea" 23 | apply plugin: "base" 24 | apply plugin: 'com.moowork.gulp' 25 | apply plugin: 'com.moowork.grunt' 26 | apply plugin: 'com.moowork.node' 27 | apply plugin: 'org.ajoberstar.grgit' 28 | apply plugin: 'maven-publish' 29 | apply from: "$rootDir/gradle/utils/repository.gradle" 30 | apply from: "$rootDir/gradle/utils/loadPackageJson.gradle" 31 | 32 | import org.ajoberstar.grgit.Grgit 33 | 34 | def packageJson = loadPackageJson() 35 | 36 | /////////////////////////////////////////////////// 37 | // // 38 | // Project Details // 39 | // // 40 | /////////////////////////////////////////////////// 41 | 42 | group = 'solutions.siren' 43 | version = customRadarVersion ?: packageJson.version 44 | 45 | /////////////////////////////////////////////////// 46 | // // 47 | // Install Kibi // 48 | // // 49 | /////////////////////////////////////////////////// 50 | 51 | /** 52 | * Define the kibi source artifact dependency 53 | */ 54 | configurations { 55 | kibi 56 | } 57 | 58 | dependencies { 59 | kibi "solutions.siren:investigate-core:${project.version}:sources@zip" 60 | } 61 | 62 | /** 63 | * Task to download locally the kibi dependency 64 | */ 65 | task downloadKibi(type: Copy) { 66 | from configurations.kibi 67 | into new File(buildDir, 'dependencies') 68 | rename { name -> 69 | def artifact = configurations.kibi.resolvedConfiguration.resolvedArtifacts.find { it.file.name == name } 70 | "${artifact.name}.${artifact.extension}" 71 | } 72 | } 73 | 74 | /** 75 | * Task to unzip kibi 76 | */ 77 | task extractKibi(type: Copy, dependsOn: [downloadKibi]) { 78 | from { zipTree(new File(downloadKibi.destinationDir, "investigate-core.zip")) } 79 | into new File(buildDir, 'investigate-core') 80 | } 81 | 82 | /** 83 | * Task to execute the `npmInstall` gradle's task from the kibi distribution 84 | */ 85 | task npmInstallKibi(type: GradleBuild, dependsOn: [extractKibi]) { 86 | dir new File(extractKibi.destinationDir, "investigate-core-${project.version}-sources") 87 | tasks = ['npmInstall'] 88 | } 89 | 90 | /** 91 | * Main task to install Kibi locally 92 | */ 93 | task installKibi(dependsOn: [downloadKibi, extractKibi, npmInstallKibi]) {} 94 | 95 | /////////////////////////////////////////////////// 96 | // // 97 | // Node // 98 | // // 99 | /////////////////////////////////////////////////// 100 | 101 | node { 102 | // Version of node to use. 103 | version = nodeVersion 104 | 105 | // Base URL for fetching node distributions (change if you have a mirror). 106 | distBaseUrl = 'https://nodejs.org/dist' 107 | 108 | // If true, it will download node using above parameters. 109 | // If false, it will try to use globally installed node. 110 | download = true 111 | 112 | // Set the work directory for unpacking node 113 | workDir = file("${buildDir}/nodejs") 114 | 115 | // Set the work directory where node_modules should be located 116 | nodeModulesDir = file("${projectDir}") 117 | } 118 | 119 | /////////////////////////////////////////////////// 120 | // // 121 | // Package // 122 | // // 123 | /////////////////////////////////////////////////// 124 | 125 | task gulpPackage(dependsOn: ['gulp_package']) << {} 126 | 127 | // run npm install 128 | gulp_package.dependsOn 'npmInstall' 129 | 130 | // run gulp install 131 | gulp_package.dependsOn 'installGulp' 132 | 133 | /////////////////////////////////////////////////// 134 | // // 135 | // Install Grunt Globally // 136 | // // 137 | /////////////////////////////////////////////////// 138 | 139 | task installGruntGlobally(type: NpmTask) { 140 | args = ['install', '-g', 'grunt'] 141 | } 142 | 143 | /////////////////////////////////////////////////// 144 | // // 145 | // Dev // 146 | // // 147 | /////////////////////////////////////////////////// 148 | 149 | task gulpDev(type: GulpTask) { 150 | args = ["dev"] 151 | if (project.hasProperty('kibiHomePath')) { 152 | args << "--kibanahomepath=${kibiHomePath}" 153 | } 154 | else { 155 | dependsOn installKibi 156 | args << "--kibanahomepath=${new File(extractKibi.destinationDir, "investigate-core-${project.version}-sources").getAbsolutePath()}" 157 | } 158 | } 159 | 160 | // run npm install 161 | gulpDev.dependsOn 'npmInstall' 162 | 163 | // run gulp install 164 | gulpDev.dependsOn 'installGulp' 165 | 166 | // makes that grunt is installed 167 | gulpDev.dependsOn 'installGruntGlobally' 168 | gulpDev.dependsOn 'installGrunt' 169 | installGrunt.mustRunAfter 'installGruntGlobally' 170 | 171 | /////////////////////////////////////////////////// 172 | // // 173 | // Test // 174 | // // 175 | /////////////////////////////////////////////////// 176 | 177 | task gulpTest(type: GulpTask) { 178 | args = ["test"] 179 | if (project.hasProperty('kibiHomePath')) { 180 | args << "--kibanahomepath=${kibiHomePath}" 181 | } 182 | else { 183 | dependsOn installKibi 184 | args << "--kibanahomepath=${new File(extractKibi.destinationDir, "investigate-core-${project.version}-sources").getAbsolutePath()}" 185 | } 186 | } 187 | 188 | // run npm install 189 | gulpTest.dependsOn 'npmInstall' 190 | 191 | // run gulp install 192 | gulpTest.dependsOn 'installGulp' 193 | 194 | // makes that grunt is installed 195 | gulpTest.dependsOn 'installGruntGlobally' 196 | gulpTest.dependsOn 'installGrunt' 197 | installGrunt.mustRunAfter 'installGruntGlobally' 198 | 199 | /////////////////////////////////////////////////// 200 | // // 201 | // Test Dev // 202 | // // 203 | /////////////////////////////////////////////////// 204 | 205 | task gulpTestDev(type: GulpTask) { 206 | args = ["testdev"] 207 | if (project.hasProperty('kibiHomePath')) { 208 | args << "--kibanahomepath=${kibiHomePath}" 209 | } 210 | else { 211 | dependsOn installKibi 212 | args << "--kibanahomepath=${new File(extractKibi.destinationDir, "investigate-core-${project.version}-sources").getAbsolutePath()}" 213 | } 214 | } 215 | 216 | // run npm install 217 | gulpTestDev.dependsOn 'npmInstall' 218 | 219 | // run gulp install 220 | gulpTestDev.dependsOn 'installGulp' 221 | 222 | // makes that grunt is installed 223 | gulpTestDev.dependsOn 'installGruntGlobally' 224 | gulpTestDev.dependsOn 'installGrunt' 225 | installGrunt.mustRunAfter 'installGruntGlobally' 226 | 227 | /////////////////////////////////////////////////// 228 | // // 229 | // Clean // 230 | // // 231 | /////////////////////////////////////////////////// 232 | 233 | // Extends clean task to clean the node_modules directory 234 | clean << { 235 | def nodeModulesDir = file("${projectDir}/node_modules") 236 | if (nodeModulesDir.exists()) { 237 | println "=== Cleaning node modules directory: ${nodeModulesDir} ===" 238 | nodeModulesDir.deleteDir() 239 | } 240 | } 241 | 242 | // Executes the Gulp's clean task 243 | clean.dependsOn 'gulp_clean' 244 | 245 | /////////////////////////////////////////////////// 246 | // // 247 | // Publishing // 248 | // // 249 | /////////////////////////////////////////////////// 250 | 251 | publishing { 252 | repositories getArtifactoryRepository() 253 | publications { 254 | zipDist(MavenPublication) { 255 | artifact(new File("${projectDir}/target/gulp", "${packageJson.name}.zip")) 256 | groupId project.group 257 | artifactId "kibi-radar-vis" 258 | version project.version 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | nodeVersion=8.11.4 2 | 3 | # placeholders needed so gradle will not complain about missing properties during init phase 4 | # these are provided by Jenkins 5 | artifactory_username= 6 | artifactory_password= 7 | artifactory_url= 8 | 9 | customRadarVersion= 10 | -------------------------------------------------------------------------------- /gradle/utils/loadPackageJson.gradle: -------------------------------------------------------------------------------- 1 | /** 2 | * Required for the loadPackageJson method 3 | */ 4 | apply plugin: 'groovy' 5 | import groovy.json.JsonSlurper 6 | 7 | def loadPackageJson() { 8 | File packageJson = new File(projectDir, "package.json") 9 | return new JsonSlurper().parseText(packageJson.text) 10 | } 11 | 12 | project.ext { 13 | loadPackageJson = this.&loadPackageJson 14 | } 15 | -------------------------------------------------------------------------------- /gradle/utils/repository.gradle: -------------------------------------------------------------------------------- 1 | def getArtifactoryRepository() { 2 | return { 3 | maven { 4 | url project.artifactory_url + "/" + (project.version.contains('SNAPSHOT') ? 'libs-snapshot-local' : 'libs-release-local') 5 | credentials { 6 | username project.artifactory_username 7 | password project.artifactory_password 8 | } 9 | } 10 | } 11 | } 12 | 13 | project.ext { 14 | getArtifactoryRepository = this.&getArtifactoryRepository 15 | } 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_radar_vis/33b01cbf9eb85bd0d1c9b6bd5f10bd203c297949/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jul 06 10:24:47 IST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var _ = require('lodash'); 3 | var path = require('path'); 4 | var mkdirp = require('mkdirp'); 5 | var Rsync = require('rsync'); 6 | var Promise = require('bluebird'); 7 | var eslint = require('gulp-eslint'); 8 | var rimraf = require('rimraf'); 9 | var zip = require('gulp-zip'); 10 | var fs = require('fs'); 11 | var spawn = require('child_process').spawn; 12 | var minimist = require('minimist'); 13 | 14 | var pkg = require('./package.json'); 15 | var packageName = pkg.name; 16 | 17 | var buildDir = path.resolve(__dirname, 'build/gulp'); 18 | var targetDir = path.resolve(__dirname, 'target/gulp'); 19 | var buildTarget = path.resolve(buildDir, 'kibana', packageName); 20 | 21 | var include = [ 22 | 'package.json', 23 | 'index.js', 24 | 'public' 25 | ]; 26 | 27 | var knownOptions = { 28 | string: 'kibanahomepath', 29 | default: { kibanahomepath: '../kibi-internal' } 30 | }; 31 | var options = minimist(process.argv.slice(2), knownOptions); 32 | 33 | var kibanaPluginDir = path.resolve(__dirname, options.kibanahomepath + '/siren_plugins/' + packageName); 34 | 35 | function syncPluginTo(dest, done) { 36 | mkdirp(dest, function (err) { 37 | if (err) return done(err); 38 | Promise.all(include.map(function (name) { 39 | var source = path.resolve(__dirname, name); 40 | return new Promise(function (resolve, reject) { 41 | var rsync = new Rsync(); 42 | rsync 43 | .source(source) 44 | .destination(dest) 45 | .flags('uav') 46 | .recursive(true) 47 | .set('delete') 48 | .output(function (data) { 49 | process.stdout.write(data.toString('utf8')); 50 | }); 51 | rsync.execute(function (err) { 52 | if (err) { 53 | console.log(err); 54 | return reject(err); 55 | } 56 | resolve(); 57 | }); 58 | }); 59 | })) 60 | .then(function () { 61 | return new Promise(function (resolve, reject) { 62 | mkdirp(path.join(buildTarget, 'node_modules'), function (err) { 63 | if (err) return reject(err); 64 | resolve(); 65 | }); 66 | }); 67 | }) 68 | .then(function () { 69 | spawn('npm', ['install', '--production'], { 70 | cwd: dest, 71 | stdio: 'inherit' 72 | }) 73 | .on('close', done); 74 | }) 75 | .catch(done); 76 | }); 77 | } 78 | 79 | gulp.task('sync', function (done) { 80 | syncPluginTo(kibanaPluginDir, done); 81 | }); 82 | 83 | gulp.task('lint', function (done) { 84 | return gulp.src([ 85 | 'server/**/*.js', 86 | 'public/**/*.js', 87 | '!public/webpackShims/**' 88 | ]).pipe(eslint()) 89 | .pipe(eslint.formatEach()) 90 | .pipe(eslint.failOnError()); 91 | }); 92 | 93 | gulp.task('clean', function (done) { 94 | Promise.each([buildDir, targetDir], function (dir) { 95 | return new Promise(function (resolve, reject) { 96 | rimraf(dir, function (err) { 97 | if (err) return reject(err); 98 | resolve(); 99 | }); 100 | }); 101 | }).nodeify(done); 102 | }); 103 | 104 | gulp.task('build', ['clean'], function (done) { 105 | syncPluginTo(buildTarget, done); 106 | }); 107 | 108 | gulp.task('package', ['build'], function (done) { 109 | return gulp.src(path.join(buildDir, '**', '*')) 110 | .pipe(zip(packageName + '.zip')) 111 | .pipe(gulp.dest(targetDir)); 112 | }); 113 | 114 | gulp.task('dev', ['sync'], function (done) { 115 | gulp.watch([ 116 | 'package.json', 117 | 'index.js', 118 | 'public/**/*', 119 | 'server/**/*' 120 | ], ['sync', 'lint']); 121 | }); 122 | 123 | gulp.task('test', ['sync'], function(done) { 124 | spawn('grunt', ['test:browser', '--grep=Kibi Radar Chart'], { 125 | cwd: options.kibanahomepath, 126 | stdio: 'inherit' 127 | }).on('close', done); 128 | }); 129 | 130 | gulp.task('coverage', ['sync'], function(done) { 131 | spawn('grunt', ['test:coverage', '--grep=Kibi Radar Chart'], { 132 | cwd: options.kibanahomepath, 133 | stdio: 'inherit' 134 | }).on('close', done); 135 | }); 136 | -------------------------------------------------------------------------------- /img/heroes-params.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_radar_vis/33b01cbf9eb85bd0d1c9b6bd5f10bd203c297949/img/heroes-params.png -------------------------------------------------------------------------------- /img/heroes-vis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_radar_vis/33b01cbf9eb85bd0d1c9b6bd5f10bd203c297949/img/heroes-vis.png -------------------------------------------------------------------------------- /img/kibana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_radar_vis/33b01cbf9eb85bd0d1c9b6bd5f10bd203c297949/img/kibana.png -------------------------------------------------------------------------------- /img/radar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirensolutions/kibi_radar_vis/33b01cbf9eb85bd0d1c9b6bd5f10bd203c297949/img/radar.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (kibana) { 2 | return new kibana.Plugin({ 3 | name: 'kibi_radar_vis', 4 | require: ['kibana', 'elasticsearch'], 5 | uiExports: { 6 | visTypes: [ 7 | 'plugins/kibi_radar_vis/kibi_radar_vis' 8 | ] 9 | } 10 | }); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kibi_radar_vis", 3 | "version": "10.2.0-SNAPSHOT", 4 | "kibana": { 5 | "version": "5.6.10" 6 | }, 7 | "authors": [ 8 | "SIREn Solutions " 9 | ], 10 | "description": "Kibi Radar Chart plugin", 11 | "main": "index.js", 12 | "license": "Apache-2.0", 13 | "homepage": "http://siren.solutions/kibi", 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com:sirensolutions/kibi-radar-chart-plugin.git" 17 | }, 18 | "scripts": { 19 | "test": "gulp test", 20 | "test:coverage": "gulp coverage", 21 | "start": "gulp dev --kibanahomepath=../kibana", 22 | "precommit": "gulp lint", 23 | "build": "gulp build" 24 | }, 25 | "devDependencies": { 26 | "eslint-plugin-mocha": "1.0.0", 27 | "babel-eslint": "^4.1.6", 28 | "bluebird": "3.5.1", 29 | "eslint": "^1.10.3", 30 | "gulp": "3.9.1", 31 | "gulp-eslint": "^1.1.1", 32 | "gulp-zip": "4.0.0", 33 | "gulp-util": "^3.0.7", 34 | "husky": "0.14.3", 35 | "lodash": "^3.10.1", 36 | "mkdirp": "^0.5.1", 37 | "rimraf": "2.6.2", 38 | "rsync": "0.6.1", 39 | "minimist": "1.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/__tests__/index.js: -------------------------------------------------------------------------------- 1 | describe('Kibi Radar Chart', function () { 2 | it('dummy test', function () { 3 | 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /public/kibi_radar_vis.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /public/kibi_radar_vis.js: -------------------------------------------------------------------------------- 1 | import { VisVisTypeProvider } from 'ui/vis/vis_type'; 2 | import { TemplateVisTypeProvider } from 'ui/template_vis_type/template_vis_type'; 3 | import { VisSchemasProvider } from 'ui/vis/schemas'; 4 | import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; 5 | 6 | // we need to load the css ourselves 7 | import 'plugins/kibi_radar_vis/kibi_radar_vis.less'; 8 | // we also need to load the controller and used by the template 9 | import 'plugins/kibi_radar_vis/kibi_radar_vis_controller'; 10 | import template from 'plugins/kibi_radar_vis/kibi_radar_vis.html'; 11 | // register the provider with the visTypes registry 12 | VisTypesRegistryProvider.register(RadarVisProvider); 13 | 14 | function RadarVisProvider(Private) { 15 | const VisType = Private(VisVisTypeProvider); 16 | const TemplateVisType = Private(TemplateVisTypeProvider); 17 | const Schemas = Private(VisSchemasProvider); 18 | 19 | // return the visType object, which kibana will use to display and configure new 20 | // Vis object of this type. 21 | return new TemplateVisType({ 22 | name: 'radar', 23 | title: 'Kibi Radar Chart', 24 | description: 'A radar chart is a graphical method of displaying multivariate data ' + 25 | 'in the form of a two-dimensional chart of three or more ' + 26 | 'quantitative variables represented on axes starting from the same point.' + 27 | ' The relative position and angle of the axes is typically uninformative.', 28 | icon: 'fa-snowflake', 29 | category: VisType.CATEGORY.KIBI, 30 | template, 31 | params: { 32 | defaults: { 33 | fontSize: 60, 34 | shareYAxis: true, 35 | addTooltip: true, 36 | addLegend: true, 37 | isDonut: false, 38 | isFacet: false, 39 | addLevel: true, 40 | addAxe: true, 41 | addVertice: true, 42 | addPolygon: true, 43 | addLevelLabel: true, 44 | addAxeLabel: true, 45 | addLevelScale: 1, 46 | addLabelScale: 0.9, 47 | addLevelNumber: 5 48 | }, 49 | editor: require('plugins/kibi_radar_vis/kibi_radar_vis_params.html') 50 | }, 51 | schemas: new Schemas([ 52 | { 53 | group: 'metrics', 54 | name: 'metric', 55 | title: 'Metric', 56 | min: 3, 57 | defaults: [ 58 | { type: 'count', schema: 'metric' }, 59 | { type: 'count', schema: 'metric' }, 60 | { type: 'count', schema: 'metric' } 61 | ], 62 | aggFilter: ['count','cardinality', 'avg', 'sum', 'min', 'max'] 63 | }, 64 | { 65 | group: 'buckets', 66 | name: 'segment', 67 | icon: 'fa fa-scissors', 68 | title: 'Split Slices', 69 | min: 1, 70 | max: 1, 71 | aggFilter: ['terms','range'] 72 | } 73 | ]) 74 | }); 75 | } 76 | 77 | // export the provider so that the visType can be required with Private() 78 | export default RadarVisProvider; 79 | -------------------------------------------------------------------------------- /public/kibi_radar_vis.less: -------------------------------------------------------------------------------- 1 | @import (reference) "~ui/styles/mixins.less"; 2 | 3 | .kibi-radar-vis { 4 | width: 100%; 5 | } 6 | 7 | .tab-dashboard.theme-dark .kibi-radar-vis { 8 | 9 | .axis-labels { 10 | stroke: #cecece; 11 | stroke-width: 0.8px; 12 | } 13 | 14 | .level-labels { 15 | stroke: #a9a9a9; 16 | stroke-width: 0.6px; 17 | } 18 | 19 | .legend-labels { 20 | stroke: #cecece; 21 | stroke-width: 0.8px; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/kibi_radar_vis_controller.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3'; 2 | import { AggResponseTabifyProvider } from 'ui/agg_response/tabify/tabify'; 3 | import { uiModules } from 'ui/modules'; 4 | // get the kibana/metric_vis module, and make sure that it requires the 'kibana' module if it 5 | // didn't already 6 | const module = uiModules.get('kibana/kibi_radar_vis', ['kibana']); 7 | 8 | module.controller('KbnRadarVisController', function ($scope, $element, $rootScope, Private) { 9 | const tabifyAggResponse = Private(AggResponseTabifyProvider); 10 | 11 | let data; 12 | let config; 13 | let chartVis; 14 | let width; 15 | let height; 16 | // declare data 17 | let tableGroups = null; 18 | 19 | //mouse events 20 | const over = 'ontouchstart' in window ? 'touchstart' : 'mouseover'; 21 | const out = 'ontouchstart' in window ? 'touchend' : 'mouseout'; 22 | const svgRoot = $element[0]; 23 | 24 | // set default config 25 | const _initConfig = function () { 26 | const chartW = width / 3; 27 | const chartH = width / 3; 28 | config = { 29 | w: chartW, 30 | h: chartH, 31 | facet: false, 32 | levels: 5, 33 | levelScale: 0.85, 34 | labelScale: 1.0, 35 | facetPaddingScale: 1.0, 36 | maxValue: 0, 37 | radians: 2 * Math.PI, 38 | polygonAreaOpacity: 0.3, 39 | polygonStrokeOpacity: 1, 40 | polygonPointSize: 4, 41 | legendBoxSize: 10, 42 | translateX: chartW / 8, 43 | translateY: chartH / 8, 44 | paddingX: chartW, 45 | paddingY: chartH, 46 | colors: d3.scale.category10(), 47 | showLevels: true, 48 | showLevelsLabels: true, 49 | showAxesLabels: true, 50 | showAxes: true, 51 | showLegend: true, 52 | showVertices: true, 53 | showPolygons: true 54 | }; 55 | 56 | // initiate main vis component 57 | chartVis = { 58 | svg: null, 59 | tooltip: null, 60 | levels: null, 61 | axis: null, 62 | vertices: null, 63 | legend: null, 64 | allAxis: null, 65 | total: null, 66 | radius: null 67 | }; 68 | }; 69 | 70 | 71 | // adjust config parameters 72 | const _updateConfig = function () { 73 | config.maxValue = Math.max(config.maxValue, d3.max(data, function (d) { 74 | return d3.max(d.axes, function (o) { return o.value; }); 75 | })); 76 | config.levelScale = $scope.vis.params.addLevelScale; 77 | config.labelScale = $scope.vis.params.addLabelScale; 78 | config.levels = $scope.vis.params.addLevelNumber; 79 | config.w *= config.levelScale; 80 | config.h *= config.levelScale; 81 | config.paddingX = config.w * config.levelScale; 82 | config.paddingY = config.h * config.levelScale; 83 | config.facet = $scope.vis.params.isFacet; 84 | 85 | // if facet required: 86 | if (config.facet) { 87 | width /= data.length; 88 | height /= data.length; 89 | config.w /= data.length; 90 | config.h /= data.length; 91 | config.paddingX /= (data.length / config.facetPaddingScale); 92 | config.paddingY /= (data.length / config.facetPaddingScale); 93 | config.polygonPointSize *= Math.pow(0.9, data.length); 94 | } 95 | }; 96 | 97 | 98 | const _updateDimensions = function () { 99 | const delta = 18; 100 | let w = $element.parent().width(); 101 | let h = $element.parent().height(); 102 | if (w) { 103 | if (w > delta) { 104 | w -= delta; 105 | } 106 | width = w; 107 | } 108 | if (h) { 109 | if (h > delta) { 110 | h -= delta; 111 | } 112 | height = h; 113 | } 114 | }; 115 | 116 | const _buildCoordinates = function (data) { 117 | data.forEach(function (group) { 118 | group.axes.forEach(function (d, i) { 119 | const sin = Math.sin(i * config.radians / chartVis.totalAxes); 120 | const cos = Math.cos(i * config.radians / chartVis.totalAxes); 121 | d.coordinates = { // [x, y] coordinates 122 | x: config.w / 2 * (1 - (parseFloat(Math.max(d.value, 0)) / config.maxValue) * sin), 123 | y: config.h / 2 * (1 - (parseFloat(Math.max(d.value, 0)) / config.maxValue) * cos) 124 | }; 125 | }); 126 | }); 127 | }; 128 | 129 | const _buildVisComponents = function () { 130 | // update vis parameters 131 | chartVis.allAxis = data[0].axes.map(function (i, j) { return i.axis; }); 132 | chartVis.totalAxes = chartVis.allAxis.length; 133 | chartVis.radius = Math.min(config.w / 2, config.h / 2); 134 | const div = d3.select(svgRoot) 135 | .append('svg') 136 | .attr('width', width) 137 | .attr('height', height); 138 | 139 | //create main chartVis svg 140 | if (config.facet) { 141 | chartVis.svg = div 142 | .append('svg').classed('svg-vis', true) 143 | .attr('width', config.w + config.paddingX) 144 | .attr('height', config.h + config.paddingY) 145 | .append('svg:g') 146 | .attr('transform', 'translate(' + config.translateX + ',' + config.translateY + ')'); 147 | } else { 148 | chartVis.svg = div.append('svg') 149 | .attr('width', width) 150 | .attr('height', height) 151 | .append('svg:g') 152 | .attr('transform', 'translate(' + width / 4 + ',' + height / 4 + ')'); 153 | } 154 | 155 | // create verticesTooltip 156 | chartVis.verticesTooltip = d3.select('body') 157 | .append('div').classed('verticesTooltip', true) 158 | .attr('opacity', 0) 159 | .style({ 160 | 'position': 'absolute', 161 | 'color': 'black', 162 | 'font-size': '10px', 163 | 'width': '100px', 164 | 'height': 'auto', 165 | 'padding': '5px', 166 | 'border': '2px solid gray', 167 | 'border-radius': '5px', 168 | 'pointer-events': 'none', 169 | 'opacity': '0', 170 | 'background': '#f4f4f4' 171 | }); 172 | 173 | // create levels 174 | chartVis.levels = chartVis.svg.selectAll('.levels') 175 | .append('svg:g').classed('levels', true); 176 | 177 | // create axes 178 | chartVis.axes = chartVis.svg.selectAll('.axes') 179 | .append('svg:g').classed('axes', true); 180 | 181 | // create vertices 182 | chartVis.vertices = chartVis.svg.selectAll('.vertices'); 183 | 184 | //Initiate Legend 185 | chartVis.legend = chartVis.svg.append('svg:g').classed('legend', true) 186 | .attr('height', config.h / 2) 187 | .attr('width', config.w / 2) 188 | .attr('transform', 'translate(' + 0 + ', ' + 1.1 * config.h + ')'); 189 | }; 190 | 191 | const _buildSingleLevelLine = function (levelFactor) { 192 | chartVis.levels 193 | .data(chartVis.allAxis).enter() 194 | .append('svg:line').classed('level-lines', true) 195 | .attr('x1', function (d, i) { return levelFactor * (1 - Math.sin(i * config.radians / chartVis.totalAxes)); }) 196 | .attr('y1', function (d, i) { return levelFactor * (1 - Math.cos(i * config.radians / chartVis.totalAxes)); }) 197 | .attr('x2', function (d, i) { return levelFactor * (1 - Math.sin((i + 1) * config.radians / chartVis.totalAxes)); }) 198 | .attr('y2', function (d, i) { return levelFactor * (1 - Math.cos((i + 1) * config.radians / chartVis.totalAxes)); }) 199 | .attr('transform', 'translate(' + (config.w / 2 - levelFactor) + ', ' + (config.h / 2 - levelFactor) + ')') 200 | .attr('stroke', 'gray') 201 | .attr('stroke-width', '0.5px'); 202 | }; 203 | 204 | // builds out the levels of the radar chart 205 | const _buildLevels = function () { 206 | for (let level = 0; level < config.levels; level++) { 207 | const levelFactor = chartVis.radius * ((level + 1) / config.levels); 208 | // build level-lines 209 | _buildSingleLevelLine(levelFactor); 210 | } 211 | }; 212 | 213 | const _buildSingleLevelLabel = function (level, levelFactor) { 214 | chartVis.levels 215 | .data([1]).enter() 216 | .append('svg:text').classed('level-labels', true) 217 | .text((config.maxValue * (level + 1) / config.levels).toFixed(2)) 218 | .attr('x', function (d) { return levelFactor * (1 - Math.sin(0)); }) 219 | .attr('y', function (d) { return levelFactor * (1 - Math.cos(0)); }) 220 | .attr('transform', 'translate(' + (config.w / 2 - levelFactor + 5) + ', ' + (config.h / 2 - levelFactor) + ')') 221 | .attr('fill', 'gray') 222 | .attr('font-family', 'sans-serif') 223 | .attr('font-size', 10 * config.labelScale + 'px'); 224 | }; 225 | 226 | // builds out the levels labels 227 | const _buildLevelsLabels = function () { 228 | for (let level = 0; level < config.levels; level++) { 229 | const levelFactor = chartVis.radius * ((level + 1) / config.levels); 230 | // build level-labels 231 | _buildSingleLevelLabel(level, levelFactor); 232 | } 233 | }; 234 | 235 | 236 | // builds out the axes 237 | const _buildAxes = function () { 238 | chartVis.axes 239 | .data(chartVis.allAxis).enter() 240 | .append('svg:line').classed('axis-lines', true) 241 | .attr('x1', config.w / 2) 242 | .attr('y1', config.h / 2) 243 | .attr('x2', function (d, i) { return config.w / 2 * (1 - Math.sin(i * config.radians / chartVis.totalAxes)); }) 244 | .attr('y2', function (d, i) { return config.h / 2 * (1 - Math.cos(i * config.radians / chartVis.totalAxes)); }) 245 | .attr('stroke', 'grey') 246 | .attr('stroke-width', '1px'); 247 | }; 248 | // builds out the axes labels 249 | const _buildAxesLabels = function () { 250 | chartVis.axes 251 | .data(chartVis.allAxis).enter() 252 | .append('svg:text').classed('axis-labels', true) 253 | .text(function (d) { return d; }) 254 | .attr('text-anchor', 'middle') 255 | .attr('x', function (d, i) { return config.w / 2 * (1 - 1.3 * Math.sin(i * config.radians / chartVis.totalAxes)); }) 256 | .attr('y', function (d, i) { return config.h / 2 * (1 - 1.1 * Math.cos(i * config.radians / chartVis.totalAxes)); }) 257 | .attr('font-family', 'sans-serif') 258 | .attr('font-size', 11 * config.labelScale + 'px'); 259 | }; 260 | 261 | // builds out the legend 262 | const _buildLegend = function (data) { 263 | //Create legend squares 264 | if (config.facet) { 265 | chartVis.legend.selectAll('.legend-tiles') 266 | .data(data).enter() 267 | .append('svg:rect').classed('legend-tiles', true) 268 | .attr('x', config.w - config.paddingX / 2) 269 | .attr('y', function (d, i) { return i * 2 * config.legendBoxSize; }) 270 | .attr('width', config.legendBoxSize) 271 | .attr('height', config.legendBoxSize) 272 | .attr('fill', function (d, g) { return config.colors(g); }); 273 | 274 | //Create text next to squares 275 | chartVis.legend.selectAll('.legend-labels') 276 | .data(data).enter() 277 | .append('svg:text').classed('legend-labels', true) 278 | .attr('x', config.w - config.paddingX / 2 + (1.5 * config.legendBoxSize)) 279 | .attr('y', function (d, i) { return i * 2 * config.legendBoxSize; }) 280 | .attr('dy', 0.07 * config.legendBoxSize + 'em') 281 | .attr('font-size', 11 * config.labelScale + 'px') 282 | .attr('fill', 'gray') 283 | .text(function (d) { 284 | return d.group; 285 | }); 286 | } else { 287 | chartVis.legend.selectAll('.legend-tiles') 288 | .data(data).enter() 289 | .append('svg:rect').classed('legend-tiles', true) 290 | .attr('x', config.w + config.w / 4) 291 | .attr('y', function (d, i) { return i * 2 * config.legendBoxSize - config.h / 2; }) 292 | .attr('width', config.legendBoxSize) 293 | .attr('height', config.legendBoxSize) 294 | .on(over, function (d,i) { 295 | chartVis.svg.selectAll('.polygon-areas') // fade all other polygons out 296 | .transition(250) 297 | .attr('fill-opacity', 0.1) 298 | .attr('stroke-opacity', 0.1); 299 | d3.select('#polygon_' + i) // focus on active polygon 300 | .transition(250) 301 | .attr('fill-opacity', 0.7) 302 | .attr('stroke-opacity', config.polygonStrokeOpacity); 303 | }) 304 | .on(out, function () { 305 | d3.selectAll('.polygon-areas') 306 | .transition(250) 307 | .attr('fill-opacity', config.polygonAreaOpacity) 308 | .attr('stroke-opacity', 1); 309 | }) 310 | .attr('fill', function (d, g) { return config.colors(g); }); 311 | 312 | //Create text next to squares 313 | chartVis.legend.selectAll('.legend-labels') 314 | .data(data).enter() 315 | .append('svg:text').classed('legend-labels', true) 316 | .attr('x', config.w + config.w / 4 + (1.5 * config.legendBoxSize)) 317 | .attr('y', function (d, i) { return i * 2 * config.legendBoxSize - config.h / 2; }) 318 | .attr('dy', 0.07 * config.legendBoxSize + 'em') 319 | .attr('font-size', 11 * config.labelScale + 'px') 320 | .attr('fill', 'gray') 321 | .text(function (d) { 322 | return d.group; 323 | }); 324 | } 325 | }; 326 | 327 | // show tooltip of vertices 328 | const _verticesTooltipShow = function (d) { 329 | chartVis.verticesTooltip.style('opacity', 0.9) 330 | .html('Value: ' + d.value + '
') 331 | .style('left', (d3.event.pageX) + 'px') 332 | .style('top', (d3.event.pageY) + 'px'); 333 | }; 334 | 335 | // hide tooltip of vertices 336 | const _verticesTooltipHide = function () { 337 | chartVis.verticesTooltip.style('opacity', 0); 338 | }; 339 | 340 | // builds out the polygon vertices of the dataset 341 | const _buildVertices = function (data) { 342 | data.forEach(function (group, g) { 343 | chartVis.vertices 344 | .data(group.axes).enter() 345 | .append('svg:circle') 346 | .attr('r', config.polygonPointSize) 347 | .attr('cx', function (d, i) { return d.coordinates.x; }) 348 | .attr('cy', function (d, i) { return d.coordinates.y; }) 349 | .attr('fill', function (d, g) { return config.colors(g); }) 350 | .on(over, _verticesTooltipShow) 351 | .on(out, _verticesTooltipHide); 352 | }); 353 | }; 354 | 355 | // builds out the polygon areas of the dataset 356 | const _buildPolygons = function (data) { 357 | chartVis.vertices 358 | .data(data).enter() 359 | .append('svg:polygon').classed('polygon-areas', true) 360 | .attr('points', function (group) { // build verticesString for each group 361 | let verticesString = ''; 362 | group.axes.forEach(function (d) { verticesString += d.coordinates.x + ',' + d.coordinates.y + ' '; }); 363 | return verticesString; 364 | }) 365 | .attr('id', function (d, i) {return 'polygon_' + i;}) 366 | .attr('stroke-width', '2px') 367 | .attr('stroke', function (d, i) { return config.colors(i); }) 368 | .attr('fill', function (d, i) { return config.colors(i); }) 369 | .attr('fill-opacity', config.polygonAreaOpacity) 370 | .attr('stroke-opacity', config.polygonStrokeOpacity) 371 | .on(over, function (d) { 372 | chartVis.svg.selectAll('.polygon-areas') // fade all other polygons out 373 | .transition(250) 374 | .attr('fill-opacity', 0.1) 375 | .attr('stroke-opacity', 0.1); 376 | d3.select(this) // focus on active polygon 377 | .transition(250) 378 | .attr('fill-opacity', 0.7) 379 | .attr('stroke-opacity', config.polygonStrokeOpacity); 380 | }) 381 | .on(out, function () { 382 | d3.selectAll('.polygon-areas') 383 | .transition(250) 384 | .attr('fill-opacity', config.polygonAreaOpacity) 385 | .attr('stroke-opacity', 1); 386 | }); 387 | }; 388 | 389 | 390 | const _buildVis = function (data) { 391 | _buildVisComponents(); 392 | _buildCoordinates(data); 393 | const showLevels = $scope.vis.params.addLevel; 394 | const showLevelsLabels = $scope.vis.params.addLevelLabel; 395 | const showAxes = $scope.vis.params.addAxe; 396 | const showAxesLabels = $scope.vis.params.addAxeLabel; 397 | const showLegend = $scope.vis.params.addLegend; 398 | const showVertices = $scope.vis.params.addVertice; 399 | const showPolygons = $scope.vis.params.addPolygon; 400 | 401 | if (showLevels) _buildLevels(); 402 | if (showLevelsLabels) _buildLevelsLabels(); 403 | if (showAxes) _buildAxes(); 404 | if (showAxesLabels) _buildAxesLabels(); 405 | if (showLegend) _buildLegend(data); 406 | if (showVertices) _buildVertices(data); 407 | if (showPolygons) _buildPolygons(data); 408 | }; 409 | 410 | 411 | const _render = function () { 412 | d3.select(svgRoot).selectAll('svg').remove(); 413 | if (config.facet) { 414 | data.forEach(function (d, i) { 415 | _buildVis([d]); // build svg for each data group 416 | 417 | // override colors 418 | chartVis.svg.selectAll('.polygon-areas') 419 | .attr('stroke', config.colors(i)) 420 | .attr('fill', config.colors(i)); 421 | chartVis.svg.selectAll('.polygon-vertices') 422 | .attr('fill', config.colors(i)); 423 | chartVis.svg.selectAll('.legend-tiles') 424 | .attr('fill', config.colors(i)); 425 | }); 426 | } else { 427 | _buildVis(data); // build svg 428 | } 429 | }; 430 | 431 | 432 | const off = $rootScope.$on('change:vis', function () { 433 | _updateDimensions(); 434 | _initConfig(); 435 | $scope.processTableGroups(tableGroups); 436 | _updateConfig(); 437 | _render(); 438 | }); 439 | 440 | $scope.$on('$destroy', off); 441 | 442 | $scope.processTableGroups = function (tableGroups) { 443 | tableGroups.tables.forEach(function (table) { 444 | data = []; 445 | const cols = table.columns; 446 | table.rows.forEach(function (row,i) { 447 | const group = {}; 448 | group.group = row[0]; 449 | const axes = []; 450 | for (let i = 1; i < row.length; i++) { 451 | const item = { 452 | axis: cols[i].title, 453 | value: row[i] 454 | }; 455 | axes.push(item); 456 | } 457 | group.axes = axes; 458 | data.push(group); 459 | }); 460 | }); 461 | }; 462 | 463 | $scope.$watch('esResponse', function (resp) { 464 | if (resp) { 465 | tableGroups = tabifyAggResponse($scope.vis, resp); 466 | if (tableGroups.tables.length > 0) { 467 | if (tableGroups.tables[0].columns.length > 2) { 468 | _updateDimensions(); 469 | _initConfig(); 470 | $scope.processTableGroups(tableGroups); 471 | _updateConfig(); 472 | _render(); 473 | } 474 | } 475 | } 476 | }); 477 | }); 478 | -------------------------------------------------------------------------------- /public/kibi_radar_vis_params.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 |
7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 | 20 |
21 |
22 | 26 |
27 |
28 | 32 |
33 |
34 | 38 |
39 |
40 | 44 |
45 |
46 | 50 |
51 |
52 | 56 |
57 | 58 | 59 | --------------------------------------------------------------------------------