Skip to content

Commit

Permalink
#45 show actual data in file-tree-evolution chart
Browse files Browse the repository at this point in the history
  • Loading branch information
Grochni committed Apr 8, 2022
1 parent 4d660fb commit e2911d9
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 81 deletions.
177 changes: 99 additions & 78 deletions ui/src/visualizations/file-tree-evolution/chart/Sunburst.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import _ from 'lodash';
import React from 'react';
import * as d3 from "d3";

import { } from 'react'

import { generateData } from "./data-generator";

export default class Sunburst extends React.Component {
Expand All @@ -12,111 +15,97 @@ export default class Sunburst extends React.Component {
}

componentDidMount() {
this.createChart();
// if (!!this.props.fileTreeHistory && this.props.fileTreeHistory.length > 0) {
// this.createChart();
// }
}

componentDidUpdate() {
this.createChart();
if (!!this.props.fileTreeHistory && this.props.fileTreeHistory.length > 0 && !this.initialized) {
this.initialized = true
this.createChart();
setInterval(() => !!this.props.fileTreeHistory[++this.index] ? this.update(this.props.fileTreeHistory[this.index], this.index) : undefined, this.settings.msBetweenIterations);
}
// console.log(this.props)
// if(!!this.props.fileTreeHistory[++this.index]) {
// this.update(this.props.fileTreeHistory[this.index], this.index)
// };
}

createChart() {
let radius = 400;
const margin = 1;
const padding = 1;
const contributorColors = {
this.settings = {};
this.settings.radius = 400;
this.settings.margin = 1;
this.settings.padding = 1;
this.settings.contributorColors = {
1: 'red',
2: 'green',
3: 'blue',
4: 'yellow',
5: 'purple'
}
const contributionVisibilityDuration = 4;
const msBetweenIterations = 500;
const variant = 'sunburst'; // 'sunburst' | 'sunrise' | 'sundown'
this.settings.contributionVisibilityDuration = 4;
this.settings.msBetweenIterations = 500;
this.settings.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;
this.settings.fullAngle = this.settings.variant === 'sunburst' ? 2 * Math.PI : Math.PI;
this.settings.width = 2 * this.settings.radius// * (variant === 'sunburst' ? 1 : 2);
this.settings.height = 2 * this.settings.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)
this.svg = d3.select(this.chartRef)
.attr("transform", "rotate(" + rotation[this.settings.variant] + ") ")
.attr("viewBox", [-this.settings.margin - this.settings.radius, -this.settings.margin - this.settings.radius, this.settings.width, this.settings.height])
.attr("width", this.settings.width)
.attr("height", this.settings.height)
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")

const color = d3.scaleSequential(d3.interpolate('#bbb', '#ccc'))
this.settings.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))
}
// this.props.fileTreeHistory = generateData();
this.index = 0;

function arcTween(d) {
if (!this._current) {
this._current = d;
}
this.update(this.props.fileTreeHistory[0], 0);
}

var interpolateStartAngle = d3.interpolate(this._current.x0, d.x0);
var interpolateEndAngle = d3.interpolate(this._current.x1, d.x1);
update(data, iteration) {
const root = d3.hierarchy(data, d => getChildren(d));
root.sum(d => Math.max(0, d.size))
// root.sort((a, b) => d3.descending(a.value, b.value))

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

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

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

cell = cell.enter()
.append("path")
.attr("class", "fileArc")
.merge(cell)
.transition()
.ease(t => t)
.duration(iteration === 0 ? 0 : this.settings.msBetweenIterations)
.attrTween("d", arcTweenFunction(this.settings.radius, this.settings.padding))
.attr("fill", d => this.getColor(d, iteration))
.attr("fill-opacity", d => d.depth === 0 ? 0 : 1)

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);
}

getColor(d, iteration) {
const baseColor = this.settings.color((d.x1 + d.x0) / 2 / this.settings.fullAngle)
if (!d.data.contributor) {
return baseColor;
}

const data = generateData();

let index = 0;

update(data[0], 0);
setInterval(() => !!data[++index] ? update(data[index], index) : undefined, msBetweenIterations);
const contributorBaseColor = this.settings.contributorColors[d.data.contributor];
return d3.interpolate(contributorBaseColor, baseColor)(Math.min(1, (iteration - d.data.changeIteration) / this.settings.contributionVisibilityDuration))
}

render() {
Expand All @@ -125,3 +114,35 @@ export default class Sunburst extends React.Component {
);
}
}


function arcTweenFunction(radius, padding) {
return function(d) {
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)

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 getChildren(data) {
return !data || !data.children ? undefined : Object.getOwnPropertyNames(data.children).map(prop => data.children[prop]);
}
8 changes: 7 additions & 1 deletion ui/src/visualizations/file-tree-evolution/chart/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ export default class FileTreeEvolution extends React.Component {
this.state = {};
}

componentWillReceiveProps(nextProps) {
this.setState({
fileTreeHistory: nextProps.fileTreeHistory
});
}

render() {
return (
<Sunburst />
<Sunburst fileTreeHistory={this.props.fileTreeHistory} />
);
}
}
4 changes: 3 additions & 1 deletion ui/src/visualizations/file-tree-evolution/chart/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { connect } from 'react-redux';
import Chart from './chart.js';

const mapStateToProps = (state) => {
return {};
return {
fileTreeHistory: state.visualizations.fileTreeEvolution.state.data.data.fileTreeHistory || []
};
};

export default connect(mapStateToProps)(Chart);
13 changes: 13 additions & 0 deletions ui/src/visualizations/file-tree-evolution/reducers/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,20 @@ import _ from 'lodash';

export default handleActions(
{
REQUEST_FILE_TREE_EVOLUTION_DATA: state => _.assign({}, state, { isFetching: true }),
RECEIVE_FILE_TREE_EVOLUTION_DATA: (state, action) => {
const ret = _.assign({}, state, {
data: action.payload,
isFetching: false,
receivedAt: action.meta.receivedAt
});

return ret;
}
},
{
data: {},
lastFetched: null,
isFetching: null
}
);
84 changes: 83 additions & 1 deletion ui/src/visualizations/file-tree-evolution/sagas/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,85 @@
'use strict';

export default function*() {}
import { createAction } from 'redux-actions';
import _ from 'lodash';
import moment from 'moment';

import { fetchFactory, timestampedActionFactory } from '../../../sagas/utils.js';
import { graphQl } from '../../../utils';
import { path } from 'd3';

export const requestFileTreeEvolutionData = createAction('REQUEST_FILE_TREE_EVOLUTION_DATA');
export const receiveFileTreeEvolutionData = timestampedActionFactory('RECEIVE_FILE_TREE_EVOLUTION_DATA');
export const receiveFileTreeEvolutionDataError = createAction('RECEIVE_FILE_TREE_EVOLUTION_DATA_ERROR');

export default function*() {
// fetch data once on entry
yield* fetchFileTreeEvolutionData();
}

export const fetchFileTreeEvolutionData = fetchFactory(
function*() {
return yield graphQl.query(
`{
commits(sort: "ASC") {
data {
date
message
files {
data {
stats {
additions
deletions
}
file {
path,
}
lineCount
}
}
}
}
}`
)
.then(resp => {
const fileTreeHistory = [];
let fileTree = {};
for (const commit of resp.commits.data) {
fileTree = applyCommit(fileTree, commit);
fileTreeHistory.push(fileTree);
}
return {
fileTreeHistory,
commits: resp
};
});
},
requestFileTreeEvolutionData,
receiveFileTreeEvolutionData,
receiveFileTreeEvolutionDataError
);

function applyCommit(fileTree, commit) {
fileTree = structuredClone(fileTree)
for (const file of commit.files.data) {
const path = file.file.path.split('/');
modifyFile(fileTree, path, file);
}
return fileTree;
}

function modifyFile(fileTree, path, file) {
if (path.length === 0) {
fileTree.size = file.lineCount;
} else {
if (!fileTree.children) {
fileTree.children = {};
}
if (!fileTree.children[path[0]]) {
fileTree.children[path[0]] = {
name: path[0]
};
}
modifyFile(fileTree.children[path[0]], path.slice(1), file);
}
}

0 comments on commit e2911d9

Please sign in to comment.