├── .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 | 
46 |
47 | You can compare the two versions either side by side ...
48 |
49 | 
50 |
51 | ... or in a merged view:
52 |
53 | 
54 |
55 | You can filter for specific properties of your classes ...
56 |
57 | 
58 |
59 | ... and map different types of metrics to your personal visualization.
60 |
61 | 
62 |
63 | Of course, you can also search for certain files in your project and highlight them in the visualization
64 |
65 | 
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 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 | Coderadar Visualization
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
29 |
30 |
31 |
32 |
33 |
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 |
141 |
142 |
143 |
144 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
158 |
159 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | | Metrik |
179 | |
180 | |
181 | Änderung |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | Erste Revision
196 |
197 |
198 |
199 | Zweite Revision
200 |
201 |
202 |
203 | hinzugefügt
204 |
205 |
206 |
207 | gelöscht
208 |
209 |
210 |
211 | unverändert
212 |
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 | }
--------------------------------------------------------------------------------