Skip to content

Commit

Permalink
#38: add timeline navigation functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
m4nv3ru committed Apr 11, 2022
1 parent 915eb44 commit 9fbeb2b
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 96 deletions.
2 changes: 0 additions & 2 deletions ui/src/visualizations/team-awareness/chart/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ export default class TeamAwareness extends React.PureComponent {
}

render() {
console.log(this.props);

const { stakeholders } = this.props.data;

return (
Expand Down
9 changes: 5 additions & 4 deletions ui/src/visualizations/team-awareness/chart/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { getState } from '../util/util.js';
import Chart from './chart.js';

const mapStateToProps = (appState /*, chartState */) => {
const vizState = getState(appState);
console.log(vizState);
const { data } = getState(appState);

console.log('chart', data);
return {
data: {
stakeholders: vizState.data.data.stakeholders,
activityTimeline: vizState.data.data.activityTimeline
stakeholders: data.data.stakeholders,
activityTimeline: data.data.activityTimeline
}
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ export default class BubbleChart extends React.Component {
}
// 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(
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import StackedAreaChart from '../../../../components/StackedAreaChart';
import _ from 'lodash';

export default class ActivityTimeline extends StackedAreaChart {
constructor(props) {
super(props);
}

// eslint-disable-next-line no-unused-vars
shouldComponentUpdate(nextProps, nextState, nextContext) {
const zoomedChanged = this.state.zoomed !== nextState.zoomed;
const zoomDimsDifference = _.difference(this.state.zoomedDims, nextState.zoomedDims);
const { onDimensionsRestricted } = this.props;

if (zoomedChanged || zoomDimsDifference.length > 0) {
onDimensionsRestricted({
activityRestricted: nextState.zoomed,
activityDims: nextState.zoomedDims
});
}
return true;
}
}
24 changes: 13 additions & 11 deletions ui/src/visualizations/team-awareness/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,28 @@

import React from 'react';
import { connect } from 'react-redux';
import { setActivityScale } from './sagas';
import StackedAreaChart from '../../components/StackedAreaChart';
import { setActivityDimensions, setActivityScale } from './sagas';
import * as d3 from 'd3';
import { getState } from './util/util';
import _ from 'lodash';
import ActivityTimeline from './components/Timeline/ActivityTimeline';

const mapStateToProps = (appState /*, ownProps*/) => {
const dataState = getState(appState).data;
console.log(dataState);
const { config, data } = getState(appState);
return {
config: {
selectedActivity: getState(appState).config.selectedActivity
selectedActivity: config.selectedActivity
},
data: {
activityTimeline: dataState.data.activityTimeline,
yDims: dataState.data.dataBoundaries
activityTimeline: data.data.activityTimeline,
yDims: data.data.dataBoundaries
}
};
};
const mapDispatchToProps = dispatch => {
return {
onSelectActivityScale: selectActivity => dispatch(setActivityScale(selectActivity))
onSelectActivityScale: selectActivity => dispatch(setActivityScale(selectActivity)),
onActivityDimensionsRestricted: restrictActivity => dispatch(setActivityDimensions(restrictActivity))
};
};

Expand All @@ -33,25 +33,27 @@ class ConfigComponent extends React.Component {
}

render() {
const { onActivityDimensionsRestricted, onSelectActivityScale } = this.props;
const { activityTimeline, yDims } = this.props.data;
return (
<div>
<div>
<div>Timeline</div>
<StackedAreaChart
<ActivityTimeline
palette={{ activity: '#00bcd4' }}
paddings={{ top: 20, left: 25, bottom: 22, right: 30 }}
paddings={{ top: 20, left: 25, bottom: 30, right: 30 }}
resolution={'weeks'}
xAxisCenter={true}
content={activityTimeline}
d3offset={d3.stackOffsetDiverging}
yDims={_.values(yDims)}
onDimensionsRestricted={dims => onActivityDimensionsRestricted(dims)}
/>
</div>
<div>
<div>Activity:</div>
<div>
<select value={this.props.config.selectedActivityScale} onChange={value => this.props.onSelectActivityScale(value)}>
<select value={this.props.config.selectedActivityScale} onChange={value => 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
Expand Up @@ -5,9 +5,12 @@ import _ from 'lodash';

export default handleActions(
{
SET_TEAM_AWARENESS_ACTIVITY_SCALE: (state, action) => _.assign({}, state, { selectedActivityScale: action.payload })
SET_TEAM_AWARENESS_ACTIVITY_SCALE: (state, action) => _.assign({}, state, { selectedActivityScale: action.payload }),
SET_TEAM_AWARENESS_ACTIVITY_DIMENSIONS: (state, action) => _.assign({}, state, action.payload)
},
{
selectedActivityScale: 'commits'
selectedActivityScale: 'commits',
activityRestricted: false,
activityDims: []
}
);
13 changes: 13 additions & 0 deletions ui/src/visualizations/team-awareness/reducers/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,25 @@ export default handleActions(
return _.assign({}, state, {
data: action.payload,
isFetching: false,
lastFetched: Date.now(),
receivedAt: action.meta.receivedAt
});
},
PROCESS_TEAM_AWARENESS_DATA: (state, action) => {
console.log('PROCESS_TEAM_AWARENESS_DATA', state, action);
return _.assign({}, state, {
data: {
commits: state.data.commits,
stakeholders: action.payload.stakeholders,
activityTimeline: action.payload.activityTimeline,
dataBoundaries: action.payload.dataBoundaries
}
});
}
},
{
data: {
commits: [],
stakeholders: []
},
lastFetched: null,
Expand Down
95 changes: 95 additions & 0 deletions ui/src/visualizations/team-awareness/sagas/calculateFigures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { put, select } from 'redux-saga/effects';
import { processTeamAwarenessData } from './index';
import { getState } from '../util/util';

export default function*() {
const appState = yield select();
yield put(processTeamAwarenessData(processData(appState)));
}

function processData(appState) {
const vizState = getState(appState);

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

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

const dataBoundaries = {
min: Number.MAX_SAFE_INTEGER,
max: Number.MIN_SAFE_INTEGER
};

let activityCalculator = calculateActivity;
if (vizState.config.activityRestricted === true) {
const from = Date.parse(vizState.config.activityDims[0]);
const to = Date.parse(vizState.config.activityDims[1]);
activityCalculator = filterCommit(from, to, calculateActivity);
}

vizState.data.data.commits.forEach(c => {
const calculatedActivity = activityCalculator(c);
if (calculatedActivity !== 0) {
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 dateString = c.date.substring(0, 10);

const dateParsed = Date.parse(c.date);
if (!activities.has(dateString)) {
activities.set(dateString, {
date: dateParsed,
activity: 0
});
}

const stakeholder = stakeholders.get(c.stakeholder.id);
stakeholder.activity += calculatedActivity;

const current = activities.get(dateString);
current.activity += calculatedActivity;
updateBoundaries(dataBoundaries, current.activity);
}
});

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

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

/**
* @param from {number}
* @param to {number}
* @param fn {function}
* @return {function(*): number}
*/
function filterCommit(from, to, fn) {
return commit => {
const parsedDate = Date.parse(commit.date);
if (from <= parsedDate && parsedDate <= to) {
return fn(commit);
}
return 0;
};
}

function calculateActivity() {
return 1;
}
94 changes: 19 additions & 75 deletions ui/src/visualizations/team-awareness/sagas/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,111 +4,55 @@ 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 test from './calculateFigures';

export const setActivityScale = createAction('SET_TEAM_AWARENESS_ACTIVITY_SCALE');
export const setActivityDimensions = createAction('SET_TEAM_AWARENESS_ACTIVITY_DIMENSIONS');

export const processTeamAwarenessData = timestampedActionFactory('PROCESS_TEAM_AWARENESS_DATA');
export const requestTeamAwarenessData = createAction('REQUEST_TEAM_AWARENESS_DATA');
export const receiveTeamAwarenessData = timestampedActionFactory('RECEIVE_TEAM_AWARENESS_DATA');
export const receiveTeamAwarenessDataError = timestampedActionFactory('RECEIVE_TEAM_AWARENESS_DATA');
export const receiveTeamAwarenessDataError = timestampedActionFactory('RECEIVE_TEAM_AWARENESS_DATA_ERROR');

export const requestRefresh = createAction('REQUEST_REFRESH');
const refresh = createAction('REFRESH');

export default function*() {
yield* fetchAwarenessData();

yield fork(watchDataReceive);
yield fork(watchActivityDimensionsSet);
yield fork(watchRefreshRequests);
yield fork(watchMessages);
yield fork(watchRefresh);

yield* fetchAwarenessData();
}

function* watchDataReceive() {
yield takeEvery('RECEIVE_TEAM_AWARENESS_DATA', test);
}

function* watchActivityDimensionsSet() {
yield takeEvery('SET_TEAM_AWARENESS_ACTIVITY_DIMENSIONS', test);
}

function* watchRefreshRequests() {
yield throttle(2000, 'REQUEST_REFRESH', mapSaga(refresh));
}

function* watchRefresh() {
yield takeEvery('REFRESH', fetchAwarenessData);
}

function* watchMessages() {
yield takeEvery('message', mapSaga(requestRefresh));
}

/**
*
* @param commits{[{
* date: string,
* 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 {Map<string, any>} */
const activities = new Map();

const dataBoundaries = {
min: Number.MAX_SAFE_INTEGER,
max: Number.MIN_SAFE_INTEGER
};

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 dateString = c.date.substring(0, 10);
if (!activities.has(dateString)) {
activities.set(dateString, {
date: Date.parse(c.date),
activity: 0
});
}

const calculatedActivity = calculateActivity(c);
const stakeholder = stakeholders.get(c.stakeholder.id);
stakeholder.activity += calculatedActivity;

const current = activities.get(dateString);
current.activity += calculatedActivity;
updateBoundaries(dataBoundaries, current.activity);
});

return {
stakeholders: Array.from(stakeholders.values()),
activityTimeline: Array.from(activities.values()),
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([getCommits()]).then(result => {
const processed = processData(result[0]);
return {
stakeholders: processed.stakeholders,
activityTimeline: processed.activityTimeline,
dataBoundaries: processed.dataBoundaries
commits: result[0]
};
});
},
Expand Down

0 comments on commit 9fbeb2b

Please sign in to comment.