Skip to content

Commit

Permalink
#45 include file-tree-evolution sunburst chart
Browse files Browse the repository at this point in the history
not configurable, uses fake data generator
  • Loading branch information
Grochni committed Apr 8, 2022
1 parent 1c03d82 commit 4d660fb
Show file tree
Hide file tree
Showing 13 changed files with 342 additions and 1 deletion.
3 changes: 2 additions & 1 deletion ui/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ import issueImpact from './visualizations/issue-impact';
import hotspotDials from './visualizations/hotspot-dials';
import codeHotspots from './visualizations/code-hotspots';
import languageModuleRiver from './visualizations/language-module-river';
import fileTreeEvolution from './visualizations/file-tree-evolution';

const visualizationModules = [dashboard, codeOwnershipRiver, issueImpact, hotspotDials, codeHotspots, languageModuleRiver];
const visualizationModules = [dashboard, codeOwnershipRiver, issueImpact, hotspotDials, codeHotspots, languageModuleRiver, fileTreeEvolution];

const visualizations = {};
_.each(visualizationModules, viz => {
Expand Down
127 changes: 127 additions & 0 deletions ui/src/visualizations/file-tree-evolution/chart/Sunburst.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
'use strict';

import _ from 'lodash';
import React from 'react';
import * as d3 from "d3";
import { generateData } from "./data-generator";

export default class Sunburst extends React.Component {
constructor(props) {
super(props);
this.state = {};
}

componentDidMount() {
this.createChart();
}

componentDidUpdate() {
this.createChart();
}

createChart() {
let radius = 400;
const margin = 1;
const padding = 1;
const contributorColors = {
1: 'red',
2: 'green',
3: 'blue',
4: 'yellow',
5: 'purple'
}
const contributionVisibilityDuration = 4;
const msBetweenIterations = 500;
const variant = 'sunburst'; // 'sunburst' | 'sunrise' | 'sundown'

const fullAngle = variant === 'sunburst' ? 2 * Math.PI : Math.PI;
const width = 2 * radius// * (variant === 'sunburst' ? 1 : 2);
const height = 2 * radius;

const rotation = {
'sunburst': 0,
'sundown': 90,
'sunrise': 270
}

const arc = d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.padAngle(d => Math.min((d.x1 - d.x0) / 2, 2 * padding / radius))
.padRadius(radius / 2)
.innerRadius(d => d.y0)
.outerRadius(d => d.y1 - padding);

const svg = d3.select(this.chartRef)
.attr("transform", "rotate(" + rotation[variant] + ") ")
.attr("viewBox", [-margin - radius, -margin - radius, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")

const color = d3.scaleSequential(d3.interpolate('#bbb', '#ccc'))

function getColor(d, iteration) {
const baseColor = color((d.x1 + d.x0) / 2 / fullAngle)
if (!d.data.contributor) {
return baseColor;
}
const contributorBaseColor = contributorColors[d.data.contributor];
return d3.interpolate(contributorBaseColor, baseColor)(Math.min(1, (iteration - d.data.changeIteration) / contributionVisibilityDuration))
}

function arcTween(d) {
if (!this._current) {
this._current = d;
}

var interpolateStartAngle = d3.interpolate(this._current.x0, d.x0);
var interpolateEndAngle = d3.interpolate(this._current.x1, d.x1);

this._current = d;

return function(t) {
d.x0 = interpolateStartAngle(t);
d.x1 = interpolateEndAngle(t);
return arc(d);
};
};

function update(data, iteration) {
const root = d3.hierarchy(data, d => !!d ? d.children : undefined);
root.sum(d => Math.max(0, d.size))
// root.sort((a, b) => d3.descending(a.value, b.value))

d3.partition().size([fullAngle, radius])(root);

root.children.forEach((child, i) => child.index = i);

let cell = svg
.selectAll("path")
.data(root.descendants())

cell = cell.enter()
.append("path")
.merge(cell)
.transition()
.ease(t => t)
.duration(iteration === 0 ? 0 : msBetweenIterations)
.attrTween("d", arcTween)
.attr("fill", d => getColor(d, iteration))
.attr("fill-opacity", d => d.depth === 0 ? 0 : 1);
}

const data = generateData();

let index = 0;

update(data[0], 0);
setInterval(() => !!data[++index] ? update(data[index], index) : undefined, msBetweenIterations);
}

render() {
return (
<svg ref={svg => this.chartRef = svg}></svg>
);
}
}
20 changes: 20 additions & 0 deletions ui/src/visualizations/file-tree-evolution/chart/chart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict';

import React from 'react';

import _ from 'lodash';

import Sunburst from './Sunburst';

export default class FileTreeEvolution extends React.Component {
constructor(props) {
super(props);
this.state = {};
}

render() {
return (
<Sunburst />
);
}
}
105 changes: 105 additions & 0 deletions ui/src/visualizations/file-tree-evolution/chart/data-generator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@

export function generateStartData(maxDepth = 5, minBreadth = 2, maxBreadth = 10, emptyChance = 0.5, firstAllowedEmptyDepth = 1, hugeFileChance = 0.1, emptyFileChance = 0.5) {
const startData = { };
const empty = firstAllowedEmptyDepth <= 0 && Math.random() < emptyChance;
if (maxDepth > 0 && !empty) {
const desiredBreadth = minBreadth + Math.round(Math.random() * (maxBreadth - minBreadth));
for (let breadth = 0; breadth < desiredBreadth; breadth++) {
if (!startData.children) {
startData.children = [];
}
startData.children.push(generateStartData(maxDepth - 1, minBreadth, maxBreadth, emptyChance, firstAllowedEmptyDepth - 1, hugeFileChance, emptyFileChance))
}
}
if (!startData.children || startData.children.length === 0) {
startData.size = Math.random();
if (Math.random() < hugeFileChance) {
startData.size *= 10;
}
if (Math.random() < emptyFileChance) {
startData.size = 0;
}
}
return startData;
}

export function pickFile(data) {
if (!data.children) {
return data;
}
const index = Math.floor(Math.random() * data.children.length);
return pickFile(data.children[index]);
}

export function smallAddition(file) {
file.size += 0.1;
}

export function bigAddition(file) {
file.size += 1;
}

export function smallDeletion(file) {
file.size -= 0.1;
}

export function bigDeletion(file) {
file.size -= 1;
}

export function deletion(file) {
file.size = 0;
}

export const fileOperations = [
smallAddition,
bigAddition,
smallDeletion,
// bigDeletion,
// deletion
]


export function generateChange(data, iteration, contributors = 4, changes = 10) {
data = structuredClone(data)

const contributor = Math.floor(Math.random() * contributors) + 1

for (let i = 0; i < changes; i++) {
const file = pickFile(data);
if (file.contributor && file.changeIteration === iteration) {
continue;
}
file.contributor = contributor;
file.changeIteration = iteration;
const operation = Math.floor(Math.random() * fileOperations.length);
fileOperations[operation](file);
}

return data;

}

export function generateData(iterations = 100, contributors = 5) {
const data = [generateStartData()];
sortData(data[0]);

for (let i = 0; i < iterations; i++) {
data.push(generateChange(data[i], i+1, contributors))
}
return data;
}

export function countLeafs(data) {
return !data.children ? 1 : data.children.reduce((sum, child) => sum + countLeafs(child), 0);
}

export function sortData(data) {
if (!data.children) {
data.__size = data.size;
} else {
data.children.forEach(sortData);
data.__size = data.children.reduce((sum, child) => sum + child.__size, 0)
data.children.sort((a, b) => b.__size - a.__size);
}
}
11 changes: 11 additions & 0 deletions ui/src/visualizations/file-tree-evolution/chart/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

import { connect } from 'react-redux';

import Chart from './chart.js';

const mapStateToProps = (state) => {
return {};
};

export default connect(mapStateToProps)(Chart);
20 changes: 20 additions & 0 deletions ui/src/visualizations/file-tree-evolution/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict';

import { connect } from 'react-redux';


const mapStateToProps = (state /*, ownProps*/) => {
return {};
};

const mapDispatchToProps = (dispatch /*, ownProps*/) => {
return {};
};

const FileTreeEvolutionConfigComponent = props => {
return (<div></div>);
};

const FileTreeEvolutionConfig = connect(mapStateToProps, mapDispatchToProps)(FileTreeEvolutionConfigComponent);

export default FileTreeEvolutionConfig;
5 changes: 5 additions & 0 deletions ui/src/visualizations/file-tree-evolution/help.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

export default () =>
<div>
</div>;
17 changes: 17 additions & 0 deletions ui/src/visualizations/file-tree-evolution/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

import ChartComponent from './chart';
import ConfigComponent from './config.js';
import HelpComponent from './help.js';
import saga from './sagas';
import reducer from './reducers';

export default {
id: 'fileTreeEvolution',
label: 'File Tree Evolution',
saga,
reducer,
ChartComponent,
ConfigComponent,
HelpComponent
};
11 changes: 11 additions & 0 deletions ui/src/visualizations/file-tree-evolution/reducers/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

import { handleActions } from 'redux-actions';
import _ from 'lodash';

export default handleActions(
{
},
{
}
);
11 changes: 11 additions & 0 deletions ui/src/visualizations/file-tree-evolution/reducers/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

import { handleActions } from 'redux-actions';
import _ from 'lodash';

export default handleActions(
{
},
{
}
);
10 changes: 10 additions & 0 deletions ui/src/visualizations/file-tree-evolution/reducers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

import config from './config.js';
import data from './data.js';
import { combineReducers } from 'redux';

export default combineReducers({
data,
config
});
3 changes: 3 additions & 0 deletions ui/src/visualizations/file-tree-evolution/sagas/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

export default function*() {}
Empty file.

0 comments on commit 4d660fb

Please sign in to comment.