├── .babelrc ├── .gitignore ├── editor ├── A_1.PNG ├── A_2.PNG ├── B_1.PNG ├── B_2.PNG ├── merged.PNG ├── reservation.json ├── 4cases.json └── scene_merged.json ├── fonts ├── icomoon.eot ├── icomoon.ttf ├── icomoon.woff └── icomoon.svg ├── css ├── _custom-icons.scss ├── app.scss ├── _config.scss ├── _legend.scss ├── _context-menu.scss ├── _popover-component.scss ├── _common.scss ├── _comparison-container.scss ├── _stage.scss ├── _tooltip.scss ├── _control-panel.scss ├── _autocomplete-component.scss ├── _form-elements.scss ├── _icon-font.scss └── _loading-indicator.scss ├── js ├── shape │ ├── Block.js │ └── BlockConnection.js ├── Constants.js ├── domain │ ├── CommitMapper.js │ └── Commit.js ├── service │ ├── CoderadarAuthorizationService.js │ ├── ServiceLocator.js │ ├── CoderadarCommitService.js │ ├── CoderadarMetricService.js │ └── MetricNameService.js ├── main.js ├── ui │ ├── components │ │ ├── SearchComponent.js │ │ ├── FilterComponent.js │ │ ├── CheckboxComponent.js │ │ ├── CommitSelectionComponent.js │ │ ├── ContextMenuComponent.js │ │ ├── PopoverComponent.js │ │ ├── ScreenshotComponent.js │ │ ├── DimensionSelectionComponent.js │ │ ├── LegendComponent.js │ │ ├── ComparisonContainerComponent.js │ │ └── AutocompleteComponent.js │ ├── UserInterface.js │ └── InteractionHandler.js ├── util │ ├── ColorHelper.js │ ├── DatetimeFormatter.js │ └── ElementAnalyzer.js ├── Config.js ├── drawer │ ├── AbstractDrawer.js │ ├── SingleDrawer.js │ └── MergedDrawer.js └── Application.js ├── package.json ├── test ├── drawer │ ├── test_MergedDrawer.js │ ├── test_SingleDrawer.js │ └── test_AbstractDrawer.js ├── domain │ ├── test_Commit.js │ └── test_CommitMapper.js ├── data │ ├── dummyCommitResponse.json │ └── deltaTree.json └── util │ └── test_ElementAnalyzer.js ├── gulpfile.js ├── scripts ├── dummyDataGeneratorScript.js └── setupProjectScript.js ├── README.md └── index.html /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .idea/ 3 | node_modules/ 4 | build/ 5 | css/*.css 6 | css/*.map -------------------------------------------------------------------------------- /editor/A_1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschild/CodeRadarVisualization/HEAD/editor/A_1.PNG -------------------------------------------------------------------------------- /editor/A_2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschild/CodeRadarVisualization/HEAD/editor/A_2.PNG -------------------------------------------------------------------------------- /editor/B_1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschild/CodeRadarVisualization/HEAD/editor/B_1.PNG -------------------------------------------------------------------------------- /editor/B_2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschild/CodeRadarVisualization/HEAD/editor/B_2.PNG -------------------------------------------------------------------------------- /editor/merged.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschild/CodeRadarVisualization/HEAD/editor/merged.PNG -------------------------------------------------------------------------------- /fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschild/CodeRadarVisualization/HEAD/fonts/icomoon.eot -------------------------------------------------------------------------------- /fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschild/CodeRadarVisualization/HEAD/fonts/icomoon.ttf -------------------------------------------------------------------------------- /fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschild/CodeRadarVisualization/HEAD/fonts/icomoon.woff -------------------------------------------------------------------------------- /css/_custom-icons.scss: -------------------------------------------------------------------------------- 1 | .custom-icon-metric-mapping { 2 | position: relative; 3 | width: 30px; 4 | 5 | .icon-stats-bars { 6 | position: absolute; 7 | left: 9px; 8 | top: -14px; 9 | } 10 | 11 | .icon-cog { 12 | position: absolute; 13 | font-size: 17px; 14 | left: -4px; 15 | top: -16px; 16 | } 17 | } -------------------------------------------------------------------------------- /css/app.scss: -------------------------------------------------------------------------------- 1 | @import "config"; 2 | 3 | @import "loading-indicator"; 4 | @import "form-elements"; 5 | @import "icon-font"; 6 | 7 | @import "common"; 8 | @import "custom-icons"; 9 | @import "tooltip"; 10 | @import "legend"; 11 | @import "control-panel"; 12 | @import "autocomplete-component"; 13 | @import "popover-component"; 14 | @import "comparison-container"; 15 | @import "context-menu"; 16 | @import "stage"; -------------------------------------------------------------------------------- /js/shape/Block.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | var geometry = new THREE.BoxGeometry(1, 1, 1); 4 | // move local coordinate system to scale the block properly 5 | geometry.translate(0.5, 0.5, 0.5); 6 | 7 | export class Block extends THREE.Mesh { 8 | constructor(color, name) { 9 | var material = new THREE.MeshLambertMaterial({color: color}); 10 | super(geometry, material); 11 | 12 | this.name = name; 13 | } 14 | } -------------------------------------------------------------------------------- /css/_config.scss: -------------------------------------------------------------------------------- 1 | $background-color: #fff; 2 | $border-color: #ccc; 3 | 4 | $transparent-background-mask-color: rgba(0, 0, 0, 0.7); 5 | $tooltip-background-color: #2B222A; 6 | 7 | $button-hover-background-color: #e6e6e6; 8 | $button-hover-border-color: #adadad; 9 | 10 | $table-row-background-color: #f3f3f3; 11 | 12 | $state-inactive-color: #ccc; 13 | $state-active-color: #000; 14 | $state-hover-color: #f3f3f3; 15 | $state-disabled-color: #eee; 16 | 17 | $highlight-color: #337ab7; -------------------------------------------------------------------------------- /js/Constants.js: -------------------------------------------------------------------------------- 1 | export const FIRST_COMMIT = 'firstCommit'; 2 | export const SECOND_COMMIT = 'secondCommit'; 3 | 4 | export const HEIGHT_DIMENSION = 'heightDimension'; 5 | export const GROUNDAREA_DIMENSION = 'groundareaDimension'; 6 | export const COLOR_DIMENSION = 'colorDimension'; 7 | 8 | export const LEFT_SCREEN = 'left'; 9 | export const RIGHT_SCREEN = 'right'; 10 | 11 | export const COMMIT_TYPE_CURRENT = 'current'; 12 | export const COMMIT_TYPE_OTHER = 'other'; 13 | 14 | export const ELEMENT_TYPE_MODULE = 'MODULE'; 15 | export const ELEMENT_TYPE_FILE = 'FILE'; 16 | export const ELEMENT_TYPE_CONNECTION = 'CONNECTION'; -------------------------------------------------------------------------------- /css/_legend.scss: -------------------------------------------------------------------------------- 1 | #legend-container { 2 | position: absolute; 3 | bottom: 0; 4 | left: 10px; 5 | background: $background-color; 6 | box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.2); 7 | z-index: 99; 8 | 9 | & .legend-item { 10 | display: inline-block; 11 | padding: 10px; 12 | 13 | & .legend-color { 14 | display: inline-block; 15 | width: 10px; 16 | height: 10px; 17 | } 18 | } 19 | } 20 | 21 | #legend-item-color-code { 22 | & .legend-color { 23 | width: 30px; 24 | background: linear-gradient(to right, #ffffff,#ffc905,#f78400,#e92100,#9b1909,#4f1609,#5d0000); 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /css/_context-menu.scss: -------------------------------------------------------------------------------- 1 | .context-menu { 2 | display: none; 3 | 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | background: $background-color; 8 | box-sizing: border-box; 9 | z-index: 1; 10 | 11 | box-shadow: rgba(0,0,0,0.2) 0 2px 6px 0; 12 | 13 | ul { 14 | padding: 0; 15 | margin: 0; 16 | list-style: none; 17 | 18 | & > li { 19 | cursor: pointer; 20 | padding: 10px 15px; 21 | max-width: 400px; 22 | overflow: hidden; 23 | text-overflow: ellipsis; 24 | 25 | &:hover { 26 | color: $state-active-color; 27 | background: $state-hover-color; 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /js/domain/CommitMapper.js: -------------------------------------------------------------------------------- 1 | import {Commit} from './Commit'; 2 | 3 | export class CommitMapper { 4 | 5 | constructor(data) { 6 | this.data = data['_embedded']['commitResourceList']; 7 | this.objects = []; 8 | } 9 | 10 | mapAll() { 11 | for (let element of this.data) { 12 | this.objects.push(this.map(element)); 13 | } 14 | } 15 | 16 | map(data) { 17 | let commit = new Commit(); 18 | commit.setName(data.name); 19 | commit.setAuthor(data.author); 20 | commit.setTimestamp(data.timestamp); 21 | commit.setAnalyzed(data.analyzed); 22 | 23 | return commit; 24 | } 25 | 26 | getAll() { 27 | return this.objects; 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /js/service/CoderadarAuthorizationService.js: -------------------------------------------------------------------------------- 1 | import {config} from '../Config'; 2 | 3 | export class CoderadarAuthorizationService { 4 | 5 | constructor() { 6 | this.URL = config.BASE_URL + '/user/auth'; 7 | } 8 | 9 | authorize() { 10 | var params = { 11 | 'username': config.USERNAME, 12 | 'password': config.PASSWORD 13 | }; 14 | 15 | return axios.post(this.URL, params).then((response) => { 16 | if (!response.data.accessToken) { 17 | throw new Error('access token could not be found in response'); 18 | } 19 | config.ACCESS_TOKEN = response.data.accessToken; 20 | axios.defaults.headers.common['Authorization'] = response.data.accessToken; 21 | }); 22 | } 23 | } -------------------------------------------------------------------------------- /js/service/ServiceLocator.js: -------------------------------------------------------------------------------- 1 | const singleton = Symbol(); 2 | const singletonEnforcer = Symbol(); 3 | 4 | export class ServiceLocator { 5 | 6 | constructor(enforcer) { 7 | if (enforcer !== singletonEnforcer) { 8 | throw new Error('Instantiating is not allowed. Use ServiceLocator.getInstance() instead.'); 9 | } 10 | 11 | this._serviceIntances = {}; 12 | } 13 | 14 | static getInstance() { 15 | if (!this[singleton]) { 16 | this[singleton] = new ServiceLocator(singletonEnforcer); 17 | } 18 | 19 | return this[singleton]; 20 | } 21 | 22 | get(name) { 23 | return this._serviceIntances[name]; 24 | } 25 | 26 | register(name, serviceInstance) { 27 | this._serviceIntances[name] = serviceInstance; 28 | } 29 | } -------------------------------------------------------------------------------- /css/_popover-component.scss: -------------------------------------------------------------------------------- 1 | .popover { 2 | height: 100%; 3 | 4 | .popover-toggle-btn { 5 | & > i.expand-icon { 6 | position: absolute; 7 | bottom: 0; 8 | left: 50%; 9 | transform: translateX(-50%); 10 | } 11 | } 12 | 13 | .popover-floating-wrapper { 14 | position: relative; 15 | display: none; 16 | 17 | .popover-content { 18 | position: absolute; 19 | z-index: 1; 20 | background-color: $background-color; 21 | padding: 10px; 22 | box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.2); 23 | min-width: 200px; 24 | 25 | & .tooltip-toggle { 26 | position: absolute; 27 | right: 10px; 28 | } 29 | 30 | & > div { 31 | padding: 10px 0; 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | import {Application} from './Application'; 2 | import {ServiceLocator} from './service/ServiceLocator'; 3 | import {CoderadarAuthorizationService} from './service/CoderadarAuthorizationService'; 4 | import {CoderadarCommitService} from './service/CoderadarCommitService'; 5 | import {CoderadarMetricService} from './service/CoderadarMetricService'; 6 | import {MetricNameService} from './service/MetricNameService'; 7 | 8 | (function () { 9 | // register services 10 | ServiceLocator.getInstance().register('authorizationService', new CoderadarAuthorizationService()); 11 | ServiceLocator.getInstance().register('commitService', new CoderadarCommitService()); 12 | ServiceLocator.getInstance().register('metricService', new CoderadarMetricService()); 13 | ServiceLocator.getInstance().register('metricNameService', new MetricNameService()); 14 | 15 | var application = new Application(); 16 | application.initialize(); 17 | })(); -------------------------------------------------------------------------------- /js/domain/Commit.js: -------------------------------------------------------------------------------- 1 | import {DatetimeFormatter} from '../util/DatetimeFormatter'; 2 | 3 | export class Commit { 4 | 5 | setName(name) { 6 | this.name = name; 7 | } 8 | 9 | getName() { 10 | return this.name; 11 | } 12 | 13 | setAuthor(author) { 14 | this.author = author; 15 | } 16 | 17 | getAuthor() { 18 | return this.author; 19 | } 20 | 21 | setTimestamp(timestamp) { 22 | this.timestamp = timestamp; 23 | } 24 | 25 | getTimestamp() { 26 | return this.timestamp; 27 | } 28 | 29 | setAnalyzed(analyzed) { 30 | this.analyzed = analyzed; 31 | } 32 | 33 | getAnalyzed() { 34 | return this.analyzed; 35 | } 36 | 37 | getShortName() { 38 | return this.name.substr(0, 7) + '...'; 39 | } 40 | 41 | getFormattedDatetime() { 42 | return new DatetimeFormatter() 43 | .withShowSeconds(false) 44 | .formatDate(new Date(this.timestamp)); 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /js/ui/components/SearchComponent.js: -------------------------------------------------------------------------------- 1 | import {AutocompleteComponent} from './AutocompleteComponent'; 2 | import * as PubSub from 'pubsub-js'; 3 | 4 | export class SearchComponent extends AutocompleteComponent { 5 | 6 | constructor(componentElement, application) { 7 | super(componentElement); 8 | this._application = application; 9 | 10 | this.hideShowSuggestionsButton(); 11 | } 12 | 13 | _bindEvents() { 14 | super._bindEvents(); 15 | 16 | PubSub.subscribe('metricsLoaded', () => { 17 | let elements = []; 18 | for (let elementName of this._application.getUniqueElementList()) { 19 | elements.push({ 20 | value: elementName, 21 | label: elementName 22 | }); 23 | } 24 | 25 | this.setElements(elements); 26 | }); 27 | } 28 | 29 | _onSelection(args) { 30 | PubSub.publish('searchEntryClicked', { elementName: args.selection }); 31 | } 32 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CodeRadarVisualization", 3 | "description": "3D visualization for code structure and code quality", 4 | "author": "Philippe Schild", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/pschild/CodeRadarVisualization.git" 8 | }, 9 | "scripts": { 10 | "test": "mocha --compilers js:babel-core/register test/**/test*.js" 11 | }, 12 | "dependencies": { 13 | "axios": "^0.19.0", 14 | "binpacking": "0.0.1", 15 | "chroma-js": "^1.2.1", 16 | "normalize.css": "^5.0.0", 17 | "pubsub-js": "^1.5.4", 18 | "three": "^0.81.2", 19 | "tween.js": "^16.3.5" 20 | }, 21 | "devDependencies": { 22 | "babel-core": "^6.18.2", 23 | "babel-preset-es2015": "^6.16.0", 24 | "babelify": "^7.3.0", 25 | "browserify": "^13.1.0", 26 | "gulp": "^3.9.1", 27 | "gulp-sourcemaps": "^2.1.1", 28 | "mocha": "^3.1.2", 29 | "sinon": "^1.17.7", 30 | "vinyl-buffer": "^1.0.0", 31 | "vinyl-source-stream": "^1.1.0", 32 | "watchify": "^3.7.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /js/service/CoderadarCommitService.js: -------------------------------------------------------------------------------- 1 | import {config} from '../Config'; 2 | import {CommitMapper} from '../domain/CommitMapper'; 3 | 4 | export class CoderadarCommitService { 5 | 6 | constructor() { 7 | this.URL = config.BASE_URL + '/projects/1/commits?page=0&size=999'; 8 | 9 | this._commits = []; 10 | } 11 | 12 | load() { 13 | return axios.get(this.URL) 14 | .then((response) => { 15 | var commitMapper = new CommitMapper(response.data); 16 | commitMapper.mapAll(); 17 | 18 | this._commits = commitMapper.getAll(); 19 | this._commits.sort(function(a, b) { 20 | return b.timestamp - a.timestamp; 21 | }); 22 | }); 23 | } 24 | 25 | getCommits() { 26 | return this._commits; 27 | } 28 | 29 | getCommitByName(name) { 30 | for (let commit of this._commits) { 31 | if (commit.getName() == name) { 32 | return commit; 33 | } 34 | } 35 | 36 | return undefined; 37 | } 38 | } -------------------------------------------------------------------------------- /test/drawer/test_MergedDrawer.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | import {MergedDrawer} from '../../js/drawer/MergedDrawer'; 4 | import * as Constants from '../../js/Constants'; 5 | import sinon from 'sinon'; 6 | import packers from 'binpacking'; 7 | 8 | // mock GrowingPacker because it's imported with script-tag 9 | MergedDrawer.prototype._getPacker = sinon.stub().returns(packers.GrowingPacker.prototype); 10 | 11 | var drawer = new MergedDrawer(null, Constants.LEFT_SCREEN); 12 | 13 | describe('MergedDrawer', function () { 14 | it('should draw the correct amount of elements', function () { 15 | var elements = require('../data/deltaTreeWithCalculatedGroundAreas.json'); 16 | 17 | // stubbing 18 | MergedDrawer.prototype.drawBlock = sinon.stub().returns('yalla yalla'); 19 | MergedDrawer.prototype.drawBlockConnection = sinon.stub().returns('yalla yalla'); 20 | 21 | drawer.drawElements(elements); 22 | sinon.assert.callCount(drawer.drawBlock, 12); // 9 files + 3 modules 23 | 24 | assert.equal(drawer.movedElements.length, 1); 25 | }); 26 | }); -------------------------------------------------------------------------------- /js/util/ColorHelper.js: -------------------------------------------------------------------------------- 1 | import * as Constants from '../Constants'; 2 | import {config} from '../Config'; 3 | import * as chroma from 'chroma-js/chroma'; 4 | import * as THREE from 'three'; 5 | 6 | export class ColorHelper { 7 | 8 | static getColorByPosition(position) { 9 | return position == Constants.LEFT_SCREEN ? config.COLOR_FIRST_COMMIT : config.COLOR_SECOND_COMMIT; 10 | } 11 | 12 | static getContraryColorByColor(color) { 13 | return color == config.COLOR_FIRST_COMMIT ? config.COLOR_SECOND_COMMIT : config.COLOR_FIRST_COMMIT; 14 | } 15 | 16 | static getColorByMetricValue(value, max, min) { 17 | return this.getColorScale(config.COLOR_HEATMAP_RANGE, value, max, min); 18 | } 19 | 20 | static getColorByBottomValue(value, max, min) { 21 | return this.getColorScale(config.COLOR_HIERARCHY_RANGE, value, max, min); 22 | } 23 | 24 | static getColorScale(range, value, max, min) { 25 | var colorScale = chroma.scale(range); 26 | var hexValue = colorScale(value / (max + min)).hex(); 27 | return new THREE.Color(hexValue); 28 | } 29 | } -------------------------------------------------------------------------------- /js/ui/components/FilterComponent.js: -------------------------------------------------------------------------------- 1 | import {PopoverComponent} from '../components/PopoverComponent'; 2 | import * as PubSub from 'pubsub-js'; 3 | 4 | export class FilterComponent extends PopoverComponent { 5 | 6 | constructor(componentElement) { 7 | super(componentElement); 8 | 9 | this.fileVisibilityCheckboxes = componentElement.querySelectorAll('input'); 10 | 11 | this._bindEvents(); 12 | } 13 | 14 | _bindEvents() { 15 | super._bindEvents(); 16 | 17 | for (let checkbox of this.fileVisibilityCheckboxes) { 18 | checkbox.addEventListener('change', (event) => { 19 | PubSub.publish('fileVisibilityChange', { 20 | type: event.target.value, 21 | enabled: event.target.checked 22 | }); 23 | }); 24 | } 25 | 26 | PubSub.subscribe('fullSplitToggle', (eventName, args) => { 27 | this._toggleVisibilityCheckboxes(!args.enabled); 28 | }); 29 | } 30 | 31 | _toggleVisibilityCheckboxes(enabled) { 32 | for (let checkbox of this.fileVisibilityCheckboxes) { 33 | checkbox.disabled = enabled; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /test/domain/test_Commit.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | import {CommitMapper} from '../../js/domain/CommitMapper'; 4 | import {Commit} from '../../js/domain/Commit'; 5 | 6 | var dummyResponse = require('../data/dummyCommitResponse.json'); 7 | 8 | describe('Commit', function () { 9 | describe('setter and getter', function () { 10 | it('should return correct data', function () { 11 | let mapper = new CommitMapper(dummyResponse); 12 | let commit = mapper.map({ 13 | "name": "b152859ca8d73f5c974c2264107fd0092af310d0", 14 | "author": "John Doe", 15 | "timestamp": 1485813773000, 16 | "analyzed": true 17 | }); 18 | 19 | assert.equal(commit.getName(), 'b152859ca8d73f5c974c2264107fd0092af310d0'); 20 | assert.equal(commit.getAuthor(), 'John Doe'); 21 | assert.equal(commit.getTimestamp(), 1485813773000); 22 | assert.equal(commit.getAnalyzed(), true); 23 | 24 | assert.equal(commit.getShortName().indexOf(commit.getName().substr(0, 7)), 0); 25 | 26 | assert.equal(typeof commit.getFormattedDatetime(), 'string'); 27 | }); 28 | }); 29 | }); -------------------------------------------------------------------------------- /js/service/CoderadarMetricService.js: -------------------------------------------------------------------------------- 1 | import {config} from '../Config'; 2 | 3 | export class CoderadarMetricService { 4 | 5 | constructor() { 6 | this.URL = config.BASE_URL + '/projects/1/metricvalues/deltaTree'; 7 | } 8 | 9 | loadByCommitId(commitId) { 10 | var params = { 11 | 'commit': commitId, 12 | 'metrics': [config.HEIGHT_METRIC_NAME, config.GROUND_AREA_METRIC_NAME, config.COLOR_METRIC_NAME] 13 | }; 14 | 15 | return axios.post(this.URL, params); 16 | } 17 | 18 | loadDeltaTree(commit1Id, commit2Id) { 19 | var params = { 20 | 'commit1': commit1Id, 21 | 'commit2': commit2Id, 22 | 'metrics': [config.HEIGHT_METRIC_NAME, config.GROUND_AREA_METRIC_NAME, config.COLOR_METRIC_NAME] 23 | }; 24 | 25 | return axios.post(this.URL, params); 26 | } 27 | 28 | // deprecated 29 | loadTwoCommits(firstCommitId, secondCommitId, callbackFn) { 30 | axios.all([this.loadByCommitId(firstCommitId), this.loadByCommitId(secondCommitId)]) 31 | .then(axios.spread(function (firstCommitResult, secondCommitResult) { 32 | callbackFn(firstCommitResult.data, secondCommitResult.data); 33 | })); 34 | } 35 | } -------------------------------------------------------------------------------- /css/_common.scss: -------------------------------------------------------------------------------- 1 | input[type=text] { 2 | outline: none; 3 | border: 0; 4 | border-bottom: 1px solid $state-inactive-color; 5 | height: 30px; 6 | max-width: 250px; 7 | 8 | &:active, &:focus { 9 | border-bottom-color: $highlight-color; 10 | } 11 | } 12 | 13 | select { 14 | outline: none; 15 | border: 0; 16 | border-bottom: 1px solid $state-inactive-color; 17 | height: 30px; 18 | max-width: 250px; 19 | 20 | &:active, &:focus { 21 | border-bottom-color: $highlight-color; 22 | } 23 | } 24 | 25 | button { 26 | display: inline-block; 27 | padding: 6px 12px; 28 | text-align: center; 29 | white-space: nowrap; 30 | background-color: $background-color; 31 | cursor: pointer; 32 | user-select: none; 33 | background-image: none; 34 | border: 1px solid $border-color; 35 | outline: none; 36 | 37 | & .large-icon { 38 | font-size: 24px; 39 | } 40 | 41 | &:active, &.active { 42 | background-color: $button-hover-background-color; 43 | border-color: $button-hover-border-color; 44 | box-shadow: inset 0 3px 5px rgba(0,0,0,.125); 45 | } 46 | 47 | &:hover { 48 | background-color: $button-hover-background-color; 49 | border-color: $button-hover-border-color; 50 | } 51 | } -------------------------------------------------------------------------------- /css/_comparison-container.scss: -------------------------------------------------------------------------------- 1 | #comparison-container { 2 | position: absolute; 3 | bottom: 0; 4 | left: 50%; 5 | transform: translate3d(-50%, 100%, 0) scale(0.1); 6 | padding: 5px 20px; 7 | max-width: 40%; 8 | height: 160px; 9 | background: $background-color; 10 | box-shadow: rgba(0,0,0,0.2) 0 2px 6px 0; 11 | z-index: 100; 12 | 13 | transition: transform 1.5s ease; 14 | 15 | &.open { 16 | transform: translate3d(-50%, 0%, 0) scale(1.0); 17 | } 18 | 19 | & > h3 { 20 | text-overflow: ellipsis; 21 | text-align: center; 22 | white-space: nowrap; 23 | overflow: hidden; 24 | direction: rtl; 25 | margin: 10px auto; 26 | padding: 0 10px; 27 | } 28 | 29 | #comparison-table { 30 | border-spacing: 0; 31 | width: 100%; 32 | 33 | thead tr { 34 | font-weight: bold; 35 | } 36 | 37 | td { 38 | padding: 5px; 39 | } 40 | 41 | tbody { 42 | tr:nth-child(odd) { 43 | background: $table-row-background-color; 44 | } 45 | 46 | td { 47 | [class^="icon-"], [class*=" icon-"] { 48 | padding-right: 10px; 49 | } 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /js/ui/components/CheckboxComponent.js: -------------------------------------------------------------------------------- 1 | import * as PubSub from 'pubsub-js'; 2 | 3 | export class CheckboxComponent { 4 | 5 | constructor() { 6 | this.screenModeRadios = document.querySelectorAll('#control-group-screen input'); 7 | this.cameraModeRadios = document.querySelectorAll('#control-group-camera input'); 8 | 9 | this._bindEvents(); 10 | } 11 | 12 | _bindEvents() { 13 | for (let radio of this.screenModeRadios) { 14 | radio.addEventListener('change', (event) => { 15 | let fullscreenEnabled = event.target.value == 'full'; 16 | 17 | this._toggleCameraRadios(fullscreenEnabled); 18 | 19 | PubSub.publish('closeComparisonContainer'); 20 | PubSub.publish('fullSplitToggle', { enabled: fullscreenEnabled }); 21 | }); 22 | } 23 | 24 | for (let radio of this.cameraModeRadios) { 25 | radio.addEventListener('change', (event) => { 26 | let syncEnabled = event.target.value == 'sync'; 27 | PubSub.publish('synchronizeEnabledChange', { enabled: syncEnabled }); 28 | }); 29 | } 30 | } 31 | 32 | _toggleCameraRadios(enabled) { 33 | for (let radio of this.cameraModeRadios) { 34 | radio.disabled = enabled; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /test/drawer/test_SingleDrawer.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | import {SingleDrawer} from '../../js/drawer/SingleDrawer'; 4 | import * as Constants from '../../js/Constants'; 5 | import sinon from 'sinon'; 6 | import packers from 'binpacking'; 7 | 8 | // mock GrowingPacker because it's imported with script-tag 9 | SingleDrawer.prototype._getPacker = sinon.stub().returns(packers.GrowingPacker.prototype); 10 | 11 | var elements = require('../data/deltaTreeWithCalculatedGroundAreas.json'); 12 | 13 | describe('SingleDrawer', function () { 14 | it('should draw the correct amount of elements', function () { 15 | var drawer = new SingleDrawer(null, Constants.LEFT_SCREEN, {}); 16 | 17 | // stubbing 18 | SingleDrawer.prototype.drawBlock = sinon.stub().returns('yalla yalla'); 19 | 20 | drawer.drawElements(elements); 21 | sinon.assert.callCount(drawer.drawBlock, 8); // 5 files + 3 modules 22 | }); 23 | 24 | it('should draw the correct amount of elements', function () { 25 | var drawer = new SingleDrawer(null, Constants.RIGHT_SCREEN, {}); 26 | 27 | // stubbing 28 | SingleDrawer.prototype.drawBlock = sinon.stub().returns('yalla yalla'); 29 | 30 | drawer.drawElements(elements); 31 | sinon.assert.callCount(drawer.drawBlock, 8); // 5 files + 3 modules 32 | }); 33 | }); -------------------------------------------------------------------------------- /js/Config.js: -------------------------------------------------------------------------------- 1 | export var config = { 2 | DEBUG_MODE_ENABLED: false, 3 | 4 | // CODERADAR CONFIG 5 | BASE_URL: 'http://localhost:8080', 6 | USERNAME: 'radar', 7 | PASSWORD: 'Password12!', 8 | 9 | // DEFAULT METRIC MAPPING 10 | GROUND_AREA_METRIC_NAME: 'coderadar:size:sloc:java', 11 | HEIGHT_METRIC_NAME: 'coderadar:size:loc:java', 12 | COLOR_METRIC_NAME: 'coderadar:size:eloc:java', 13 | 14 | // VISUALIZATION SETTINGS 15 | GROUND_AREA_FACTOR: 0.1, 16 | HEIGHT_FACTOR: 0.1, 17 | GLOBAL_MAX_GROUND_AREA: 100, 18 | GLOBAL_MIN_GROUND_AREA: 1, 19 | GLOBAL_MAX_HEIGHT: 100, 20 | GLOBAL_MIN_HEIGHT: 1, 21 | BLOCK_SPACING: 5, 22 | DEFAULT_BLOCK_HEIGHT: 0.2, 23 | SCREEN_PADDING: 0, 24 | 25 | // CAMERA SETTINGS 26 | CAMERA_NEAR: 1, 27 | CAMERA_FAR: 100000, 28 | CAMERA_DISTANCE_TO_FOCUSSED_ELEMENT: 200, 29 | CAMERA_START_POSITION: { 30 | x: 1000, y: 1000, z: 1000 31 | }, 32 | CAMERA_ANIMATION_DURATION: 1500, 33 | 34 | // COLORS 35 | COLOR_HIERARCHY_RANGE: ['#cccccc', '#525252'], 36 | COLOR_HEATMAP_RANGE: ['#ffffff','#ffc905','#f78400','#e92100','#9b1909','#4f1609','#5d0000'], 37 | COLOR_CONNECTION: '#000000', 38 | 39 | COLOR_FIRST_COMMIT: '#0e8cf3', 40 | COLOR_SECOND_COMMIT: '#ffb100', 41 | 42 | COLOR_ADDED_FILE: '#49c35c', 43 | COLOR_DELETED_FILE: '#d90206', 44 | COLOR_UNCHANGED_FILE: '#cccccc' 45 | }; -------------------------------------------------------------------------------- /js/shape/BlockConnection.js: -------------------------------------------------------------------------------- 1 | import {config} from '../Config'; 2 | import * as Constants from '../Constants'; 3 | 4 | export class BlockConnection { 5 | constructor(fromElement, toElement) { 6 | var from = fromElement.position.clone(); 7 | from.x += fromElement.scale.x / 2; 8 | from.y += fromElement.scale.y; 9 | from.z += fromElement.scale.z / 2; 10 | 11 | var to = toElement.position.clone(); 12 | to.x += toElement.scale.x / 2; 13 | to.y += toElement.scale.y; 14 | to.z += toElement.scale.z / 2; 15 | 16 | var distance = from.distanceTo(to); 17 | 18 | var via = new THREE.Vector3((from.x + to.x) / 2, this._getHeightByDistance(distance), (from.z + to.z) / 2); 19 | 20 | var curve = new THREE.QuadraticBezierCurve3(from, via, to); 21 | 22 | var geometry = new THREE.Geometry(); 23 | geometry.vertices = curve.getPoints(50); 24 | var material = new THREE.LineBasicMaterial({ color: config.COLOR_CONNECTION }); 25 | this.curveObject = new THREE.Line(geometry, material); 26 | 27 | this.curveObject.userData = { 28 | type: Constants.ELEMENT_TYPE_CONNECTION, 29 | changeTypes: { 30 | moved: true 31 | } 32 | }; 33 | } 34 | 35 | getCurve() { 36 | return this.curveObject; 37 | } 38 | 39 | _getHeightByDistance(distance) { 40 | return 0.0001 * Math.pow(distance, 2) + 0.8 * distance + 30; 41 | } 42 | } -------------------------------------------------------------------------------- /js/service/MetricNameService.js: -------------------------------------------------------------------------------- 1 | export class MetricNameService { 2 | 3 | constructor() { 4 | this._metricNamesMap = { 5 | 'Lines of Code (LOC)': 'coderadar:size:loc:java', 6 | 'Source Lines of Code (SLOC)': 'coderadar:size:sloc:java', 7 | 'Effective Lines of Code (ELOC)': 'coderadar:size:eloc:java', 8 | 'MagicNumber': 'checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck', 9 | 'ReturnCount': 'checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.ReturnCountCheck', 10 | 'CyclomaticComplexity': 'checkstyle:com.puppycrawl.tools.checkstyle.checks.metrics.CyclomaticComplexityCheck', 11 | 'JavaNCSS': 'checkstyle:com.puppycrawl.tools.checkstyle.checks.metrics.JavaNCSSCheck', 12 | 'NPathComplexity': 'checkstyle:com.puppycrawl.tools.checkstyle.checks.metrics.NPathComplexityCheck', 13 | 'ExecutableStatementCount': 'checkstyle:com.puppycrawl.tools.checkstyle.checks.sizes.ExecutableStatementCountCheck' 14 | }; 15 | } 16 | 17 | getAll() { 18 | return this._metricNamesMap; 19 | } 20 | 21 | getMetricNameByShortName(shortName) { 22 | return this._metricNamesMap[shortName]; 23 | } 24 | 25 | getShortNameByFullName(fullName) { 26 | for (let shortName of Object.keys(this._metricNamesMap)) { 27 | if (this._metricNamesMap[shortName] == fullName) { 28 | return shortName; 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /test/domain/test_CommitMapper.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | import {CommitMapper} from '../../js/domain/CommitMapper'; 4 | import {Commit} from '../../js/domain/Commit'; 5 | 6 | var dummyResponse = require('../data/dummyCommitResponse.json'); 7 | 8 | describe('CommitMapper', function () { 9 | describe('mapAll', function () { 10 | it('should map data to Commit objects', function () { 11 | let mapper = new CommitMapper(dummyResponse); 12 | mapper.mapAll(); 13 | 14 | assert.equal(mapper.objects.length, 4); 15 | for (let obj of mapper.objects) { 16 | assert.ok(obj instanceof Commit); 17 | } 18 | }); 19 | }); 20 | 21 | describe('map', function () { 22 | it('should create a commit object from json data', function () { 23 | let mapper = new CommitMapper(dummyResponse); 24 | let commit = mapper.map({ 25 | "name": "b152859ca8d73f5c974c2264107fd0092af310d0", 26 | "author": "John Doe", 27 | "timestamp": 1485813773000, 28 | "analyzed": true 29 | }); 30 | 31 | assert.ok(commit instanceof Commit); 32 | assert.equal(commit.getName(), 'b152859ca8d73f5c974c2264107fd0092af310d0'); 33 | assert.equal(commit.getAuthor(), 'John Doe'); 34 | assert.equal(commit.getTimestamp(), 1485813773000); 35 | assert.equal(commit.getAnalyzed(), true); 36 | }); 37 | }); 38 | }); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var sourcemaps = require('gulp-sourcemaps'); 3 | var source = require('vinyl-source-stream'); 4 | var buffer = require('vinyl-buffer'); 5 | var browserify = require('browserify'); 6 | var watchify = require('watchify'); 7 | var babel = require('babelify'); 8 | 9 | function compile(watch) { 10 | var bundler = watchify( 11 | browserify( 12 | './js/main.js', {debug: true} 13 | ).transform(babel), 14 | { 15 | poll: true 16 | } 17 | ); 18 | 19 | function rebundle() { 20 | bundler.bundle() 21 | .on('error', function (err) { 22 | console.error(err); 23 | this.emit('end'); 24 | }) 25 | .pipe(source('build.js')) 26 | .pipe(buffer()) 27 | .pipe(sourcemaps.init({loadMaps: true})) 28 | .pipe(sourcemaps.write('./')) 29 | .pipe(gulp.dest('./build')); 30 | } 31 | 32 | if (watch) { 33 | bundler.on('update', function () { 34 | console.log('-> bundling... [' + new Date() + ']'); 35 | rebundle(); 36 | }); 37 | } 38 | 39 | rebundle(); 40 | } 41 | 42 | function watch() { 43 | return compile(true); 44 | } 45 | 46 | gulp.task('build', function () { 47 | return compile(); 48 | }); 49 | gulp.task('watch', function () { 50 | return watch(); 51 | }); 52 | gulp.task('test', function () { 53 | return watchify(); 54 | }); 55 | 56 | gulp.task('default', ['watch']); -------------------------------------------------------------------------------- /css/_stage.scss: -------------------------------------------------------------------------------- 1 | #stage { 2 | position: absolute; 3 | width: 100%; 4 | height: 100%; 5 | overflow: hidden; 6 | 7 | & > .vertical-line { 8 | position: absolute; 9 | top: 0; 10 | left: 100%; 11 | height: 100%; 12 | width: 2px; 13 | background: #535353; 14 | z-index: 98; 15 | 16 | transition: left 1s ease; 17 | } 18 | 19 | &.split { 20 | & > .vertical-line { 21 | left: 50%; 22 | } 23 | 24 | & > .loading-indicator-container { 25 | & > .left { 26 | left: 25%; 27 | } 28 | 29 | & > .right { 30 | left: 75%; 31 | } 32 | } 33 | 34 | canvas:last-child { 35 | opacity: 1; 36 | left: 50%; 37 | } 38 | } 39 | 40 | & > .loading-indicator-container { 41 | position: fixed; 42 | width: 100%; 43 | height: 100%; 44 | background: $transparent-background-mask-color; 45 | top: 70px; 46 | left: 0; 47 | z-index: 99; 48 | 49 | & > .left { 50 | left: 50%; 51 | } 52 | 53 | & > .right { 54 | left: 200%; 55 | } 56 | } 57 | 58 | canvas { 59 | position: absolute; 60 | cursor: -webkit-grab; 61 | transition: all 1s ease; 62 | 63 | &:first-child { 64 | left: 0; 65 | } 66 | 67 | &:last-child { 68 | opacity: 0; 69 | left: 100%; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /js/ui/components/CommitSelectionComponent.js: -------------------------------------------------------------------------------- 1 | import {AutocompleteComponent} from './AutocompleteComponent'; 2 | import * as PubSub from 'pubsub-js'; 3 | import * as Constants from '../../Constants'; 4 | 5 | export class CommitSelectionComponent extends AutocompleteComponent { 6 | 7 | constructor(componentElement, application, commitType) { 8 | super(componentElement); 9 | this._application = application; 10 | this._commitType = commitType; 11 | } 12 | 13 | _bindEvents() { 14 | super._bindEvents(); 15 | 16 | PubSub.subscribe('commitsLoaded', (eventName, args) => { 17 | let elements = []; 18 | for (let commit of args.commits) { 19 | elements.push({ 20 | value: commit.getName(), 21 | label: commit.getFormattedDatetime() + ', ' + commit.getAuthor() + ', ' + commit.getName() 22 | }); 23 | } 24 | 25 | this.setElements(elements); 26 | 27 | if (this._commitType == Constants.FIRST_COMMIT) { 28 | this.setSelection(this._application.leftCommitId); 29 | } else if (this._commitType == Constants.SECOND_COMMIT) { 30 | this.setSelection(this._application.rightCommitId); 31 | } else { 32 | throw new Error(`Unknown commit type ${this._commitType}!`); 33 | } 34 | }); 35 | } 36 | 37 | _onSelection(args) { 38 | PubSub.publish('commitChange', { 39 | commitType: this._commitType, 40 | commitId: args.selection 41 | }); 42 | } 43 | } -------------------------------------------------------------------------------- /css/_tooltip.scss: -------------------------------------------------------------------------------- 1 | #tooltip { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | background-color: $tooltip-background-color; 6 | color: $background-color; 7 | padding: 10px; 8 | font-size: 14px; 9 | max-width: 300px; 10 | z-index: 99; 11 | 12 | opacity: 0; 13 | transition: opacity 0.75s ease; 14 | 15 | &.visible { 16 | opacity: 1; 17 | } 18 | 19 | & > .element-name { 20 | text-overflow: ellipsis; 21 | text-align: left; 22 | white-space: nowrap; 23 | overflow: hidden; 24 | direction: rtl; 25 | padding-bottom: 10px; 26 | } 27 | 28 | & table { 29 | & td.metric-name-column { 30 | font-weight: bold; 31 | } 32 | } 33 | } 34 | 35 | .tooltip-toggle { 36 | cursor: pointer; 37 | position: relative; 38 | 39 | &::before { 40 | position: absolute; 41 | top: 20px; 42 | left: -80px; 43 | background-color: $tooltip-background-color; 44 | color: $background-color; 45 | content: attr(aria-label); 46 | padding: 10px; 47 | text-transform: none; 48 | transition: all 0.5s ease; 49 | width: 160px; 50 | z-index: 1; 51 | } 52 | 53 | &::before, 54 | &::after { 55 | color: $background-color; 56 | font-size: 14px; 57 | opacity: 0; 58 | pointer-events: none; 59 | text-align: center; 60 | } 61 | 62 | &:focus::before, 63 | &:focus::after, 64 | &:hover::before, 65 | &:hover::after { 66 | opacity: 1; 67 | transition: all 0.75s ease; 68 | } 69 | } -------------------------------------------------------------------------------- /js/util/DatetimeFormatter.js: -------------------------------------------------------------------------------- 1 | export class DatetimeFormatter { 2 | 3 | constructor() { 4 | this.dateSeparator = '.'; 5 | this.timeSeparator = ':'; 6 | this.datetimeSeparator = ' '; 7 | this.label = undefined; 8 | this.showSeconds = true; 9 | } 10 | 11 | withDateSeparator(dateSeparator) { 12 | this.dateSeparator = dateSeparator; 13 | return this; 14 | } 15 | 16 | withTimeSeparator(timeSeparator) { 17 | this.timeSeparator = timeSeparator; 18 | return this; 19 | } 20 | 21 | withDatetimeSeparator(datetimeSeparator) { 22 | this.datetimeSeparator = datetimeSeparator; 23 | return this; 24 | } 25 | 26 | withLabel(label) { 27 | this.label = label; 28 | return this; 29 | } 30 | 31 | withShowSeconds(showSeconds) { 32 | this.showSeconds = showSeconds; 33 | return this; 34 | } 35 | 36 | formatDate(date = new Date()) { 37 | let stringParts = [ 38 | date.getDate() < 10 ? '0' + date.getDate() : date.getDate(), 39 | this.dateSeparator, 40 | date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1, 41 | this.dateSeparator, 42 | date.getFullYear(), 43 | this.datetimeSeparator, 44 | date.getHours() < 10 ? '0' + date.getHours() : date.getHours(), 45 | this.timeSeparator, 46 | date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() 47 | ]; 48 | 49 | if (this.showSeconds) { 50 | stringParts.push(this.timeSeparator); 51 | stringParts.push(date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()); 52 | } 53 | 54 | if (this.label) { 55 | stringParts.push(' '); 56 | stringParts.push(this.label); 57 | } 58 | 59 | return stringParts.join(''); 60 | } 61 | } -------------------------------------------------------------------------------- /css/_control-panel.scss: -------------------------------------------------------------------------------- 1 | #control-panel { 2 | position: absolute; 3 | top: 0; 4 | width: 100%; 5 | height: 70px; 6 | background: #fff; 7 | box-shadow: rgba(0,0,0,0.2) 0 2px 6px 0; 8 | z-index: 101; 9 | 10 | & > .control-container { 11 | position: relative; 12 | float: left; 13 | margin: 10px; 14 | height: 50px; 15 | 16 | &:last-child { 17 | float: right; 18 | } 19 | 20 | .control-item { 21 | display: inline-block; 22 | padding: 5px; 23 | } 24 | 25 | .control-label { 26 | font-size: 12px; 27 | display: block; 28 | margin-top: 5px; 29 | } 30 | 31 | .checkbox-container { 32 | position: relative; 33 | } 34 | 35 | .checkbox-icon { 36 | position: absolute; 37 | top: 0; 38 | width: 30px; 39 | height: 30px; 40 | } 41 | 42 | .checkbox-icon.left { 43 | left: 0; 44 | } 45 | 46 | .checkbox-icon.right { 47 | right: 0; 48 | } 49 | 50 | #search-input { 51 | padding-right: 30px; 52 | } 53 | 54 | & button { 55 | height: 100%; 56 | } 57 | } 58 | 59 | .divider { 60 | height: 60px; 61 | border-left: 1px solid $state-inactive-color; 62 | float: left; 63 | margin: 5px 10px; 64 | } 65 | 66 | #search-auto-complete-wrapper { 67 | i { 68 | position: absolute; 69 | padding: 0 5px; 70 | background: #fff; 71 | line-height: 30px; 72 | color: #ccc; 73 | } 74 | 75 | input[type=text] { 76 | text-indent: 28px; 77 | } 78 | } 79 | 80 | #render-calls { 81 | position: absolute; 82 | top: 0; 83 | right: 0; 84 | font-size: 10px; 85 | z-index: 10000; 86 | } 87 | } -------------------------------------------------------------------------------- /js/ui/components/ContextMenuComponent.js: -------------------------------------------------------------------------------- 1 | import * as PubSub from 'pubsub-js'; 2 | 3 | export class ContextMenuComponent { 4 | 5 | constructor() { 6 | this._contextMenu = undefined; 7 | this._clickedElementName = undefined; 8 | 9 | this.menuItems = [ 10 | { label: 'Kindelemente ein-/ausblenden', handler: this._handleToggleChildElements.bind(this)} 11 | ]; 12 | 13 | this._createContextMenu(); 14 | this._bindEvents(); 15 | } 16 | 17 | _createContextMenu() { 18 | var contextMenu = document.createElement('div'); 19 | contextMenu.classList.add('context-menu'); 20 | 21 | var ul = document.createElement('ul'); 22 | 23 | var li; 24 | for (let item of this.menuItems) { 25 | li = document.createElement('li'); 26 | li.innerHTML = item.label; 27 | li.addEventListener('click', item.handler); 28 | ul.appendChild(li); 29 | } 30 | 31 | contextMenu.appendChild(ul); 32 | document.body.appendChild(contextMenu); 33 | 34 | this._contextMenu = contextMenu; 35 | } 36 | 37 | _showContextMenu(position) { 38 | this._contextMenu.style.top = position.y + 'px'; 39 | this._contextMenu.style.left = position.x + 'px'; 40 | this._contextMenu.style.display = 'block'; 41 | } 42 | 43 | _hideContextMenu() { 44 | this._contextMenu.style.display = 'none'; 45 | } 46 | 47 | _bindEvents() { 48 | document.addEventListener('click', (event) => { 49 | this._hideContextMenu(); 50 | }); 51 | 52 | PubSub.subscribe('elementRightClicked', (eventName, args) => { 53 | this._clickedElementName = args.elementName; 54 | this._showContextMenu(args.position); 55 | }); 56 | } 57 | 58 | _handleToggleChildElements() { 59 | PubSub.publish('toggleChildElements', { 60 | elementName: this._clickedElementName 61 | }); 62 | } 63 | } -------------------------------------------------------------------------------- /js/ui/components/PopoverComponent.js: -------------------------------------------------------------------------------- 1 | export class PopoverComponent { 2 | 3 | constructor(componentElement) { 4 | this._componentElement = componentElement; 5 | 6 | this.toggleButton = componentElement.querySelector('.popover-toggle-btn'); 7 | this.popoverFloatingWrapper = componentElement.querySelector('.popover-floating-wrapper'); 8 | } 9 | 10 | _bindEvents() { 11 | document.addEventListener('click', (event) => { 12 | var path = event.path; 13 | var close = true; 14 | for (var obj of path) { 15 | if (obj.id && obj.id == this._componentElement.id) { 16 | close = false; 17 | break; 18 | } 19 | } 20 | 21 | if (close) { 22 | this._hidePopoverContainer(); 23 | this._setButtonStateInactive(); 24 | } 25 | }); 26 | 27 | this.toggleButton.addEventListener('click', () => { 28 | this._toggleDimensionSelectionContainerVisibility(); 29 | this._toggleButtonActiveState(); 30 | }); 31 | } 32 | 33 | _toggleDimensionSelectionContainerVisibility() { 34 | if (this.popoverFloatingWrapper.style.display == 'block') { 35 | this._hidePopoverContainer(); 36 | } else { 37 | this._showPopoverContainer(); 38 | } 39 | } 40 | 41 | _toggleButtonActiveState() { 42 | if (this.toggleButton.classList.contains('active')) { 43 | this._setButtonStateInactive() 44 | } else { 45 | this._setButtonStateActive(); 46 | } 47 | } 48 | 49 | _showPopoverContainer() { 50 | this.popoverFloatingWrapper.style.display = 'block'; 51 | } 52 | 53 | _hidePopoverContainer() { 54 | this.popoverFloatingWrapper.style.display = 'none'; 55 | } 56 | 57 | _setButtonStateActive() { 58 | this.toggleButton.classList.add('active'); 59 | } 60 | 61 | _setButtonStateInactive() { 62 | this.toggleButton.classList.remove('active'); 63 | } 64 | } -------------------------------------------------------------------------------- /test/data/dummyCommitResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "_embedded": { 3 | "commitResourceList": [ 4 | { 5 | "name": "b152859ca8d73f5c974c2264107fd0092af310d0", 6 | "author": "John Doe", 7 | "timestamp": 1485813773000, 8 | "analyzed": true, 9 | "_links": { 10 | "project": { 11 | "href": "http://localhost:8080/projects/1" 12 | } 13 | } 14 | }, 15 | { 16 | "name": "2beb1d1d720c1256cedfdf483331f65861079705", 17 | "author": "John Doe", 18 | "timestamp": 1485726067000, 19 | "analyzed": true, 20 | "_links": { 21 | "project": { 22 | "href": "http://localhost:8080/projects/1" 23 | } 24 | } 25 | }, 26 | { 27 | "name": "cbba0662f48f139da4973cc610bd4caa6213ed08", 28 | "author": "John Doe", 29 | "timestamp": 1485633721000, 30 | "analyzed": true, 31 | "_links": { 32 | "project": { 33 | "href": "http://localhost:8080/projects/1" 34 | } 35 | } 36 | }, 37 | { 38 | "name": "6ffebfad9e79dfa4ddfa7d043d84eb424a28c0cd", 39 | "author": "John Doe", 40 | "timestamp": 1485561434000, 41 | "analyzed": true, 42 | "_links": { 43 | "project": { 44 | "href": "http://localhost:8080/projects/1" 45 | } 46 | } 47 | } 48 | ] 49 | }, 50 | "_links": { 51 | "self": { 52 | "href": "http://localhost:8080/projects/1/commits?page=0&size=999" 53 | } 54 | }, 55 | "page": { 56 | "size": 999, 57 | "totalElements": 4, 58 | "totalPages": 1, 59 | "number": 0 60 | } 61 | } -------------------------------------------------------------------------------- /js/ui/components/ScreenshotComponent.js: -------------------------------------------------------------------------------- 1 | import {DatetimeFormatter} from '../../util/DatetimeFormatter'; 2 | 3 | export class ScreenshotComponent { 4 | 5 | constructor(application) { 6 | this._application = application; 7 | 8 | this.screenshotButton = document.querySelector('#screenshot-btn'); 9 | 10 | this._bindEvents(); 11 | } 12 | 13 | _bindEvents() { 14 | this.screenshotButton.addEventListener('click', () => { 15 | var downloads = []; 16 | 17 | // decide if we need to download one or two screenshots 18 | if (!this._application.getIsFullscreen()) { 19 | downloads.push({ commitInfo: this._application.getLeftScreen().getCommitId().substr(0, 5), renderer: this._application.getLeftScreen().getRenderer() }); 20 | downloads.push({ commitInfo: this._application.getRightScreen().getCommitId().substr(0, 5), renderer: this._application.getRightScreen().getRenderer() }); 21 | } else { 22 | downloads.push({ 23 | commitInfo: this._application.getLeftScreen().getCommitId().substr(0, 5) + '_' + this._application.getRightScreen().getCommitId().substr(0, 5), 24 | renderer: this._application.getLeftScreen().getRenderer() 25 | }); 26 | } 27 | 28 | for (let download of downloads) { 29 | var imgFromCanvas = download.renderer.domElement.toDataURL('image/png'); 30 | var pngFile = imgFromCanvas.replace(/^data:image\/png/, 'data:application/octet-stream'); 31 | 32 | var link = document.querySelector('#screenshot-link'); 33 | link.download = this._getDateTimeAsString() + '_' + download.commitInfo + '.png'; 34 | link.href = pngFile; 35 | link.click(); // execute hidden link to trigger download 36 | } 37 | }); 38 | } 39 | 40 | _getDateTimeAsString() { 41 | return new DatetimeFormatter() 42 | .withDateSeparator('-') 43 | .withTimeSeparator('-') 44 | .withDatetimeSeparator('_') 45 | .withShowSeconds(false) 46 | .formatDate(); 47 | } 48 | } -------------------------------------------------------------------------------- /css/_autocomplete-component.scss: -------------------------------------------------------------------------------- 1 | .autocomplete-wrapper { 2 | position: relative; 3 | 4 | .button-container { 5 | position: absolute; 6 | top: 0; 7 | right: 0; 8 | background: $background-color; 9 | padding: 0 5px; 10 | 11 | .show-suggestions-button { 12 | display: inline-block; 13 | height: 100%; 14 | line-height: 32px; 15 | cursor: pointer; 16 | color: $state-inactive-color; 17 | 18 | &:hover { 19 | color: $state-active-color; 20 | } 21 | } 22 | 23 | .clear-button { 24 | display: inline-block; 25 | height: 100%; 26 | line-height: 32px; 27 | cursor: pointer; 28 | color: $state-inactive-color; 29 | 30 | &:hover { 31 | color: $state-active-color; 32 | } 33 | } 34 | } 35 | 36 | .suggestions-container { 37 | position: absolute; 38 | left: 0; 39 | top: 32px; 40 | min-width: 100%; 41 | background: $background-color; 42 | box-sizing: border-box; 43 | max-height: 250px; 44 | overflow-y: auto; 45 | white-space: nowrap; 46 | z-index: 1; 47 | 48 | box-shadow: rgba(0,0,0,0.2) 0 2px 6px 0; 49 | 50 | .suggestions-list { 51 | padding: 0; 52 | margin: 0; 53 | list-style: none; 54 | 55 | &.rtl > li { 56 | direction: rtl; 57 | } 58 | 59 | & > li { 60 | cursor: pointer; 61 | padding: 10px 15px; 62 | max-width: 400px; 63 | overflow: hidden; 64 | text-overflow: ellipsis; 65 | 66 | &.inactive { 67 | color: $state-inactive-color; 68 | } 69 | 70 | &.selected { 71 | color: $state-active-color; 72 | background: $state-hover-color; 73 | } 74 | 75 | &:hover { 76 | color: $state-active-color; 77 | background: $state-hover-color; 78 | } 79 | } 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /js/ui/UserInterface.js: -------------------------------------------------------------------------------- 1 | import {SearchComponent} from './components/SearchComponent'; 2 | import {LegendComponent} from './components/LegendComponent'; 3 | import {CommitSelectionComponent} from './components/CommitSelectionComponent'; 4 | import {CheckboxComponent} from './components/CheckboxComponent'; 5 | import {ComparisonContainerComponent} from './components/ComparisonContainerComponent'; 6 | import {DimensionSelectionComponent} from './components/DimensionSelectionComponent'; 7 | import {FilterComponent} from './components/FilterComponent'; 8 | import {ContextMenuComponent} from './components/ContextMenuComponent'; 9 | import {ScreenshotComponent} from './components/ScreenshotComponent'; 10 | import * as Constants from '../Constants'; 11 | 12 | export class UserInterface { 13 | 14 | constructor(application) { 15 | let searchComponentElement = document.querySelector('#search-auto-complete-wrapper'); 16 | let firstCommitComponentElement = document.querySelector('#first-commit-auto-complete-wrapper'); 17 | let secondCommitComponentElement = document.querySelector('#second-commit-auto-complete-wrapper'); 18 | let dimensionSelectionComponentElement = document.querySelector('#mapping-component'); 19 | let filterComponentElement = document.querySelector('#filter-component'); 20 | 21 | let searchComponent = new SearchComponent(searchComponentElement, application); 22 | let firstCommitSelectionComponent = new CommitSelectionComponent(firstCommitComponentElement, application, Constants.FIRST_COMMIT); 23 | let secondCommitSelectionComponent = new CommitSelectionComponent(secondCommitComponentElement, application, Constants.SECOND_COMMIT); 24 | let checkboxComponent = new CheckboxComponent(); 25 | let comparisonContainerComponent = new ComparisonContainerComponent(application); 26 | 27 | let dimensionSelectionComponent = new DimensionSelectionComponent(dimensionSelectionComponentElement); 28 | let filterComponent = new FilterComponent(filterComponentElement); 29 | 30 | let contextMenuComponent = new ContextMenuComponent(); 31 | let screenshotComponent = new ScreenshotComponent(application); 32 | 33 | this.legendComponent = new LegendComponent(); 34 | } 35 | 36 | getLegendComponent() { 37 | return this.legendComponent; 38 | } 39 | 40 | showLoadingIndicator() { 41 | document.querySelector('.loading-indicator-container').style.display = 'block'; 42 | } 43 | 44 | hideLoadingIndicator() { 45 | document.querySelector('.loading-indicator-container').style.display = 'none'; 46 | } 47 | } -------------------------------------------------------------------------------- /css/_form-elements.scss: -------------------------------------------------------------------------------- 1 | input[type="checkbox"], input[type="radio"] { 2 | opacity: 0; 3 | z-index: 1; 4 | 5 | & + label { 6 | cursor: pointer; 7 | display: inline-block; 8 | vertical-align: middle; 9 | position: relative; 10 | padding-left: 5px; 11 | } 12 | 13 | &:disabled + label { 14 | opacity: 0.65; 15 | 16 | &::before { 17 | background-color: $state-disabled-color; 18 | cursor: not-allowed; 19 | } 20 | } 21 | } 22 | 23 | input[type="radio"] { 24 | & + label::before, & + label::after { 25 | display: inline-block; 26 | position: absolute; 27 | margin-left: -20px; 28 | border-radius: 50%; 29 | } 30 | 31 | & + label::before { 32 | content: ""; 33 | width: 17px; 34 | height: 17px; 35 | left: 0; 36 | border: 1px solid $border-color; 37 | background-color: $background-color; 38 | transition: border 0.15s ease-in-out; 39 | } 40 | 41 | & + label::after { 42 | content: " "; 43 | width: 11px; 44 | height: 11px; 45 | left: 4px; 46 | top: 4px; 47 | background-color: $highlight-color; 48 | transform: scale(0, 0); 49 | transition: transform 0.1s ease-in-out; 50 | } 51 | 52 | &:checked + label::before { 53 | border-color: #aaa; 54 | } 55 | 56 | &:checked + label::after { 57 | transform: scale(1, 1); 58 | } 59 | } 60 | 61 | input[type="checkbox"] { 62 | & + label::before, & + label::after { 63 | display: inline-block; 64 | position: absolute; 65 | left: 0; 66 | margin-left: -20px; 67 | } 68 | 69 | & + label::before { 70 | content: ""; 71 | width: 17px; 72 | height: 17px; 73 | border: 1px solid $border-color; 74 | border-radius: 3px; 75 | background-color: $background-color; 76 | -webkit-transition: border 0.15s ease-in-out, color 0.15s ease-in-out; 77 | -o-transition: border 0.15s ease-in-out, color 0.15s ease-in-out; 78 | transition: border 0.15s ease-in-out, color 0.15s ease-in-out; 79 | } 80 | 81 | & + label::after { 82 | width: 16px; 83 | height: 16px; 84 | top: 0; 85 | padding-left: 4px; 86 | padding-top: 4px; 87 | font-size: 11px; 88 | } 89 | 90 | &:checked + label::before { 91 | background-color: $highlight-color; 92 | border-color: $highlight-color; 93 | } 94 | 95 | &:checked + label::after { 96 | color: $background-color; 97 | font-family: "icomoon"; 98 | content: "\e913"; 99 | } 100 | } -------------------------------------------------------------------------------- /js/ui/components/DimensionSelectionComponent.js: -------------------------------------------------------------------------------- 1 | import {MetricNameService} from '../../service/MetricNameService'; 2 | import {PopoverComponent} from '../components/PopoverComponent'; 3 | import {config} from '../../Config'; 4 | import * as Constants from '../../Constants'; 5 | import * as PubSub from 'pubsub-js'; 6 | 7 | export class DimensionSelectionComponent extends PopoverComponent { 8 | 9 | constructor(componentElement) { 10 | super(componentElement); 11 | 12 | this.metricNameService = new MetricNameService(); 13 | 14 | this.heightDimensionSelect = componentElement.querySelector('#height-metric-name'); 15 | this.groundAreaDimensionSelect = componentElement.querySelector('#ground-area-metric-name'); 16 | this.colorDimensionSelect = componentElement.querySelector('#color-metric-name'); 17 | 18 | this._fillDropdowns(this.heightDimensionSelect); 19 | this._fillDropdowns(this.groundAreaDimensionSelect); 20 | this._fillDropdowns(this.colorDimensionSelect); 21 | 22 | this._setSelectedOptions(); 23 | 24 | this._bindEvents(); 25 | } 26 | 27 | _bindEvents() { 28 | super._bindEvents(); 29 | 30 | this.heightDimensionSelect.addEventListener('change', function() { 31 | PubSub.publish('dimensionChange', { 32 | dimension: Constants.HEIGHT_DIMENSION, 33 | metricName: this.value 34 | }); 35 | }); 36 | 37 | this.groundAreaDimensionSelect.addEventListener('change', function() { 38 | PubSub.publish('dimensionChange', { 39 | dimension: Constants.GROUNDAREA_DIMENSION, 40 | metricName: this.value 41 | }); 42 | }); 43 | 44 | this.colorDimensionSelect.addEventListener('change', function() { 45 | PubSub.publish('dimensionChange', { 46 | dimension: Constants.COLOR_DIMENSION, 47 | metricName: this.value 48 | }); 49 | }); 50 | } 51 | 52 | _fillDropdowns(selectElement) { 53 | var metricNames = this.metricNameService.getAll(); 54 | for (let shortName of Object.keys(metricNames)) { 55 | var optionEl = document.createElement('option'); 56 | optionEl.innerHTML = shortName; 57 | optionEl.value = shortName; 58 | selectElement.appendChild(optionEl); 59 | } 60 | } 61 | 62 | _setSelectedOptions() { 63 | this.heightDimensionSelect.value = this.metricNameService.getShortNameByFullName(config.HEIGHT_METRIC_NAME); 64 | this.groundAreaDimensionSelect.value = this.metricNameService.getShortNameByFullName(config.GROUND_AREA_METRIC_NAME); 65 | this.colorDimensionSelect.value = this.metricNameService.getShortNameByFullName(config.COLOR_METRIC_NAME); 66 | } 67 | } -------------------------------------------------------------------------------- /css/_icon-font.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Generated by https://icomoon.io/ 3 | */ 4 | 5 | @font-face { 6 | font-family: 'icomoon'; 7 | src: url('../fonts/icomoon.eot?cu25y4'); 8 | src: url('../fonts/icomoon.eot?cu25y4#iefix') format('embedded-opentype'), 9 | url('../fonts/icomoon.ttf?cu25y4') format('truetype'), 10 | url('../fonts/icomoon.woff?cu25y4') format('woff'), 11 | url('../fonts/icomoon.svg?cu25y4#icomoon') format('svg'); 12 | font-weight: normal; 13 | font-style: normal; 14 | } 15 | 16 | [class^="icon-"], [class*=" icon-"] { 17 | /* use !important to prevent issues with browser extensions that change fonts */ 18 | font-family: 'icomoon' !important; 19 | speak: none; 20 | font-style: normal; 21 | font-weight: normal; 22 | font-variant: normal; 23 | text-transform: none; 24 | line-height: 1; 25 | 26 | /* Better Font Rendering =========== */ 27 | -webkit-font-smoothing: antialiased; 28 | -moz-osx-font-smoothing: grayscale; 29 | } 30 | 31 | .icon-square:before { 32 | content: "\e906"; 33 | } 34 | .icon-path:before { 35 | content: "\e906"; 36 | } 37 | .icon-vector:before { 38 | content: "\e906"; 39 | } 40 | .icon-cubes:before { 41 | content: "\e900"; 42 | } 43 | .icon-cube:before { 44 | content: "\e904"; 45 | } 46 | .icon-caret-up:before { 47 | content: "\e911"; 48 | } 49 | .icon-caret-down:before { 50 | content: "\e915"; 51 | } 52 | .icon-caret-right:before { 53 | content: "\e916"; 54 | } 55 | .icon-arrows-h:before { 56 | content: "\e901"; 57 | } 58 | .icon-arrows-v:before { 59 | content: "\e902"; 60 | } 61 | .icon-close2:before { 62 | content: "\e907"; 63 | } 64 | .icon-keyboard_arrow_up:before { 65 | content: "\e90f"; 66 | } 67 | .icon-keyboard_arrow_down:before { 68 | content: "\e914"; 69 | } 70 | .icon-paintcan:before { 71 | content: "\e905"; 72 | } 73 | .icon-checkmark:before { 74 | content: "\e913"; 75 | } 76 | .icon-tick:before { 77 | content: "\e913"; 78 | } 79 | .icon-correct:before { 80 | content: "\e913"; 81 | } 82 | .icon-accept:before { 83 | content: "\e913"; 84 | } 85 | .icon-ok:before { 86 | content: "\e913"; 87 | } 88 | .icon-info:before { 89 | content: "\e912"; 90 | } 91 | .icon-information:before { 92 | content: "\e912"; 93 | } 94 | .icon-stats-bars:before { 95 | content: "\e903"; 96 | } 97 | .icon-stats:before { 98 | content: "\e903"; 99 | } 100 | .icon-statistics:before { 101 | content: "\e903"; 102 | } 103 | .icon-chart:before { 104 | content: "\e903"; 105 | } 106 | .icon-image:before { 107 | content: "\e90e"; 108 | } 109 | .icon-search:before { 110 | content: "\e986"; 111 | } 112 | .icon-enlarge2:before { 113 | content: "\e98b"; 114 | } 115 | .icon-cog:before { 116 | content: "\e994"; 117 | } 118 | .icon-download2:before { 119 | content: "\e9c5"; 120 | } 121 | .icon-download3:before { 122 | content: "\e9c7"; 123 | } 124 | .icon-checkbox-unchecked:before { 125 | content: "\ea53"; 126 | } 127 | .icon-filter:before { 128 | content: "\ea5b"; 129 | } -------------------------------------------------------------------------------- /js/ui/components/LegendComponent.js: -------------------------------------------------------------------------------- 1 | import {config} from '../../Config'; 2 | import {MetricNameService} from '../../service/MetricNameService'; 3 | import * as PubSub from 'pubsub-js'; 4 | 5 | export class LegendComponent { 6 | 7 | constructor() { 8 | this.legendItemCommit1 = document.querySelector('#legend-item-commit-1'); 9 | this.legendItemCommit2 = document.querySelector('#legend-item-commit-2'); 10 | this.legendItemColorCode = document.querySelector('#legend-item-color-code'); 11 | this.legendItemAddedFiles = document.querySelector('#legend-item-added-files'); 12 | this.legendItemDeletedFiles = document.querySelector('#legend-item-deleted-files'); 13 | this.legendItemUnchangedFiles = document.querySelector('#legend-item-unchanged-files'); 14 | 15 | this.metricNameService = new MetricNameService(); 16 | 17 | this.setColorCode(); 18 | this.setCommitColors(); 19 | this.setAddedDeletedUnchangedColors(); 20 | 21 | this._bindEvents(); 22 | } 23 | 24 | setCommitColors() { 25 | this.legendItemCommit1.querySelector('.legend-color').style.background = config.COLOR_FIRST_COMMIT; 26 | this.legendItemCommit2.querySelector('.legend-color').style.background = config.COLOR_SECOND_COMMIT; 27 | } 28 | 29 | setAddedDeletedUnchangedColors() { 30 | this.legendItemAddedFiles.querySelector('.legend-color').style.background = config.COLOR_ADDED_FILE; 31 | this.legendItemDeletedFiles.querySelector('.legend-color').style.background = config.COLOR_DELETED_FILE; 32 | this.legendItemUnchangedFiles.querySelector('.legend-color').style.background = config.COLOR_UNCHANGED_FILE; 33 | } 34 | 35 | setColorCode() { 36 | this.legendItemColorCode.querySelector('.legend-label').innerHTML = this.metricNameService.getShortNameByFullName(config.COLOR_METRIC_NAME); 37 | } 38 | 39 | _showCommitItems() { 40 | this.legendItemCommit1.style.display = 'inline-block'; 41 | this.legendItemCommit2.style.display = 'inline-block'; 42 | } 43 | 44 | _hideCommitItems() { 45 | this.legendItemCommit1.style.display = 'none'; 46 | this.legendItemCommit2.style.display = 'none'; 47 | } 48 | 49 | _showAddedDeletedUnchangedFilesItems() { 50 | this.legendItemAddedFiles.style.display = 'inline-block'; 51 | this.legendItemDeletedFiles.style.display = 'inline-block'; 52 | this.legendItemUnchangedFiles.style.display = 'inline-block'; 53 | } 54 | 55 | _hideAddedDeletedUnchangedFilesItems() { 56 | this.legendItemAddedFiles.style.display = 'none'; 57 | this.legendItemDeletedFiles.style.display = 'none'; 58 | this.legendItemUnchangedFiles.style.display = 'none'; 59 | } 60 | 61 | _showColorCodeItem() { 62 | this.setColorCode(); 63 | this.legendItemColorCode.style.display = 'inline-block'; 64 | } 65 | 66 | _hideColorCodeItem() { 67 | this.legendItemColorCode.style.display = 'none'; 68 | } 69 | 70 | _bindEvents() { 71 | PubSub.subscribe('fullSplitToggle', (eventName, args) => { 72 | if (args.enabled) { 73 | this._showCommitItems(); 74 | this._showAddedDeletedUnchangedFilesItems(); 75 | 76 | this._hideColorCodeItem(); 77 | } else { 78 | this._showColorCodeItem(); 79 | 80 | this._hideCommitItems(); 81 | this._hideAddedDeletedUnchangedFilesItems(); 82 | } 83 | }); 84 | } 85 | } -------------------------------------------------------------------------------- /scripts/dummyDataGeneratorScript.js: -------------------------------------------------------------------------------- 1 | var MIN_MODULE_COUNT = 25; 2 | var MAX_MODULE_COUNT = 25; 3 | var MIN_FILE_COUNT = 25; 4 | var MAX_FILE_COUNT = 25; 5 | var CHANCE_TO_CREATE_SUBMODULE = 50; 6 | 7 | var LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); 8 | for (var k = 0; k < 2; k++) { 9 | for (var j = 0; j < 10; j++) { 10 | LETTERS.push(LETTERS[k] + LETTERS[j]); 11 | } 12 | } 13 | 14 | var json = []; 15 | var submoduleCounter = 0; 16 | var subsubmoduleCounter = 0; 17 | for (var i = 0; i < random(MIN_MODULE_COUNT, MAX_MODULE_COUNT); i++) { 18 | var module = createModule(i); 19 | addFilesToModule(module); 20 | 21 | if (random(1, 100) >= 100 - CHANCE_TO_CREATE_SUBMODULE) { 22 | var submodule = createModule(i, module.name, submoduleCounter++); 23 | addFilesToModule(submodule); 24 | module.children.push(submodule); 25 | 26 | if (random(1, 10000) >= 100 - CHANCE_TO_CREATE_SUBMODULE) { 27 | var subsubmodule = createModule(i, submodule.name, subsubmoduleCounter++); 28 | addFilesToModule(subsubmodule); 29 | submodule.children.push(subsubmodule); 30 | } 31 | } 32 | 33 | json.push(module); 34 | } 35 | 36 | var root = { 37 | "name": "root", 38 | "type": "MODULE", 39 | "children": json 40 | }; 41 | 42 | // console.clear(); 43 | // console.log(root); 44 | console.log(JSON.stringify(root)); 45 | 46 | function addFilesToModule(module) { 47 | for (var i = 0; i < random(MIN_FILE_COUNT, MAX_FILE_COUNT); i++) { 48 | module.children.push(createFile(i, module.name)); 49 | } 50 | } 51 | 52 | function createModule(index, parentName, submoduleCounter) { 53 | var module = {}; 54 | module.name = parentName ? parentName + '/' : ''; 55 | module.name += parentName ? 'Submodule' + LETTERS[submoduleCounter] : 'Module' + LETTERS[index]; 56 | 57 | module.type = 'MODULE'; 58 | module.children = []; 59 | return module; 60 | } 61 | 62 | function createFile(index, moduleName) { 63 | var added = false; 64 | var deleted = false; 65 | 66 | var chanceToBeAddedOrDeleted = random(0, 100); 67 | if (chanceToBeAddedOrDeleted <= 20) { 68 | added = true; 69 | } else if (chanceToBeAddedOrDeleted <= 40) { 70 | deleted = true; 71 | } 72 | 73 | var file = {}; 74 | file.name = moduleName + '/Class' + LETTERS[index]; 75 | file.type = 'FILE'; 76 | file.children = []; 77 | file.commit1Metrics = added ? null : { 78 | "coderadar:size:loc:java": random(10, 800), 79 | "coderadar:size:sloc:java": random(1, 80), 80 | "coderadar:size:eloc:java": random(0, 20) 81 | }; 82 | file.commit2Metrics = deleted ? null : { 83 | "coderadar:size:loc:java": random(10, 800), 84 | "coderadar:size:sloc:java": random(1, 80), 85 | "coderadar:size:eloc:java": random(0, 20) 86 | }; 87 | 88 | // chance that file hasn't changed 89 | if (random(0, 100) <= 70 && file.commit1Metrics != null && file.commit2Metrics != null) { 90 | file.commit2Metrics = file.commit1Metrics; 91 | } 92 | 93 | file.changes = { 94 | renamed: false, 95 | modified: file.commit1Metrics != file.commit2Metrics, 96 | added: added, 97 | deleted: deleted 98 | }; 99 | file.renamedFrom = null; 100 | file.renamedTo = null; 101 | return file; 102 | } 103 | 104 | function random(min, max) { 105 | return Math.floor(Math.random() * (max - min + 1)) + min; 106 | } 107 | 108 | function randomBool() { 109 | return random(0, 1) == 1; 110 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Preface 2 | The whole application was rewritten using **Angular 6** - checkout the [angular-ui](https://github.com/pschild/CodeRadarVisualization/tree/angular-ui) branch and the **[demo](https://pschild.github.io/CodeRadarVisualization/)**. 3 | Feel free to contribute :-) 4 | 5 | # Visualization of software quality and evolution 6 | 7 | ## Background 8 | In the context of my bachelor thesis, I developed a prototypic application that can **visualize the structure and quality of software**. It has been developed with the help of web technologies HTML, CSS and JavaScript. For the three-dimensional visualization, the library [Three.js](https://github.com/mrdoob/three.js/) was used. 9 | 10 | With the **comparison of different versions** of this software, tendencies of the **software's evolution** shall be revealed and become visible. 11 | According to that, the aim of this application is that developers and also project managers are able to **intuitively explore and localize flaws and possibilities to improve their projects**. 12 | Therefore, **results of static code analyses are visualized** in the form of a city with buildings representing the files and districts representing the modules of the project. 13 | 14 | ## How to install 15 | ### Checkout and install dependencies 16 | After checking out the project to your local harddrive, you can install all needed dependencies with npm: 17 | ``` 18 | npm install 19 | ``` 20 | 21 | ### Coderadar 22 | The application is yet designed to visualize results of static code analyses of the tool **Coderadar** exclusively. So at the moment, you would need to have a locally running Coderadar server and a fully analyzed sample project in order to use the application. 23 | To see how this works, just have a look at the [GitHub project](https://github.com/reflectoring/coderadar) and at the [administration guide](http://www.reflectoring.io/coderadar/current/docs/admin.html). 24 | 25 | ## How to develop 26 | ### Transpiling to ES5 27 | Because ES6 is used for writing the JavaScript code, you need to transpile the code into ES5 to make the app run in all browsers. You can easily do that with the help of gulp: 28 | ``` 29 | gulp 30 | ``` 31 | To make the development a lot more comfortable, you can also start a code watcher with gulp. It will automatically transpile the JavaScript code to ES5 whenever it detects a change in the source files: 32 | ``` 33 | gulp watch 34 | ``` 35 | 36 | ### Execute tests 37 | To make sure the code works properly, you can run unit tests with 38 | ``` 39 | npm test 40 | ``` 41 | 42 | # Screenshots 43 | Just choose two versions of your software project (based on GIT) and the type of view: 44 | 45 | ![Choose Versions](https://cloud.githubusercontent.com/assets/1246566/23557895/fbbbf66e-0031-11e7-8192-5d9c41db98a6.PNG) 46 | 47 | You can compare the two versions either side by side ... 48 | 49 | ![Split View](https://cloud.githubusercontent.com/assets/1246566/22399780/f8e23356-e5a4-11e6-9871-d08730dedda5.png) 50 | 51 | ... or in a merged view: 52 | 53 | ![Merged View](https://cloud.githubusercontent.com/assets/1246566/23557874/e3137ff6-0031-11e7-9174-f8ceb05f1550.PNG) 54 | 55 | You can filter for specific properties of your classes ... 56 | 57 | ![filter](https://cloud.githubusercontent.com/assets/1246566/23557936/1872fe88-0032-11e7-8437-ae6f0a79ae3e.PNG) 58 | 59 | ... and map different types of metrics to your personal visualization. 60 | 61 | ![mapping](https://cloud.githubusercontent.com/assets/1246566/23557926/11553c7e-0032-11e7-9661-5968ab5226db.PNG) 62 | 63 | Of course, you can also search for certain files in your project and highlight them in the visualization 64 | 65 | ![search](https://cloud.githubusercontent.com/assets/1246566/23557911/08b60db4-0032-11e7-8ca4-01bd8d27d6fc.PNG) 66 | -------------------------------------------------------------------------------- /js/drawer/AbstractDrawer.js: -------------------------------------------------------------------------------- 1 | import {config} from '../Config'; 2 | import * as Constants from '../Constants'; 3 | import {ElementAnalyzer} from '../util/ElementAnalyzer'; 4 | import {ColorHelper} from '../util/ColorHelper'; 5 | import {MetricNameService} from '../service/MetricNameService'; 6 | 7 | export class AbstractDrawer { 8 | 9 | constructor(scene, position) { 10 | if (new.target === AbstractDrawer) { 11 | throw new TypeError('Instantiating AbstractDrawer not allowed.'); 12 | } 13 | 14 | this.minBottomValue = 0; 15 | this.maxBottomValue = Number.MIN_VALUE; 16 | 17 | this.scene = scene; 18 | this.position = position; 19 | this.packer = this._getPacker(); 20 | 21 | this.metricNameService = new MetricNameService(); 22 | 23 | this._initializeEventListeners(); 24 | } 25 | 26 | calculateGroundAreas(elements) { 27 | if (!Array.isArray(elements)) { 28 | elements = [elements]; 29 | } 30 | 31 | for (let element of elements) { 32 | element.w = 0; 33 | element.h = 0; 34 | 35 | if (element.type == Constants.ELEMENT_TYPE_FILE) { 36 | var groundArea = this._getValueForGroundArea(element.commit1Metrics, element.commit2Metrics); 37 | if (!groundArea) { 38 | element.w = element.h = 0; 39 | } else { 40 | element.w = groundArea * config.GROUND_AREA_FACTOR + config.GLOBAL_MIN_GROUND_AREA + config.BLOCK_SPACING; 41 | element.h = groundArea * config.GROUND_AREA_FACTOR + config.GLOBAL_MIN_GROUND_AREA + config.BLOCK_SPACING; 42 | } 43 | } 44 | 45 | // recursion 46 | if (element.children && element.children.length > 0) { 47 | var result = this.calculateGroundAreas(element.children); 48 | element.w = result.w + config.BLOCK_SPACING * 3; 49 | element.h = result.h + config.BLOCK_SPACING * 3; 50 | } 51 | } 52 | 53 | elements.sort(function (a, b) { 54 | return b.w - a.w; 55 | }); 56 | 57 | this.packer.fit(elements); 58 | return { 59 | packer: this.packer.root, 60 | w: this.packer.root.w, 61 | h: this.packer.root.h 62 | }; 63 | } 64 | 65 | drawElements(elements, parent, bottom = 0) { } 66 | 67 | drawBlock(element, parent, color, currentCommitSize, bottom, height, isTransparent) { } 68 | 69 | colorizeModules() { 70 | for (var i = this.scene.children.length - 1; i >= 0; i--) { 71 | var child = this.scene.children[i]; 72 | 73 | if (child.userData && child.userData.type == Constants.ELEMENT_TYPE_MODULE) { 74 | child.material.color.set( 75 | ColorHelper.getColorByBottomValue(child.userData.bottom, this.maxBottomValue, this.minBottomValue) 76 | ); 77 | } 78 | } 79 | } 80 | 81 | _initializeEventListeners() { } 82 | 83 | _generateTooltipHtml(elementName, metrics) { 84 | var tooltipHtml = ['
' + elementName + '
']; 85 | 86 | if (metrics) { 87 | tooltipHtml.push(''); 88 | for (let metricName of Object.keys(metrics)) { 89 | tooltipHtml.push(this._generateTableRow(metricName, metrics[metricName])); 90 | } 91 | tooltipHtml.push('
'); 92 | } 93 | return tooltipHtml.join(''); 94 | } 95 | 96 | _generateTableRow(metricName, metricValue) { 97 | var html = ['']; 98 | html.push('' + this.metricNameService.getShortNameByFullName(metricName) + ':'); 99 | html.push('' + (metricValue || 'N/A') + ''); 100 | html.push(''); 101 | return html.join(''); 102 | } 103 | 104 | _getPacker() { 105 | return new GrowingPacker(); 106 | } 107 | 108 | _getValueForGroundArea(commit1Metrics, commit2Metrics) { 109 | return ElementAnalyzer.getMaxMetricValueByMetricName(commit1Metrics, commit2Metrics, config.GROUND_AREA_METRIC_NAME); 110 | } 111 | } -------------------------------------------------------------------------------- /js/ui/components/ComparisonContainerComponent.js: -------------------------------------------------------------------------------- 1 | import {MetricNameService} from '../../service/MetricNameService'; 2 | import {config} from '../../Config'; 3 | import * as PubSub from 'pubsub-js'; 4 | import {ServiceLocator} from '../../service/ServiceLocator'; 5 | 6 | export class ComparisonContainerComponent { 7 | 8 | constructor(application) { 9 | this._application = application; 10 | 11 | this.metricNameService = new MetricNameService(); 12 | 13 | this.comparisonContainer = document.querySelector('#comparison-container'); 14 | 15 | this._bindEvents(); 16 | } 17 | 18 | _bindEvents() { 19 | PubSub.subscribe('openComparisonContainer', (eventName, args) => { 20 | // If we don't have metric values in at least one of the elements, close the container and return. 21 | // No information can be shown then. 22 | if ( 23 | (args.leftElement && !args.leftElement.userData.metrics) 24 | || (args.rightElement && !args.rightElement.userData.metrics)) 25 | { 26 | this.comparisonContainer.classList.remove('open'); 27 | return; 28 | } 29 | 30 | if (args.leftElement) { 31 | this.comparisonContainer.querySelector('h3').innerHTML = args.leftElement.name; 32 | } else { 33 | this.comparisonContainer.querySelector('h3').innerHTML = args.rightElement.name; 34 | } 35 | 36 | var commitService = ServiceLocator.getInstance().get('commitService'); 37 | var leftCommit = commitService.getCommitByName(this._application.leftCommitId); 38 | var rightCommit = commitService.getCommitByName(this._application.rightCommitId); 39 | 40 | this.comparisonContainer.querySelector('#first-commit-id').innerHTML = leftCommit.getFormattedDatetime(); 41 | this.comparisonContainer.querySelector('#second-commit-id').innerHTML = rightCommit.getFormattedDatetime(); 42 | 43 | this.comparisonContainer.querySelector('#comparison-table tbody').innerHTML = ''; 44 | this._addMetricRows(args); 45 | 46 | this.comparisonContainer.classList.add('open'); 47 | }); 48 | 49 | PubSub.subscribe('closeComparisonContainer', () => { 50 | this.comparisonContainer.classList.remove('open'); 51 | }); 52 | } 53 | 54 | _addMetricRows(args) { 55 | var metricNames = [config.HEIGHT_METRIC_NAME, config.GROUND_AREA_METRIC_NAME, config.COLOR_METRIC_NAME]; 56 | for (let metricName of metricNames) { 57 | var rowEl = document.createElement('tr'); 58 | 59 | var metricNameEl = document.createElement('td'); 60 | metricNameEl.innerHTML = this.metricNameService.getShortNameByFullName(metricName); 61 | rowEl.appendChild(metricNameEl); 62 | 63 | var firstCommitMetricValueEl = document.createElement('td'); 64 | firstCommitMetricValueEl.innerHTML = args.leftElement ? args.leftElement.userData.metrics[metricName] || 'N/A' : '-'; 65 | rowEl.appendChild(firstCommitMetricValueEl); 66 | 67 | var secondCommitMetricValueEl = document.createElement('td'); 68 | secondCommitMetricValueEl.innerHTML = args.rightElement ? args.rightElement.userData.metrics[metricName] || 'N/A' : '-'; 69 | rowEl.appendChild(secondCommitMetricValueEl); 70 | 71 | var diffMetricValueEl = document.createElement('td'); 72 | 73 | var diffLabel, iconEl; 74 | if (args.leftElement && args.rightElement) { 75 | var diff = args.rightElement.userData.metrics[metricName] - args.leftElement.userData.metrics[metricName]; 76 | if (diff > 0) { 77 | diffLabel = '+' + diff; 78 | iconEl = ''; 79 | } else if (diff < 0) { 80 | diffLabel = diff; 81 | iconEl = ''; 82 | } else if (diff == 0) { 83 | diffLabel = diff; 84 | iconEl = ''; 85 | } else { 86 | diffLabel = '-'; 87 | iconEl = ''; 88 | } 89 | } else { 90 | diffLabel = '-'; 91 | iconEl = ''; 92 | } 93 | 94 | diffMetricValueEl.innerHTML = iconEl + diffLabel; 95 | rowEl.appendChild(diffMetricValueEl); 96 | 97 | this.comparisonContainer.querySelector('#comparison-table tbody').appendChild(rowEl); 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /js/drawer/SingleDrawer.js: -------------------------------------------------------------------------------- 1 | import {Block} from '../shape/Block'; 2 | import {config} from '../Config'; 3 | import * as Constants from '../Constants'; 4 | import {AbstractDrawer} from './AbstractDrawer'; 5 | import {ElementAnalyzer} from '../util/ElementAnalyzer'; 6 | import {ColorHelper} from '../util/ColorHelper'; 7 | 8 | export class SingleDrawer extends AbstractDrawer { 9 | 10 | constructor(scene, position, minMaxPairOfColorMetric) { 11 | super(scene, position); 12 | 13 | this.minColorMetricValue = minMaxPairOfColorMetric ? minMaxPairOfColorMetric.min : 0; 14 | this.maxColorMetricValue = minMaxPairOfColorMetric ? minMaxPairOfColorMetric.max : 0; 15 | } 16 | 17 | // override 18 | drawElements(elements, parent, bottom = 0) { 19 | if (!Array.isArray(elements)) { 20 | elements = [elements]; 21 | } 22 | 23 | elements.forEach((element) => { 24 | // don't draw empty modules 25 | if (element.type == Constants.ELEMENT_TYPE_MODULE && !ElementAnalyzer.hasChildrenForCurrentCommit(element, false, this.position)) { 26 | return; 27 | } 28 | 29 | if (!element.fit) { 30 | console.info(`element ${element.name} at position ${this.position} has no fit!`); 31 | return; 32 | } 33 | 34 | var heightMetric = ElementAnalyzer.getMetricValueOfElementAndCommitType(element, config.HEIGHT_METRIC_NAME, Constants.COMMIT_TYPE_CURRENT, this.position); 35 | var groundAreaMetric = ElementAnalyzer.getMetricValueOfElementAndCommitType(element, config.GROUND_AREA_METRIC_NAME, Constants.COMMIT_TYPE_CURRENT, this.position); 36 | var colorMetric = ElementAnalyzer.getMetricValueOfElementAndCommitType(element, config.COLOR_METRIC_NAME, Constants.COMMIT_TYPE_CURRENT, this.position); 37 | 38 | var metrics = { 39 | [config.HEIGHT_METRIC_NAME]: heightMetric, 40 | [config.GROUND_AREA_METRIC_NAME]: groundAreaMetric, 41 | [config.COLOR_METRIC_NAME]: colorMetric 42 | }; 43 | 44 | var myHeight; 45 | if (element.type == Constants.ELEMENT_TYPE_FILE) { 46 | if (!heightMetric || !groundAreaMetric) { 47 | return; 48 | } 49 | 50 | myHeight = heightMetric * config.HEIGHT_FACTOR + config.GLOBAL_MIN_HEIGHT; 51 | 52 | var myGA = groundAreaMetric * config.GROUND_AREA_FACTOR + config.GLOBAL_MIN_GROUND_AREA + config.BLOCK_SPACING; 53 | var otherGA = ElementAnalyzer.getMetricValueOfElementAndCommitType(element, config.GROUND_AREA_METRIC_NAME, Constants.COMMIT_TYPE_OTHER, this.position) * config.GROUND_AREA_FACTOR + config.GLOBAL_MIN_GROUND_AREA + config.BLOCK_SPACING; 54 | 55 | var myColor = ColorHelper.getColorByMetricValue(colorMetric, this.maxColorMetricValue, this.minColorMetricValue); 56 | 57 | if (myGA < otherGA) { 58 | element.fit.x += (otherGA - myGA) / 2; 59 | element.fit.y += (otherGA - myGA) / 2; 60 | } 61 | this.drawBlock(element, parent, myColor, myGA, bottom, myHeight, false, metrics); 62 | 63 | } else { 64 | if (bottom > this.maxBottomValue) { 65 | this.maxBottomValue = bottom; 66 | } 67 | 68 | myHeight = config.DEFAULT_BLOCK_HEIGHT; 69 | this.drawBlock(element, parent, config.COLOR_HIERARCHY_RANGE[0], undefined, bottom, myHeight, false, metrics); 70 | } 71 | 72 | // recursion 73 | if (element.children && element.children.length > 0) { 74 | this.drawElements(element.children, element, bottom + myHeight); 75 | } 76 | }); 77 | } 78 | 79 | // override 80 | drawBlock(element, parent, color, currentCommitSize, bottom, height, isTransparent, metrics) { 81 | var finalX, finalY, finalZ; 82 | var finalWidth, finalHeight, finalDepth; 83 | 84 | var cube = new Block(color, element.name); 85 | finalX = element.fit.x + (parent ? parent.renderedX : 0) + config.BLOCK_SPACING; 86 | finalY = bottom; 87 | finalZ = element.fit.y + (parent ? parent.renderedY : 0) + config.BLOCK_SPACING; 88 | 89 | // save the rendered positions to draw children relative to their parent 90 | element.renderedX = finalX; 91 | element.renderedY = finalZ; 92 | 93 | finalWidth = element.type == Constants.ELEMENT_TYPE_FILE ? currentCommitSize - config.BLOCK_SPACING : element.w - config.BLOCK_SPACING * 2; 94 | finalHeight = height; 95 | finalDepth = element.type == Constants.ELEMENT_TYPE_FILE ? currentCommitSize - config.BLOCK_SPACING : element.h - config.BLOCK_SPACING * 2; 96 | 97 | if (isTransparent) { 98 | cube.material.transparent = true; 99 | cube.material.opacity = 0.4; 100 | } 101 | 102 | cube.position.x = finalX; 103 | cube.position.y = finalY; 104 | cube.position.z = finalZ; 105 | 106 | cube.scale.x = finalWidth; 107 | cube.scale.y = finalHeight; 108 | cube.scale.z = finalDepth; 109 | 110 | cube.userData = { 111 | parentName: parent ? parent.name : undefined, 112 | bottom: bottom, 113 | metrics: metrics, 114 | type: element.type, 115 | tooltipLabel: this._generateTooltipHtml(element.name, metrics), 116 | isHelper: isTransparent 117 | }; 118 | 119 | this.scene.add(cube); 120 | } 121 | 122 | } -------------------------------------------------------------------------------- /editor/reservation.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "version": 4.4, 4 | "type": "Object", 5 | "generator": "Object3D.toJSON" 6 | }, 7 | "geometries": [ 8 | { 9 | "uuid": "9ADB9797-5668-4ED4-A035-8782F7789439", 10 | "type": "BoxBufferGeometry", 11 | "width": 2.5, 12 | "height": 1.5, 13 | "depth": 2.5, 14 | "widthSegments": 0, 15 | "heightSegments": 0, 16 | "depthSegments": 0 17 | }, 18 | { 19 | "uuid": "5DCE5513-BCC0-4397-91FF-210ABD55A927", 20 | "type": "BoxBufferGeometry", 21 | "width": 2, 22 | "height": 3, 23 | "depth": 2, 24 | "widthSegments": 0, 25 | "heightSegments": 0, 26 | "depthSegments": 0 27 | }, 28 | { 29 | "uuid": "BE7AA922-2947-4759-937B-D322C0E37EC5", 30 | "type": "BoxBufferGeometry", 31 | "width": 10, 32 | "height": 0.1, 33 | "depth": 9.5, 34 | "widthSegments": 0, 35 | "heightSegments": 0, 36 | "depthSegments": 0 37 | }, 38 | { 39 | "uuid": "4F22ACEE-0CB1-4D1E-ACD5-BA430FD36C11", 40 | "type": "BoxBufferGeometry", 41 | "width": 5, 42 | "height": 1, 43 | "depth": 5, 44 | "widthSegments": 0, 45 | "heightSegments": 0, 46 | "depthSegments": 0 47 | }, 48 | { 49 | "uuid": "E873E4F9-F73B-4350-B4F6-9F7AFD3CE79B", 50 | "type": "BoxBufferGeometry", 51 | "width": 2, 52 | "height": 5, 53 | "depth": 2, 54 | "widthSegments": 0, 55 | "heightSegments": 0, 56 | "depthSegments": 0 57 | }], 58 | "materials": [ 59 | { 60 | "uuid": "23A88537-4B22-4E9A-9FE4-FF6D46642118", 61 | "type": "MeshLambertMaterial", 62 | "color": 12632256, 63 | "emissive": 0, 64 | "opacity": 0.5, 65 | "depthFunc": 3, 66 | "depthTest": true, 67 | "depthWrite": true, 68 | "skinning": false, 69 | "morphTargets": false 70 | }, 71 | { 72 | "uuid": "FDD4DD9F-A67B-4DBA-871C-E396CD01A2A1", 73 | "type": "MeshLambertMaterial", 74 | "color": 12632256, 75 | "emissive": 0, 76 | "opacity": 0, 77 | "transparent": true, 78 | "depthFunc": 3, 79 | "depthTest": true, 80 | "depthWrite": true, 81 | "skinning": false, 82 | "morphTargets": false 83 | }, 84 | { 85 | "uuid": "61ECE943-F210-48E5-AA7C-658059EB7FF5", 86 | "type": "MeshLambertMaterial", 87 | "color": 12632256, 88 | "emissive": 0, 89 | "depthFunc": 3, 90 | "depthTest": true, 91 | "depthWrite": true, 92 | "skinning": false, 93 | "morphTargets": false 94 | }, 95 | { 96 | "uuid": "354DC3EC-CC46-4F2D-9BAD-2A3BBDC03516", 97 | "type": "MeshLambertMaterial", 98 | "color": 12632256, 99 | "emissive": 0, 100 | "opacity": 0, 101 | "transparent": true, 102 | "depthFunc": 3, 103 | "depthTest": true, 104 | "depthWrite": true, 105 | "skinning": false, 106 | "morphTargets": false 107 | }, 108 | { 109 | "uuid": "CB6B3A5E-C98C-49B9-AEEA-7ABA9F7C8FFC", 110 | "type": "MeshLambertMaterial", 111 | "color": 12632256, 112 | "emissive": 0, 113 | "opacity": 0, 114 | "depthFunc": 3, 115 | "depthTest": true, 116 | "depthWrite": true, 117 | "skinning": false, 118 | "morphTargets": false 119 | }], 120 | "object": { 121 | "uuid": "ADAFE518-1954-4B13-989B-41C607790468", 122 | "type": "Scene", 123 | "name": "Scene", 124 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1], 125 | "children": [ 126 | { 127 | "uuid": "D67849FB-195D-4F29-AC72-5846A08EC194", 128 | "type": "Mesh", 129 | "name": "Box 1", 130 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,-1.25,0.5,-4.5,1], 131 | "geometry": "9ADB9797-5668-4ED4-A035-8782F7789439", 132 | "material": "23A88537-4B22-4E9A-9FE4-FF6D46642118" 133 | }, 134 | { 135 | "uuid": "E4F5903A-856A-4136-8937-FF19873F1313", 136 | "type": "Mesh", 137 | "name": "Box 1", 138 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,4,1.5,1.5,1], 139 | "geometry": "5DCE5513-BCC0-4397-91FF-210ABD55A927", 140 | "material": "FDD4DD9F-A67B-4DBA-871C-E396CD01A2A1" 141 | }, 142 | { 143 | "uuid": "0F60BBD5-228C-4BB9-988C-AD484BEF6699", 144 | "type": "DirectionalLight", 145 | "name": "DirectionalLight 1", 146 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,1,0,1], 147 | "color": 16777215, 148 | "intensity": 0.4, 149 | "shadow": { 150 | "camera": { 151 | "uuid": "38C6835D-6AEF-42E1-AA3F-F984C057421C", 152 | "type": "OrthographicCamera", 153 | "zoom": 1, 154 | "left": -5, 155 | "right": 5, 156 | "top": 5, 157 | "bottom": -5, 158 | "near": 0.5, 159 | "far": 500 160 | } 161 | } 162 | }, 163 | { 164 | "uuid": "D76E6EDE-823D-4A3A-864A-BD7FCA9092CB", 165 | "type": "AmbientLight", 166 | "name": "AmbientLight 10", 167 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,11.319485,0,1], 168 | "color": 12632256, 169 | "intensity": 0.5 170 | }, 171 | { 172 | "uuid": "57100341-57B6-43E2-AE86-927F09602079", 173 | "type": "Mesh", 174 | "name": "Floor", 175 | "matrix": [0.896966,0,0,0,0,1,0,0,0,0,1,0,1,-0.05,-1.5,1], 176 | "geometry": "BE7AA922-2947-4759-937B-D322C0E37EC5", 177 | "material": "61ECE943-F210-48E5-AA7C-658059EB7FF5" 178 | }, 179 | { 180 | "uuid": "5DEFB23D-BEBD-4F5F-9C52-9326E465F395", 181 | "type": "Mesh", 182 | "name": "Box 1", 183 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,0.5,0,1], 184 | "geometry": "4F22ACEE-0CB1-4D1E-ACD5-BA430FD36C11", 185 | "material": "354DC3EC-CC46-4F2D-9BAD-2A3BBDC03516" 186 | }, 187 | { 188 | "uuid": "72B8138F-5174-40ED-A214-C0F66EE3929E", 189 | "type": "Mesh", 190 | "name": "Box 1", 191 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,2.5,0,1], 192 | "geometry": "E873E4F9-F73B-4350-B4F6-9F7AFD3CE79B", 193 | "material": "CB6B3A5E-C98C-49B9-AEEA-7ABA9F7C8FFC" 194 | }], 195 | "background": 16777215 196 | } 197 | } -------------------------------------------------------------------------------- /test/drawer/test_AbstractDrawer.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | import {AbstractDrawer} from '../../js/drawer/AbstractDrawer'; 4 | import {MergedDrawer} from '../../js/drawer/MergedDrawer'; 5 | import * as Constants from '../../js/Constants'; 6 | import {config} from '../../js/Config'; 7 | import sinon from 'sinon'; 8 | import packers from 'binpacking'; 9 | 10 | // mock GrowingPacker because it's imported with script-tag 11 | MergedDrawer.prototype._getPacker = sinon.stub().returns(packers.GrowingPacker.prototype); 12 | 13 | // initialize a MergedDrawer because AbstractDrawer cannot be initialized 14 | var drawer = new MergedDrawer(null, Constants.LEFT_SCREEN); 15 | 16 | describe('AbstractDrawer', function () { 17 | it('should calculate ground areas for each file', sinon.test(function () { 18 | var elements = require('../data/deltaTree.json'); 19 | 20 | // stubbing 21 | this.stub(AbstractDrawer.prototype, '_getValueForGroundArea'); 22 | 23 | var result = drawer.calculateGroundAreas(elements); 24 | assert.equal(typeof result, 'object'); 25 | assert.equal(Object.keys(result)[0], 'packer'); 26 | 27 | sinon.assert.callCount(drawer._getValueForGroundArea, 7); // there are 7 files 28 | })); 29 | 30 | it('should calculate the ground areas correctly with bin packing', function () { 31 | var elements = { 32 | "name": "root", 33 | "type": "MODULE", 34 | "children": [ 35 | { 36 | "name": "ModuleA", 37 | "type": "MODULE", 38 | "children": [ 39 | { 40 | "name": "ClassA", 41 | "type": "FILE", 42 | "children": [], 43 | "commit1Metrics": { 44 | "coderadar:size:loc:java": 100, 45 | "coderadar:size:sloc:java": 100, 46 | "coderadar:size:eloc:java": 100 47 | }, 48 | "commit2Metrics": null, 49 | "changes": { 50 | "renamed": true, 51 | "modified": true, 52 | "added": false, 53 | "deleted": false 54 | }, 55 | "renamedFrom": null, 56 | "renamedTo": null 57 | }, 58 | { 59 | "name": "ClassB", 60 | "type": "FILE", 61 | "children": [], 62 | "commit1Metrics": { 63 | "coderadar:size:loc:java": 200, 64 | "coderadar:size:sloc:java": 200, 65 | "coderadar:size:eloc:java": 200 66 | }, 67 | "commit2Metrics": null, 68 | "changes": { 69 | "renamed": false, 70 | "modified": true, 71 | "added": true, 72 | "deleted": true 73 | }, 74 | "renamedFrom": null, 75 | "renamedTo": null 76 | }, 77 | { 78 | "name": "ClassC", 79 | "type": "FILE", 80 | "children": [], 81 | "commit1Metrics": { 82 | "coderadar:size:loc:java": 300, 83 | "coderadar:size:sloc:java": 300, 84 | "coderadar:size:eloc:java": 300 85 | }, 86 | "commit2Metrics": null, 87 | "changes": { 88 | "renamed": false, 89 | "modified": true, 90 | "added": true, 91 | "deleted": true 92 | }, 93 | "renamedFrom": null, 94 | "renamedTo": null 95 | } 96 | ] 97 | } 98 | ] 99 | }; 100 | 101 | // overwrite config values for the test 102 | config.GROUND_AREA_FACTOR = 1; 103 | config.BLOCK_SPACING = 0; 104 | config.GLOBAL_MIN_GROUND_AREA = 0; 105 | 106 | var result = drawer.calculateGroundAreas(elements); 107 | 108 | var root = findChildElementByName(elements, 'root'); 109 | assert.equal(root.w, 500); 110 | assert.equal(root.h, 300); 111 | 112 | var moduleA = findChildElementByName(elements, 'ModuleA'); 113 | assert.equal(moduleA.w, 500); 114 | assert.equal(moduleA.h, 300); 115 | 116 | var classA = findChildElementByName(elements, 'ClassA'); 117 | assert.equal(classA.fit.x, 300); 118 | assert.equal(classA.fit.y, 200); 119 | 120 | var classB = findChildElementByName(elements, 'ClassB'); 121 | assert.equal(classB.fit.x, 300); 122 | assert.equal(classB.fit.y, 0); 123 | 124 | var classC = findChildElementByName(elements, 'ClassC'); 125 | assert.equal(classC.fit.x, 0); 126 | assert.equal(classC.fit.y, 0); 127 | }); 128 | }); 129 | 130 | function findChildElementByName(elements, name) { 131 | if (!Array.isArray(elements)) { 132 | elements = [elements]; 133 | } 134 | 135 | for (let element of elements) { 136 | if (element.name == name) { 137 | return element; 138 | } 139 | 140 | // recursion 141 | if (element.children && element.children.length > 0) { 142 | return findChildElementByName(element.children, name); 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /css/_loading-indicator.scss: -------------------------------------------------------------------------------- 1 | .uil-cube-css { 2 | background: none; 3 | position: absolute; 4 | width: 200px; 5 | height: 200px; 6 | 7 | &.left { 8 | -webkit-transform: scale(0.5) translateX(-50%); 9 | top: 40%; 10 | } 11 | 12 | &.right { 13 | -webkit-transform: scale(0.5) translateX(-50%); 14 | top: 40%; 15 | } 16 | 17 | & > div { 18 | position: absolute; 19 | width: 80px; 20 | height: 80px; 21 | -ms-animation: uil-cube-css 1s cubic-bezier(0.2, 0.8, 0.2, 0.8) infinite; 22 | -moz-animation: uil-cube-css 1s cubic-bezier(0.2, 0.8, 0.2, 0.8) infinite; 23 | -webkit-animation: uil-cube-css 1s cubic-bezier(0.2, 0.8, 0.2, 0.8) infinite; 24 | -o-animation: uil-cube-css 1s cubic-bezier(0.2, 0.8, 0.2, 0.8) infinite; 25 | animation: uil-cube-css 1s cubic-bezier(0.2, 0.8, 0.2, 0.8) infinite; 26 | } 27 | & > div:nth-of-type(1) { 28 | top: 10px; 29 | left: 10px; 30 | background: $background-color; 31 | opacity: 0.9; 32 | -ms-animation-delay: 0s; 33 | -moz-animation-delay: 0s; 34 | -webkit-animation-delay: 0s; 35 | -o-animation-delay: 0s; 36 | animation-delay: 0s; 37 | } 38 | & > div:nth-of-type(2) { 39 | top: 10px; 40 | left: 110px; 41 | background: $background-color; 42 | opacity: 0.8; 43 | -ms-animation-delay: 0.1s; 44 | -moz-animation-delay: 0.1s; 45 | -webkit-animation-delay: 0.1s; 46 | -o-animation-delay: 0.1s; 47 | animation-delay: 0.1s; 48 | } 49 | & > div:nth-of-type(3) { 50 | top: 110px; 51 | left: 10px; 52 | background: $background-color; 53 | opacity: 0.7; 54 | -ms-animation-delay: 0.3s; 55 | -moz-animation-delay: 0.3s; 56 | -webkit-animation-delay: 0.3s; 57 | -o-animation-delay: 0.3s; 58 | animation-delay: 0.3s; 59 | } 60 | & > div:nth-of-type(4) { 61 | top: 110px; 62 | left: 110px; 63 | background: $background-color; 64 | opacity: 0.6; 65 | -ms-animation-delay: 0.2s; 66 | -moz-animation-delay: 0.2s; 67 | -webkit-animation-delay: 0.2s; 68 | -o-animation-delay: 0.2s; 69 | animation-delay: 0.2s; 70 | } 71 | } 72 | 73 | @-webkit-keyframes uil-cube-css { 74 | 0% { 75 | -ms-transform: scale(1.4); 76 | -moz-transform: scale(1.4); 77 | -webkit-transform: scale(1.4); 78 | -o-transform: scale(1.4); 79 | transform: scale(1.4); 80 | } 81 | 100% { 82 | -ms-transform: scale(1); 83 | -moz-transform: scale(1); 84 | -webkit-transform: scale(1); 85 | -o-transform: scale(1); 86 | transform: scale(1); 87 | } 88 | } 89 | @-webkit-keyframes uil-cube-css { 90 | 0% { 91 | -ms-transform: scale(1.4); 92 | -moz-transform: scale(1.4); 93 | -webkit-transform: scale(1.4); 94 | -o-transform: scale(1.4); 95 | transform: scale(1.4); 96 | } 97 | 100% { 98 | -ms-transform: scale(1); 99 | -moz-transform: scale(1); 100 | -webkit-transform: scale(1); 101 | -o-transform: scale(1); 102 | transform: scale(1); 103 | } 104 | } 105 | @-moz-keyframes uil-cube-css { 106 | 0% { 107 | -ms-transform: scale(1.4); 108 | -moz-transform: scale(1.4); 109 | -webkit-transform: scale(1.4); 110 | -o-transform: scale(1.4); 111 | transform: scale(1.4); 112 | } 113 | 100% { 114 | -ms-transform: scale(1); 115 | -moz-transform: scale(1); 116 | -webkit-transform: scale(1); 117 | -o-transform: scale(1); 118 | transform: scale(1); 119 | } 120 | } 121 | @-ms-keyframes uil-cube-css { 122 | 0% { 123 | -ms-transform: scale(1.4); 124 | -moz-transform: scale(1.4); 125 | -webkit-transform: scale(1.4); 126 | -o-transform: scale(1.4); 127 | transform: scale(1.4); 128 | } 129 | 100% { 130 | -ms-transform: scale(1); 131 | -moz-transform: scale(1); 132 | -webkit-transform: scale(1); 133 | -o-transform: scale(1); 134 | transform: scale(1); 135 | } 136 | } 137 | @-moz-keyframes uil-cube-css { 138 | 0% { 139 | -ms-transform: scale(1.4); 140 | -moz-transform: scale(1.4); 141 | -webkit-transform: scale(1.4); 142 | -o-transform: scale(1.4); 143 | transform: scale(1.4); 144 | } 145 | 100% { 146 | -ms-transform: scale(1); 147 | -moz-transform: scale(1); 148 | -webkit-transform: scale(1); 149 | -o-transform: scale(1); 150 | transform: scale(1); 151 | } 152 | } 153 | @-webkit-keyframes uil-cube-css { 154 | 0% { 155 | -ms-transform: scale(1.4); 156 | -moz-transform: scale(1.4); 157 | -webkit-transform: scale(1.4); 158 | -o-transform: scale(1.4); 159 | transform: scale(1.4); 160 | } 161 | 100% { 162 | -ms-transform: scale(1); 163 | -moz-transform: scale(1); 164 | -webkit-transform: scale(1); 165 | -o-transform: scale(1); 166 | transform: scale(1); 167 | } 168 | } 169 | @-o-keyframes uil-cube-css { 170 | 0% { 171 | -ms-transform: scale(1.4); 172 | -moz-transform: scale(1.4); 173 | -webkit-transform: scale(1.4); 174 | -o-transform: scale(1.4); 175 | transform: scale(1.4); 176 | } 177 | 100% { 178 | -ms-transform: scale(1); 179 | -moz-transform: scale(1); 180 | -webkit-transform: scale(1); 181 | -o-transform: scale(1); 182 | transform: scale(1); 183 | } 184 | } 185 | @keyframes uil-cube-css { 186 | 0% { 187 | -ms-transform: scale(1.4); 188 | -moz-transform: scale(1.4); 189 | -webkit-transform: scale(1.4); 190 | -o-transform: scale(1.4); 191 | transform: scale(1.4); 192 | } 193 | 100% { 194 | -ms-transform: scale(1); 195 | -moz-transform: scale(1); 196 | -webkit-transform: scale(1); 197 | -o-transform: scale(1); 198 | transform: scale(1); 199 | } 200 | } -------------------------------------------------------------------------------- /js/util/ElementAnalyzer.js: -------------------------------------------------------------------------------- 1 | import * as Constants from '../Constants'; 2 | 3 | export class ElementAnalyzer { 4 | 5 | static generateUniqueElementList(elements, uniqueElements = []) { 6 | if (!Array.isArray(elements)) { 7 | elements = [elements]; 8 | } 9 | 10 | for (let element of elements) { 11 | if (uniqueElements.indexOf(element.name) < 0) { 12 | uniqueElements.push(element.name); 13 | } 14 | 15 | // recursion 16 | if (element.children && element.children.length > 0) { 17 | this.generateUniqueElementList(element.children, uniqueElements); 18 | } 19 | } 20 | 21 | return uniqueElements; 22 | } 23 | 24 | static findSmallestAndBiggestMetricValueByMetricName(elements, metricName) { 25 | if (typeof elements != 'object' || elements == null) { 26 | throw new Error('elements is not an object or null!'); 27 | } 28 | 29 | if (!Array.isArray(elements)) { 30 | elements = [elements]; 31 | } 32 | 33 | var min = Number.MAX_VALUE; 34 | var max = Number.MIN_VALUE; 35 | 36 | for (let element of elements) { 37 | // investigate only FILEs, because only files can have different sizes and colors 38 | if (element.type == Constants.ELEMENT_TYPE_FILE) { 39 | var commit1Metrics = element.commit1Metrics || null; 40 | var commit2Metrics = element.commit2Metrics || null; 41 | 42 | var big = this.getMaxMetricValueByMetricName(commit1Metrics, commit2Metrics, metricName); 43 | if (big > max) { 44 | max = big; 45 | } 46 | 47 | var small = this.getMinMetricValueByMetricName(commit1Metrics, commit2Metrics, metricName); 48 | if (small < min) { 49 | min = small; 50 | } 51 | } 52 | 53 | // recursion 54 | if (element.children && element.children.length > 0) { 55 | var result = this.findSmallestAndBiggestMetricValueByMetricName(element.children, metricName); 56 | if (result.max > max) { 57 | max = result.max; 58 | } 59 | if (result.min < min) { 60 | min = result.min; 61 | } 62 | } 63 | } 64 | 65 | return { 66 | min: min, 67 | max: max 68 | }; 69 | } 70 | 71 | static getMinMetricValueByMetricName(commit1Metrics, commit2Metrics, metricName) { 72 | if (commit1Metrics == null && commit2Metrics == null) { 73 | throw new Error(`No metric objects given`); 74 | } 75 | 76 | if (commit1Metrics == null) { 77 | return commit2Metrics[metricName]; 78 | } else if (commit2Metrics == null) { 79 | return commit1Metrics[metricName]; 80 | } else { 81 | return commit1Metrics[metricName] < commit2Metrics[metricName] ? commit1Metrics[metricName] : commit2Metrics[metricName]; 82 | } 83 | } 84 | 85 | static getMaxMetricValueByMetricName(commit1Metrics, commit2Metrics, metricName) { 86 | if (commit1Metrics == null && commit2Metrics == null) { 87 | throw new Error(`No metric objects given`); 88 | } 89 | 90 | if (commit1Metrics == null) { 91 | return commit2Metrics[metricName]; 92 | } else if (commit2Metrics == null) { 93 | return commit1Metrics[metricName]; 94 | } else { 95 | return commit1Metrics[metricName] > commit2Metrics[metricName] ? commit1Metrics[metricName] : commit2Metrics[metricName]; 96 | } 97 | } 98 | 99 | static hasChildrenForCurrentCommit(element, isFullscreen, screenPosition) { 100 | var found = false; 101 | 102 | for (let child of element.children) { 103 | if (this.hasMetricValuesForCurrentCommit(child, isFullscreen, screenPosition)) { 104 | found = true; 105 | } 106 | 107 | // recursion 108 | if (child.children && child.children.length > 0 && !found) { 109 | found = this.hasChildrenForCurrentCommit(child, isFullscreen, screenPosition); 110 | } 111 | } 112 | 113 | return found; 114 | } 115 | 116 | static hasMetricValuesForCurrentCommit(element, isFullscreen, screenPosition) { 117 | // when in fullscreen mode, metrics for at least one commit should be present 118 | if (isFullscreen) { 119 | return element.commit1Metrics != null || element.commit2Metrics != null; 120 | } 121 | 122 | if (screenPosition == Constants.LEFT_SCREEN) { 123 | return element.commit1Metrics != null; 124 | } else if (screenPosition == Constants.RIGHT_SCREEN) { 125 | return element.commit2Metrics != null; 126 | } else { 127 | throw new Error(`Unknown screen position ${screenPosition}!`); 128 | } 129 | } 130 | 131 | static getMetricValueOfElementAndCommitType(element, metricName, commitType, screenPosition) { 132 | if (screenPosition == Constants.LEFT_SCREEN) { 133 | if (commitType == Constants.COMMIT_TYPE_CURRENT) { 134 | return element.commit1Metrics ? element.commit1Metrics[metricName] : undefined; 135 | } else if (commitType == Constants.COMMIT_TYPE_OTHER) { 136 | return element.commit2Metrics ? element.commit2Metrics[metricName] : undefined; 137 | } else { 138 | throw new Error(`Unknown commitType ${commitType}!`); 139 | } 140 | 141 | } else if (screenPosition == Constants.RIGHT_SCREEN) { 142 | if (commitType == Constants.COMMIT_TYPE_CURRENT) { 143 | return element.commit2Metrics ? element.commit2Metrics[metricName] : undefined; 144 | } else if (commitType == Constants.COMMIT_TYPE_OTHER) { 145 | return element.commit1Metrics ? element.commit1Metrics[metricName] : undefined; 146 | } else { 147 | throw new Error(`Unknown commitType ${commitType}!`); 148 | } 149 | 150 | } else { 151 | throw new Error(`Unknown screen position ${screenPosition}!`); 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /js/ui/components/AutocompleteComponent.js: -------------------------------------------------------------------------------- 1 | const MAX_WIDTH = 400; 2 | 3 | export class AutocompleteComponent { 4 | 5 | constructor(componentElement) { 6 | this._componentElement = componentElement; 7 | 8 | this.input = componentElement.querySelector('input[type=text]'); 9 | this.showSuggestionsButton = componentElement.querySelector('.show-suggestions-button'); 10 | this.clearButton = componentElement.querySelector('.clear-button'); 11 | this.suggestionsContainer = componentElement.querySelector('.suggestions-container'); 12 | this.suggestionsList = this.suggestionsContainer.querySelector('ul.suggestions-list'); 13 | 14 | this._elements = []; 15 | this._filteredElements = []; 16 | 17 | this._hideSuggestions(); 18 | this._bindEvents(); 19 | 20 | this._fitSuggestionsContainerToScreen(); 21 | } 22 | 23 | setElements(elements) { 24 | this._elements = elements; 25 | this._resetFilteredElements(); 26 | this._updateSuggestionList(); 27 | } 28 | 29 | setSelection(value) { 30 | for (let element of this._filteredElements) { 31 | if (element.value == value) { 32 | this.input.value = element.label; 33 | this._markSelectedElement(element); 34 | } 35 | } 36 | } 37 | 38 | hideShowSuggestionsButton() { 39 | this.showSuggestionsButton.style.display = 'none'; 40 | } 41 | 42 | hideClearButton() { 43 | this.clearButton.style.display = 'none'; 44 | } 45 | 46 | disableFirstOption() { 47 | var listItems = this.suggestionsList.querySelectorAll('li'); 48 | listItems[0].classList.add('inactive'); 49 | } 50 | 51 | disableLastOption() { 52 | var listItems = this.suggestionsList.querySelectorAll('li'); 53 | listItems[listItems.length - 1].classList.add('inactive'); 54 | } 55 | 56 | _fitSuggestionsContainerToScreen() { 57 | var left = this._componentElement.getBoundingClientRect().left; 58 | var windowWidth = window.innerWidth; 59 | if (left + MAX_WIDTH > windowWidth) { 60 | this.suggestionsContainer.style.left = 'auto'; 61 | this.suggestionsContainer.style.right = '0px'; 62 | 63 | this.suggestionsList.classList.add('rtl'); 64 | } 65 | } 66 | 67 | _bindEvents() { 68 | document.addEventListener('click', (event) => { 69 | if (event.target != this.showSuggestionsButton) { 70 | this._hideSuggestions(); 71 | } 72 | }); 73 | 74 | this.input.addEventListener('keyup', () => { 75 | this._filterElements(this.input.value); 76 | this._updateSuggestionList(); 77 | this._showSuggestions(); 78 | }); 79 | 80 | this.showSuggestionsButton.addEventListener('click', () => { 81 | this._toggleSuggestions(); 82 | }); 83 | 84 | this.clearButton.addEventListener('click', () => { 85 | this.input.value = ''; 86 | this.input.focus(); 87 | this._resetFilteredElements(); 88 | }); 89 | } 90 | 91 | _updateSuggestionList() { 92 | this.suggestionsList.innerHTML = ''; 93 | for (let element of this._filteredElements) { 94 | let listItem = this._createListItem(element); 95 | this.suggestionsList.appendChild(listItem); 96 | } 97 | } 98 | 99 | _createListItem(element) { 100 | let li = document.createElement('li'); 101 | 102 | if (this.input.value.length > 0) { 103 | li.innerHTML = this._highlightSubstring(element.label, this.input.value); 104 | } else { 105 | li.innerHTML = element.label; 106 | } 107 | 108 | li.dataset.value = element.value; 109 | 110 | li.addEventListener('click', (event) => { 111 | if (event.target.classList.contains('inactive')) { 112 | return; 113 | } 114 | 115 | this.input.value = element.label; 116 | this._markSelectedElement(element); 117 | this._clearHighlights(); 118 | 119 | this._onSelection({ 120 | selection: element.value 121 | }); 122 | }); 123 | 124 | return li; 125 | } 126 | 127 | _onSelection(args) { console.warn('not implemented'); } 128 | 129 | _markSelectedElement(element) { 130 | var listItems = this.suggestionsList.querySelectorAll('li'); 131 | for (let item of listItems) { 132 | if (item.dataset.value == element.value) { 133 | item.classList.add('selected'); 134 | } else { 135 | item.classList.remove('selected'); 136 | } 137 | } 138 | } 139 | 140 | _filterElements(value) { 141 | this._filteredElements = []; 142 | for (let element of this._elements) { 143 | if (element.label.toLowerCase().indexOf(value.toLowerCase()) >= 0) { 144 | this._filteredElements.push(element); 145 | } 146 | } 147 | } 148 | 149 | _highlightSubstring(label, searchValue) { 150 | return label.replace(new RegExp('(' + searchValue + ')', 'ig'), '$1'); 151 | } 152 | 153 | _clearHighlights() { 154 | var listItems = this.suggestionsList.querySelectorAll('li'); 155 | for (let item of listItems) { 156 | item.innerHTML = item.innerHTML.replace('', ''); 157 | item.innerHTML = item.innerHTML.replace('', ''); 158 | } 159 | } 160 | 161 | _resetFilteredElements() { 162 | this._filteredElements = this._elements; 163 | this._updateSuggestionList(); 164 | } 165 | 166 | _showSuggestions() { 167 | this.suggestionsContainer.style.display = 'block'; 168 | } 169 | 170 | _hideSuggestions() { 171 | this.suggestionsContainer.style.display = 'none'; 172 | } 173 | 174 | _toggleSuggestions() { 175 | if (this.suggestionsContainer.style.display == 'none') { 176 | this._showSuggestions(); 177 | 178 | var selectedElement = this.suggestionsList.querySelector('.selected'); 179 | if (selectedElement) { 180 | selectedElement.scrollIntoView(); 181 | } 182 | } else { 183 | this._hideSuggestions(); 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /js/ui/InteractionHandler.js: -------------------------------------------------------------------------------- 1 | import {config} from '../Config'; 2 | import * as Constants from '../Constants'; 3 | import * as PubSub from 'pubsub-js'; 4 | 5 | export class InteractionHandler { 6 | 7 | constructor(scene, renderer, position) { 8 | this._enabled = false; 9 | this._isFullscreen = false; 10 | this._mouseOverScreen = false; 11 | 12 | this._scene = scene; 13 | this._renderer = renderer; 14 | this._position = position; 15 | 16 | this._raycaster = new THREE.Raycaster(); 17 | this._mouse = new THREE.Vector2(); 18 | this._mouseForRaycaster = new THREE.Vector2(); 19 | 20 | this.tooltipElement = document.querySelector('#tooltip'); 21 | this._hoveredElementUuid = undefined; 22 | this._clickedElementUuid = undefined; 23 | 24 | this._startingPosition = {}; 25 | 26 | this.bindEvents(); 27 | } 28 | 29 | setEnabled(enabled) { 30 | this._enabled = enabled; 31 | } 32 | 33 | setFullscreen() { 34 | this._isFullscreen = true; 35 | } 36 | 37 | setSplitscreen() { 38 | this._isFullscreen = false; 39 | } 40 | 41 | update(camera) { 42 | if (!this._enabled || !this._mouseOverScreen) { 43 | return; 44 | } 45 | 46 | this._raycaster.setFromCamera(this._mouseForRaycaster, camera); 47 | 48 | var intersects = this._raycaster.intersectObjects(this._scene.children); 49 | var target = this._findFirstNonHelperBlock(intersects); 50 | 51 | this._updateTooltip(target); 52 | } 53 | 54 | _updateTooltip(target) { 55 | if (target) { 56 | if (target.uuid != this._hoveredElementUuid) { 57 | this.tooltipElement.innerHTML = target.userData.tooltipLabel; 58 | this._hoveredElementUuid = target.uuid; 59 | } 60 | 61 | if (!this.tooltipElement.classList.contains('visible')) { 62 | this.tooltipElement.classList.add('visible'); 63 | } 64 | 65 | this.tooltipElement.style.left = this._mouse.x + 15 + 'px'; 66 | this.tooltipElement.style.top = this._mouse.y + 15 + 'px'; 67 | } else { 68 | if (this.tooltipElement.classList.contains('visible')) { 69 | this._hideTooltip(); 70 | } 71 | } 72 | } 73 | 74 | _hideTooltip() { 75 | this.tooltipElement.classList.remove('visible'); 76 | this.tooltipElement.style.left = '-1000px'; 77 | this.tooltipElement.style.top = '-1000px'; 78 | } 79 | 80 | _findFirstNonHelperBlock(intersects) { 81 | if (intersects.length > 0) { 82 | for (let i = 0; i < intersects.length; i++) { 83 | // find the first block that is not a helper block 84 | // this lets the clicks go through the helper blocks 85 | if (!intersects[i].object.userData.isHelper) { 86 | return intersects[i].object; 87 | } 88 | } 89 | } 90 | 91 | return undefined; 92 | } 93 | 94 | _getScreenWidth() { 95 | if (this._isFullscreen) { 96 | return window.innerWidth - config.SCREEN_PADDING; 97 | } 98 | return window.innerWidth / 2 - config.SCREEN_PADDING; 99 | } 100 | 101 | _onDocumentMouseMove(event) { 102 | if (!this._enabled) { 103 | return; 104 | } 105 | 106 | this._mouse.x = event.clientX; 107 | this._mouse.y = event.clientY; 108 | 109 | var screenOffset = this._position == Constants.LEFT_SCREEN ? 0 : this._getScreenWidth(); 110 | 111 | this._mouseForRaycaster.x = ((event.clientX - screenOffset) / this._getScreenWidth()) * 2 - 1; 112 | this._mouseForRaycaster.y = -(event.clientY / window.innerHeight) * 2 + 1; 113 | } 114 | 115 | _onDocumentMouseOver() { 116 | this._mouseOverScreen = true; 117 | } 118 | 119 | _onDocumentMouseOut() { 120 | this._mouseOverScreen = false; 121 | this._hideTooltip(); 122 | } 123 | 124 | _onDocumentMouseDown(event) { 125 | this._renderer.domElement.style.cursor = '-webkit-grabbing'; 126 | 127 | this._startingPosition = { 128 | x: event.clientX, 129 | y: event.clientY 130 | }; 131 | } 132 | 133 | _onDocumentMouseUp(event) { 134 | this._renderer.domElement.style.cursor = '-webkit-grab'; 135 | 136 | if (!this._enabled) { 137 | return; 138 | } 139 | 140 | if (Math.abs(event.clientX - this._startingPosition.x) > 0 || Math.abs(event.clientY - this._startingPosition.y) > 0) { 141 | return; 142 | } 143 | 144 | var intersects = this._raycaster.intersectObjects(this._scene.children); 145 | var target = this._findFirstNonHelperBlock(intersects); 146 | if (target) { 147 | if (event.which == 1) { // left mouse button 148 | var doReset; 149 | if (target.uuid != this._clickedElementUuid) { 150 | doReset = false; 151 | this._clickedElementUuid = target.uuid; 152 | } else { 153 | doReset = true; 154 | this._clickedElementUuid = undefined; 155 | } 156 | 157 | PubSub.publish('elementClicked', { elementName: target.name, doReset: doReset }); 158 | 159 | } else if (event.which == 3) { // right mouse button 160 | if (target.userData && target.userData.type == Constants.ELEMENT_TYPE_MODULE) { 161 | event.preventDefault(); 162 | PubSub.publish('elementRightClicked', { elementName: target.name, position: { x: event.clientX, y: event.clientY } }); 163 | } 164 | } 165 | } 166 | } 167 | 168 | bindEvents() { 169 | this._renderer.domElement.addEventListener('mouseover', this._onDocumentMouseOver.bind(this), false); 170 | this._renderer.domElement.addEventListener('mouseout', this._onDocumentMouseOut.bind(this), false); 171 | this._renderer.domElement.addEventListener('mousemove', this._onDocumentMouseMove.bind(this), false); 172 | this._renderer.domElement.addEventListener('mousedown', this._onDocumentMouseDown.bind(this), false); 173 | this._renderer.domElement.addEventListener('mouseup', this._onDocumentMouseUp.bind(this), false); 174 | } 175 | } -------------------------------------------------------------------------------- /scripts/setupProjectScript.js: -------------------------------------------------------------------------------- 1 | var axios = require('axios'); 2 | 3 | var username = "radar"; 4 | var password = "Password12!"; 5 | 6 | var repoList = { 7 | 'coderadar': { 8 | 'repoName': 'coderadar', 9 | 'repoUrl': 'https://github.com/reflectoring/coderadar.git' 10 | }, 11 | 'coderadarDemo': { 12 | 'repoName': 'coderadar-demo', 13 | 'repoUrl': 'https://github.com/pschild/coderadar-demo.git' 14 | }, 15 | 'junit': { 16 | 'repoName': 'junit4', 17 | 'repoUrl': 'https://github.com/junit-team/junit4.git' 18 | }, 19 | 'javaDesignPatterns': { 20 | 'repoName': 'java-design-patterns', 21 | 'repoUrl': 'https://github.com/iluwatar/java-design-patterns.git' 22 | }, 23 | 'retrofit': { 24 | 'repoName': 'retrofit', 25 | 'repoUrl': 'https://github.com/square/retrofit.git' 26 | }, 27 | 'javaAlgorithms': { 28 | 'repoName': 'java-algorithms', 29 | 'repoUrl': 'https://github.com/posborne/java-algorithms.git' 30 | } 31 | }; 32 | var activeRepo = repoList.javaAlgorithms; 33 | 34 | var fromYear = 2016; 35 | var fromMonth = 1; // 1 = january 36 | var fromDay = 1; 37 | 38 | var accessToken = undefined; 39 | 40 | function registerUser() { 41 | console.log('registering user...'); 42 | return axios.post('http://localhost:8080/user/registration', 43 | { 44 | "username" : username, 45 | "password" : password 46 | } 47 | ); 48 | } 49 | 50 | function authorizeUser() { 51 | console.log('authorizing user...'); 52 | return axios.post('http://localhost:8080/user/auth', 53 | { 54 | "username" : username, 55 | "password" : password 56 | } 57 | ).then(function(response) { 58 | if (!response.data.accessToken) { 59 | throw new Error('no access token could be found'); 60 | } 61 | accessToken = response.data.accessToken; 62 | }); 63 | } 64 | 65 | function createProject() { 66 | console.log('creating project...'); 67 | return axios.post('http://localhost:8080/projects', 68 | { 69 | "name": activeRepo.repoName, 70 | "vcsUrl": activeRepo.repoUrl, 71 | "startDate" : [ 2016, 1, 1 ], 72 | "endDate" : [ 2016, 12, 31 ] 73 | }, 74 | { 75 | headers: {'Authorization': accessToken} 76 | } 77 | ); 78 | } 79 | 80 | function addFilePattern() { 81 | console.log('adding file pattern...'); 82 | return axios.post('http://localhost:8080/projects/1/files', 83 | { 84 | "filePatterns": [{ 85 | "pattern": "**/*.java", 86 | "inclusionType": "INCLUDE", 87 | "fileSetType": "SOURCE" 88 | }] 89 | }, 90 | { 91 | headers: {'Authorization': accessToken} 92 | } 93 | ); 94 | } 95 | 96 | function addAnalyzerConfig() { 97 | console.log('adding analyzing configs...'); 98 | 99 | var enabledAnalyzerPlugins = [ 100 | 'org.wickedsource.coderadar.analyzer.loc.LocAnalyzerPlugin', 101 | 'org.wickedsource.coderadar.analyzer.checkstyle.CheckstyleSourceCodeFileAnalyzerPlugin' 102 | ]; 103 | 104 | var promises = []; 105 | for (var pluginName of enabledAnalyzerPlugins) { 106 | promises.push( 107 | axios.post('http://localhost:8080/projects/1/analyzers', 108 | { 109 | "analyzerName": pluginName, 110 | "enabled": true 111 | }, 112 | { 113 | headers: {'Authorization': accessToken} 114 | } 115 | ) 116 | ); 117 | } 118 | 119 | return axios.all(promises); 120 | } 121 | 122 | function addAnalyzingStrategy() { 123 | console.log('adding analyzing strategy...'); 124 | var fromDate = new Date(fromYear, fromMonth - 1, fromDay); 125 | 126 | return axios.post('http://localhost:8080/projects/1/analyzingJob', 127 | { 128 | "fromDate" : fromDate.getTime(), 129 | "active" : true, 130 | "rescan" : true 131 | }, 132 | { 133 | headers: {'Authorization': accessToken} 134 | } 135 | ); 136 | } 137 | 138 | function addModules() { 139 | console.log('adding modules...'); 140 | 141 | if (activeRepo.repoName != 'coderadar') { 142 | return; 143 | } 144 | 145 | var modules = [ 146 | 'coderadar-plugin-api', 147 | 'coderadar-plugins/checkstyle-analyzer-plugin', 148 | 'coderadar-plugins/findbugs-adapter-plugin', 149 | 'coderadar-plugins/loc-analyzer-plugin', 150 | 'coderadar-plugins/todo-analyzer-plugin', 151 | 'coderadar-server', 152 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/analyzer', 153 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/analyzingjob', 154 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/commit', 155 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/core', 156 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/file', 157 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/filepattern', 158 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/job', 159 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/metric', 160 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/metricquery', 161 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/module', 162 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/project', 163 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/qualityprofile', 164 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/security', 165 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/user', 166 | 'coderadar-server/src/main/java/org/wickedsource/coderadar/vcs' 167 | ]; 168 | 169 | var promises = []; 170 | for (var moduleName of modules) { 171 | promises.push( 172 | axios.post('http://localhost:8080/projects/1/modules', 173 | { 174 | "modulePath": moduleName 175 | }, 176 | { 177 | headers: {'Authorization': accessToken} 178 | } 179 | ) 180 | ); 181 | } 182 | 183 | return axios.all(promises); 184 | } 185 | 186 | registerUser() 187 | .then(authorizeUser) 188 | .then(createProject) 189 | .then(addFilePattern) 190 | .then(addAnalyzerConfig) 191 | .then(addAnalyzingStrategy); 192 | // .then(addModules); -------------------------------------------------------------------------------- /editor/4cases.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "version": 4.4, 4 | "type": "Object", 5 | "generator": "Object3D.toJSON" 6 | }, 7 | "geometries": [ 8 | { 9 | "uuid": "61885183-AED5-4707-9E26-9BA4AE7125CD", 10 | "type": "BoxBufferGeometry", 11 | "width": 4, 12 | "height": 8, 13 | "depth": 4, 14 | "widthSegments": 0, 15 | "heightSegments": 0, 16 | "depthSegments": 0 17 | }, 18 | { 19 | "uuid": "A08D03EB-9791-4A2E-9B58-CEC83E0DFDBA", 20 | "type": "BoxBufferGeometry", 21 | "width": 20, 22 | "height": 0.1, 23 | "depth": 6, 24 | "widthSegments": 0, 25 | "heightSegments": 0, 26 | "depthSegments": 0 27 | }, 28 | { 29 | "uuid": "4F22ACEE-0CB1-4D1E-ACD5-BA430FD36C11", 30 | "type": "BoxBufferGeometry", 31 | "width": 5, 32 | "height": 1, 33 | "depth": 5, 34 | "widthSegments": 0, 35 | "heightSegments": 0, 36 | "depthSegments": 0 37 | }, 38 | { 39 | "uuid": "E873E4F9-F73B-4350-B4F6-9F7AFD3CE79B", 40 | "type": "BoxBufferGeometry", 41 | "width": 2, 42 | "height": 5, 43 | "depth": 2, 44 | "widthSegments": 0, 45 | "heightSegments": 0, 46 | "depthSegments": 0 47 | }, 48 | { 49 | "uuid": "50356D09-6886-4620-BEFD-1300DE4D9267", 50 | "type": "BoxBufferGeometry", 51 | "width": 2, 52 | "height": 3, 53 | "depth": 2, 54 | "widthSegments": 0, 55 | "heightSegments": 0, 56 | "depthSegments": 0 57 | }], 58 | "materials": [ 59 | { 60 | "uuid": "A790CFFE-C84C-46D9-BCEA-B2BEAB01E04A", 61 | "type": "MeshLambertMaterial", 62 | "color": 16744448, 63 | "emissive": 0, 64 | "opacity": 0.5, 65 | "transparent": true, 66 | "depthFunc": 3, 67 | "depthTest": true, 68 | "depthWrite": true, 69 | "skinning": false, 70 | "morphTargets": false 71 | }, 72 | { 73 | "uuid": "61ECE943-F210-48E5-AA7C-658059EB7FF5", 74 | "type": "MeshLambertMaterial", 75 | "color": 12632256, 76 | "emissive": 0, 77 | "depthFunc": 3, 78 | "depthTest": true, 79 | "depthWrite": true, 80 | "skinning": false, 81 | "morphTargets": false 82 | }, 83 | { 84 | "uuid": "354DC3EC-CC46-4F2D-9BAD-2A3BBDC03516", 85 | "type": "MeshLambertMaterial", 86 | "color": 16744448, 87 | "emissive": 0, 88 | "opacity": 0.5, 89 | "depthFunc": 3, 90 | "depthTest": true, 91 | "depthWrite": true, 92 | "skinning": false, 93 | "morphTargets": false 94 | }, 95 | { 96 | "uuid": "01463680-5BF0-47E6-BAC9-766E6ACAB3BC", 97 | "type": "MeshLambertMaterial", 98 | "color": 160, 99 | "emissive": 0, 100 | "depthFunc": 3, 101 | "depthTest": true, 102 | "depthWrite": true, 103 | "skinning": false, 104 | "morphTargets": false 105 | }, 106 | { 107 | "uuid": "EA84807D-CA2B-4C88-B2CF-9F8C340CB239", 108 | "type": "MeshLambertMaterial", 109 | "color": 255, 110 | "emissive": 0, 111 | "depthFunc": 3, 112 | "depthTest": true, 113 | "depthWrite": true, 114 | "skinning": false, 115 | "morphTargets": false 116 | }, 117 | { 118 | "uuid": "668E8AFD-F90D-4C12-9278-09BD155B2CC9", 119 | "type": "MeshLambertMaterial", 120 | "color": 255, 121 | "emissive": 0, 122 | "opacity": 0.5, 123 | "transparent": true, 124 | "depthFunc": 3, 125 | "depthTest": true, 126 | "depthWrite": true, 127 | "skinning": false, 128 | "morphTargets": false 129 | }, 130 | { 131 | "uuid": "9E724706-DEE6-4D12-A72A-B71114FE888E", 132 | "type": "MeshLambertMaterial", 133 | "color": 16744448, 134 | "emissive": 0, 135 | "depthFunc": 3, 136 | "depthTest": true, 137 | "depthWrite": true, 138 | "skinning": false, 139 | "morphTargets": false 140 | }, 141 | { 142 | "uuid": "6454A190-4971-4B28-B06F-8E46326210D3", 143 | "type": "MeshLambertMaterial", 144 | "color": 160, 145 | "emissive": 0, 146 | "depthFunc": 3, 147 | "depthTest": true, 148 | "depthWrite": true, 149 | "skinning": false, 150 | "morphTargets": false 151 | }, 152 | { 153 | "uuid": "A8F15D2F-BAB6-44C4-831F-1B950E5EDCEB", 154 | "type": "MeshLambertMaterial", 155 | "color": 16744448, 156 | "emissive": 0, 157 | "depthFunc": 3, 158 | "depthTest": true, 159 | "depthWrite": true, 160 | "skinning": false, 161 | "morphTargets": false 162 | }], 163 | "object": { 164 | "uuid": "ADAFE518-1954-4B13-989B-41C607790468", 165 | "type": "Scene", 166 | "name": "Scene", 167 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1], 168 | "children": [ 169 | { 170 | "uuid": "8B70AC3C-680E-4FEE-AE27-57DE4F79EE5E", 171 | "type": "Mesh", 172 | "name": "Box 1", 173 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,-5,4,-0.5,1], 174 | "geometry": "61885183-AED5-4707-9E26-9BA4AE7125CD", 175 | "material": "A790CFFE-C84C-46D9-BCEA-B2BEAB01E04A" 176 | }, 177 | { 178 | "uuid": "0F60BBD5-228C-4BB9-988C-AD484BEF6699", 179 | "type": "DirectionalLight", 180 | "name": "DirectionalLight 1", 181 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,1,0,1], 182 | "color": 16777215, 183 | "intensity": 0.4, 184 | "shadow": { 185 | "camera": { 186 | "uuid": "38C6835D-6AEF-42E1-AA3F-F984C057421C", 187 | "type": "OrthographicCamera", 188 | "zoom": 1, 189 | "left": -5, 190 | "right": 5, 191 | "top": 5, 192 | "bottom": -5, 193 | "near": 0.5, 194 | "far": 500 195 | } 196 | } 197 | }, 198 | { 199 | "uuid": "D76E6EDE-823D-4A3A-864A-BD7FCA9092CB", 200 | "type": "AmbientLight", 201 | "name": "AmbientLight 10", 202 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,11.319485,0,1], 203 | "color": 12632256, 204 | "intensity": 0.5 205 | }, 206 | { 207 | "uuid": "57100341-57B6-43E2-AE86-927F09602079", 208 | "type": "Mesh", 209 | "name": "Floor", 210 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,2.75,-0.05,0,1], 211 | "geometry": "A08D03EB-9791-4A2E-9B58-CEC83E0DFDBA", 212 | "material": "61ECE943-F210-48E5-AA7C-658059EB7FF5" 213 | }, 214 | { 215 | "uuid": "5DEFB23D-BEBD-4F5F-9C52-9326E465F395", 216 | "type": "Mesh", 217 | "name": "Box 1", 218 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,0.5,0,1], 219 | "geometry": "4F22ACEE-0CB1-4D1E-ACD5-BA430FD36C11", 220 | "material": "354DC3EC-CC46-4F2D-9BAD-2A3BBDC03516" 221 | }, 222 | { 223 | "uuid": "72B8138F-5174-40ED-A214-C0F66EE3929E", 224 | "type": "Mesh", 225 | "name": "Box 1", 226 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,2.5,0,1], 227 | "geometry": "E873E4F9-F73B-4350-B4F6-9F7AFD3CE79B", 228 | "material": "01463680-5BF0-47E6-BAC9-766E6ACAB3BC" 229 | }, 230 | { 231 | "uuid": "E5A533DC-6812-4F03-848F-74B679993AE0", 232 | "type": "Mesh", 233 | "name": "Box 1", 234 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,-5,1.5,-0.5,1], 235 | "geometry": "50356D09-6886-4620-BEFD-1300DE4D9267", 236 | "material": "EA84807D-CA2B-4C88-B2CF-9F8C340CB239" 237 | }, 238 | { 239 | "uuid": "64905B8D-6B15-4693-91A5-8512F96A19BB", 240 | "type": "Mesh", 241 | "name": "Box 1", 242 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,5,4,-0.5,1], 243 | "geometry": "61885183-AED5-4707-9E26-9BA4AE7125CD", 244 | "material": "668E8AFD-F90D-4C12-9278-09BD155B2CC9" 245 | }, 246 | { 247 | "uuid": "A66EAD09-2757-44A7-B550-C3F1B79B2448", 248 | "type": "Mesh", 249 | "name": "Box 1", 250 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,5,2.5,0,1], 251 | "geometry": "E873E4F9-F73B-4350-B4F6-9F7AFD3CE79B", 252 | "material": "9E724706-DEE6-4D12-A72A-B71114FE888E" 253 | }, 254 | { 255 | "uuid": "AC98BBA5-EAEC-4D60-92E6-BDC21E19B0D1", 256 | "type": "Mesh", 257 | "name": "Box 1", 258 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,10,0.5,0,1], 259 | "geometry": "4F22ACEE-0CB1-4D1E-ACD5-BA430FD36C11", 260 | "material": "6454A190-4971-4B28-B06F-8E46326210D3" 261 | }, 262 | { 263 | "uuid": "11BEE45D-4639-4546-A0DA-AE46BC170C16", 264 | "type": "Mesh", 265 | "name": "Box 1", 266 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,10,2.5,0,1], 267 | "geometry": "E873E4F9-F73B-4350-B4F6-9F7AFD3CE79B", 268 | "material": "A8F15D2F-BAB6-44C4-831F-1B950E5EDCEB" 269 | }], 270 | "background": 16777215 271 | } 272 | } -------------------------------------------------------------------------------- /test/data/deltaTree.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "type": "MODULE", 4 | "commit1Metrics": { 5 | "coderadar:size:loc:java": 800, 6 | "coderadar:size:eloc:java": 4, 7 | "coderadar:size:sloc:java": 8 8 | }, 9 | "commit2Metrics": { 10 | "coderadar:size:loc:java": 800, 11 | "coderadar:size:eloc:java": 4, 12 | "coderadar:size:sloc:java": 8 13 | }, 14 | "renamedFrom": null, 15 | "renamedTo": null, 16 | "changes": null, 17 | "children": [ 18 | { 19 | "name": "moduleA", 20 | "type": "MODULE", 21 | "commit1Metrics": { 22 | "coderadar:size:loc:java": 4, 23 | "coderadar:size:eloc:java": 2, 24 | "coderadar:size:sloc:java": 4 25 | }, 26 | "commit2Metrics": { 27 | "coderadar:size:loc:java": 2, 28 | "coderadar:size:eloc:java": 1, 29 | "coderadar:size:sloc:java": 2 30 | }, 31 | "renamedFrom": null, 32 | "renamedTo": null, 33 | "changes": null, 34 | "children": [ 35 | { 36 | "name": "moduleA/src/main/java/UnchangedClass.java", 37 | "type": "FILE", 38 | "commit1Metrics": { 39 | "coderadar:size:loc:java": 2, 40 | "coderadar:size:eloc:java": 1, 41 | "coderadar:size:sloc:java": 2 42 | }, 43 | "commit2Metrics": { 44 | "coderadar:size:loc:java": 2, 45 | "coderadar:size:eloc:java": 1, 46 | "coderadar:size:sloc:java": 2 47 | }, 48 | "renamedFrom": null, 49 | "renamedTo": null, 50 | "changes": { 51 | "renamed": false, 52 | "modified": false, 53 | "deleted": false, 54 | "added": false 55 | }, 56 | "children": [] 57 | }, 58 | { 59 | "name": "moduleA/src/main/java/MovedClassFromAToB.java", 60 | "type": "FILE", 61 | "commit1Metrics": { 62 | "coderadar:size:loc:java": 2, 63 | "coderadar:size:eloc:java": 1, 64 | "coderadar:size:sloc:java": 2 65 | }, 66 | "commit2Metrics": null, 67 | "renamedFrom": null, 68 | "renamedTo": "moduleB/src/main/java/MovedClassFromAToB.java", 69 | "changes": { 70 | "renamed": true, 71 | "modified": false, 72 | "deleted": false, 73 | "added": false 74 | }, 75 | "children": [] 76 | }, 77 | { 78 | "name": "moduleB/src/main/java/RemovedClass.java", 79 | "type": "FILE", 80 | "commit1Metrics": { 81 | "coderadar:size:loc:java": 2, 82 | "coderadar:size:eloc:java": 1, 83 | "coderadar:size:sloc:java": 2 84 | }, 85 | "commit2Metrics": null, 86 | "renamedFrom": null, 87 | "renamedTo": null, 88 | "changes": { 89 | "renamed": false, 90 | "modified": false, 91 | "deleted": false, 92 | "added": true 93 | }, 94 | "children": [] 95 | } 96 | ] 97 | }, 98 | { 99 | "name": "moduleB", 100 | "type": "MODULE", 101 | "commit1Metrics": { 102 | "coderadar:size:loc:java": 4, 103 | "coderadar:size:eloc:java": 2, 104 | "coderadar:size:sloc:java": 4 105 | }, 106 | "commit2Metrics": { 107 | "coderadar:size:loc:java": 6, 108 | "coderadar:size:eloc:java": 3, 109 | "coderadar:size:sloc:java": 6 110 | }, 111 | "renamedFrom": null, 112 | "renamedTo": null, 113 | "changes": null, 114 | "children": [ 115 | { 116 | "name": "moduleB/src/main/java/AddedClass.java", 117 | "type": "FILE", 118 | "commit1Metrics": null, 119 | "commit2Metrics": { 120 | "coderadar:size:loc:java": 2, 121 | "coderadar:size:eloc:java": 1, 122 | "coderadar:size:sloc:java": 2 123 | }, 124 | "renamedFrom": null, 125 | "renamedTo": null, 126 | "changes": { 127 | "renamed": false, 128 | "modified": false, 129 | "deleted": false, 130 | "added": true 131 | }, 132 | "children": [] 133 | }, 134 | { 135 | "name": "moduleB/src/main/java/IncreasedClass.java", 136 | "type": "FILE", 137 | "commit1Metrics": { 138 | "coderadar:size:loc:java": 100, 139 | "coderadar:size:eloc:java": 100, 140 | "coderadar:size:sloc:java": 100 141 | }, 142 | "commit2Metrics": { 143 | "coderadar:size:loc:java": 200, 144 | "coderadar:size:eloc:java": 200, 145 | "coderadar:size:sloc:java": 200 146 | }, 147 | "renamedFrom": null, 148 | "renamedTo": null, 149 | "changes": { 150 | "renamed": false, 151 | "modified": false, 152 | "deleted": false, 153 | "added": false 154 | }, 155 | "children": [] 156 | }, 157 | { 158 | "name": "moduleB/src/main/java/DecreasedClass.java", 159 | "type": "FILE", 160 | "commit1Metrics": { 161 | "coderadar:size:loc:java": 200, 162 | "coderadar:size:eloc:java": 200, 163 | "coderadar:size:sloc:java": 200 164 | }, 165 | "commit2Metrics": { 166 | "coderadar:size:loc:java": 100, 167 | "coderadar:size:eloc:java": 100, 168 | "coderadar:size:sloc:java": 100 169 | }, 170 | "renamedFrom": null, 171 | "renamedTo": null, 172 | "changes": { 173 | "renamed": false, 174 | "modified": false, 175 | "deleted": false, 176 | "added": false 177 | }, 178 | "children": [] 179 | }, 180 | { 181 | "name": "moduleB/src/main/java/MovedClassFromAToB.java", 182 | "type": "FILE", 183 | "commit1Metrics": null, 184 | "commit2Metrics": { 185 | "coderadar:size:loc:java": 2, 186 | "coderadar:size:eloc:java": 1, 187 | "coderadar:size:sloc:java": 2 188 | }, 189 | "renamedFrom": "moduleA/src/main/java/MovedClassFromAToB.java", 190 | "renamedTo": null, 191 | "changes": { 192 | "renamed": true, 193 | "modified": false, 194 | "deleted": false, 195 | "added": false 196 | }, 197 | "children": [] 198 | } 199 | ] 200 | } 201 | ] 202 | } -------------------------------------------------------------------------------- /fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Coderadar Visualization 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 |
24 | 25 |
26 |
    27 |
    28 |
    29 |
    30 | 31 |
    32 | 33 |
    34 | 35 |
    36 |
    37 |
    38 |
    39 | 40 |
    41 |
      42 |
      43 |
      44 |
      45 | 46 |
      47 | 48 |
      49 |
      50 | Ansicht 51 |
      52 |
      53 | 54 | 55 |
      56 |
      57 | 58 | 59 |
      60 |
      61 | 62 |
      63 |
      64 | Kamerabewegung 65 |
      66 |
      67 | 68 | 69 |
      70 |
      71 | 72 | 73 |
      74 |
      75 | 76 |
      77 | 78 |
      79 |
      80 | 87 |
      88 |
      89 | 90 | 91 |
      92 | 93 | 94 |
      95 | 96 |
      97 | 98 | 99 |
      100 | 101 |
      102 | 103 | 104 |
      105 |
      106 |
      107 |
      108 |
      109 | 110 |
      111 |
      112 | 113 |
      114 |
      115 | 116 | 117 |
      118 | 119 | 120 |
      121 |
      122 | 123 | 124 |
      125 |
      126 | 127 | 128 |
      129 |
      130 | 131 | 132 |
      133 |
      134 | 135 | 136 |
      137 |
      138 |
      139 |
      140 |
      141 | 142 |
      143 | 144 |
      145 | 146 | 147 |
      148 | 149 |
      150 | 151 |
      152 | 153 | 154 |
      155 |
      156 |
      157 |
      158 | 159 |
      160 |
        161 |
        162 |
        163 |
        164 |
        165 | 166 |
        167 |
        168 |
        169 |
        170 |
        171 |
        172 |
        173 |

        174 |
        175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 |
        MetrikÄnderung
        186 |
        187 |
        188 |
        189 |
        190 | 191 | 192 |
        193 | 197 | 201 | 205 | 209 | 213 |
        214 |
        215 | 216 |
        217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | -------------------------------------------------------------------------------- /js/drawer/MergedDrawer.js: -------------------------------------------------------------------------------- 1 | import {Block} from '../shape/Block'; 2 | import {BlockConnection} from '../shape/BlockConnection'; 3 | import {config} from '../Config'; 4 | import * as Constants from '../Constants'; 5 | import {AbstractDrawer} from './AbstractDrawer'; 6 | import {ElementAnalyzer} from '../util/ElementAnalyzer'; 7 | import {ColorHelper} from '../util/ColorHelper'; 8 | 9 | export class MergedDrawer extends AbstractDrawer { 10 | 11 | constructor(scene, position) { 12 | super(scene, position); 13 | 14 | this.movedElements = []; 15 | } 16 | 17 | // override 18 | drawElements(elements, parent, bottom = 0) { 19 | if (!Array.isArray(elements)) { 20 | elements = [elements]; 21 | } 22 | 23 | elements.forEach((element) => { 24 | if (!element.fit) { 25 | console.warn(`element ${element.name} at position ${this.position} has no fit!`); 26 | return; 27 | } 28 | 29 | var blueHeight; 30 | 31 | // FILE 32 | if (element.type == Constants.ELEMENT_TYPE_FILE) { 33 | var blueHeightMetric = ElementAnalyzer.getMetricValueOfElementAndCommitType(element, config.HEIGHT_METRIC_NAME, Constants.COMMIT_TYPE_CURRENT, this.position); 34 | var orangeHeightMetric = ElementAnalyzer.getMetricValueOfElementAndCommitType(element, config.HEIGHT_METRIC_NAME, Constants.COMMIT_TYPE_OTHER, this.position); 35 | 36 | var blueGroundAreaMetric = ElementAnalyzer.getMetricValueOfElementAndCommitType(element, config.GROUND_AREA_METRIC_NAME, Constants.COMMIT_TYPE_CURRENT, this.position); 37 | var orangeGroundAreaMetric = ElementAnalyzer.getMetricValueOfElementAndCommitType(element, config.GROUND_AREA_METRIC_NAME, Constants.COMMIT_TYPE_OTHER, this.position); 38 | 39 | var blueColorMetric = ElementAnalyzer.getMetricValueOfElementAndCommitType(element, config.COLOR_METRIC_NAME, Constants.COMMIT_TYPE_CURRENT, this.position); 40 | var orangeColorMetric = ElementAnalyzer.getMetricValueOfElementAndCommitType(element, config.COLOR_METRIC_NAME, Constants.COMMIT_TYPE_OTHER, this.position); 41 | 42 | var blueMetrics = { 43 | [config.HEIGHT_METRIC_NAME]: blueHeightMetric, 44 | [config.GROUND_AREA_METRIC_NAME]: blueGroundAreaMetric, 45 | [config.COLOR_METRIC_NAME]: blueColorMetric 46 | }; 47 | 48 | var orangeMetrics = { 49 | [config.HEIGHT_METRIC_NAME]: orangeHeightMetric, 50 | [config.GROUND_AREA_METRIC_NAME]: orangeGroundAreaMetric, 51 | [config.COLOR_METRIC_NAME]: orangeColorMetric 52 | }; 53 | 54 | blueHeight = blueHeightMetric * config.HEIGHT_FACTOR + config.GLOBAL_MIN_HEIGHT; 55 | var orangeHeight = orangeHeightMetric * config.HEIGHT_FACTOR + config.GLOBAL_MIN_HEIGHT; 56 | 57 | var blueGA = blueGroundAreaMetric * config.GROUND_AREA_FACTOR + config.GLOBAL_MIN_GROUND_AREA + config.BLOCK_SPACING; 58 | var orangeGA = orangeGroundAreaMetric * config.GROUND_AREA_FACTOR + config.GLOBAL_MIN_GROUND_AREA + config.BLOCK_SPACING; 59 | 60 | var blueColor = ColorHelper.getColorByPosition(this.position); 61 | var orangeColor = ColorHelper.getContraryColorByColor(blueColor); 62 | 63 | var blueTransparency = blueHeight >= orangeHeight && blueGA >= orangeGA; 64 | var orangeTransparency = orangeHeight >= blueHeight && orangeGA >= blueGA; 65 | 66 | if (!isNaN(blueGA) && !isNaN(orangeGA)) { 67 | // both blocks 68 | if (blueGA < orangeGA) { 69 | // draw the bigger block ... 70 | this.drawBlock(element, parent, orangeColor, orangeGA, bottom, orangeHeight, orangeTransparency, orangeMetrics, Constants.COMMIT_TYPE_OTHER, { modified: true }); 71 | 72 | // ... calculate the center position for the smaller block ... 73 | element.fit.x += (orangeGA - blueGA) / 2; 74 | element.fit.y += (orangeGA - blueGA) / 2; 75 | 76 | // ... draw the smaller block 77 | this.drawBlock(element, parent, blueColor, blueGA, bottom, blueHeight, blueTransparency, blueMetrics, Constants.COMMIT_TYPE_CURRENT, { modified: true }); 78 | } else if (blueGA > orangeGA) { 79 | // draw the bigger block ... 80 | this.drawBlock(element, parent, blueColor, blueGA, bottom, blueHeight, blueTransparency, blueMetrics, Constants.COMMIT_TYPE_CURRENT, { modified: true }); 81 | 82 | // ... calculate the center position for the smaller block ... 83 | element.fit.x += (blueGA - orangeGA) / 2; 84 | element.fit.y += (blueGA - orangeGA) / 2; 85 | 86 | // ... draw the smaller block 87 | this.drawBlock(element, parent, orangeColor, orangeGA, bottom, orangeHeight, orangeTransparency, orangeMetrics, Constants.COMMIT_TYPE_OTHER, { modified: true }); 88 | } else { 89 | // ground areas are the same 90 | if (blueHeight != orangeHeight) { 91 | // heights are different, so draw both blocks 92 | this.drawBlock(element, parent, blueColor, blueGA, bottom, blueHeight, blueTransparency, blueMetrics, Constants.COMMIT_TYPE_CURRENT, { modified: true }); 93 | this.drawBlock(element, parent, orangeColor, orangeGA, bottom, orangeHeight, orangeTransparency, orangeMetrics, Constants.COMMIT_TYPE_OTHER, { modified: true }); 94 | } else { 95 | // heights are the same, so the file has not changed 96 | this.drawBlock(element, parent, config.COLOR_UNCHANGED_FILE, orangeGA, bottom, orangeHeight, false, orangeMetrics, undefined, { modified: false }); 97 | } 98 | } 99 | 100 | } else if (isNaN(orangeGA)) { 101 | // only blue block 102 | 103 | var changeTypes = { deleted: true }; 104 | // cache element to draw connections 105 | if (this._isElementMoved(element)) { 106 | this.movedElements.push({ 107 | fromElementName: element.name, 108 | toElementName: element.renamedTo 109 | }); 110 | 111 | changeTypes.moved = true; 112 | } 113 | 114 | this.drawBlock(element, parent, config.COLOR_DELETED_FILE, blueGA, bottom, blueHeight, false, blueMetrics, Constants.COMMIT_TYPE_CURRENT, changeTypes); 115 | 116 | } else if (isNaN(blueGA)) { 117 | // only orange block 118 | 119 | var changeTypes = { added: true }; 120 | if (this._isElementMoved(element)) { 121 | changeTypes.moved = true; 122 | } 123 | 124 | this.drawBlock(element, parent, config.COLOR_ADDED_FILE, orangeGA, bottom, orangeHeight, false, orangeMetrics, Constants.COMMIT_TYPE_OTHER, changeTypes); 125 | } 126 | 127 | // MODULE 128 | } else { 129 | // don't draw empty modules 130 | if (ElementAnalyzer.hasChildrenForCurrentCommit(element, true, this.position)) { 131 | if (bottom > this.maxBottomValue) { 132 | this.maxBottomValue = bottom; 133 | } 134 | 135 | blueHeight = config.DEFAULT_BLOCK_HEIGHT; 136 | this.drawBlock(element, parent, config.COLOR_HIERARCHY_RANGE[0], undefined, bottom, blueHeight, false); 137 | } 138 | } 139 | 140 | // recursion 141 | if (element.children && element.children.length > 0) { 142 | this.drawElements(element.children, element, bottom + blueHeight); 143 | } 144 | }); 145 | } 146 | 147 | // override 148 | drawBlock(element, parent, color, currentCommitSize, bottom, height, isTransparent, metrics, commitType, changeTypes) { 149 | var finalX, finalY, finalZ; 150 | var finalWidth, finalHeight, finalDepth; 151 | 152 | var cube = new Block(color, element.name); 153 | finalX = element.fit.x + (parent ? parent.renderedX : 0) + config.BLOCK_SPACING; 154 | finalY = bottom; 155 | finalZ = element.fit.y + (parent ? parent.renderedY : 0) + config.BLOCK_SPACING; 156 | 157 | // save the rendered positions to draw children relative to their parent 158 | element.renderedX = finalX; 159 | element.renderedY = finalZ; 160 | 161 | finalWidth = element.type == Constants.ELEMENT_TYPE_FILE ? currentCommitSize - config.BLOCK_SPACING : element.w - config.BLOCK_SPACING * 2; 162 | finalHeight = height; 163 | finalDepth = element.type == Constants.ELEMENT_TYPE_FILE ? currentCommitSize - config.BLOCK_SPACING : element.h - config.BLOCK_SPACING * 2; 164 | 165 | if (isTransparent) { 166 | cube.material.transparent = true; 167 | cube.material.opacity = 0.4; 168 | } 169 | 170 | cube.position.x = finalX; 171 | cube.position.y = finalY; 172 | cube.position.z = finalZ; 173 | 174 | cube.scale.x = finalWidth; 175 | cube.scale.y = finalHeight; 176 | cube.scale.z = finalDepth; 177 | 178 | cube.userData = { 179 | parentName: parent ? parent.name : undefined, 180 | bottom: bottom, 181 | metrics: metrics, 182 | type: element.type, 183 | tooltipLabel: this._generateTooltipHtml(element.name, metrics), 184 | isHelper: isTransparent, 185 | changeTypes: changeTypes, 186 | commitType: commitType 187 | }; 188 | 189 | this.scene.add(cube); 190 | } 191 | 192 | drawBlockConnections() { 193 | for (let movedElementPair of this.movedElements) { 194 | var fromElement = this.scene.getObjectByName(movedElementPair.fromElementName); 195 | var toElement = this.scene.getObjectByName(movedElementPair.toElementName); 196 | 197 | if (fromElement && toElement) { 198 | this.drawBlockConnection(fromElement, toElement); 199 | } else { 200 | console.warn(`A connection could not be drawn because at least one element could not be found in the scene.`); 201 | } 202 | } 203 | } 204 | 205 | drawBlockConnection(fromElement, toElement) { 206 | this.scene.add(new BlockConnection(fromElement, toElement).getCurve()); 207 | } 208 | 209 | _isElementMoved(element) { 210 | return element.renamedTo != null || element.renamedFrom != null; 211 | } 212 | } -------------------------------------------------------------------------------- /test/util/test_ElementAnalyzer.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | import {ElementAnalyzer} from '../../js/util/ElementAnalyzer'; 4 | import * as Constants from '../../js/Constants'; 5 | 6 | var commit1Metrics = { 7 | "metric1Name": 111, 8 | "metric2Name": 222, 9 | "metric3Name": 333 10 | }; 11 | 12 | var commit2Metrics = { 13 | "metric1Name": 444, 14 | "metric2Name": 555, 15 | "metric3Name": 666 16 | }; 17 | 18 | var validJsonContainingCommitId = { 19 | type: 'MODULE', 20 | children: [ 21 | { 22 | type: 'FILE', 23 | 'commit1Metrics': null, 24 | 'commit2Metrics': { 25 | 'coderadar:size:loc:java': 9, 26 | 'coderadar:size:eloc:java': 3, 27 | 'coderadar:size:sloc:java': 5 28 | }, 29 | children: [] 30 | }, 31 | { 32 | type: 'MODULE', 33 | children: [ 34 | { 35 | type: 'FILE', 36 | 'commit1Metrics': { 37 | 'coderadar:size:loc:java': 9, 38 | 'coderadar:size:eloc:java': 3, 39 | 'coderadar:size:sloc:java': 5 40 | }, 41 | 'commit2Metrics': { 42 | 'coderadar:size:loc:java': 9, 43 | 'coderadar:size:eloc:java': 3, 44 | 'coderadar:size:sloc:java': 5 45 | }, 46 | children: [] 47 | } 48 | ] 49 | } 50 | ] 51 | }; 52 | var validJsonNotContainingCommitId = { 53 | type: 'MODULE', 54 | children: [ 55 | { 56 | type: 'FILE', 57 | 'commit1Metrics': null, 58 | 'commit2Metrics': { 59 | 'coderadar:size:loc:java': 9, 60 | 'coderadar:size:eloc:java': 3, 61 | 'coderadar:size:sloc:java': 5 62 | }, 63 | children: [] 64 | }, 65 | { 66 | type: 'MODULE', 67 | children: [ 68 | { 69 | type: 'FILE', 70 | 'commit1Metrics': null, 71 | 'commit2Metrics': { 72 | 'coderadar:size:loc:java': 9, 73 | 'coderadar:size:eloc:java': 3, 74 | 'coderadar:size:sloc:java': 5 75 | }, 76 | children: [] 77 | } 78 | ] 79 | } 80 | ] 81 | }; 82 | var exampleElement = { 83 | 'type': 'FILE', 84 | 'commit1Metrics': { 85 | 'coderadar:size:loc:java': 1, 86 | 'coderadar:size:eloc:java': 2, 87 | 'coderadar:size:sloc:java': 3 88 | }, 89 | 'commit2Metrics': { 90 | 'coderadar:size:loc:java': 4, 91 | 'coderadar:size:eloc:java': 5, 92 | 'coderadar:size:sloc:java': 6 93 | } 94 | }; 95 | var exampleElementWithoutFirstCommitData = { 96 | 'type': 'FILE', 97 | 'commit1Metrics': null, 98 | 'commit2Metrics': { 99 | 'coderadar:size:loc:java': 4, 100 | 'coderadar:size:eloc:java': 5, 101 | 'coderadar:size:sloc:java': 6 102 | } 103 | }; 104 | 105 | describe('ElementAnalyzer', function () { 106 | describe('generateUniqueElementList', function () { 107 | var elements = [ 108 | { name: 'a', children: [] }, 109 | { name: 'b', children: [] }, 110 | { name: 'c', children: [] }, 111 | { name: 'a', children: [ 112 | { name: 'd', children: [] }, 113 | { name: 'b', children: [] } 114 | ] } 115 | ]; 116 | 117 | it('should return empty array when no elements are given', function () { 118 | assert.deepEqual( 119 | ElementAnalyzer.generateUniqueElementList([]), 120 | [] 121 | ); 122 | }); 123 | 124 | it('should return unique elements', function () { 125 | assert.deepEqual( 126 | ElementAnalyzer.generateUniqueElementList(elements), 127 | ['a', 'b', 'c', 'd'] 128 | ); 129 | }); 130 | }); 131 | 132 | describe('getMinMetricValueByMetricName', function () { 133 | it('should return the minimum value', function () { 134 | assert.equal( 135 | ElementAnalyzer.getMinMetricValueByMetricName(commit1Metrics, commit2Metrics, 'metric1Name'), 136 | 111 137 | ); 138 | }); 139 | 140 | it('should return undefined when an unknown metric name is given', function () { 141 | assert.equal( 142 | ElementAnalyzer.getMinMetricValueByMetricName(commit1Metrics, commit2Metrics, 'unknown'), 143 | undefined 144 | ); 145 | }); 146 | 147 | it('should return results of first commit metrics if second commit metrics are null', function () { 148 | assert.equal( 149 | ElementAnalyzer.getMinMetricValueByMetricName(commit1Metrics, null, 'metric1Name'), 150 | 111 151 | ); 152 | }); 153 | 154 | it('should return results of second commit metrics if first commit metrics are null', function () { 155 | assert.equal( 156 | ElementAnalyzer.getMinMetricValueByMetricName(null, commit2Metrics, 'metric1Name'), 157 | 444 158 | ); 159 | }); 160 | 161 | it('should throw an error if both commit metrics are null', function () { 162 | assert.throws(() => { 163 | ElementAnalyzer.getMinMetricValueByMetricName(null, null, 'metric1Name'); 164 | }); 165 | }); 166 | }); 167 | 168 | describe('getMaxMetricValueByMetricName', function () { 169 | it('should return the maximum value', function () { 170 | assert.equal( 171 | ElementAnalyzer.getMaxMetricValueByMetricName(commit1Metrics, commit2Metrics, 'metric1Name'), 172 | 444 173 | ); 174 | }); 175 | 176 | it('should return undefined when an unknown metric name is given', function () { 177 | assert.equal( 178 | ElementAnalyzer.getMaxMetricValueByMetricName(commit1Metrics, commit2Metrics, 'unknown'), 179 | undefined 180 | ); 181 | }); 182 | 183 | it('should return results of first commit metrics if second commit metrics are null', function () { 184 | assert.equal( 185 | ElementAnalyzer.getMaxMetricValueByMetricName(commit1Metrics, null, 'metric1Name'), 186 | 111 187 | ); 188 | }); 189 | 190 | it('should return results of second commit metrics if first commit metrics are null', function () { 191 | assert.equal( 192 | ElementAnalyzer.getMaxMetricValueByMetricName(null, commit2Metrics, 'metric1Name'), 193 | 444 194 | ); 195 | }); 196 | 197 | it('should throw an error if both commit metrics are null', function () { 198 | assert.throws(() => { 199 | ElementAnalyzer.getMaxMetricValueByMetricName(null, null, 'metric1Name'); 200 | }); 201 | }); 202 | }); 203 | 204 | describe('findSmallestAndBiggestMetricValueByMetricName', function () { 205 | var deltaTree = require('./../data/deltaTree.json'); 206 | it('should return smallest and biggest metric value of given delta tree', function () { 207 | assert.deepEqual( 208 | ElementAnalyzer.findSmallestAndBiggestMetricValueByMetricName(deltaTree, 'coderadar:size:loc:java'), 209 | { 210 | min: 2, 211 | max: 200 212 | } 213 | ); 214 | }); 215 | }); 216 | 217 | describe('hasChildrenForCurrentCommit', function () { 218 | it('should return true when children are found', function () { 219 | assert.equal(ElementAnalyzer.hasChildrenForCurrentCommit(validJsonContainingCommitId, false, Constants.LEFT_SCREEN), true); 220 | assert.equal(ElementAnalyzer.hasChildrenForCurrentCommit(validJsonContainingCommitId, false, Constants.RIGHT_SCREEN), true); 221 | }); 222 | 223 | it('should return false when no children are found', function () { 224 | assert.equal(ElementAnalyzer.hasChildrenForCurrentCommit(validJsonNotContainingCommitId, false, Constants.LEFT_SCREEN), false); 225 | }); 226 | }); 227 | 228 | describe('hasMetricValuesForCurrentCommit', function () { 229 | it('should return true when current commit is found', function () { 230 | assert.equal(ElementAnalyzer.hasMetricValuesForCurrentCommit(exampleElement, false, Constants.LEFT_SCREEN), true); 231 | assert.equal(ElementAnalyzer.hasMetricValuesForCurrentCommit(exampleElement, false, Constants.RIGHT_SCREEN), true); 232 | }); 233 | 234 | it('should return false when current commit is not found', function () { 235 | assert.equal(ElementAnalyzer.hasMetricValuesForCurrentCommit(exampleElementWithoutFirstCommitData, false, Constants.LEFT_SCREEN), false); 236 | }); 237 | }); 238 | 239 | describe('getMetricValueOfElementAndCurrentCommit', function () { 240 | it('should return metricValue when metric is found for current commit', function () { 241 | assert.equal(ElementAnalyzer.getMetricValueOfElementAndCommitType(exampleElement, 'coderadar:size:loc:java', Constants.COMMIT_TYPE_CURRENT, Constants.LEFT_SCREEN), 1); 242 | }); 243 | 244 | it('should return metricValue when metric is found for other commit', function () { 245 | assert.equal(ElementAnalyzer.getMetricValueOfElementAndCommitType(exampleElement, 'coderadar:size:loc:java', Constants.COMMIT_TYPE_OTHER, Constants.LEFT_SCREEN), 4); 246 | }); 247 | 248 | it('should return undefined when an empty element is given', function () { 249 | assert.equal(ElementAnalyzer.getMetricValueOfElementAndCommitType({}, 'coderadar:size:loc:java', Constants.COMMIT_TYPE_CURRENT, Constants.LEFT_SCREEN), undefined); 250 | }); 251 | 252 | it('should return undefined when metric is not found', function () { 253 | assert.equal(ElementAnalyzer.getMetricValueOfElementAndCommitType(exampleElement, 'unknown', Constants.COMMIT_TYPE_CURRENT, Constants.LEFT_SCREEN), undefined); 254 | }); 255 | 256 | it('should throw an error when unknown commit type is given', function () { 257 | assert.throws(() => { 258 | ElementAnalyzer.getMetricValueOfElementAndCommitType(exampleElement, 'coderadar:size:loc:java', 'unknown', Constants.LEFT_SCREEN); 259 | }); 260 | }); 261 | 262 | it('should throw an error when unknown screen position is given', function () { 263 | assert.throws(() => { 264 | ElementAnalyzer.getMetricValueOfElementAndCommitType(exampleElement, 'coderadar:size:loc:java', Constants.COMMIT_TYPE_OTHER, 'unknown'); 265 | }); 266 | }); 267 | }); 268 | }); -------------------------------------------------------------------------------- /js/Application.js: -------------------------------------------------------------------------------- 1 | import {UserInterface} from './ui/UserInterface'; 2 | import {Screen} from './Screen'; 3 | import {config} from './Config'; 4 | import * as Constants from './Constants'; 5 | import {ElementAnalyzer} from './util/ElementAnalyzer'; 6 | import {MergedDrawer} from './drawer/MergedDrawer'; 7 | import {SingleDrawer} from './drawer/SingleDrawer'; 8 | import {ServiceLocator} from './service/ServiceLocator'; 9 | import * as PubSub from 'pubsub-js'; 10 | 11 | export class Application { 12 | 13 | constructor() { 14 | this.SYNCHRONIZE_ENABLED = true; 15 | this.IS_FULLSCREEN = false; 16 | 17 | this._uniqueElementList = []; 18 | 19 | this.authorizationService = ServiceLocator.getInstance().get('authorizationService'); 20 | this.commitService = ServiceLocator.getInstance().get('commitService'); 21 | this.metricService = ServiceLocator.getInstance().get('metricService'); 22 | this.metricNameService = ServiceLocator.getInstance().get('metricNameService'); 23 | 24 | this._createUserInterface(); 25 | this._initializeEventListeners(); 26 | 27 | this.leftCommitId = undefined; 28 | this.rightCommitId = undefined; 29 | 30 | this.result = undefined; 31 | this.minMaxPairOfColorMetric = undefined; 32 | 33 | this.screens = {}; 34 | this.createLeftScreen(); 35 | this.createRightScreen(); 36 | } 37 | 38 | initialize() { 39 | this.login() 40 | .then(this.loadCommits.bind(this)) 41 | .then(this.loadMetricData.bind(this)); 42 | } 43 | 44 | login() { 45 | return this.authorizationService.authorize(); 46 | } 47 | 48 | loadCommits() { 49 | return this.commitService.load() 50 | .then(() => { 51 | var commits = this.commitService.getCommits(); 52 | 53 | this.leftCommitId = commits[1].getName(); 54 | this.rightCommitId = commits[0].getName(); 55 | 56 | PubSub.publish('commitsLoaded', { commits: commits }); 57 | 58 | this.getLeftScreen().setCommitId(this.leftCommitId); 59 | this.getRightScreen().setCommitId(this.rightCommitId); 60 | }); 61 | } 62 | 63 | loadMetricData() { 64 | this.userInterface.showLoadingIndicator(); 65 | return this.metricService.loadDeltaTree(this.leftCommitId, this.rightCommitId) 66 | .then((result) => { 67 | this.result = result.data; 68 | 69 | this._uniqueElementList = ElementAnalyzer.generateUniqueElementList(this.result); 70 | var minMaxPairOfHeight = ElementAnalyzer.findSmallestAndBiggestMetricValueByMetricName(this.result, config.HEIGHT_METRIC_NAME); 71 | var minMaxPairOfGroundArea = ElementAnalyzer.findSmallestAndBiggestMetricValueByMetricName(this.result, config.GROUND_AREA_METRIC_NAME); 72 | this.minMaxPairOfColorMetric = ElementAnalyzer.findSmallestAndBiggestMetricValueByMetricName(this.result, config.COLOR_METRIC_NAME); 73 | 74 | config.HEIGHT_FACTOR = config.GLOBAL_MAX_HEIGHT / minMaxPairOfHeight.max; 75 | config.GROUND_AREA_FACTOR = config.GLOBAL_MAX_GROUND_AREA / minMaxPairOfGroundArea.max; 76 | 77 | this._initializeScreens(); 78 | this.userInterface.hideLoadingIndicator(); 79 | 80 | PubSub.publish('metricsLoaded'); 81 | }); 82 | } 83 | 84 | _initializeScreens() { 85 | if (this.IS_FULLSCREEN) { 86 | this.getLeftScreen().reset(); 87 | this.getLeftScreen().setData(this.result, this.minMaxPairOfColorMetric); 88 | this.getLeftScreen().setDrawer(MergedDrawer); 89 | this.getLeftScreen().render(); 90 | this.getLeftScreen().centerCamera(); 91 | } else { 92 | this.getLeftScreen().reset(); 93 | this.getLeftScreen().setData(this.result, this.minMaxPairOfColorMetric); 94 | this.getLeftScreen().setDrawer(SingleDrawer); 95 | this.getLeftScreen().render(); 96 | this.getLeftScreen().centerCamera(); 97 | 98 | this.getRightScreen().reset(); 99 | this.getRightScreen().setData(this.result, this.minMaxPairOfColorMetric); 100 | this.getRightScreen().setDrawer(SingleDrawer); 101 | this.getRightScreen().render(); 102 | this.getRightScreen().centerCamera(); 103 | } 104 | } 105 | 106 | _handleSingleSplitToggle(enabled) { 107 | this.IS_FULLSCREEN = enabled; 108 | 109 | if (this.IS_FULLSCREEN) { 110 | document.querySelector('#stage').classList.remove('split'); 111 | this.getLeftScreen().reset(); 112 | this.getLeftScreen().setFullscreen(); 113 | this.getLeftScreen().setDrawer(MergedDrawer); 114 | this.getLeftScreen().render(); 115 | 116 | this.getRightScreen().setFullscreen(); 117 | } else { 118 | document.querySelector('#stage').classList.add('split'); 119 | this.getLeftScreen().reset(); 120 | this.getLeftScreen().setSplitscreen(); 121 | this.getLeftScreen().setDrawer(SingleDrawer); 122 | this.getLeftScreen().render(); 123 | 124 | this.getRightScreen().reset(); 125 | this.getRightScreen().setSplitscreen(); 126 | this.getRightScreen().setData(this.result, this.minMaxPairOfColorMetric); 127 | this.getRightScreen().setDrawer(SingleDrawer); 128 | this.getRightScreen().render(); 129 | } 130 | } 131 | 132 | createLeftScreen() { 133 | this.screens[Constants.LEFT_SCREEN] = new Screen(Constants.LEFT_SCREEN); 134 | } 135 | 136 | createRightScreen() { 137 | this.screens[Constants.RIGHT_SCREEN] = new Screen(Constants.RIGHT_SCREEN); 138 | } 139 | 140 | getLeftScreen() { 141 | return this.screens[Constants.LEFT_SCREEN]; 142 | } 143 | 144 | getRightScreen() { 145 | return this.screens[Constants.RIGHT_SCREEN]; 146 | } 147 | 148 | getIsFullscreen() { 149 | return this.IS_FULLSCREEN; 150 | } 151 | 152 | _createUserInterface() { 153 | this.userInterface = new UserInterface(this); 154 | } 155 | 156 | getUniqueElementList() { 157 | return this._uniqueElementList; 158 | } 159 | 160 | _initializeEventListeners() { 161 | PubSub.subscribe('dimensionChange', (eventName, args) => { 162 | switch (args.dimension) { 163 | case Constants.HEIGHT_DIMENSION: 164 | config.HEIGHT_METRIC_NAME = this.metricNameService.getMetricNameByShortName(args.metricName); 165 | break; 166 | case Constants.GROUNDAREA_DIMENSION: 167 | config.GROUND_AREA_METRIC_NAME = this.metricNameService.getMetricNameByShortName(args.metricName); 168 | break; 169 | case Constants.COLOR_DIMENSION: 170 | config.COLOR_METRIC_NAME = this.metricNameService.getMetricNameByShortName(args.metricName); 171 | this.userInterface.getLegendComponent().setColorCode(); 172 | break; 173 | default: 174 | throw new Error(`Unknown dimension ${args.dimension}!`); 175 | } 176 | 177 | this.loadMetricData(); 178 | }); 179 | 180 | PubSub.subscribe('commitChange', (eventName, args) => { 181 | if (args.commitType == Constants.FIRST_COMMIT) { 182 | this.leftCommitId = args.commitId; 183 | this.getLeftScreen().setCommitId(this.leftCommitId); 184 | } else if (args.commitType == Constants.SECOND_COMMIT) { 185 | this.rightCommitId = args.commitId; 186 | this.getRightScreen().setCommitId(this.rightCommitId); 187 | } else { 188 | throw new Error(`Unknown screen type ${args.commitType}!`); 189 | } 190 | 191 | this.loadMetricData(); 192 | PubSub.publish('closeComparisonContainer'); 193 | }); 194 | 195 | PubSub.subscribe('synchronizeEnabledChange', (eventName, args) => { 196 | if (args.enabled) { 197 | this.getLeftScreen().centerCamera(); 198 | this.getRightScreen().centerCamera(); 199 | } 200 | 201 | this.SYNCHRONIZE_ENABLED = args.enabled; 202 | }); 203 | 204 | PubSub.subscribe('fullSplitToggle', (eventName, args) => { 205 | this._handleSingleSplitToggle(args.enabled); 206 | }); 207 | 208 | PubSub.subscribe('mouseMove', (eventName, args) => { 209 | if (args.screen == Constants.LEFT_SCREEN) { 210 | this.getLeftScreen().getControls().enabled = true; 211 | this.getRightScreen().getControls().enabled = this.SYNCHRONIZE_ENABLED; 212 | 213 | this.getLeftScreen().getInteractionHandler().setEnabled(true); 214 | this.getRightScreen().getInteractionHandler().setEnabled(false); 215 | } else if (args.screen == Constants.RIGHT_SCREEN) { 216 | this.getLeftScreen().getControls().enabled = this.SYNCHRONIZE_ENABLED; 217 | this.getRightScreen().getControls().enabled = true; 218 | 219 | this.getLeftScreen().getInteractionHandler().setEnabled(false); 220 | this.getRightScreen().getInteractionHandler().setEnabled(true); 221 | } 222 | }); 223 | 224 | PubSub.subscribe('elementClicked', (eventName, args) => { 225 | if (args.doReset) { 226 | PubSub.publish('closeComparisonContainer'); 227 | } else { 228 | PubSub.publish('openComparisonContainer', { 229 | leftElement: this._findLeftElementForComparisonByName(args.elementName), 230 | rightElement: this._findRightElementForComparisonByName(args.elementName) 231 | }); 232 | } 233 | }); 234 | 235 | PubSub.subscribe('searchEntryClicked', (eventName, args) => { 236 | PubSub.publish('openComparisonContainer', { 237 | leftElement: this._findLeftElementForComparisonByName(args.elementName), 238 | rightElement: this._findRightElementForComparisonByName(args.elementName) 239 | }); 240 | }); 241 | } 242 | 243 | _findLeftElementForComparisonByName(elementName) { 244 | // when we are in fullscreen mode, we need to look for the comparing elements only in the left screen. 245 | if (this.IS_FULLSCREEN) { 246 | for (var i = this.getLeftScreen().getScene().children.length - 1; i >= 0; i--) { 247 | var child = this.getLeftScreen().getScene().children[i]; 248 | if (child.name == elementName) { 249 | if (child.userData.commitType != Constants.COMMIT_TYPE_OTHER) { 250 | return child; 251 | } 252 | } 253 | } 254 | } else { 255 | return this.getLeftScreen().getScene().getObjectByName(elementName); 256 | } 257 | } 258 | 259 | _findRightElementForComparisonByName(elementName) { 260 | // when we are in fullscreen mode, we need to look for the comparing elements only in the left screen. 261 | if (this.IS_FULLSCREEN) { 262 | for (var i = this.getLeftScreen().getScene().children.length - 1; i >= 0; i--) { 263 | var child = this.getLeftScreen().getScene().children[i]; 264 | if (child.name == elementName) { 265 | if (child.userData.commitType != Constants.COMMIT_TYPE_CURRENT) { 266 | return child; 267 | } 268 | } 269 | } 270 | } else { 271 | return this.getRightScreen().getScene().getObjectByName(elementName); 272 | } 273 | } 274 | } -------------------------------------------------------------------------------- /editor/scene_merged.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "version": 4.4, 4 | "type": "Object", 5 | "generator": "Object3D.toJSON" 6 | }, 7 | "geometries": [ 8 | { 9 | "uuid": "EEFD52EF-B323-4308-8C64-A1E468D80A3D", 10 | "type": "BoxBufferGeometry", 11 | "width": 2, 12 | "height": 3, 13 | "depth": 2, 14 | "widthSegments": 0, 15 | "heightSegments": 0, 16 | "depthSegments": 0 17 | }, 18 | { 19 | "uuid": "505D8D30-A583-4FD6-A0E1-0E1CE800F89B", 20 | "type": "BoxBufferGeometry", 21 | "width": 2.5, 22 | "height": 5, 23 | "depth": 2.5, 24 | "widthSegments": 0, 25 | "heightSegments": 0, 26 | "depthSegments": 0 27 | }, 28 | { 29 | "uuid": "A144D93E-1061-4893-9E10-FF84CA392CCC", 30 | "type": "BoxBufferGeometry", 31 | "width": 1, 32 | "height": 5, 33 | "depth": 1, 34 | "widthSegments": 0, 35 | "heightSegments": 0, 36 | "depthSegments": 0 37 | }, 38 | { 39 | "uuid": "50356D09-6886-4620-BEFD-1300DE4D9267", 40 | "type": "BoxBufferGeometry", 41 | "width": 2, 42 | "height": 3, 43 | "depth": 2, 44 | "widthSegments": 0, 45 | "heightSegments": 0, 46 | "depthSegments": 0 47 | }, 48 | { 49 | "uuid": "FBB2B58B-493E-4A65-BAF0-D5B22824FA87", 50 | "type": "BoxBufferGeometry", 51 | "width": 0.5, 52 | "height": 3, 53 | "depth": 0.5, 54 | "widthSegments": 0, 55 | "heightSegments": 0, 56 | "depthSegments": 0 57 | }, 58 | { 59 | "uuid": "E873E4F9-F73B-4350-B4F6-9F7AFD3CE79B", 60 | "type": "BoxBufferGeometry", 61 | "width": 2, 62 | "height": 5, 63 | "depth": 2, 64 | "widthSegments": 0, 65 | "heightSegments": 0, 66 | "depthSegments": 0 67 | }, 68 | { 69 | "uuid": "4F22ACEE-0CB1-4D1E-ACD5-BA430FD36C11", 70 | "type": "BoxBufferGeometry", 71 | "width": 5, 72 | "height": 1, 73 | "depth": 5, 74 | "widthSegments": 0, 75 | "heightSegments": 0, 76 | "depthSegments": 0 77 | }, 78 | { 79 | "uuid": "5CBD9923-F393-43AA-B2DB-9E8B119952C6", 80 | "type": "BoxBufferGeometry", 81 | "width": 18, 82 | "height": 0.1, 83 | "depth": 9, 84 | "widthSegments": 0, 85 | "heightSegments": 0, 86 | "depthSegments": 0 87 | }, 88 | { 89 | "uuid": "61885183-AED5-4707-9E26-9BA4AE7125CD", 90 | "type": "BoxBufferGeometry", 91 | "width": 4, 92 | "height": 8, 93 | "depth": 4, 94 | "widthSegments": 0, 95 | "heightSegments": 0, 96 | "depthSegments": 0 97 | }, 98 | { 99 | "uuid": "F27802B3-8EE7-40FD-B0F6-4F76F6E2B9AF", 100 | "type": "BoxBufferGeometry", 101 | "width": 4, 102 | "height": 4.5, 103 | "depth": 4, 104 | "widthSegments": 0, 105 | "heightSegments": 0, 106 | "depthSegments": 0 107 | }, 108 | { 109 | "uuid": "9ADB9797-5668-4ED4-A035-8782F7789439", 110 | "type": "BoxBufferGeometry", 111 | "width": 2.5, 112 | "height": 1.5, 113 | "depth": 2.5, 114 | "widthSegments": 0, 115 | "heightSegments": 0, 116 | "depthSegments": 0 117 | }, 118 | { 119 | "uuid": "E219AED0-9827-4857-9A4A-C7A64EE53B0D", 120 | "type": "BoxBufferGeometry", 121 | "width": 1, 122 | "height": 4, 123 | "depth": 1, 124 | "widthSegments": 0, 125 | "heightSegments": 0, 126 | "depthSegments": 0 127 | }, 128 | { 129 | "uuid": "962E1D07-9AAF-4765-8F73-1C67EFC4C161", 130 | "type": "BoxBufferGeometry", 131 | "width": 2, 132 | "height": 3, 133 | "depth": 2, 134 | "widthSegments": 0, 135 | "heightSegments": 0, 136 | "depthSegments": 0 137 | }], 138 | "materials": [ 139 | { 140 | "uuid": "0CA672B9-5ECC-4E1C-A14E-3CE7417EB9E7", 141 | "type": "MeshLambertMaterial", 142 | "color": 16744448, 143 | "emissive": 0, 144 | "depthFunc": 3, 145 | "depthTest": true, 146 | "depthWrite": true, 147 | "skinning": false, 148 | "morphTargets": false 149 | }, 150 | { 151 | "uuid": "45E8B75D-26A9-4D6F-8AD3-486DAB684D85", 152 | "type": "MeshLambertMaterial", 153 | "color": 255, 154 | "emissive": 0, 155 | "depthFunc": 3, 156 | "depthTest": true, 157 | "depthWrite": true, 158 | "skinning": false, 159 | "morphTargets": false 160 | }, 161 | { 162 | "uuid": "EA84807D-CA2B-4C88-B2CF-9F8C340CB239", 163 | "type": "MeshLambertMaterial", 164 | "color": 255, 165 | "emissive": 0, 166 | "depthFunc": 3, 167 | "depthTest": true, 168 | "depthWrite": true, 169 | "skinning": false, 170 | "morphTargets": false 171 | }, 172 | { 173 | "uuid": "32096515-1B36-4463-AA88-D1FFDD3B24B9", 174 | "type": "MeshLambertMaterial", 175 | "color": 16744448, 176 | "emissive": 0, 177 | "depthFunc": 3, 178 | "depthTest": true, 179 | "depthWrite": true, 180 | "skinning": false, 181 | "morphTargets": false 182 | }, 183 | { 184 | "uuid": "CB6B3A5E-C98C-49B9-AEEA-7ABA9F7C8FFC", 185 | "type": "MeshLambertMaterial", 186 | "color": 255, 187 | "emissive": 0, 188 | "depthFunc": 3, 189 | "depthTest": true, 190 | "depthWrite": true, 191 | "skinning": false, 192 | "morphTargets": false 193 | }, 194 | { 195 | "uuid": "354DC3EC-CC46-4F2D-9BAD-2A3BBDC03516", 196 | "type": "MeshLambertMaterial", 197 | "color": 16744448, 198 | "emissive": 0, 199 | "opacity": 0.5, 200 | "transparent": true, 201 | "depthFunc": 3, 202 | "depthTest": true, 203 | "depthWrite": true, 204 | "skinning": false, 205 | "morphTargets": false 206 | }, 207 | { 208 | "uuid": "61ECE943-F210-48E5-AA7C-658059EB7FF5", 209 | "type": "MeshLambertMaterial", 210 | "color": 12632256, 211 | "emissive": 0, 212 | "depthFunc": 3, 213 | "depthTest": true, 214 | "depthWrite": true, 215 | "skinning": false, 216 | "morphTargets": false 217 | }, 218 | { 219 | "uuid": "A790CFFE-C84C-46D9-BCEA-B2BEAB01E04A", 220 | "type": "MeshLambertMaterial", 221 | "color": 16744448, 222 | "emissive": 0, 223 | "opacity": 0.5, 224 | "transparent": true, 225 | "depthFunc": 3, 226 | "depthTest": true, 227 | "depthWrite": true, 228 | "skinning": false, 229 | "morphTargets": false 230 | }, 231 | { 232 | "uuid": "BA83E094-7BA2-469E-8930-C6898B285133", 233 | "type": "MeshLambertMaterial", 234 | "color": 255, 235 | "emissive": 0, 236 | "opacity": 0.5, 237 | "transparent": true, 238 | "depthFunc": 3, 239 | "depthTest": true, 240 | "depthWrite": true, 241 | "skinning": false, 242 | "morphTargets": false 243 | }, 244 | { 245 | "uuid": "23A88537-4B22-4E9A-9FE4-FF6D46642118", 246 | "type": "MeshLambertMaterial", 247 | "color": 255, 248 | "emissive": 0, 249 | "opacity": 0.5, 250 | "transparent": true, 251 | "depthFunc": 3, 252 | "depthTest": true, 253 | "depthWrite": true, 254 | "skinning": false, 255 | "morphTargets": false 256 | }, 257 | { 258 | "uuid": "712BD72F-85FB-4FA2-9E9C-52962BD50AFD", 259 | "type": "MeshLambertMaterial", 260 | "color": 255, 261 | "emissive": 0, 262 | "depthFunc": 3, 263 | "depthTest": true, 264 | "depthWrite": true, 265 | "skinning": false, 266 | "morphTargets": false 267 | }], 268 | "object": { 269 | "uuid": "ADAFE518-1954-4B13-989B-41C607790468", 270 | "type": "Scene", 271 | "name": "Scene", 272 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1], 273 | "children": [ 274 | { 275 | "uuid": "E73D0C3D-8D30-4160-B016-E40E5C1AC961", 276 | "type": "Mesh", 277 | "name": "Box 1", 278 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,4,1.5,-1.5,1], 279 | "geometry": "EEFD52EF-B323-4308-8C64-A1E468D80A3D", 280 | "material": "0CA672B9-5ECC-4E1C-A14E-3CE7417EB9E7" 281 | }, 282 | { 283 | "uuid": "D01FEDBD-03CE-4E6C-A894-0FFB1F098677", 284 | "type": "Mesh", 285 | "name": "Box 1", 286 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,8.75,2.5,4.25,1], 287 | "geometry": "505D8D30-A583-4FD6-A0E1-0E1CE800F89B", 288 | "material": "45E8B75D-26A9-4D6F-8AD3-486DAB684D85" 289 | }, 290 | { 291 | "uuid": "6EC8D920-C39B-4ABC-8C32-0835FBE276D7", 292 | "type": "Mesh", 293 | "name": "Box 1", 294 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,8.75,2.5,-1.25,1], 295 | "geometry": "A144D93E-1061-4893-9E10-FF84CA392CCC", 296 | "material": "0CA672B9-5ECC-4E1C-A14E-3CE7417EB9E7" 297 | }, 298 | { 299 | "uuid": "E5A533DC-6812-4F03-848F-74B679993AE0", 300 | "type": "Mesh", 301 | "name": "Box 1", 302 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,-5,1.5,-0.5,1], 303 | "geometry": "50356D09-6886-4620-BEFD-1300DE4D9267", 304 | "material": "EA84807D-CA2B-4C88-B2CF-9F8C340CB239" 305 | }, 306 | { 307 | "uuid": "D61B5924-51BD-4D5E-B71D-CCD1C70D735F", 308 | "type": "Mesh", 309 | "name": "Box 1", 310 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,5,1.5,3.5,1], 311 | "geometry": "FBB2B58B-493E-4A65-BAF0-D5B22824FA87", 312 | "material": "32096515-1B36-4463-AA88-D1FFDD3B24B9" 313 | }, 314 | { 315 | "uuid": "72B8138F-5174-40ED-A214-C0F66EE3929E", 316 | "type": "Mesh", 317 | "name": "Box 1", 318 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,2.5,0,1], 319 | "geometry": "E873E4F9-F73B-4350-B4F6-9F7AFD3CE79B", 320 | "material": "CB6B3A5E-C98C-49B9-AEEA-7ABA9F7C8FFC" 321 | }, 322 | { 323 | "uuid": "5DEFB23D-BEBD-4F5F-9C52-9326E465F395", 324 | "type": "Mesh", 325 | "name": "Box 1", 326 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,0.5,0,1], 327 | "geometry": "4F22ACEE-0CB1-4D1E-ACD5-BA430FD36C11", 328 | "material": "354DC3EC-CC46-4F2D-9BAD-2A3BBDC03516" 329 | }, 330 | { 331 | "uuid": "57100341-57B6-43E2-AE86-927F09602079", 332 | "type": "Mesh", 333 | "name": "Floor", 334 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,1.5,-0.05000000074505806,1.5,1], 335 | "geometry": "5CBD9923-F393-43AA-B2DB-9E8B119952C6", 336 | "material": "61ECE943-F210-48E5-AA7C-658059EB7FF5" 337 | }, 338 | { 339 | "uuid": "D76E6EDE-823D-4A3A-864A-BD7FCA9092CB", 340 | "type": "AmbientLight", 341 | "name": "AmbientLight 10", 342 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,11.31948471069336,0,1], 343 | "color": 12632256, 344 | "intensity": 0.5 345 | }, 346 | { 347 | "uuid": "0F60BBD5-228C-4BB9-988C-AD484BEF6699", 348 | "type": "DirectionalLight", 349 | "name": "DirectionalLight 1", 350 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,0,1,0,1], 351 | "color": 16777215, 352 | "intensity": 0.4, 353 | "shadow": { 354 | "camera": { 355 | "uuid": "38C6835D-6AEF-42E1-AA3F-F984C057421C", 356 | "type": "OrthographicCamera", 357 | "zoom": 1, 358 | "left": -5, 359 | "right": 5, 360 | "top": 5, 361 | "bottom": -5, 362 | "near": 0.5, 363 | "far": 500 364 | } 365 | } 366 | }, 367 | { 368 | "uuid": "8B70AC3C-680E-4FEE-AE27-57DE4F79EE5E", 369 | "type": "Mesh", 370 | "name": "Box 1", 371 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,-5,4,-0.5,1], 372 | "geometry": "61885183-AED5-4707-9E26-9BA4AE7125CD", 373 | "material": "A790CFFE-C84C-46D9-BCEA-B2BEAB01E04A" 374 | }, 375 | { 376 | "uuid": "38F45D8C-CB59-4D16-9830-EFD9B0CB23FC", 377 | "type": "Mesh", 378 | "name": "Box 1", 379 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,5,2.25,3.5,1], 380 | "geometry": "F27802B3-8EE7-40FD-B0F6-4F76F6E2B9AF", 381 | "material": "BA83E094-7BA2-469E-8930-C6898B285133" 382 | }, 383 | { 384 | "uuid": "D67849FB-195D-4F29-AC72-5846A08EC194", 385 | "type": "Mesh", 386 | "name": "Box 1", 387 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,8.75,0.75,-1.25,1], 388 | "geometry": "9ADB9797-5668-4ED4-A035-8782F7789439", 389 | "material": "23A88537-4B22-4E9A-9FE4-FF6D46642118" 390 | }, 391 | { 392 | "uuid": "AA77CD4B-C300-4803-BF08-CB750104C69B", 393 | "type": "Mesh", 394 | "name": "Box 1", 395 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,-6.5,2,2.5,1], 396 | "geometry": "E219AED0-9827-4857-9A4A-C7A64EE53B0D", 397 | "material": "0CA672B9-5ECC-4E1C-A14E-3CE7417EB9E7" 398 | }, 399 | { 400 | "uuid": "EBD0A4AB-5BAE-47D5-831F-86163289C694", 401 | "type": "Mesh", 402 | "name": "Box 1", 403 | "matrix": [1,0,0,0,0,1,0,0,0,0,1,0,-6,1.5,4.5,1], 404 | "geometry": "962E1D07-9AAF-4765-8F73-1C67EFC4C161", 405 | "material": "712BD72F-85FB-4FA2-9E9C-52962BD50AFD" 406 | }], 407 | "background": 11184810 408 | } 409 | } --------------------------------------------------------------------------------