├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── deploy_key.enc ├── docs ├── animated_images.gif └── screenshot.png ├── eslint-plugin-arena ├── index.js └── package.json ├── package-lock.json ├── package.json ├── public ├── CNAME └── index.html ├── src ├── App.vue ├── assets │ ├── FiraSans-Bold.otf │ ├── FiraSans-Regular.otf │ ├── SIL Open Font License.txt │ ├── grid.png │ └── logo.png ├── components │ ├── Annotations.vue │ ├── Block.config.js │ ├── Block.interactions.js │ ├── Block.vue │ ├── BlockHead.vue │ ├── DeleteZone.vue │ ├── FullscreenBlock.vue │ ├── Loading.vue │ ├── NameConflicts.vue │ ├── Navbar.vue │ ├── NavbarHelp.vue │ ├── Notification.vue │ ├── Notifications.vue │ ├── ObservationDetails.vue │ ├── PagesBar.vue │ ├── PlotDropdown.vue │ ├── PlotOptions.vue │ ├── PlotProxy.vue │ ├── Plotly.vue │ ├── Preview.vue │ ├── SearchDropdown.vue │ ├── SearchDropdownElement.vue │ ├── SearchMenu.vue │ ├── SelectMenu.vue │ ├── Settings.vue │ ├── SettingsTabOptions.vue │ ├── SettingsTabPrivacy.vue │ ├── SettingsTabSessions.vue │ ├── SettingsTabSources.vue │ ├── Sidepanel.vue │ ├── SidepanelDropdown.vue │ ├── SidepanelHelp.vue │ ├── SidepanelOptions.vue │ ├── Slider.vue │ ├── SlotsListElement.vue │ └── WelcomeScreen.vue ├── configuration │ ├── OptionsSchemas.js │ ├── PlotsInfo.js │ └── config.js ├── main.js ├── plots │ ├── Breakdown.vue │ ├── CategoricalCeterisParibus.vue │ ├── CategoricalDependence.vue │ ├── CategoricalShapleyDependence.vue │ ├── DistributionCounts.vue │ ├── DistributionHistogram.vue │ ├── Fairness.vue │ ├── FairnessSubplotAbsolute.vue │ ├── FairnessSubplotRelative.vue │ ├── FeatureImportance.vue │ ├── FunnelMeasure.vue │ ├── HtmlWidget.vue │ ├── LinearDependence.vue │ ├── LinearShapleyDependence.vue │ ├── Message.vue │ ├── Metrics.vue │ ├── NumericalCeterisParibus.vue │ ├── REC.vue │ ├── ROC.vue │ ├── RegressionFairness.vue │ ├── RegressionFairnessSubplotRelative.vue │ ├── SHAPValues.vue │ ├── ShapleyValuesVariableImportance.vue │ ├── SubsetsPerformance.vue │ └── VariableAgainstAnother.vue ├── store │ ├── dataSources.js │ ├── datasources │ │ ├── arenarLiveDatasource.js │ │ ├── dataSourceCommon.js │ │ ├── jsonDatasource.js │ │ └── peerDatasource.js │ ├── index.js │ ├── miscellaneous.js │ ├── notifications.js │ ├── params.js │ ├── schemas │ │ ├── arenarLive-1.1.0.schema.json │ │ ├── arenarLive-1.2.0.schema.json │ │ ├── arenarLive.schema.json │ │ ├── data-1.1.0.schema.json │ │ ├── data-1.2.0.schema.json │ │ ├── data.schema.json │ │ ├── recentURLSources.schema.json │ │ ├── session-1.1.0.schema.json │ │ └── session.schema.json │ ├── slots.js │ └── telemetry.js ├── style.css └── utils │ ├── OptionsMixin.js │ ├── Resize.js │ ├── fontAwesomeLoader.js │ ├── format.js │ ├── lolipopAxis.js │ ├── streams.js │ └── zIndexIncrementor.js └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | plugins: ['arena'], 7 | 'extends': [ 8 | 'plugin:vue/essential', 9 | '@vue/standard' 10 | ], 11 | rules: { 12 | 'no-console': 'off', 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 14 | 'arena/no-hardcode-param-types': ['error', ['observation', 'variable', 'model', 'dataset']] 15 | }, 16 | globals: { 'BUILDINFO': true }, 17 | parserOptions: { 18 | parser: 'babel-eslint' 19 | }, 20 | overrides: [ 21 | { 22 | files: ['src/plots/**', 'src/configuration/**'], 23 | rules: { 24 | 'arena/no-hardcode-param-types': 'off' 25 | } 26 | } 27 | ], 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | deploy_key 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | cache: npm 5 | script: 6 | - npm run build 7 | deploy: 8 | provider: pages:git 9 | edge: true 10 | local_dir: build 11 | deploy_key: deploy_key 12 | cleanup: false 13 | on: 14 | all_branches: true 15 | repo: ModelOriented/Arena 16 | before_install: 17 | - sudo add-apt-repository -y ppa:deadsnakes/ppa 18 | - sudo apt-get update 19 | - sudo apt-get -y install python3.8 python3.8-distutils 20 | - curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && sudo python3.8 get-pip.py && rm -f get-pip.py 21 | - sudo rm -rf /usr/bin/python3 && sudo ln -s /usr/bin/python3.8 /usr/bin/python3 22 | - python3 --version 23 | before_deploy: 24 | - openssl aes-256-cbc -K $encrypted_d496dfbd0e99_key -iv $encrypted_d496dfbd0e99_iv 25 | -in deploy_key.enc -out ./deploy_key -d 26 | - chmod 600 ./deploy_key 27 | - ssh -i ./deploy_key -T git@github.com 2>&1 | grep successful > /dev/null 28 | - echo 'ssh -i ./deploy_key "$@"' > use_ssh.sh 29 | - chmod +x use_ssh.sh 30 | - export GIT_SSH="./use_ssh.sh" 31 | - git clone --quiet --branch="gh-pages" --depth=1 "git@github.com:ModelOriented/Arena.git" repo 32 | - mkdir -p repo/branch build 33 | - cp -R repo/branch build/ 34 | - rm -rf build/branch/$TRAVIS_BRANCH 35 | - cp -R dist build/branch/$TRAVIS_BRANCH 36 | - cp -R build/branch/master/* build/ 37 | - mkdir -p build/branch/docs 38 | - cp -R build/branch/docs build/ 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arena - Interactive XAI dashboard 2 | 3 | [![](docs/animated_images.gif)](https://arena.drwhy.ai) 4 | 5 | ## How to use it 6 | 7 | Arena dashboard is available at [`https://arena.drwhy.ai/?app`](https://arena.drwhy.ai/?app). Needed data can be generated using: 8 | 9 | * **R** - [ArenaR](https://github.com/ModelOriented/ArenaR) 10 | * **Python** - [dalex](https://python.drwhy.ai) 11 | 12 | ## [Examples](https://arena.drwhy.ai/docs/guide/first-datasource) 13 | 14 | ## Demos 15 | * [Apartments from 2009-2010 price per m2](https://arena.drwhy.ai/?demo=0) 16 | * [HR classification](https://arena.drwhy.ai/?demo=2) 17 | * [FIFA 20 Players value](https://arena.drwhy.ai/?demo=1) 18 | 19 | ## Local Build 20 | 21 | ### Compiles and hot-reloads for development 22 | ``` 23 | npm install 24 | npm run serve 25 | ``` 26 | 27 | ### Compiles and minifies for production 28 | Remember to set path in `vue.config.js`. 29 | ``` 30 | npm install 31 | npm run build 32 | ``` 33 | 34 | ## License 35 | 36 | ### Code 37 | All scripts are licensed under the terms of the [GPL v3](LICENSE) license. 38 | 39 | ### Assets 40 | **Fira Sans** - Files `src/assets/FiraSans-Bold.otf` and `src/assets/FiraSans-Regular.otf` are licensed under the terms of the [SIL OPEN FONT LICENSE Version 1.1]() license. 41 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /deploy_key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModelOriented/Arena/57e170ad02cfa878daa7474206fddb6d72048579/deploy_key.enc -------------------------------------------------------------------------------- /docs/animated_images.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModelOriented/Arena/57e170ad02cfa878daa7474206fddb6d72048579/docs/animated_images.gif -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModelOriented/Arena/57e170ad02cfa878daa7474206fddb6d72048579/docs/screenshot.png -------------------------------------------------------------------------------- /eslint-plugin-arena/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'no-hardcode-param-types': { 4 | meta: { 5 | schema: [ 6 | { 'type': 'array', 'items': { 'type': 'string' } } 7 | ] 8 | }, 9 | create: function (context) { 10 | return { 11 | Literal (node) { 12 | if (context.options[0].includes(node.value)) { 13 | context.report(node, 'Do not use hardcoded param types') 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /eslint-plugin-arena/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-arena", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Arena", 3 | "version": "0.4.1", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@fortawesome/fontawesome-svg-core": "^1.2.36", 12 | "@fortawesome/free-regular-svg-icons": "^5.15.4", 13 | "@fortawesome/free-solid-svg-icons": "^5.15.4", 14 | "@fortawesome/vue-fontawesome": "^0.1.9", 15 | "ajv": "^6.12.6", 16 | "clone-deep": "^4.0.1", 17 | "core-js": "^3.19.1", 18 | "eslint-plugin-arena": "file:eslint-plugin-arena", 19 | "fast-deep-equal": "^3.1.1", 20 | "file-saver": "^2.0.5", 21 | "fuse.js": "^4.0.0-beta", 22 | "interactjs": "^1.10.11", 23 | "peerjs": "github:peers/peerjs#master", 24 | "plotly.js": "^1.58.5", 25 | "stats-webpack-plugin": "^0.7.0", 26 | "uuid": "^3.4.0", 27 | "vue": "^2.6.14", 28 | "vue-dropdowns": "^1.1.2", 29 | "vue-resize-directive": "^1.2.0", 30 | "vue-resource": "^1.5.3", 31 | "vuex": "^3.6.2", 32 | "webpack-visualizer-plugin": "^0.1.11" 33 | }, 34 | "devDependencies": { 35 | "@vue/cli-plugin-babel": "^4.5.15", 36 | "@vue/cli-plugin-eslint": "^4.5.15", 37 | "@vue/cli-service": "^4.5.15", 38 | "@vue/eslint-config-standard": "^4.0.0", 39 | "babel-eslint": "^10.1.0", 40 | "eslint": "^5.16.0", 41 | "eslint-plugin-vue": "^5.0.0", 42 | "vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0", 43 | "vue-template-compiler": "^2.6.14" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | arena.drwhy.ai 2 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Arena 8 | 9 | 10 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/FiraSans-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModelOriented/Arena/57e170ad02cfa878daa7474206fddb6d72048579/src/assets/FiraSans-Bold.otf -------------------------------------------------------------------------------- /src/assets/FiraSans-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModelOriented/Arena/57e170ad02cfa878daa7474206fddb6d72048579/src/assets/FiraSans-Regular.otf -------------------------------------------------------------------------------- /src/assets/SIL Open Font License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Mozilla Foundation https://mozilla.org/ with Reserved Font Name Fira Sans. 2 | 3 | Copyright (c) 2014, Telefonica S.A. 4 | 5 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 6 | This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. 14 | 15 | The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. 16 | 17 | DEFINITIONS 18 | "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. 19 | 20 | "Reserved Font Name" refers to any names specified as such after the copyright statement(s). 21 | 22 | "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). 23 | 24 | "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. 25 | 26 | "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. 27 | 28 | PERMISSION & CONDITIONS 29 | Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 30 | 31 | 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 32 | 33 | 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 34 | 35 | 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 36 | 37 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 38 | 39 | 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. 40 | 41 | TERMINATION 42 | This license becomes null and void if any of the above conditions are not met. 43 | 44 | DISCLAIMER 45 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. -------------------------------------------------------------------------------- /src/assets/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModelOriented/Arena/57e170ad02cfa878daa7474206fddb6d72048579/src/assets/grid.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModelOriented/Arena/57e170ad02cfa878daa7474206fddb6d72048579/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Annotations.vue: -------------------------------------------------------------------------------- 1 | 6 | 148 | 161 | -------------------------------------------------------------------------------- /src/components/Block.config.js: -------------------------------------------------------------------------------- 1 | import interact from 'interactjs' 2 | 3 | export default { 4 | dragModifiers: [ 5 | interact.modifiers.snap({ 6 | targets: [interact.createSnapGrid({ x: 32, y: 32 })], 7 | range: Infinity, 8 | offset: { x: 0, y: 0 }, 9 | relativePoints: [ { x: 0, y: 0 } ] 10 | }), 11 | interact.modifiers.restrict({ 12 | restriction: '#playground', 13 | endOnly: true 14 | }) 15 | ], 16 | resizeConfig: { 17 | edges: { left: '.handle-left', right: '.handle-right', bottom: '.handle-bottom', top: '.handle-top' }, 18 | modifiers: [ 19 | interact.modifiers.restrict({ 20 | restriction: '#playground', 21 | endOnly: true 22 | }), 23 | interact.modifiers.snap({ 24 | targets: [ 25 | interact.createSnapGrid({ x: 32, y: 32 }) 26 | ], 27 | range: Infinity, 28 | offset: { x: 0, y: 0 }, 29 | relativePoints: [ { x: 0, y: 0 } ] 30 | }), 31 | interact.modifiers.restrictSize({ 32 | min: { width: 320, height: 320 } 33 | }) 34 | ], 35 | inertia: true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Block.interactions.js: -------------------------------------------------------------------------------- 1 | import interact from 'interactjs' 2 | import BlockConfig from '@/components/Block.config.js' 3 | import { mapMutations, mapGetters } from 'vuex' 4 | 5 | export default { 6 | computed: { 7 | singleDropzone () { return this.mode === 'single-dropzone' }, 8 | dualDropzone () { return this.mode === 'dual-dropzone' }, 9 | activeLeftDropzone () { return this.dualDropzone && this.activeDropzone === 'left' }, 10 | activeRightDropzone () { return this.dualDropzone && this.activeDropzone === 'right' }, 11 | activeFullDropzone () { return this.singleDropzone && this.activeDropzone === 'full' }, 12 | ...mapGetters(['getSlotInitInfo']) 13 | }, 14 | methods: { 15 | initInteractions () { 16 | // Link to component from dom element 17 | this.$el.block = this 18 | this.interactable = interact(this.$refs.block).pointerEvents({ holdDuration: 250 }) 19 | .draggable({ 20 | interia: true, 21 | autoScroll: true, 22 | modifiers: BlockConfig.dragModifiers, 23 | onmove: event => this.dragOnMove(event), 24 | onstart: event => this.dragOnStart(event), 25 | onend: event => this.dragOnEnd(event), 26 | cursorChecker: (action, interactable, element, interacting) => interacting ? 'grabbing' : 'grab' 27 | }).resizable(BlockConfig.resizeConfig) 28 | .on('resizemove', event => this.resizeOnMove(event)) 29 | .on('hold', event => this.onHold(event)) 30 | 31 | // Init component moving using interaction object from holding button event (See SlotsListElement) 32 | let initInfo = this.getSlotInitInfo(this.slotv) 33 | if (initInfo) { 34 | let target = this.$refs.block 35 | let x = initInfo.x - target.parentElement.offsetLeft - (target.offsetWidth / 2) 36 | let y = initInfo.y - target.parentElement.offsetTop - (target.offsetHeight / 2) 37 | this.updateTargetPosition(target, x, y) 38 | initInfo.interaction.start({ name: 'drag' }, this.interactable, this.$el) 39 | this.removeSlotInitInfo(this.slotv) 40 | this.moving = true 41 | } 42 | 43 | /* Init dropzone */ 44 | let dropzoneCommonProperties = { 45 | overlap: 'pointer', 46 | accept: '.block', 47 | ondropactivate: this.validateAndRun(target => { 48 | if (this.moving) return /* Do not run placeholder on moving block */ 49 | this.mode = this.canMerge(target) ? 'dual-dropzone' : 'single-dropzone' 50 | this.activeDropzone = 'none' 51 | }, true, false), 52 | ondropdeactivate: e => { if (e.relatedTarget.block) this.mode = 'normal' }, 53 | checker: (dragEvent, event, dropped, dropzone, dropElement, draggable, draggableElement) => { 54 | return dropped && draggableElement !== this.$el 55 | } 56 | } 57 | 58 | interact(this.$refs.leftdropzone).dropzone(Object.assign({}, dropzoneCommonProperties, { 59 | ondragenter: this.validateAndRun(target => { this.activeDropzone = this.dualDropzone ? 'left' : 'full' }), 60 | ondragleave: this.validateAndRun(target => { if (this.singleDropzone || this.activeLeftDropzone) this.activeDropzone = 'none' }), 61 | ondrop: this.validateAndRun(target => { this.singleDropzone ? this.onSwapDrop(target) : this.onMergeDrop(target) }, false) 62 | })) 63 | interact(this.$refs.rightdropzone).dropzone(Object.assign({}, dropzoneCommonProperties, { 64 | ondragenter: this.validateAndRun(target => { this.activeDropzone = this.dualDropzone ? 'right' : 'full' }), 65 | ondragleave: this.validateAndRun(target => { if (this.singleDropzone || this.activeRightDropzone) this.activeDropzone = 'none' }, false), 66 | ondrop: this.validateAndRun(this.onSwapDrop, false) 67 | })) 68 | }, 69 | roundToGrid (x) { 70 | return Math.round(x / 32) * 32 71 | }, 72 | validateAndRun (f, mustBeMoving = true, checkMode = true) { 73 | return e => { 74 | if (this.mode === 'normal' && checkMode) return 75 | if (!e.relatedTarget.block) return 76 | let target = e.relatedTarget.block 77 | if (!target.moving && mustBeMoving) return 78 | return f(target) 79 | } 80 | }, 81 | dragOnMove (event) { 82 | event.stopPropagation() 83 | event.preventDefault() 84 | this.updateTargetProperties(event) 85 | }, 86 | dragOnStart (event) { 87 | this.moving = true 88 | }, 89 | dragOnEnd (event) { 90 | this.moving = false 91 | }, 92 | onHold (event) { 93 | if (!event.target || Array.prototype.includes.call(event.target.classList, 'handle')) return 94 | let interaction = event.interaction 95 | if (!interaction.interacting()) { 96 | interaction.start({ name: 'drag' }, event.interactable, event.currentTarget) 97 | } 98 | }, 99 | resizeOnMove (event) { 100 | event.preventDefault() 101 | this.updateTargetProperties(event) 102 | }, 103 | updateTargetPosition (target, x, y) { 104 | target.setAttribute('data-x', this.roundToGrid(x)) 105 | target.setAttribute('data-y', this.roundToGrid(y)) 106 | this.setSlotPosition({ slot: this.slotv, x: this.roundToGrid(x), y: this.roundToGrid(y) }) 107 | }, 108 | updateTargetProperties (event) { 109 | var x = this.slotv.positionX + (event.type === 'resizemove' ? event.deltaRect.left : event.dx) 110 | var y = this.slotv.positionY + (event.type === 'resizemove' ? event.deltaRect.top : event.dy) 111 | this.updateTargetPosition(event.target, x, y) 112 | this.setSlotSize({ slot: this.slotv, width: this.roundToGrid(event.rect.width), height: this.roundToGrid(event.rect.height) }) 113 | }, 114 | ...mapMutations(['setSlotPosition', 'setSlotSize', 'removeSlotInitInfo']) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/components/BlockHead.vue: -------------------------------------------------------------------------------- 1 | 25 | 91 | 157 | -------------------------------------------------------------------------------- /src/components/DeleteZone.vue: -------------------------------------------------------------------------------- 1 | 6 | 57 | 82 | -------------------------------------------------------------------------------- /src/components/FullscreenBlock.vue: -------------------------------------------------------------------------------- 1 | 8 | 25 | 49 | -------------------------------------------------------------------------------- /src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 8 | 19 | 51 | -------------------------------------------------------------------------------- /src/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 21 | 57 | 153 | -------------------------------------------------------------------------------- /src/components/NavbarHelp.vue: -------------------------------------------------------------------------------- 1 | 8 | 21 | 61 | -------------------------------------------------------------------------------- /src/components/Notification.vue: -------------------------------------------------------------------------------- 1 | 11 | 24 | 58 | -------------------------------------------------------------------------------- /src/components/Notifications.vue: -------------------------------------------------------------------------------- 1 | 8 | 27 | 38 | -------------------------------------------------------------------------------- /src/components/ObservationDetails.vue: -------------------------------------------------------------------------------- 1 | 17 | 106 | 133 | -------------------------------------------------------------------------------- /src/components/PagesBar.vue: -------------------------------------------------------------------------------- 1 | 19 | 58 | 119 | -------------------------------------------------------------------------------- /src/components/PlotDropdown.vue: -------------------------------------------------------------------------------- 1 | 12 | 65 | 106 | -------------------------------------------------------------------------------- /src/components/PlotOptions.vue: -------------------------------------------------------------------------------- 1 | 19 | 82 | 131 | -------------------------------------------------------------------------------- /src/components/Plotly.vue: -------------------------------------------------------------------------------- 1 | 7 | 95 | 108 | -------------------------------------------------------------------------------- /src/components/Preview.vue: -------------------------------------------------------------------------------- 1 | 6 | 17 | 44 | -------------------------------------------------------------------------------- /src/components/SearchDropdownElement.vue: -------------------------------------------------------------------------------- 1 | 6 | 58 | 74 | -------------------------------------------------------------------------------- /src/components/SearchMenu.vue: -------------------------------------------------------------------------------- 1 | 10 | 51 | 82 | -------------------------------------------------------------------------------- /src/components/SelectMenu.vue: -------------------------------------------------------------------------------- 1 | 9 | 24 | 46 | -------------------------------------------------------------------------------- /src/components/Settings.vue: -------------------------------------------------------------------------------- 1 | 10 | 39 | 89 | -------------------------------------------------------------------------------- /src/components/SettingsTabOptions.vue: -------------------------------------------------------------------------------- 1 | 15 | 48 | 76 | -------------------------------------------------------------------------------- /src/components/SettingsTabPrivacy.vue: -------------------------------------------------------------------------------- 1 | 20 | 63 | 98 | -------------------------------------------------------------------------------- /src/components/SettingsTabSources.vue: -------------------------------------------------------------------------------- 1 | 21 | 84 | 171 | -------------------------------------------------------------------------------- /src/components/SidepanelHelp.vue: -------------------------------------------------------------------------------- 1 | 8 | 21 | 58 | -------------------------------------------------------------------------------- /src/components/SidepanelOptions.vue: -------------------------------------------------------------------------------- 1 | 17 | 70 | 95 | -------------------------------------------------------------------------------- /src/components/Slider.vue: -------------------------------------------------------------------------------- 1 | 8 | 91 | 130 | -------------------------------------------------------------------------------- /src/components/SlotsListElement.vue: -------------------------------------------------------------------------------- 1 | 8 | 43 | 75 | -------------------------------------------------------------------------------- /src/components/WelcomeScreen.vue: -------------------------------------------------------------------------------- 1 | 27 | 66 | 143 | -------------------------------------------------------------------------------- /src/configuration/OptionsSchemas.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | name: 'breakdown_max_variables', 4 | displayName: 'Maximum variables in Break Down', 5 | category: 'Break Down', 6 | type: 'integer', 7 | default: 6, 8 | min: 1, 9 | max: 30 10 | }, 11 | { 12 | name: 'featureimportance_max_variables', 13 | displayName: 'Maximum variables in Variable Importance', 14 | category: 'Variable Importance', 15 | type: 'integer', 16 | default: 7, 17 | min: 1, 18 | max: 30 19 | }, 20 | { 21 | name: 'featureimportance_boxplots', 22 | displayName: 'Display boxplots over Variable Importance', 23 | category: 'Variable Importance', 24 | type: 'boolean', 25 | default: true 26 | }, 27 | { 28 | name: 'shapvalues_max_variables', 29 | displayName: 'Maximum variables in Shapley Values', 30 | category: 'Shapley Values', 31 | type: 'integer', 32 | default: 7, 33 | min: 1, 34 | max: 30 35 | }, 36 | { 37 | name: 'funnelmeasure_page_size', 38 | displayName: 'Maximum variables in one page of Funnel Plot', 39 | category: 'Funnel Plot', 40 | type: 'integer', 41 | default: 6, 42 | min: 1, 43 | max: 15 44 | }, 45 | { 46 | name: 'subsetsperformance_page_size', 47 | displayName: 'Maximum variables in one page of Subset Performance', 48 | category: 'Subset Performance', 49 | type: 'integer', 50 | default: 6, 51 | min: 1, 52 | max: 15 53 | }, 54 | { 55 | name: 'shapvalues_boxplots', 56 | displayName: 'Display boxplots over Shapley Values', 57 | category: 'Shapley Values', 58 | type: 'boolean', 59 | default: false 60 | }, 61 | { 62 | name: 'left_margin', 63 | displayName: 'Left margin for variables names with values', 64 | category: 'Margins', 65 | type: 'integer', 66 | default: 140, 67 | min: 80, 68 | max: 300 69 | }, 70 | { 71 | name: 'left_margin_values', 72 | displayName: 'Left margin for variables values', 73 | category: 'Margins', 74 | type: 'integer', 75 | default: 90, 76 | min: 60, 77 | max: 300 78 | }, 79 | { 80 | name: 'shapley_dependence_jitter', 81 | displayName: 'Shapley dependence jitter range as per mill of chart range', 82 | category: 'Shapley Dependence', 83 | type: 'integer', 84 | default: 4, 85 | min: 0, 86 | max: 100 87 | }, 88 | { 89 | name: 'shapley_dependence_error_bar', 90 | displayName: 'Display error bars over Shapley dependence points', 91 | category: 'Shapley Dependence', 92 | type: 'boolean', 93 | default: true 94 | } 95 | ] 96 | -------------------------------------------------------------------------------- /src/configuration/PlotsInfo.js: -------------------------------------------------------------------------------- 1 | import Breakdown from '@/plots/Breakdown.vue' 2 | import FeatureImportance from '@/plots/FeatureImportance.vue' 3 | import CategoricalDependence from '@/plots/CategoricalDependence.vue' 4 | import LinearDependence from '@/plots/LinearDependence.vue' 5 | import NumericalCeterisParibus from '@/plots/NumericalCeterisParibus.vue' 6 | import CategoricalCeterisParibus from '@/plots/CategoricalCeterisParibus.vue' 7 | import SHAPValues from '@/plots/SHAPValues.vue' 8 | import HtmlWidget from '@/plots/HtmlWidget.vue' 9 | import ROC from '@/plots/ROC.vue' 10 | import REC from '@/plots/REC.vue' 11 | import Metrics from '@/plots/Metrics.vue' 12 | import FunnelMeasure from '@/plots/FunnelMeasure.vue' 13 | import Fairness from '@/plots/Fairness.vue' 14 | import SubsetsPerformance from '@/plots/SubsetsPerformance.vue' 15 | import Message from '@/plots/Message.vue' 16 | import DistributionCounts from '@/plots/DistributionCounts.vue' 17 | import DistributionHistogram from '@/plots/DistributionHistogram.vue' 18 | import VariableAgainstAnother from '@/plots/VariableAgainstAnother.vue' 19 | import LinearShapleyDependence from '@/plots/LinearShapleyDependence.vue' 20 | import CategoricalShapleyDependence from '@/plots/CategoricalShapleyDependence.vue' 21 | import ShapleyValuesVariableImportance from '@/plots/ShapleyValuesVariableImportance.vue' 22 | import RegressionFairness from '@/plots/RegressionFairness.vue' 23 | 24 | export default { 25 | plotComponents: { 26 | Breakdown, FeatureImportance, CategoricalDependence, LinearDependence, NumericalCeterisParibus, CategoricalCeterisParibus, SHAPValues, HtmlWidget, ROC, REC, Metrics, FunnelMeasure, Fairness, SubsetsPerformance, Message, DistributionCounts, DistributionHistogram, VariableAgainstAnother, RegressionFairness, LinearShapleyDependence, CategoricalShapleyDependence, ShapleyValuesVariableImportance 27 | }, 28 | canMerge (slot1, slot2) { 29 | if (!slot1 || !slot2 || slot1 === slot2 || slot1.plotType !== slot2.plotType) return false 30 | let type = slot1.plotType 31 | let testSameParamName = paramType => (new Set([...slot1.localParams, ...slot2.localParams].map(params => params[paramType])).size === 1) 32 | let sameVariable = testSameParamName('variable') 33 | let sameObservation = testSameParamName('observation') 34 | if (type === 'PartialDependence' || type === 'AccumulatedDependence' || type === 'CeterisParibus' || type === 'Fairness' || type === 'ShapleyValuesDependence') return sameVariable 35 | if (type === 'FeatureImportance' || type === 'ShapleyValuesVariableImportance') return true 36 | if (type === 'SHAPValues') return sameObservation 37 | if (type === 'ROC') return true 38 | if (type === 'REC') return true 39 | if (type === 'Metrics') return true 40 | if (type === 'FunnelMeasure') return true 41 | if (type === 'SubsetsPerformance') return true 42 | if (type === 'VariableDistribution') return sameVariable 43 | return false 44 | }, 45 | lockableParams: { // for each plotType 46 | Breakdown: ['observation'], 47 | FeatureImportance: [], 48 | PartialDependence: ['variable'], 49 | AccumulatedDependence: ['variable'], 50 | ShapleyValuesDependence: ['variable'], 51 | CeterisParibus: ['variable', 'observation'], 52 | SHAPValues: ['observation'], 53 | Fairness: ['variable'], 54 | VariableDistribution: ['variable'], 55 | VariableAgainstAnother: ['variable'], 56 | ShapleyValuesVariableImportance: [] 57 | }, 58 | getPlotDoc (plotType) { 59 | const docs = { 60 | 'CeterisParibus': 'https://arena.drwhy.ai/docs/guide/observation-level#ceteris-paribus', 61 | 'Breakdown': 'https://arena.drwhy.ai/docs/guide/observation-level#break-down', 62 | 'SHAPValues': 'https://arena.drwhy.ai/docs/guide/observation-level#shapley-values', 63 | 'FeatureImportance': 'https://arena.drwhy.ai/docs/guide/dataset-level#variable-importance', 64 | 'PartialDependence': 'https://arena.drwhy.ai/docs/guide/dataset-level#partial-dependence', 65 | 'AccumulatedDependence': 'https://arena.drwhy.ai/docs/guide/dataset-level#accumulated-dependence', 66 | 'Fairness': 'https://arena.drwhy.ai/docs/guide/fairness#fairness-check', 67 | 'Metrics': 'https://arena.drwhy.ai/docs/guide/model-performance#metrics', 68 | 'ROC': 'https://arena.drwhy.ai/docs/guide/model-performance#receiver-operating-characteristic', 69 | 'REC': 'https://arena.drwhy.ai/docs/guide/model-performance#receiver-error-characteristic', 70 | 'SubsetsPerformance': 'https://arena.drwhy.ai/docs/guide/model-performance#subset-performance', 71 | 'FunnelMeasure': 'https://arena.drwhy.ai/docs/guide/model-performance#funnel-plot', 72 | 'VariableDistribution': 'https://arena.drwhy.ai/docs/guide/eda-charts#variable-distribution', 73 | 'VariableAgainstAnother': 'https://arena.drwhy.ai/docs/guide/eda-charts#variable-against-another' 74 | } 75 | return docs[plotType] || '' 76 | }, 77 | optionsCategories: { 78 | Breakdown: ['Break Down', ['Margins', 'left_margin']], 79 | FeatureImportance: ['Variable Importance', ['Margins', 'left_margin']], 80 | CategoricalDependence: [['Margins', 'left_margin_values']], 81 | LinearDependence: [], 82 | NumericalCeterisParibus: [], 83 | CategoricalCeterisParibus: [['Margins', 'left_margin_values']], 84 | SHAPValues: ['Shapley Values', ['Margins', 'left_margin']], 85 | HtmlWidget: [], 86 | ROC: [], 87 | REC: [], 88 | Metrics: [], 89 | FunnelMeasure: ['Funnel Plot'], 90 | Fairness: [], 91 | SubsetsPerformance: ['Subset Performance'], 92 | Message: [], 93 | DistributionCounts: [['Margins', 'left_margin_values']], 94 | DistributionHistogram: [], 95 | VariableAgainstAnother: [], 96 | LinearShapleyDependence: ['Shapley Dependence'], 97 | CategoricalShapleyDependence: [['Margins', 'left_margin_values']], 98 | ShapleyValuesVariableImportance: ['Variable Importance', ['Margins', 'left_margin']] 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/configuration/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | examples: [ 3 | { 4 | name: 'Warsaw apartments', 5 | url: 'https://gist.githubusercontent.com/piotrpiatyszek/3bd276c312af555c9f19cfd524b52c01/raw/63c6d7dbad270c10cafa8a252cecda21b7abe54c/data.json', 6 | session: 'https://gist.githubusercontent.com/piotrpiatyszek/af2f1c4ad85bc48ca8641d9b31f73dfc/raw/2d832406e63a55ed9741da4e5aedbe07953e731b/session.json' 7 | }, 8 | { 9 | name: 'FIFA Players Value', 10 | url: 'https://gist.githubusercontent.com/piotrpiatyszek/af3191b582ef264d399683c4adc29f98/raw/c3566beed204b9dc73b21daf319a5ff5cc2b253f/data.json', 11 | session: 'https://gist.githubusercontent.com/piotrpiatyszek/e5872fb4c62a2600e30393a66ff3413a/raw/08903b0f185506de85910d74ac163b0a8ac3dd80/session.json' 12 | }, 13 | { 14 | name: 'Employees status classification', 15 | url: 'https://gist.githubusercontent.com/piotrpiatyszek/c7786a137e85695d9183f23f7223ebb6/raw/8163e7b39f0f400a6d4b8f9c1bdffbb5da80fb7d/data.json', 16 | session: 'https://gist.githubusercontent.com/piotrpiatyszek/97fabafba2c20b9f0ab6c2c691eb17cf/raw/a023546e95fbddbe6d6edbe5c9f60f3204452eb7/session.json' 17 | } 18 | ], 19 | url: 'https://arena.drwhy.ai', 20 | // All params used in app 21 | params: ['variable', 'observation', 'model', 'dataset'], 22 | scopes: ['model', 'dataset'], 23 | helpMessages: { 24 | 1: 'Select one or more models to create plots for them', 25 | 2: 'Hold any of generated plots to open it', 26 | 3: 'Change parameters to manipulete plots' 27 | }, 28 | // Plots that are generated after holding a value in SearchDropdown for auxilary params. 29 | searchDropdownPlots: { 30 | variable: { name: 'Partial Dependence', plotType: 'PartialDependence', plotCategory: 'Dataset Level', scope: 'model' }, 31 | observation: { name: 'Break Down', plotType: 'Breakdown', plotCategory: 'Observation Level', scope: 'model' } 32 | }, 33 | githubClientId_https: 'd7d96eec80f68c16954b', 34 | githubClientId_http: '6d8ede55c3c23b0b1a0f', 35 | githubAuthorizeServer: 'https://arena.mini.pw.edu.pl/github', 36 | telemetryServer: 'https://arena.mini.pw.edu.pl/telemetry' 37 | } 38 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import store from './store/index.js' 4 | import VueResource from 'vue-resource' 5 | import { loadFontAwesome } from './utils/fontAwesomeLoader.js' 6 | 7 | loadFontAwesome() 8 | 9 | const searchParams = new URLSearchParams(window.location.search) 10 | if ([...searchParams.entries()].length === 0) { 11 | window.location.replace('https://arena.drwhy.ai/docs') 12 | } 13 | 14 | let appendURL = searchParams.get('append') 15 | let clearStorage = searchParams.get('clear_storage') 16 | 17 | if (clearStorage) { 18 | localStorage.clear() 19 | document.write('Local Storage is now clear') 20 | } else if (appendURL) { 21 | localStorage.setItem('append', '') 22 | localStorage.setItem('append', appendURL) 23 | document.write('You can close this window') 24 | } else { 25 | Vue.config.productionTip = false 26 | Vue.use(VueResource) 27 | 28 | new Vue({ 29 | render: h => h(App), 30 | store 31 | }).$mount('#app') 32 | } 33 | -------------------------------------------------------------------------------- /src/plots/CategoricalCeterisParibus.vue: -------------------------------------------------------------------------------- 1 | 6 | 128 | 130 | -------------------------------------------------------------------------------- /src/plots/CategoricalDependence.vue: -------------------------------------------------------------------------------- 1 | 6 | 127 | 129 | -------------------------------------------------------------------------------- /src/plots/CategoricalShapleyDependence.vue: -------------------------------------------------------------------------------- 1 | 6 | 120 | 122 | -------------------------------------------------------------------------------- /src/plots/DistributionCounts.vue: -------------------------------------------------------------------------------- 1 | 9 | 168 | 186 | -------------------------------------------------------------------------------- /src/plots/HtmlWidget.vue: -------------------------------------------------------------------------------- 1 | 6 | 22 | 34 | -------------------------------------------------------------------------------- /src/plots/LinearDependence.vue: -------------------------------------------------------------------------------- 1 | 6 | 88 | 90 | -------------------------------------------------------------------------------- /src/plots/LinearShapleyDependence.vue: -------------------------------------------------------------------------------- 1 | 6 | 100 | 102 | -------------------------------------------------------------------------------- /src/plots/Message.vue: -------------------------------------------------------------------------------- 1 | 7 | 25 | 32 | -------------------------------------------------------------------------------- /src/plots/Metrics.vue: -------------------------------------------------------------------------------- 1 | 7 | 137 | 167 | -------------------------------------------------------------------------------- /src/plots/NumericalCeterisParibus.vue: -------------------------------------------------------------------------------- 1 | 6 | 110 | 112 | -------------------------------------------------------------------------------- /src/plots/REC.vue: -------------------------------------------------------------------------------- 1 | 6 | 84 | 86 | -------------------------------------------------------------------------------- /src/plots/ROC.vue: -------------------------------------------------------------------------------- 1 | 6 | 112 | 114 | -------------------------------------------------------------------------------- /src/plots/RegressionFairness.vue: -------------------------------------------------------------------------------- 1 | 16 | 89 | 141 | -------------------------------------------------------------------------------- /src/store/datasources/dataSourceCommon.js: -------------------------------------------------------------------------------- 1 | import config from '@/configuration/config.js' 2 | import streams from '@/utils/streams.js' 3 | import format from '@/utils/format.js' 4 | import Vue from 'vue' 5 | 6 | const state = () => { 7 | return { 8 | sources: [] 9 | } 10 | } 11 | 12 | const getters = { 13 | translateAttributes: (state, getters) => (uuid, paramType, attributes) => { 14 | /* eslint-disable arena/no-hardcode-param-types */ 15 | if (attributes && paramType === 'observation') { 16 | attributes = Object.entries(attributes).reduce((acu, [variableName, variableValue]) => { 17 | return { ...acu, [getters.translateIfPossible(uuid, 'variable', variableName)]: variableValue } 18 | }, {}) 19 | } 20 | /* eslint-enable arena/no-hardcode-param-types */ 21 | return attributes 22 | }, 23 | availableParams (state, getters) { 24 | return getters.sources 25 | .reduce((acu, s) => { 26 | config.params.forEach(paramType => { 27 | acu[paramType] = [...acu[paramType], ...(getters.translatedAvailableParams[s.uuid][paramType] || [])] 28 | }) 29 | return acu 30 | }, streams.createObjectWithArrays(config.params)) 31 | }, 32 | translatedAvailableParams (state, getters, rootState, rootGetters) { 33 | return state.sources.reduce((acu, s) => { 34 | acu[s.uuid] = {} 35 | config.params.forEach(paramType => { 36 | let translation = (rootGetters.translations[s.uuid] || {})[paramType] || {} 37 | acu[s.uuid][paramType] = s.availableParams[paramType].map(paramName => translation[paramName] || paramName) 38 | }) 39 | return acu 40 | }, {}) 41 | }, 42 | translateIfPossible: (state, getters, rootState, rootGetters) => (uuid, paramType, paramName) => { 43 | return ((rootGetters.translations[uuid] || {})[paramType] || {})[paramName] || paramName 44 | }, 45 | // Published sources 46 | sources (state, getters, rootState, rootGetters) { 47 | return state.sources.filter(s => rootGetters.canPublishParams(s.uuid)) 48 | }, 49 | validateParams: (state, getters, rootState, rootGetters) => (source, fullParams) => { 50 | if (!rootGetters.canPublishParams(source.uuid)) return {} 51 | return config.params.reduce((acu, paramType) => { 52 | acu[paramType] = getters.translatedAvailableParams[source.uuid][paramType].includes(fullParams[paramType]) ? fullParams[paramType] : null 53 | return acu 54 | }, {}) 55 | }, 56 | translateBackParams: (state, getters, rootState, rootGetters) => (source, fullParams) => { 57 | if (!rootGetters.canPublishParams(source.uuid)) return {} 58 | return config.params.reduce((acu, paramType) => { 59 | /* eslint-disable arena/no-hardcode-param-types */ 60 | if (format.isCustomParam(fullParams[paramType]) && paramType === 'observation') { 61 | acu[paramType] = {} 62 | let parsed = JSON.parse(fullParams[paramType]) 63 | for (let variable in parsed) { 64 | let index = getters.translatedAvailableParams[source.uuid]['variable'].indexOf(variable) 65 | // if (index === -1) return { ...acu, [paramType]: null } 66 | if (index !== -1) acu[paramType][source.availableParams['variable'][index]] = parsed[variable] 67 | } 68 | acu[paramType] = JSON.stringify(acu[paramType]) 69 | } else { 70 | let index = getters.translatedAvailableParams[source.uuid][paramType].indexOf(fullParams[paramType]) 71 | acu[paramType] = index === -1 ? null : source.availableParams[paramType][index] 72 | } 73 | /* eslint-enable arena/no-hardcode-param-types */ 74 | return acu 75 | }, {}) 76 | }, 77 | translateParams: (state, getters) => (source, params) => { 78 | return Object.keys(params).reduce((acu, paramType) => { 79 | acu[paramType] = getters.translateIfPossible(source.uuid, paramType, params[paramType]) 80 | return acu 81 | }, {}) 82 | } 83 | } 84 | 85 | const mutations = { 86 | addSource (state, source) { 87 | if (!source.timestamp) Vue.set(source, 'timestamp', new Date().getTime()) 88 | Vue.set(state, 'sources', [...state.sources, source]) 89 | }, 90 | expandSource (state, source) { 91 | if (!source.uuid) return 92 | let original = state.sources.find(s => s.uuid === source.uuid) 93 | if (!original) return 94 | state.sources = [...state.sources.filter(s => s.uuid !== source.uuid), { ...original, ...source }] 95 | }, 96 | removeSource (state, source) { 97 | Vue.set(state, 'sources', state.sources.filter(s => s !== source && s.uuid !== source)) 98 | }, 99 | clearSources (state) { 100 | Vue.set(state, 'sources', []) 101 | } 102 | } 103 | 104 | export default { 105 | getNew () { 106 | return { 107 | state: state(), 108 | getters: { ...getters }, 109 | mutations: { ...mutations }, 110 | namespaced: false 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/store/datasources/jsonDatasource.js: -------------------------------------------------------------------------------- 1 | import uuidGenerator from 'uuid/v4' 2 | import Ajv from 'ajv' 3 | import streams from '@/utils/streams.js' 4 | import format from '@/utils/format.js' 5 | import config from '@/configuration/config.js' 6 | import dataSourceCommon from '@/store/datasources/dataSourceCommon.js' 7 | 8 | const ajv = new Ajv() 9 | ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')) 10 | /* eslint-disable camelcase */ 11 | const validator_1_0_0 = ajv.compile(require('@/store/schemas/data.schema.json')) 12 | const validator_1_1_0 = ajv.compile(require('@/store/schemas/data-1.1.0.schema.json')) 13 | const validator_1_2_0 = ajv.compile(require('@/store/schemas/data-1.2.0.schema.json')) 14 | /* eslint-enable camelcase */ 15 | 16 | const state = {} 17 | 18 | const getters = { 19 | getAvailableSlots: (state, getters) => (fullParams, scope) => { 20 | return getters.sources.map(source => { 21 | let params = getters.translateBackParams(source, fullParams) 22 | if (params[scope] === null) return [] 23 | 24 | return source.plotsData.filter(d => { 25 | // for each single plot data we check if params of a plot are same in fullParams 26 | return Object.keys(d.params).reduce((acc, paramType) => { 27 | return acc && d.params[paramType] === params[paramType] 28 | }, true) && Object.keys(d.params).includes(scope) 29 | }).map(d => { 30 | return { 31 | localParams: [{ [scope]: fullParams[scope] }], 32 | name: d.name, 33 | plotType: d.plotType, 34 | plotCategory: d.plotCategory 35 | } 36 | }) 37 | }).flat() 38 | } 39 | } 40 | 41 | const mutations = {} 42 | 43 | // Checks if names array have unique elements, after removing special characters and in lower case 44 | const isUnique = (array) => { 45 | return (new Set(array.map(format.simplify))).size === array.length 46 | } 47 | 48 | const actions = { 49 | loadData ({ state, commit, dispatch, rootGetters }, { data, src, uuid }) { 50 | let params = null 51 | let attributes = streams.createObjectWithArrays(config.params) 52 | if (validator_1_0_0(data)) { 53 | params = config.params.reduce((acu, paramType) => { 54 | acu[paramType] = data[paramType + 's'] || [] 55 | return acu 56 | }, {}) 57 | } else if (validator_1_1_0(data)) { 58 | params = data.availableParams 59 | } else if (validator_1_2_0(data)) { 60 | params = data.availableParams 61 | attributes = data.paramsAttributes 62 | } else { 63 | return false 64 | } 65 | if (!config.params.reduce((acu, p) => acu && isUnique(params[p]), true)) return false 66 | let source = { 67 | availableParams: params, 68 | uuid: uuid || uuidGenerator(), 69 | attributes, 70 | address: src, 71 | plotsData: [] 72 | } 73 | data.data.forEach(d => { 74 | let obj = { 75 | plotType: d.plotType, 76 | plotCategory: d.plotCategory ? d.plotCategory : 'Other', 77 | plotComponent: d.plotComponent, 78 | name: d.name ? d.name : format.firstCharUpper(d.plotType), 79 | plotData: d.data, 80 | params: d.params 81 | } 82 | 83 | if (Object.values(obj).includes(undefined)) return 84 | source.plotsData.push(obj) 85 | }) 86 | Object.freeze(source.plotData) 87 | Object.freeze(source.availableParams) 88 | Object.freeze(source.attributes) 89 | let waitingParams = { 90 | uuid: source.uuid, 91 | params: source.availableParams 92 | } 93 | commit('addWaitingParams', waitingParams, { root: true }) 94 | commit('addSource', source) 95 | return true 96 | }, 97 | query ({ state, getters }, { params, plotType }) { 98 | for (let source of getters.sources) { 99 | // translate params back to original names 100 | let queryParams = getters.translateBackParams(source, params) 101 | let plotData = source.plotsData.find(d => { 102 | return d.plotType === plotType && 103 | Object.keys(d.params).reduce((acc, paramType) => acc && d.params[paramType] === queryParams[paramType], true) 104 | }) 105 | if (plotData) return { params: getters.translateParams(source, plotData.params), ...plotData } 106 | } 107 | return null 108 | }, 109 | init () {}, 110 | getAttributes ({ state, commit, getters }, { paramValue, paramType }) { 111 | for (let source of getters.sources) { 112 | let index = getters.translatedAvailableParams[source.uuid][paramType].indexOf(paramValue) 113 | if (index !== -1) { 114 | /* eslint-disable-next-line arena/no-hardcode-param-types */ 115 | if (paramType === 'variable') return null 116 | let attrObject = source.attributes[paramType].reduce((acu, a) => { 117 | acu[a.name] = a.values[index] 118 | return acu 119 | }, {}) 120 | if (Object.keys(attrObject).length > 0) return getters.translateAttributes(source.uuid, paramType, attrObject) 121 | } 122 | } 123 | return null 124 | } 125 | } 126 | 127 | export default { 128 | state, 129 | getters, 130 | mutations, 131 | actions, 132 | modules: { 133 | dataSourceCommon: dataSourceCommon.getNew() 134 | }, 135 | namespaced: true 136 | } 137 | -------------------------------------------------------------------------------- /src/store/datasources/peerDatasource.js: -------------------------------------------------------------------------------- 1 | import uuidGenerator from 'uuid/v4' 2 | import Vue from 'vue' 3 | import config from '@/configuration/config.js' 4 | import streams from '@/utils/streams.js' 5 | import dataSourceCommon from '@/store/datasources/dataSourceCommon.js' 6 | import Ajv from 'ajv' 7 | 8 | let requestCounter = 1 9 | let getRequestId = () => { 10 | return requestCounter++ 11 | } 12 | const state = { 13 | requestPromises: [], 14 | availableSlots: [] 15 | } 16 | 17 | const ajv = new Ajv() 18 | ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')) 19 | /* eslint-disable camelcase */ 20 | const validatorSession_1_0_0 = ajv.compile(require('@/store/schemas/session.schema.json')) 21 | const validatorSession_1_1_0 = ajv.compile(require('@/store/schemas/session-1.1.0.schema.json')) 22 | /* eslint-enable camelcase */ 23 | 24 | const getters = { 25 | getAvailableSlots: (state, getters) => (fullParams, scope) => { 26 | return getters.sources.map(source => { 27 | let params = getters.translateBackParams(source, fullParams) 28 | if (!params[scope]) return [] 29 | 30 | return (getters.availableSlots.find(s => s.uuid === source.uuid && s.scope === scope) || {}).slots || [] 31 | }).flat() 32 | }, 33 | availableSlots (state) { 34 | return state.availableSlots 35 | }, 36 | getRequestPromise: (state) => (id) => { 37 | return state.requestPromises.find(d => d.id === id) 38 | } 39 | } 40 | 41 | const mutations = { 42 | addRequestPromise (state, data) { 43 | state.requestPromises = [...state.requestPromises, data] 44 | }, 45 | removeRequestPromise (state, id) { 46 | state.requestPromises = state.requestPromises.filter(d => d.id !== id) 47 | }, 48 | clearAvailableSlots (state) { 49 | Vue.set(state, 'availableSlots', []) 50 | }, 51 | addAvailableSlots (state, slots) { 52 | Vue.set(state, 'availableSlots', [...state.availableSlots.filter(s => s.uuid !== slots.uuid || s.scope !== slots.scope), slots]) 53 | }, 54 | addToCache (state, { source, slotData }) { 55 | Vue.set(source, 'cache', [...source.cache, slotData]) 56 | } 57 | } 58 | 59 | const actions = { 60 | doRequest ({ commit, dispatch, rootGetters }, { request, conn }) { 61 | request = { id: getRequestId(), ...request } 62 | let promise = new Promise((resolve, reject) => { 63 | commit('addRequestPromise', { id: request.id, resolve, reject }) 64 | conn.send(request) 65 | }) 66 | return promise 67 | }, 68 | async loadData ({ state, commit, dispatch, rootGetters, getters }, { data, uuid }) { 69 | if (data.type !== 'peer' || !data.peerId) return false 70 | if (!rootGetters.peer) await dispatch('initPeer', { root: true }) 71 | let conn = rootGetters.peer.connect(data.peerId) 72 | conn.on('open', () => { 73 | let source = { 74 | availableParams: streams.createObjectWithArrays(config.params), 75 | uuid: uuid || uuidGenerator(), 76 | address: null, 77 | cache: [], 78 | conn 79 | } 80 | commit('addSource', source) 81 | conn.on('close', () => commit('removeSource', source.uuid)) 82 | conn.on('error', e => { 83 | console.error(e) 84 | commit('removeSource', source.uuid) 85 | }) 86 | conn.on('data', async d => { 87 | if (validatorSession_1_1_0(d) || validatorSession_1_0_0(d)) { 88 | d.sources = [] 89 | let mySource = getters.sources.find(s => s.uuid === source.uuid) 90 | await dispatch('importSession', d, { root: true }) 91 | commit('addSource', mySource) 92 | return 93 | } 94 | let promise = getters.getRequestPromise(d.id) 95 | if (!promise) return 96 | if (d.error) promise.reject(d.response) 97 | else promise.resolve(d.response) 98 | commit('removeRequestPromise', d.id) 99 | }) 100 | dispatch('doRequest', { conn, request: { type: 'getParams' } }).then(params => { 101 | commit('expandSource', { uuid: source.uuid, availableParams: params }) 102 | let waitingParams = { 103 | uuid: source.uuid, 104 | params 105 | } 106 | commit('addWaitingParams', waitingParams, { root: true }) 107 | }) 108 | }) 109 | return true 110 | }, 111 | query ({ state, commit, getters, dispatch }, { params, plotType }) { 112 | let promises = [] 113 | for (let source of getters.sources) { 114 | // translate params back to original names 115 | let queryParams = getters.translateBackParams(source, params) 116 | let cached = source.cache.find(d => { 117 | return d.plotType === plotType && 118 | Object.keys(d.params).reduce((acc, paramType) => acc && d.params[paramType] === queryParams[paramType], true) 119 | }) 120 | if (cached) return { ...cached, params: getters.translateParams(source, cached.params) } 121 | 122 | let promise = dispatch('doRequest', { conn: source.conn, request: { type: 'getPlot', params: queryParams, plotType } }).then(slotData => { 123 | if (slotData) commit('addToCache', { source, slotData }) 124 | else return null 125 | return { ...slotData, params: getters.translateParams(source, slotData.params) } 126 | }) 127 | promises.push(promise) 128 | } 129 | return Promise.all(promises).then(list => list.find(x => x)) 130 | }, 131 | globalParamsUpdated ({ commit, rootGetters, getters, dispatch }) { 132 | commit('clearAvailableSlots') 133 | getters.sources.forEach(source => { 134 | config.scopes.forEach(scope => { 135 | dispatch('doRequest', { conn: source.conn, request: { type: 'getAvailableSlots', params: rootGetters.globalParams, scope } }).then(slots => { 136 | commit('addAvailableSlots', { uuid: source.uuid, scope, slots }) 137 | }) 138 | }) 139 | }) 140 | }, 141 | getAttributes ({ state, commit, getters }, { paramValue, paramType }) { 142 | // TODO 143 | return null 144 | }, 145 | init () {} 146 | } 147 | 148 | export default { 149 | state, 150 | getters, 151 | mutations, 152 | actions, 153 | modules: { 154 | dataSourceCommon: dataSourceCommon.getNew() 155 | }, 156 | namespaced: true 157 | } 158 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | /* Vuex Modules */ 5 | import params from '@/store/params.js' 6 | import slots from '@/store/slots.js' 7 | import dataSources from '@/store/dataSources.js' 8 | import miscellaneous from '@/store/miscellaneous.js' 9 | import telemetry from '@/store/telemetry.js' 10 | import notifications from '@/store/notifications.js' 11 | 12 | Vue.use(Vuex) 13 | 14 | const actions = { 15 | init ({ commit, dispatch }) { 16 | dispatch('initMiscellaneous') 17 | dispatch('initDataSources') 18 | dispatch('initTelemetry') 19 | } 20 | } 21 | 22 | export default new Vuex.Store({ 23 | modules: { 24 | params, 25 | slots, 26 | dataSources, 27 | miscellaneous, 28 | telemetry, 29 | notifications 30 | }, 31 | strict: false, 32 | actions 33 | }) 34 | -------------------------------------------------------------------------------- /src/store/miscellaneous.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import OptionsSchemas from '@/configuration/OptionsSchemas.js' 3 | 4 | const state = { 5 | options: {}, 6 | closedElements: [], 7 | annotationsActive: false, 8 | annotationsColor: '#371ea8', 9 | annotations: [] 10 | } 11 | 12 | const getters = { 13 | getOption: (state, getters) => (name) => { 14 | if (state.options[name] !== undefined) return state.options[name] 15 | return (OptionsSchemas.find(s => s.name === name) || {}).default 16 | }, 17 | allOptions (state, getters) { 18 | return OptionsSchemas.map(s => { 19 | return { name: s.name, value: getters.getOption(s.name) } 20 | }) 21 | }, 22 | isElementClosed: (state, getters) => (name) => { 23 | return !!state.closedElements.find(f => f === name) 24 | }, 25 | annotationsActive (state) { 26 | return state.annotationsActive 27 | }, 28 | annotationsColor (state) { 29 | return state.annotationsColor 30 | }, 31 | getAnnotations: (state) => (pageNumber) => { 32 | return Object.assign({ paths: [], pageNumber }, state.annotations.find(a => a.pageNumber === pageNumber)) 33 | }, 34 | annotations (state) { 35 | return state.annotations 36 | }, 37 | lastPageAnnotations (state) { 38 | return Math.max.call(null, ...state.annotations.map(a => a.pageNumber)) 39 | } 40 | } 41 | 42 | const mutations = { 43 | setOption (state, { name, value }) { 44 | Vue.set(state.options, name, value) 45 | }, 46 | closeElement (state, name) { 47 | state.closedElements = [...state.closedElements.filter(e => e !== name), name] 48 | localStorage.setItem('closedElements', JSON.stringify(state.closedElements)) 49 | }, 50 | setAnnotationsActive (state, active) { 51 | Vue.set(state, 'annotationsActive', !!active) 52 | }, 53 | setAnnotationsColor (state, color) { 54 | Vue.set(state, 'annotationsColor', color) 55 | }, 56 | setAnnotations (state, annotations) { 57 | Vue.set(state, 'annotations', [...state.annotations.filter(a => a.pageNumber !== annotations.pageNumber), annotations]) 58 | }, 59 | loadAnnotations (state, annotations) { 60 | Vue.set(state, 'annotations', annotations) 61 | }, 62 | clearOptions (state) { 63 | Vue.set(state, 'options', {}) 64 | } 65 | } 66 | 67 | const actions = { 68 | initMiscellaneous ({ commit, dispatch }) { 69 | try { 70 | let closed = JSON.parse(localStorage.getItem('closedElements')) 71 | if (closed) { 72 | if (!Array.isArray(closed)) throw new Error('value is not an array') 73 | closed.forEach(name => { 74 | if (typeof name === 'string' || name instanceof String) commit('closeElement', name) 75 | }) 76 | } 77 | } catch (e) { 78 | console.error('Failed to read closedElements from localStorage', e) 79 | } 80 | } 81 | } 82 | 83 | export default { 84 | state, 85 | getters, 86 | mutations, 87 | actions, 88 | namespaced: false 89 | } 90 | -------------------------------------------------------------------------------- /src/store/notifications.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import uuid from 'uuid/v4' 3 | 4 | const state = { 5 | notifications: [] 6 | } 7 | 8 | const getters = { 9 | notifications (state) { 10 | return state.notifications 11 | } 12 | } 13 | 14 | const mutations = { 15 | addNotification (state, { type, text, id }) { 16 | Vue.set(state, 'notifications', [...state.notifications, { type, text, id }]) 17 | }, 18 | delNotification (state, id) { 19 | Vue.set(state, 'notifications', state.notifications.filter(x => x.id !== id)) 20 | } 21 | } 22 | 23 | const actions = { 24 | createNotification ({ commit }, { type, text }) { 25 | const id = uuid() 26 | commit('addNotification', { id, type, text }) 27 | return id 28 | } 29 | } 30 | 31 | export default { 32 | state, 33 | getters, 34 | mutations, 35 | actions, 36 | namespaced: false 37 | } 38 | -------------------------------------------------------------------------------- /src/store/schemas/arenarLive-1.1.0.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "title": "Arenar live root response", 4 | "description": "Data structure returned by arenar live server at /", 5 | "type": "object", 6 | "properties": { 7 | "version": { 8 | "type": "string", 9 | "const": "1.1.0" 10 | }, 11 | "api": { 12 | "type": "string", 13 | "const": "arenar_api" 14 | }, 15 | "timestamp": { 16 | "type": "number" 17 | }, 18 | "availableParams": { 19 | "type": "object", 20 | "properties": { 21 | "variable": { 22 | "type": "array", 23 | "title": "Array of variables' names used in this data file", 24 | "items": { 25 | "type": "string" 26 | }, 27 | "uniqueItems": true 28 | }, 29 | "model": { 30 | "type": "array", 31 | "title": "Array of models' names used in this data file", 32 | "items": { 33 | "type": "string" 34 | }, 35 | "uniqueItems": true 36 | }, 37 | "observation": { 38 | "type": "array", 39 | "title": "Array of observations' names used in this data file", 40 | "items": { 41 | "type": "string" 42 | }, 43 | "uniqueItems": true 44 | }, 45 | "dataset": { 46 | "type": "array", 47 | "title": "Array of observations' names used in this data file", 48 | "items": { 49 | "type": "string" 50 | }, 51 | "uniqueItems": true 52 | } 53 | }, 54 | "required": ["variable", "model", "observation", "dataset"] 55 | }, 56 | "availablePlots": { 57 | "type": "array", 58 | "title": "Array of plots that server can produce", 59 | "items": { 60 | "type": "object", 61 | "title": "Object with basic properties of each plot type", 62 | "properties": { 63 | "plotType": { 64 | "type": "string", 65 | "title": "Type of plot", 66 | "description": "Represents what plot presents", 67 | "examples": ["FeatureImportance", "PartialDependency"] 68 | }, 69 | "plotCategory": { 70 | "type": "string", 71 | "title": "Category of plot", 72 | "examples": ["Local", "Global"] 73 | }, 74 | "name": { 75 | "type": "string", 76 | "title": "Title of plot" 77 | }, 78 | "requiredParams": { 79 | "type": "array", 80 | "title": "Params that must be provided to calculate plot", 81 | "items": { 82 | "type": "string" 83 | } 84 | } 85 | }, 86 | "required": ["plotType", "plotCategory", "requiredParams", "name"] 87 | } 88 | } 89 | }, 90 | "required": ["version", "api", "timestamp", "availableParams", "availablePlots"] 91 | } 92 | -------------------------------------------------------------------------------- /src/store/schemas/arenarLive-1.2.0.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "title": "Arenar live root response", 4 | "description": "Data structure returned by arenar live server at /", 5 | "type": "object", 6 | "properties": { 7 | "version": { 8 | "type": "string", 9 | "const": "1.2.0" 10 | }, 11 | "api": { 12 | "type": "string", 13 | "const": "arenar_api" 14 | }, 15 | "timestamp": { 16 | "type": "number" 17 | }, 18 | "options": { 19 | "type": "object", 20 | "properties": { 21 | "customParams": { "type": "boolean" }, 22 | "attributes": { "type": "boolean" } 23 | }, 24 | "required": ["customParams", "attributes"] 25 | }, 26 | "availableParams": { 27 | "type": "object", 28 | "properties": { 29 | "variable": { 30 | "type": "array", 31 | "title": "Array of variables' names used in this data file", 32 | "items": { 33 | "type": "string" 34 | }, 35 | "uniqueItems": true 36 | }, 37 | "model": { 38 | "type": "array", 39 | "title": "Array of models' names used in this data file", 40 | "items": { 41 | "type": "string" 42 | }, 43 | "uniqueItems": true 44 | }, 45 | "observation": { 46 | "type": "array", 47 | "title": "Array of observations' names used in this data file", 48 | "items": { 49 | "type": "string" 50 | }, 51 | "uniqueItems": true 52 | }, 53 | "dataset": { 54 | "type": "array", 55 | "title": "Array of datasets' names used in this data file", 56 | "items": { 57 | "type": "string" 58 | }, 59 | "uniqueItems": true 60 | } 61 | }, 62 | "required": ["variable", "model", "observation", "dataset"] 63 | }, 64 | "availablePlots": { 65 | "type": "array", 66 | "title": "Array of plots that server can produce", 67 | "items": { 68 | "type": "object", 69 | "title": "Object with basic properties of each plot type", 70 | "properties": { 71 | "plotType": { 72 | "type": "string", 73 | "title": "Type of plot", 74 | "description": "Represents what plot presents", 75 | "examples": ["FeatureImportance", "PartialDependency"] 76 | }, 77 | "plotCategory": { 78 | "type": "string", 79 | "title": "Category of plot", 80 | "examples": ["Local", "Global"] 81 | }, 82 | "name": { 83 | "type": "string", 84 | "title": "Title of plot" 85 | }, 86 | "requiredParams": { 87 | "type": "array", 88 | "title": "Params that must be provided to calculate plot", 89 | "items": { 90 | "type": "string" 91 | } 92 | } 93 | }, 94 | "required": ["plotType", "plotCategory", "requiredParams", "name"] 95 | } 96 | } 97 | }, 98 | "required": ["version", "api", "timestamp", "options", "availableParams", "availablePlots"] 99 | } 100 | -------------------------------------------------------------------------------- /src/store/schemas/arenarLive.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "title": "Arenar live root response", 4 | "description": "Data structure returned by arenar live server at /", 5 | "type": "object", 6 | "properties": { 7 | "version": { 8 | "type": "string", 9 | "const": "1.0.0" 10 | }, 11 | "api": { 12 | "type": "string", 13 | "const": "arenar_api" 14 | }, 15 | "timestamp": { 16 | "type": "number" 17 | }, 18 | "models": { 19 | "type": "array", 20 | "title": "Array of models' names used in this data file", 21 | "items": { 22 | "type": "string" 23 | }, 24 | "uniqueItems": true 25 | }, 26 | "variables": { 27 | "type": "array", 28 | "title": "Array of variables' names used in this data file", 29 | "items": { 30 | "type": "string" 31 | }, 32 | "uniqueItems": true 33 | }, 34 | "observations": { 35 | "type": "array", 36 | "title": "Array of observations' names used in this data file", 37 | "items": { 38 | "type": "string" 39 | }, 40 | "uniqueItems": true 41 | }, 42 | "availablePlots": { 43 | "type": "array", 44 | "title": "Array of plots that server can produce", 45 | "items": { 46 | "type": "object", 47 | "title": "Object with basic properties of each plot type", 48 | "properties": { 49 | "plotType": { 50 | "type": "string", 51 | "title": "Type of plot", 52 | "description": "Represents what plot presents", 53 | "examples": ["FeatureImportance", "PartialDependency"] 54 | }, 55 | "plotCategory": { 56 | "type": "string", 57 | "title": "Category of plot", 58 | "examples": ["Local", "Global"] 59 | }, 60 | "name": { 61 | "type": "string", 62 | "title": "Title of plot" 63 | }, 64 | "requiredParams": { 65 | "type": "array", 66 | "title": "Params that must be provided to calculate plot", 67 | "items": { 68 | "type": "string" 69 | } 70 | } 71 | }, 72 | "required": ["plotType", "plotCategory", "requiredParams", "name"] 73 | } 74 | } 75 | }, 76 | "required": ["version", "api", "timestamp", "models", "observations", "variables", "availablePlots"] 77 | } 78 | -------------------------------------------------------------------------------- /src/store/schemas/data-1.1.0.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "title": "Data file", 4 | "description": "Main data structure, that should be uploaded to application as .json file", 5 | "type": "object", 6 | "properties": { 7 | "version": { 8 | "type": "string", 9 | "const": "1.1.0" 10 | }, 11 | "availableParams": { 12 | "type": "object", 13 | "properties": { 14 | "variable": { 15 | "type": "array", 16 | "title": "Array of variables' names used in this data file", 17 | "items": { 18 | "type": "string" 19 | }, 20 | "uniqueItems": true 21 | }, 22 | "model": { 23 | "type": "array", 24 | "title": "Array of models' names used in this data file", 25 | "items": { 26 | "type": "string" 27 | }, 28 | "uniqueItems": true 29 | }, 30 | "observation": { 31 | "type": "array", 32 | "title": "Array of observations' names used in this data file", 33 | "items": { 34 | "type": "string" 35 | }, 36 | "uniqueItems": true 37 | }, 38 | "dataset": { 39 | "type": "array", 40 | "title": "Array of observations' names used in this data file", 41 | "items": { 42 | "type": "string" 43 | }, 44 | "uniqueItems": true 45 | } 46 | }, 47 | "required": ["variable", "model", "observation", "dataset"] 48 | }, 49 | "data": { 50 | "type": "array", 51 | "title": "Array of plots data", 52 | "items": { 53 | "type": "object", 54 | "title": "Single plot object", 55 | "properties": { 56 | "plotComponent": { 57 | "type": "string", 58 | "title": "Name of vue component to plot this object", 59 | "description": "Represents how the plot is build", 60 | "examples": ["FeatureImportance", "LinearPartialDependency", "CategoricalPartialDependency"] 61 | }, 62 | "plotType": { 63 | "type": "string", 64 | "title": "Type of plot", 65 | "description": "Represents what plot presents", 66 | "examples": ["FeatureImportance", "PartialDependency"] 67 | }, 68 | "plotCategory": { 69 | "type": "string", 70 | "title": "Category of plot", 71 | "examples": ["Local", "Global"] 72 | }, 73 | "name": { 74 | "type": "string", 75 | "title": "Title of plot" 76 | }, 77 | "params": { 78 | "type": "object", 79 | "title": "Plot params", 80 | "description": "Represents what the data is related to.", 81 | "properties": { 82 | "model": { 83 | "type": "string" 84 | }, 85 | "observation": { 86 | "type": "string" 87 | }, 88 | "variable": { 89 | "type": "string" 90 | } 91 | } 92 | }, 93 | "data": { 94 | "type": "object", 95 | "title": "Plot traces", 96 | "description": "This object should be formatted for specified plotComponent" 97 | } 98 | }, 99 | "required": ["plotType", "plotComponent", "plotCategory", "params", "name", "data"] 100 | } 101 | } 102 | }, 103 | "required": ["version", "availableParams", "data"] 104 | } 105 | -------------------------------------------------------------------------------- /src/store/schemas/data-1.2.0.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "title": "Data file", 4 | "description": "Main data structure, that should be uploaded to application as .json file", 5 | "type": "object", 6 | "properties": { 7 | "version": { 8 | "type": "string", 9 | "const": "1.2.0" 10 | }, 11 | "availableParams": { 12 | "type": "object", 13 | "properties": { 14 | "variable": { 15 | "type": "array", 16 | "title": "Array of variables' names used in this data file", 17 | "items": { 18 | "type": "string" 19 | }, 20 | "uniqueItems": true 21 | }, 22 | "model": { 23 | "type": "array", 24 | "title": "Array of models' names used in this data file", 25 | "items": { 26 | "type": "string" 27 | }, 28 | "uniqueItems": true 29 | }, 30 | "observation": { 31 | "type": "array", 32 | "title": "Array of observations' names used in this data file", 33 | "items": { 34 | "type": "string" 35 | }, 36 | "uniqueItems": true 37 | }, 38 | "dataset": { 39 | "type": "array", 40 | "title": "Array of observations' names used in this data file", 41 | "items": { 42 | "type": "string" 43 | }, 44 | "uniqueItems": true 45 | } 46 | }, 47 | "required": ["variable", "model", "observation", "dataset"] 48 | }, 49 | "paramsAttributes": { 50 | "type": "object", 51 | "properties": { 52 | "variable": { 53 | "type": "array", 54 | "items": { 55 | "type": "object", 56 | "properties": { 57 | "name": { 58 | "type": "string" 59 | }, 60 | "values": { 61 | "type": "array" 62 | } 63 | } 64 | } 65 | }, 66 | "model": { 67 | "type": "array", 68 | "items": { 69 | "type": "object", 70 | "properties": { 71 | "name": { 72 | "type": "string" 73 | }, 74 | "values": { 75 | "type": "array" 76 | } 77 | } 78 | } 79 | }, 80 | "observation": { 81 | "type": "array", 82 | "items": { 83 | "type": "object", 84 | "properties": { 85 | "name": { 86 | "type": "string" 87 | }, 88 | "values": { 89 | "type": "array" 90 | } 91 | } 92 | } 93 | }, 94 | "dataset": { 95 | "type": "array", 96 | "items": { 97 | "type": "object", 98 | "properties": { 99 | "name": { 100 | "type": "string" 101 | }, 102 | "values": { 103 | "type": "array" 104 | } 105 | } 106 | } 107 | } 108 | }, 109 | "required": ["variable", "model", "observation", "dataset"] 110 | }, 111 | "data": { 112 | "type": "array", 113 | "title": "Array of plots data", 114 | "items": { 115 | "type": "object", 116 | "title": "Single plot object", 117 | "properties": { 118 | "plotComponent": { 119 | "type": "string", 120 | "title": "Name of vue component to plot this object", 121 | "description": "Represents how the plot is build", 122 | "examples": ["FeatureImportance", "LinearPartialDependency", "CategoricalPartialDependency"] 123 | }, 124 | "plotType": { 125 | "type": "string", 126 | "title": "Type of plot", 127 | "description": "Represents what plot presents", 128 | "examples": ["FeatureImportance", "PartialDependency"] 129 | }, 130 | "plotCategory": { 131 | "type": "string", 132 | "title": "Category of plot", 133 | "examples": ["Local", "Global"] 134 | }, 135 | "name": { 136 | "type": "string", 137 | "title": "Title of plot" 138 | }, 139 | "params": { 140 | "type": "object", 141 | "title": "Plot params", 142 | "description": "Represents what the data is related to.", 143 | "properties": { 144 | "model": { 145 | "type": "string" 146 | }, 147 | "observation": { 148 | "type": "string" 149 | }, 150 | "variable": { 151 | "type": "string" 152 | } 153 | } 154 | }, 155 | "data": { 156 | "type": "object", 157 | "title": "Plot traces", 158 | "description": "This object should be formatted for specified plotComponent" 159 | } 160 | }, 161 | "required": ["plotType", "plotComponent", "plotCategory", "params", "name", "data"] 162 | } 163 | } 164 | }, 165 | "required": ["version", "availableParams", "paramsAttributes", "data"] 166 | } 167 | -------------------------------------------------------------------------------- /src/store/schemas/data.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "title": "Data file", 4 | "description": "Main data structure, that should be uploaded to application as .json file", 5 | "type": "object", 6 | "properties": { 7 | "version": { 8 | "type": "string", 9 | "const": "1.0.0" 10 | }, 11 | "models": { 12 | "type": "array", 13 | "title": "Array of models' names used in this data file", 14 | "items": { 15 | "type": "string" 16 | }, 17 | "uniqueItems": true 18 | }, 19 | "variables": { 20 | "type": "array", 21 | "title": "Array of variables' names used in this data file", 22 | "items": { 23 | "type": "string" 24 | }, 25 | "uniqueItems": true 26 | }, 27 | "observations": { 28 | "type": "array", 29 | "title": "Array of observations' names used in this data file", 30 | "items": { 31 | "type": "string" 32 | }, 33 | "uniqueItems": true 34 | }, 35 | "data": { 36 | "type": "array", 37 | "title": "Array of plots data", 38 | "items": { 39 | "type": "object", 40 | "title": "Single plot object", 41 | "properties": { 42 | "plotComponent": { 43 | "type": "string", 44 | "title": "Name of vue component to plot this object", 45 | "description": "Represents how the plot is build", 46 | "examples": ["FeatureImportance", "LinearPartialDependency", "CategoricalPartialDependency"] 47 | }, 48 | "plotType": { 49 | "type": "string", 50 | "title": "Type of plot", 51 | "description": "Represents what plot presents", 52 | "examples": ["FeatureImportance", "PartialDependency"] 53 | }, 54 | "plotCategory": { 55 | "type": "string", 56 | "title": "Category of plot", 57 | "examples": ["Local", "Global"] 58 | }, 59 | "name": { 60 | "type": "string", 61 | "title": "Title of plot" 62 | }, 63 | "params": { 64 | "type": "object", 65 | "title": "Plot params", 66 | "description": "Represents what the data is related to.", 67 | "properties": { 68 | "model": { 69 | "type": "string" 70 | }, 71 | "observation": { 72 | "type": "string" 73 | }, 74 | "variable": { 75 | "type": "string" 76 | } 77 | } 78 | }, 79 | "data": { 80 | "type": "object", 81 | "title": "Plot traces", 82 | "description": "This object should be formatted for specified plotComponent" 83 | } 84 | }, 85 | "required": ["plotType", "plotComponent", "plotCategory", "params", "name", "data"] 86 | } 87 | } 88 | }, 89 | "required": ["version", "models", "observations", "variables", "data"] 90 | } 91 | -------------------------------------------------------------------------------- /src/store/schemas/recentURLSources.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "title": "recentURLSources", 4 | "description": "Simple structure to save recently used source URLs", 5 | "type": "object", 6 | "properties": { 7 | "version": { 8 | "type": "string", 9 | "const": "1.0.0" 10 | }, 11 | "sources": { 12 | "type": "array", 13 | "title": "Array of sources", 14 | "items": { 15 | "type": "object", 16 | "title": "Single source", 17 | "properties": { 18 | "url": { 19 | "type": "string", 20 | "title": "URL of source" 21 | }, 22 | "time": { 23 | "type": "number", 24 | "title": "timestamp of last usage in miliseconds" 25 | } 26 | }, 27 | "required": ["url", "time"] 28 | } 29 | } 30 | }, 31 | "required": ["version", "sources"] 32 | } 33 | -------------------------------------------------------------------------------- /src/store/schemas/session-1.1.0.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "title": "Session file", 4 | "type": "object", 5 | "properties": { 6 | "version": { 7 | "type": "string", 8 | "const": "1.1.0" 9 | }, 10 | "sources": { 11 | "type": "array", 12 | "items": { 13 | "type": "object", 14 | "properties": { 15 | "address": { 16 | "type": "string" 17 | }, 18 | "translations": { 19 | "type": "object" 20 | }, 21 | "uuid": { 22 | "type": "string" 23 | } 24 | }, 25 | "required": ["address", "translations", "uuid"] 26 | } 27 | }, 28 | "slots": { 29 | "type": "array", 30 | "items": { 31 | "type": "object", 32 | "properties": { 33 | "name": { 34 | "type": "string" 35 | }, 36 | "plotType": { 37 | "type": "string" 38 | }, 39 | "plotCategory": { 40 | "type": "string" 41 | }, 42 | "uuid": { 43 | "type": "string" 44 | }, 45 | "localParams": { 46 | "type": "array", 47 | "items": { 48 | "type": "object" 49 | } 50 | }, 51 | "positionX": { 52 | "type": "number" 53 | }, 54 | "positionY": { 55 | "type": "number" 56 | }, 57 | "width": { 58 | "type": "number" 59 | }, 60 | "height": { 61 | "type": "number" 62 | }, 63 | "pageNumber": { 64 | "type": "number" 65 | }, 66 | "archived": { 67 | "type": "boolean" 68 | }, 69 | "scope": { 70 | "type": "string" 71 | }, 72 | "customData": { 73 | "type": ["object", "null"] 74 | } 75 | }, 76 | "required": ["name", "plotType", "plotCategory", "localParams", "uuid", "positionX", "positionY", "width", "height", "pageNumber", "archived", "scope", "customData"] 77 | } 78 | }, 79 | "colors": { 80 | "type": "object" 81 | }, 82 | "annotations": { 83 | "type": "array" 84 | }, 85 | "name": { 86 | "type": "string" 87 | }, 88 | "uuid": { 89 | "type": "string" 90 | }, 91 | "time": { 92 | "type": "number" 93 | }, 94 | "options": { 95 | "type": "array", 96 | "items": { 97 | "type": "object", 98 | "properties": { 99 | "name": { 100 | "type": "string" 101 | } 102 | }, 103 | "required": ["name", "value"] 104 | } 105 | } 106 | }, 107 | "required": ["version", "sources", "slots", "colors", "annotations", "name", "uuid", "time", "options"] 108 | } 109 | -------------------------------------------------------------------------------- /src/store/schemas/session.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "title": "Session file", 4 | "type": "object", 5 | "properties": { 6 | "version": { 7 | "type": "string", 8 | "const": "1.0.0" 9 | }, 10 | "sources": { 11 | "type": "array", 12 | "items": { 13 | "type": "object", 14 | "properties": { 15 | "address": { 16 | "type": "string" 17 | }, 18 | "translations": { 19 | "type": "object" 20 | }, 21 | "uuid": { 22 | "type": "string" 23 | } 24 | }, 25 | "required": ["address", "translations", "uuid"] 26 | } 27 | }, 28 | "slots": { 29 | "type": "array", 30 | "items": { 31 | "type": "object", 32 | "properties": { 33 | "name": { 34 | "type": "string" 35 | }, 36 | "plotType": { 37 | "type": "string" 38 | }, 39 | "plotCategory": { 40 | "type": "string" 41 | }, 42 | "uuid": { 43 | "type": "string" 44 | }, 45 | "localParams": { 46 | "type": "array", 47 | "items": { 48 | "type": "object" 49 | } 50 | }, 51 | "positionX": { 52 | "type": "number" 53 | }, 54 | "positionY": { 55 | "type": "number" 56 | }, 57 | "width": { 58 | "type": "number" 59 | }, 60 | "height": { 61 | "type": "number" 62 | }, 63 | "pageNumber": { 64 | "type": "number" 65 | }, 66 | "archived": { 67 | "type": "boolean" 68 | } 69 | }, 70 | "required": ["name", "plotType", "plotCategory", "localParams", "uuid", "positionX", "positionY", "width", "height", "pageNumber", "archived"] 71 | } 72 | }, 73 | "colors": { 74 | "type": "object" 75 | }, 76 | "annotations": { 77 | "type": "array" 78 | }, 79 | "name": { 80 | "type": "string" 81 | }, 82 | "uuid": { 83 | "type": "string" 84 | }, 85 | "time": { 86 | "type": "number" 87 | }, 88 | "options": { 89 | "type": "array", 90 | "items": { 91 | "type": "object", 92 | "properties": { 93 | "name": { 94 | "type": "string" 95 | } 96 | }, 97 | "required": ["name", "value"] 98 | } 99 | } 100 | }, 101 | "required": ["version", "sources", "slots", "colors", "annotations", "name", "uuid", "time", "options"] 102 | } 103 | -------------------------------------------------------------------------------- /src/store/telemetry.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import config from '@/configuration/config.js' 3 | import streams from '@/utils/streams.js' 4 | 5 | const state = { 6 | lastActivity: new Date().getTime(), 7 | lastActivitySended: 0, 8 | telemetryUUID: null 9 | } 10 | 11 | const getters = { 12 | telemetryUUID (state) { 13 | return state.telemetryUUID 14 | }, 15 | lastActivity (state) { 16 | return state.lastActivity 17 | }, 18 | lastActivitySended (state) { 19 | return state.lastActivitySended 20 | } 21 | } 22 | 23 | const mutations = { 24 | updateLastActivity (state) { 25 | Vue.set(state, 'lastActivity', new Date().getTime()) 26 | }, 27 | setLastActivitySended (state, time) { 28 | Vue.set(state, 'lastActivitySended', time) 29 | }, 30 | setTelemetryUUID (state, v) { 31 | Vue.set(state, 'telemetryUUID', v) 32 | } 33 | } 34 | 35 | const actions = { 36 | async getSimplifiedTelemetryState ({ getters }) { 37 | let pages = getters.allSlots.reduce((agg, slot) => { 38 | agg[slot.pageNumber] = [...(agg[slot.pageNumber] || []), slot.plotType] 39 | return agg 40 | }, {}) 41 | let paramsCount = streams.runOnChildren(getters.availableParams, x => x.length) 42 | return { pages, page: getters.pageNumber, paramsCount } 43 | }, 44 | initTelemetry ({ commit, getters, dispatch }) { 45 | document.addEventListener('pointerdown', () => { 46 | commit('updateLastActivity') 47 | }) 48 | setInterval(async () => { 49 | if (localStorage.getItem('disableTelemetry')) return 50 | if (getters.lastActivity <= getters.lastActivitySended) return 51 | if (!getters.telemetryUUID) return 52 | Vue.http.post(config.telemetryServer + '/state', { 53 | uuid: getters.telemetryUUID, 54 | type: 'simple', 55 | data: JSON.stringify(await dispatch('getSimplifiedTelemetryState')) 56 | }).catch(console.error) 57 | commit('setLastActivitySended', getters.lastActivity) 58 | }, 1000 * 60 * 1) 59 | } 60 | } 61 | 62 | export default { 63 | state, 64 | getters, 65 | mutations, 66 | actions, 67 | namespaced: false 68 | } 69 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | /* File for common css rules */ 2 | body, html { 3 | margin: 0; 4 | padding: 0; 5 | font-family: 'Fira Sans'; 6 | user-select: none; 7 | } 8 | .right { 9 | float: right; 10 | } 11 | .left { 12 | float: left; 13 | } 14 | .blured { 15 | filter: blur(2px); 16 | } 17 | 18 | /* dropdown */ 19 | .dropdown > .dropdown-toggle { 20 | background: #f1f1f1; 21 | } 22 | .dropdown > .dropdown-toggle:hover { 23 | background: #e1e1e1; 24 | } 25 | .dropdown-menu { 26 | width: 100%; 27 | z-index: 10001; 28 | } 29 | 30 | /* Fonts */ 31 | @font-face { 32 | font-family: 'Fira Sans'; 33 | src: url('assets/FiraSans-Regular.otf'); 34 | } 35 | @font-face { 36 | font-family: 'FiraSansBold'; 37 | src: url('assets/FiraSans-Bold.otf'); 38 | } 39 | 40 | /* Tooltips */ 41 | .tooltiped { 42 | position: relative; 43 | } 44 | .tooltiped > span.tooltip { 45 | position: absolute; 46 | left: 50%; 47 | transform: translateX(-50%); 48 | top: calc(100% + 5px); 49 | max-width: 300px; 50 | min-width: 150px; 51 | background: #4378bf; 52 | border-radius: 5px; 53 | box-shadow: 0 0 2px 0 #4378bf; 54 | padding: 5px; 55 | color: white; 56 | text-align: left; 57 | z-index: 10000000; 58 | font-size: 14px; 59 | line-height: 14px; 60 | display: none; 61 | white-space: normal; 62 | text-align: center; 63 | } 64 | .tooltiped:hover > span.tooltip { 65 | display: block; 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/OptionsMixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: { 3 | slotv: Object 4 | }, 5 | computed: { 6 | getOption () { 7 | return (name) => { 8 | let local = (((this.slotv || {}).customData || {}).options || {})[name] 9 | return local === undefined || local === null ? this.$store.getters.getOption(name) : local 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/Resize.js: -------------------------------------------------------------------------------- 1 | import resize from 'vue-resize-directive' 2 | 3 | export default { 4 | data () { 5 | return { width: 0, height: 0 } 6 | }, 7 | updated () { 8 | this.onResize() 9 | }, 10 | mounted () { 11 | this.onResize() 12 | }, 13 | methods: { 14 | onResize () { 15 | let el = this.$refs.plot 16 | if (!el) return 17 | if (el.$el) el = el.$el 18 | if (this.$refs.plot) this.width = el.offsetWidth 19 | if (this.$refs.plot) this.height = el.offsetHeight 20 | } 21 | }, 22 | directives: { 23 | resize 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/fontAwesomeLoader.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { library } from '@fortawesome/fontawesome-svg-core' 3 | import { faArchive, faGripVertical, faMinusSquare, faPlusSquare, faCompressArrowsAlt, 4 | faLayerGroup, faExpand, faBars, faAngleDown, faCaretDown, faAngleUp, faPlus, faMinus, faSquare as fasSquare, 5 | faChartBar, faPoll, faListAlt, faAngleLeft, faAngleRight, faFileDownload, faEraser, faCheckCircle, faUserEdit, 6 | faEllipsisH, faExclamationCircle, faExclamationTriangle, faTimes, faInfo, faCheck, faCog } from '@fortawesome/free-solid-svg-icons' 7 | import { faQuestionCircle, faTimesCircle, faCheckSquare, faSquare, faClone, faEdit, faCircle } from '@fortawesome/free-regular-svg-icons' 8 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' 9 | 10 | export function loadFontAwesome () { 11 | library.add(faArchive, faGripVertical, faMinusSquare, faPlusSquare, faCompressArrowsAlt, faLayerGroup, faExpand, faBars) 12 | library.add(faAngleDown, faCaretDown, faPlus, faMinus, faAngleUp, fasSquare, faChartBar, faPoll, faListAlt, faAngleLeft, faAngleRight) 13 | library.add(faFileDownload, faEraser, faCheckCircle, faUserEdit, faEllipsisH, faExclamationCircle, faExclamationTriangle, faTimes, faInfo, faCheck) 14 | library.add(faCog) 15 | library.add(faQuestionCircle, faTimesCircle, faCheckSquare, faSquare, faClone, faEdit, faCircle) 16 | Vue.component('font-awesome-icon', FontAwesomeIcon) 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/format.js: -------------------------------------------------------------------------------- 1 | const round = (x, p = 3) => { 2 | if (x === 0) return 0 3 | let lg = Math.round(Math.log10(Math.abs(x))) 4 | let k = p - lg 5 | return Math.round(x * (10 ** k)) / (10 ** k) 6 | } 7 | 8 | const isCustomParam = (param) => { 9 | if (!(typeof param === 'string' || param instanceof String) || !param.startsWith('{') || !param.endsWith('}')) return false 10 | try { 11 | JSON.parse(param) 12 | return true 13 | } catch (e) { 14 | return false 15 | } 16 | } 17 | 18 | export default { 19 | addNewLines (label, width = 140) { 20 | let words = label.split('_').join(' ').split(' ') 21 | let output = '' 22 | let line = '' 23 | const limit = Math.floor(width / 7) 24 | words.forEach(word => { 25 | if (word.length > limit) { 26 | let n = limit - line.length - 1 27 | output += '
' + line + (n < 0 ? '' : ' ' + word.substr(0, n)) 28 | line = word.substr(n >= 0 ? n : 0) 29 | } else if (word.length + line.length + 1 > limit) { 30 | output += '
' + line 31 | line = word 32 | } else line += line.length === 0 ? word : (' ' + word) 33 | }) 34 | if (line.length > 0) output += '
' + line 35 | return output.split('
').filter(x => x.length > 0).join('
') // Remove first one or two
36 | }, 37 | round, 38 | isCustomParam, 39 | formatTitle (title) { 40 | if (isCustomParam(title)) return '[Custom]' 41 | return title.replace(/[._-]/gi, ' ') 42 | }, 43 | formatValue (x, addSign = false, space = '', k = 0) { 44 | let r = round(x, 3 + k) 45 | if (Math.abs(2 * x) >= 10 ** 6) r = round(x / (10 ** 6), 1 + k) + 'M' 46 | else if (Math.abs(2 * x) >= 10 ** 3) r = round(x / (10 ** 3), 2 + k) + 'K' 47 | 48 | if (x >= 0) return space + (addSign ? '+' : '') + r 49 | else return r + space 50 | }, 51 | simplify (name) { 52 | return name.replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>{}[\]\\/]/gi, '').toLowerCase() 53 | }, 54 | firstCharUpper (name) { 55 | return name.split(' ').map(word => word.length > 0 ? word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() : '').join(' ') 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/lolipopAxis.js: -------------------------------------------------------------------------------- 1 | class AxisRange { 2 | constructor (start, end) { 3 | let invert = start > end 4 | this.start = invert ? end : start 5 | this.end = invert ? start : end 6 | } 7 | get len () { 8 | return this.end - this.start 9 | } 10 | get mid () { 11 | return this.start + this.len * 0.5 12 | } 13 | getRelativePoint (alpha) { 14 | return this.start + this.len * alpha 15 | } 16 | static fromLen (start, len) { 17 | return new AxisRange(start, start + len) 18 | } 19 | } 20 | 21 | class LolipopAxis { 22 | constructor (facets, traces, groups, facetTitleSpace = 0.03, headerSpace = 0.03, groupMargin = 2) { 23 | if (facets.length > 20) throw new Error('Facets count limit exceeded') 24 | this.facets = facets 25 | this.traces = traces 26 | this.groups = groups 27 | if (!groups.every(Array.isArray)) this.groups = Array(facets.length).fill(null).map(x => [...groups]) 28 | this.facetTitleSpace = facetTitleSpace 29 | this.headerSpace = headerSpace 30 | this.groupMargin = groupMargin 31 | this.calcAxisLength() 32 | this.calcFacetsOffset() 33 | } 34 | // Length of group is count of points in a group plus upper and bottom margin 35 | get groupLength () { 36 | return this.traces.length + 2 * this.groupMargin 37 | } 38 | // How many space is filled by poins and margins (the rest is for facets' labels) 39 | get pointsSpacePropotion () { 40 | return 1 - this.facets.length * this.facetTitleSpace - this.headerSpace 41 | } 42 | calcAxisLength () { 43 | this.facetsSpaceLength = this.facets.map((f, i) => this.groups[i].length * this.groupLength) 44 | this.axisLength = (1 / this.pointsSpacePropotion) * this.facetsSpaceLength.reduce((acu, x) => acu + x, 0) 45 | } 46 | getFacetOffset (facet) { 47 | return this.facetsOffset[this.facets.indexOf(facet)] 48 | } 49 | calcFacetsOffset () { 50 | let facetTitleAbsoluteSpace = this.facetTitleSpace * this.axisLength 51 | let headerAbsoluteSpace = this.headerSpace * this.axisLength 52 | this.facetsOffset = this.facetsSpaceLength 53 | .reduce((acu, x) => [...acu, acu[acu.length - 1] + x + facetTitleAbsoluteSpace], [headerAbsoluteSpace]) 54 | } 55 | getFacetTitleRange (facet) { 56 | return AxisRange.fromLen(this.getFacetOffset(facet), this.facetTitleSpace * this.axisLength) 57 | } 58 | getFacetRange (facet) { 59 | let facetId = this.facets.indexOf(facet) 60 | return new AxisRange(this.facetsOffset[facetId], this.facetsOffset[facetId + 1]) 61 | } 62 | getGroupRange (facet, group) { 63 | let facetId = this.facets.indexOf(facet) 64 | let groupId = this.groups[facetId].indexOf(group) 65 | let insideOffset = groupId * this.groupLength + this.groupMargin 66 | return AxisRange.fromLen(this.getFacetTitleRange(facet).end + insideOffset, this.traces.length) 67 | } 68 | getPointRange (facet, group, trace) { 69 | let traceId = this.traces.indexOf(trace) 70 | return AxisRange.fromLen(this.getGroupRange(facet, group).start + traceId, 1) 71 | } 72 | getAxisRange (margin) { 73 | let absMargin = margin * this.axisLength 74 | return [-1 * absMargin, this.axisLength + absMargin] 75 | } 76 | getHeaderRange () { 77 | return new AxisRange(0, this.axisLength * this.headerSpace) 78 | } 79 | getAxisTicks () { 80 | let tmp = this.facets.map((facet, facetId) => { 81 | return { ticks: this.groups[facetId].map(group => this.getGroupRange(facet, group).mid), labels: this.groups[facetId] } 82 | }) 83 | return { ticks: tmp.map(x => x.ticks).flat(), labels: tmp.map(x => x.labels).flat() } 84 | } 85 | } 86 | 87 | export default LolipopAxis 88 | -------------------------------------------------------------------------------- /src/utils/streams.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mapChildren (obj, f) { 3 | return Object.keys(obj).reduce((acu, key) => { 4 | acu[key] = obj[key].map(f) 5 | return acu 6 | }, {}) 7 | }, 8 | filterChildren (obj, f) { 9 | return Object.keys(obj).reduce((acu, key) => { 10 | acu[key] = obj[key].filter(f) 11 | return acu 12 | }, {}) 13 | }, 14 | runOnChildren (obj, f) { 15 | return Object.keys(obj).reduce((acu, key) => { 16 | acu[key] = f(obj[key], key) 17 | return acu 18 | }, {}) 19 | }, 20 | findIndexAll (arr, f) { 21 | return arr.reduce((acu, x, index) => { 22 | if (f(x)) acu.push(index) 23 | return acu 24 | }, []) 25 | }, 26 | createObjectWithArrays (keys) { 27 | return keys.reduce((acu, key) => { 28 | acu[key] = [] 29 | return acu 30 | }, {}) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/zIndexIncrementor.js: -------------------------------------------------------------------------------- 1 | let z = 100 2 | 3 | export default { 4 | get () { 5 | z += 1 6 | return z 7 | }, 8 | getWithoutInc () { 9 | return z 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const StatsPlugin = require('stats-webpack-plugin') 2 | const webpack = require('webpack') 3 | 4 | module.exports = { 5 | chainWebpack: config => config.optimization.minimize(false), 6 | publicPath: process.env.NODE_ENV === 'production' ? (process.env.TRAVIS ? (process.env.TRAVIS_BRANCH === 'master' ? '/' : ('/branch/' + process.env.TRAVIS_BRANCH + '/')) : '/') : '/', 7 | pluginOptions: { 8 | webpackBundleAnalyzer: { 9 | openAnalyzer: false 10 | } 11 | }, 12 | configureWebpack: { 13 | plugins: [ 14 | new webpack.DefinePlugin({ 15 | 'BUILDINFO': JSON.stringify({ 16 | time: new Date().getTime(), 17 | branch: process.env.TRAVIS_BRANCH, 18 | commit: process.env.TRAVIS_COMMIT, 19 | buildurl: process.env.TRAVIS_BUILD_WEB_URL 20 | }) 21 | }) 22 | ] 23 | }, 24 | outputDir: 'dist' 25 | } 26 | --------------------------------------------------------------------------------