Skip to content

Commit

Permalink
#38: add team activity visualization
Browse files Browse the repository at this point in the history
  • Loading branch information
m4nv3ru committed Mar 14, 2022
1 parent fa4c038 commit 8bec541
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 19 deletions.
13 changes: 10 additions & 3 deletions ui/src/visualizations/team-awareness/chart/chart.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
'use strict';

import React from 'react';
import BubbleChart from '../components/BubbleChart/BubbleChart';

export default class TeamArwareness extends React.Component {
export default class TeamAwareness extends React.PureComponent {
constructor(props) {
super(props);
}

render() {
console.log('Chraph state');
console.log(this.props);
return <h1>Hello World</h1>;

const { stakeholders } = this.props.data;

return (
<div>
<BubbleChart content={stakeholders} />
</div>
);
}
}
3 changes: 2 additions & 1 deletion ui/src/visualizations/team-awareness/chart/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import Chart from './chart.js';

const mapStateToProps = (appState /*, chartState */) => {
const vizState = getState(appState);
console.log(vizState);
return {
data: {
stakeholders: vizState.data.data.stakeholders,
activity: vizState.data.data.activity
activityTimeline: vizState.data.data.activityTimeline
}
};
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import _ from 'lodash';
import * as chartStyle from './BubbleChart.scss';
import * as d3 from 'd3';

export default class BubbleChart extends React.Component {
constructor(props) {
super(props);
this.styles = _.assign({}, chartStyle);
this.state = {
componentMounted: false,
content: this.props.content,
data: {
data: []
},
width: this.props.width || 0,
height: this.props.height || 0
};
window.addEventListener('resize', () => this.visualize());
}

render() {
console.log('render bubble chart');
return (
<div className={this.styles.chartArea}>
<svg className={this.styles.chartDrawingArea} ref={svg => (this.svgRef = svg)} />
<div className={this.styles.chartTooltip} ref={div => (this.tooltipRef = div)} />
</div>
);
}

componentDidMount() {
this.setState({ componentMounted: true });
}
componentWillUnmount() {
this.setState({ componentMounted: false });
}
// eslint-disable-next-line no-unused-vars
componentDidUpdate(prevProps, prevState, snapshot) {
console.log(prevState);
console.log(this.state);
if (this.state.componentMounted && this.state.content !== this.props.content) {
this.setState({ content: this.props.content }, this.visualize);
} else {
this.visualize();
}
}

visualize() {
console.log(this.state);
this.drawChart(d3.select(this.svgRef));
}

drawChart(svg) {
svg.selectAll('*').remove();
svg.attr('font-size', 10).attr('font-family', 'sans-serif').attr('text-anchor', 'middle');

const { clientWidth, clientHeight } = this.svgRef;
const colors = this.createColorSchema(_.map(this.state.content, 'id'));

const layout = d3.pack().size([clientWidth, clientHeight]);
const root = d3.hierarchy({ children: this.state.content }).sum(d => d.activity);
layout(root);

const leaf = svg.selectAll('a').data(root.leaves()).join('a').attr('transform', d => `translate(${d.x},${d.y})`);
leaf.append('title').text(d => `${d.data.signature}\nActivity: ${d.data.activity}`);
leaf.append('circle').attr('fill', d => colors.get(d.data.id)).attr('r', d => d.r);
}

createColorSchema(data) {
return new Map(
data.map((v, i) => {
return [v, getColor((i + 1) / data.length)];
})
);
}
}

function getColor(t) {
t = Math.max(0, Math.min(1, t));
return (
'rgb(' +
Math.max(0, Math.min(255, Math.round(34.61 + t * (1172.33 - t * (10793.56 - t * (33300.12 - t * (38394.49 - t * 14825.05))))))) +
', ' +
Math.max(0, Math.min(255, Math.round(23.31 + t * (557.33 + t * (1225.33 - t * (3574.96 - t * (1073.77 + t * 707.56))))))) +
', ' +
Math.max(0, Math.min(255, Math.round(27.2 + t * (3211.1 - t * (15327.97 - t * (27814 - t * (22569.18 - t * 6838.66))))))) +
')'
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.chartArea {
width: 100%;
}
.chartTooltip {
width: 100%;
}
.chartDrawingArea {
width: 100%;
height: 100%;
border: 1px solid #000;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import StackedAreaChart from '../../../../components/StackedAreaChart';

export default class ActivityTimeline extends StackedAreaChart {
constructor(props) {
super(props);
}
}
Empty file.
6 changes: 3 additions & 3 deletions ui/src/visualizations/team-awareness/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import React from 'react';
import { connect } from 'react-redux';
import { setSelectActivity } from './sagas';
import { setActivityScale } from './sagas';

const mapStateToProps = (appState /*, ownProps*/) => {
const awarenessState = appState.visualizations.teamAwareness.state.config;
Expand All @@ -14,7 +14,7 @@ const mapStateToProps = (appState /*, ownProps*/) => {
};
const mapDispatchToProps = dispatch => {
return {
onSelectActivity: selectActivity => dispatch(setSelectActivity(selectActivity))
onSelectActivityScale: selectActivity => dispatch(setActivityScale(selectActivity))
};
};

Expand All @@ -29,7 +29,7 @@ class ConfigComponent extends React.Component {
<div>
<div>Activity:</div>
<div>
<select value={this.props.config.selectedActivity} onChange={value => this.props.onSelectActivity(value)}>
<select value={this.props.config.selectedActivityScale} onChange={value => this.props.onSelectActivityScale(value)}>
<option value="commits">Commits</option>
<option value="activity">Additions & Deletions</option>
<option value="additions">Additions</option>
Expand Down
7 changes: 5 additions & 2 deletions ui/src/visualizations/team-awareness/reducers/config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
'use strict';

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

export default handleActions(
{},
{
selectedActivity: 'commits'
SET_TEAM_AWARENESS_ACTIVITY_SCALE: (state, action) => _.assign({}, state, { selectedActivityScale: action.payload })
},
{
selectedActivityScale: 'commits'
}
);
4 changes: 3 additions & 1 deletion ui/src/visualizations/team-awareness/reducers/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export default handleActions(
}
},
{
data: {},
data: {
stakeholders: []
},
lastFetched: null,
isFetching: null
}
Expand Down
3 changes: 2 additions & 1 deletion ui/src/visualizations/team-awareness/sagas/getCommits.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ const getCommits = (page, perPage) => {
query($page:Int, $perPage:Int) {
commits(page:$page, perPage:$perPage) {
data {
date
date
stakeholder {
id
gitSignature
}
stats {
additions
Expand Down
6 changes: 3 additions & 3 deletions ui/src/visualizations/team-awareness/sagas/getStakeholders.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ const getStakeholders = (page, perPage) => {
query($page:Int, $perPage:Int){
stakeholders(page:$page, perPage: $perPage) {
data {
id
gitSignature
}
id
gitSignature
}
}
}`,
page,
Expand Down
69 changes: 64 additions & 5 deletions ui/src/visualizations/team-awareness/sagas/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { createAction } from 'redux-actions';
import { fork, takeEvery, throttle } from 'redux-saga/effects';
import { fetchFactory, mapSaga, timestampedActionFactory } from '../../../sagas/utils';
import getCommits from './getCommits';
import getStakeholders from './getStakeholders';

export const setSelectActivity = createAction('SET_SELECT_ACTIVITY');
export const setActivityScale = createAction('SET_TEAM_AWARENESS_ACTIVITY_SCALE');

export const requestTeamAwarenessData = createAction('REQUEST_TEAM_AWARENESS_DATA');
export const receiveTeamAwarenessData = timestampedActionFactory('RECEIVE_TEAM_AWARENESS_DATA');
Expand All @@ -33,13 +32,73 @@ function* watchMessages() {
yield takeEvery('message', mapSaga(requestRefresh));
}

/**
*
* @param commits{[{
* date: Date,
* stats: {additions: number, deletions: number},
* stakeholder: {id: number}
* }]} Commit data
* @return {{activityTimeline: *[], stakeholders: *[]}}
*/
function processData(commits) {

/** @type {Map<number, any>} */
const stakeholders = new Map();

/** @type [{date:Date, amount: number} ] */
const activity = [];

const dataBoundaries = {
min: Number.MAX_SAFE_INTEGER,
max: Number.MIN_SAFE_INTEGER
};
console.log(commits);
commits.forEach(c => {
if (!stakeholders.has(c.stakeholder.id)) {
stakeholders.set(c.stakeholder.id, {
id: c.stakeholder.id,
signature: c.stakeholder.gitSignature,
name: c.stakeholder.id,
activity: 0
});
}
const calculatedActivity = calculateActivity(c);
const stakeholder = stakeholders.get(c.stakeholder.id);
stakeholder.activity += calculatedActivity;
activity.push({ date: c.date, amount: calculatedActivity });
updateBoundaries(dataBoundaries, stakeholder.activity);
});

return {
stakeholders: Array.from(stakeholders.values()),
activityTimeline: activity,
dataBoundaries
};
}

function updateBoundaries(boundaries, value) {
if (value < boundaries.min) {
boundaries.min = value;
}
if (value > boundaries.max) {
boundaries.max = value;
}
}

function calculateActivity(commit) {
return 1;
}

export const fetchAwarenessData = fetchFactory(
function*() {
//const state = getState(yield select());
return yield Promise.all([getStakeholders(), getCommits()]).then(result => {
return yield Promise.all([getCommits()]).then(result => {
const processed = processData(result[0]);
return {
stakeholders: result[0],
activity: result[1]
stakeholders: processed.stakeholders,
activityTimeline: processed.activityTimeline,
dataBoundaries: processed.dataBoundaries
};
});
},
Expand Down

0 comments on commit 8bec541

Please sign in to comment.