diff --git a/ui/src/visualizations/team-awareness/chart/chart.js b/ui/src/visualizations/team-awareness/chart/chart.js index 617ec48e..79d7a7c5 100644 --- a/ui/src/visualizations/team-awareness/chart/chart.js +++ b/ui/src/visualizations/team-awareness/chart/chart.js @@ -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

Hello World

; + + const { stakeholders } = this.props.data; + + return ( +
+ +
+ ); } } diff --git a/ui/src/visualizations/team-awareness/chart/index.js b/ui/src/visualizations/team-awareness/chart/index.js index fa3f8f70..bee21a03 100644 --- a/ui/src/visualizations/team-awareness/chart/index.js +++ b/ui/src/visualizations/team-awareness/chart/index.js @@ -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 } }; }; diff --git a/ui/src/visualizations/team-awareness/components/BubbleChart/BubbleChart.js b/ui/src/visualizations/team-awareness/components/BubbleChart/BubbleChart.js new file mode 100644 index 00000000..f8bda48a --- /dev/null +++ b/ui/src/visualizations/team-awareness/components/BubbleChart/BubbleChart.js @@ -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 ( +
+ (this.svgRef = svg)} /> +
(this.tooltipRef = 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))))))) + + ')' + ); +} diff --git a/ui/src/visualizations/team-awareness/components/BubbleChart/BubbleChart.scss b/ui/src/visualizations/team-awareness/components/BubbleChart/BubbleChart.scss new file mode 100644 index 00000000..50a41d91 --- /dev/null +++ b/ui/src/visualizations/team-awareness/components/BubbleChart/BubbleChart.scss @@ -0,0 +1,11 @@ +.chartArea { + width: 100%; +} +.chartTooltip { + width: 100%; +} +.chartDrawingArea { + width: 100%; + height: 100%; + border: 1px solid #000; +} diff --git a/ui/src/visualizations/team-awareness/components/Timeline/ActivityTimeline.js b/ui/src/visualizations/team-awareness/components/Timeline/ActivityTimeline.js new file mode 100644 index 00000000..d97ee027 --- /dev/null +++ b/ui/src/visualizations/team-awareness/components/Timeline/ActivityTimeline.js @@ -0,0 +1,7 @@ +import StackedAreaChart from '../../../../components/StackedAreaChart'; + +export default class ActivityTimeline extends StackedAreaChart { + constructor(props) { + super(props); + } +} diff --git a/ui/src/visualizations/team-awareness/components/Timeline/ActivityTimeline.scss b/ui/src/visualizations/team-awareness/components/Timeline/ActivityTimeline.scss new file mode 100644 index 00000000..e69de29b diff --git a/ui/src/visualizations/team-awareness/config.js b/ui/src/visualizations/team-awareness/config.js index e1e49b55..7014564c 100644 --- a/ui/src/visualizations/team-awareness/config.js +++ b/ui/src/visualizations/team-awareness/config.js @@ -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; @@ -14,7 +14,7 @@ const mapStateToProps = (appState /*, ownProps*/) => { }; const mapDispatchToProps = dispatch => { return { - onSelectActivity: selectActivity => dispatch(setSelectActivity(selectActivity)) + onSelectActivityScale: selectActivity => dispatch(setActivityScale(selectActivity)) }; }; @@ -29,7 +29,7 @@ class ConfigComponent extends React.Component {
Activity:
- this.props.onSelectActivityScale(value)}> diff --git a/ui/src/visualizations/team-awareness/reducers/config.js b/ui/src/visualizations/team-awareness/reducers/config.js index ef817d53..d8091235 100644 --- a/ui/src/visualizations/team-awareness/reducers/config.js +++ b/ui/src/visualizations/team-awareness/reducers/config.js @@ -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' } ); diff --git a/ui/src/visualizations/team-awareness/reducers/data.js b/ui/src/visualizations/team-awareness/reducers/data.js index 518e098a..ffb006c7 100644 --- a/ui/src/visualizations/team-awareness/reducers/data.js +++ b/ui/src/visualizations/team-awareness/reducers/data.js @@ -17,7 +17,9 @@ export default handleActions( } }, { - data: {}, + data: { + stakeholders: [] + }, lastFetched: null, isFetching: null } diff --git a/ui/src/visualizations/team-awareness/sagas/getCommits.js b/ui/src/visualizations/team-awareness/sagas/getCommits.js index e6e31f5c..f82487bf 100644 --- a/ui/src/visualizations/team-awareness/sagas/getCommits.js +++ b/ui/src/visualizations/team-awareness/sagas/getCommits.js @@ -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 diff --git a/ui/src/visualizations/team-awareness/sagas/getStakeholders.js b/ui/src/visualizations/team-awareness/sagas/getStakeholders.js index 5c0eec35..d410825c 100644 --- a/ui/src/visualizations/team-awareness/sagas/getStakeholders.js +++ b/ui/src/visualizations/team-awareness/sagas/getStakeholders.js @@ -22,9 +22,9 @@ const getStakeholders = (page, perPage) => { query($page:Int, $perPage:Int){ stakeholders(page:$page, perPage: $perPage) { data { - id - gitSignature - } + id + gitSignature + } } }`, page, diff --git a/ui/src/visualizations/team-awareness/sagas/index.js b/ui/src/visualizations/team-awareness/sagas/index.js index ac979f07..bdee1f8c 100644 --- a/ui/src/visualizations/team-awareness/sagas/index.js +++ b/ui/src/visualizations/team-awareness/sagas/index.js @@ -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'); @@ -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} */ + 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 }; }); },