Skip to content
This repository has been archived by the owner on Feb 9, 2022. It is now read-only.

Commit

Permalink
Feature/new attendee graph (#238)
Browse files Browse the repository at this point in the history
* fix legend title cut off

* WIP

* fix sizes

* un comment get loop

* re-enable pooling

* use new checkins endpoint

* change interval
  • Loading branch information
santipalenque authored Jul 10, 2023
1 parent 1908ed6 commit a5da104
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 148 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"babel-loader": "^8.1.0",
"browser-tabs-lock": "1.2.15",
"buffer": "^6.0.3",
"chart.js": "^3.7.1",
"chartjs-plugin-datalabels": "^2.1.0",
"chart.js": "^4.3.0",
"chartjs-plugin-datalabels": "^2.2.0",
"clean-webpack-plugin": "^4.0.0",
"crypto-browserify": "^3.12.0",
"crypto-js": "^4.1.1",
Expand Down Expand Up @@ -70,7 +70,7 @@
"react-bootstrap": "^0.31.5",
"react-breadcrumbs": "^2.1.6",
"react-burger-menu": "^2.6.10",
"react-chartjs-2": "^3.1.0",
"react-chartjs-2": "^5.2.0",
"react-data-export": "^0.5.0",
"react-datetime": "^2.15.0",
"react-device-detect": "^2.2.2",
Expand Down
81 changes: 70 additions & 11 deletions src/actions/summit-stats-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,22 @@ import {getAccessTokenSafely} from '../utils/methods';

export const REQUEST_REGISTRATION_STATS = 'REQUEST_REGISTRATION_STATS';
export const RECEIVE_REGISTRATION_STATS = 'RECEIVE_REGISTRATION_STATS';
export const REQUEST_ATTENDEE_CHECK_INS = 'REQUEST_ATTENDEE_CHECK_INS';
export const RECEIVE_ATTENDEE_CHECK_INS = 'RECEIVE_ATTENDEE_CHECK_INS';
export const REGISTRATION_DATA_REQUESTED = 'REGISTRATION_DATA_REQUESTED';
export const REGISTRATION_DATA_LOADED = 'REGISTRATION_DATA_LOADED';

/**
* @param fromDate
* @param toDate
* @param shouldDispatchLoad
* @returns {function(*=, *): *}
*/
export const getRegistrationStats = (fromDate = null , toDate = null, shouldDispatchLoad = true) => async (dispatch, getState) => {
export const getRegistrationStats = (fromDate = null , toDate = null) => async (dispatch, getState) => {
const { currentSummitState } = getState();
const { currentSummit } = currentSummitState;
const filter = [];
const accessToken = await getAccessTokenSafely();

if(shouldDispatchLoad)
dispatch(startLoading());

if (fromDate) {
filter.push(`start_date>=${fromDate}`);
}
Expand All @@ -58,10 +58,69 @@ export const getRegistrationStats = (fromDate = null , toDate = null, shouldDisp
createAction(REQUEST_REGISTRATION_STATS),
createAction(RECEIVE_REGISTRATION_STATS),
`${window.API_BASE_URL}/api/v1/summits/all/${currentSummit.id}/registration-stats`,
authErrorHandler, {}, true // use ETAGS
)(params)(dispatch).then(() => {
if(shouldDispatchLoad)
dispatch(stopLoading());
}
);
authErrorHandler,
{},
true // use ETAGS
)(params)(dispatch);
}

export const getAttendeeData = (fromDate = null , toDate = null, page = 1, groupBy = null) => async (dispatch, getState) => {
const {currentSummitState, summitStatsState} = getState();
const {currentSummit} = currentSummitState;
const {timeUnit} = summitStatsState;
const filter = [];
const accessToken = await getAccessTokenSafely();

if (fromDate) {
filter.push(`start_date>=${fromDate}`);
}

if (toDate) {
filter.push(`end_date<=${toDate}`);
}

const params = {
access_token: accessToken,
per_page: 100,
page,
group_by: groupBy || timeUnit
};

if (filter.length > 0) {
params['filter[]'] = filter;
}

return getRequest(
createAction(REQUEST_ATTENDEE_CHECK_INS),
createAction(RECEIVE_ATTENDEE_CHECK_INS),
`${window.API_BASE_URL}/api/v1/summits/all/${currentSummit.id}/registration-stats/check-ins`,
authErrorHandler,
{timeUnit: groupBy || timeUnit},
true // use ETAGS
)(params)(dispatch).then(({response}) => {
if (page < response.last_page) {
return getAttendeeData(fromDate, toDate, page + 1)(dispatch, getState);
}

return Promise.resolve();
});
};

export const getRegistrationData = (fromDate = null , toDate = null, shouldDispatchLoad = true) => async (dispatch, getState) => {

if (shouldDispatchLoad) dispatch(startLoading());

dispatch(createAction(REGISTRATION_DATA_REQUESTED)({}));

const regStatsPromise = getRegistrationStats(fromDate, toDate)(dispatch, getState);
const attendeeDataPromise = getAttendeeData(fromDate, toDate)(dispatch, getState);

Promise.all([regStatsPromise, attendeeDataPromise]).finally(() => {
if (shouldDispatchLoad) dispatch(stopLoading());
dispatch(createAction(REGISTRATION_DATA_LOADED)({}));
})
}

export const changeTimeUnit = (unit, fromDate, toDate) => (dispatch, getState) => {
getAttendeeData(fromDate, toDate, 1, unit)(dispatch, getState);
}
9 changes: 7 additions & 2 deletions src/components/filters/date-interval-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,18 @@ const DateIntervalFilter = ({ onFilter, timezone = 'UTC'}) => {
setToDate(null);
onFilter(null, null);
}

const onChangeFromDate = (newDate) => {
if (!toDate) setToDate(newDate);
setFromDate(newDate);
}

return (
<div className="inline">
From: &nbsp;&nbsp;
<DateTimePicker
id="fromDate"
onChange={ev => setFromDate(ev.target.value)}
onChange={ev => onChangeFromDate(ev.target.value)}
format={{date: "YYYY-MM-DD", time: "HH:mm"}}
value={fromDate}
timezone={timezone}
Expand All @@ -43,7 +48,7 @@ const DateIntervalFilter = ({ onFilter, timezone = 'UTC'}) => {
<DateTimePicker
id="toDate"
onChange={ev => setToDate(ev.target.value)}
validation={{ before: fromDate?.unix(), after: '>=' }}
validation={{ before: (fromDate?.unix() -1), after: '>=' }}
format={{date: "YYYY-MM-DD", time: "HH:mm"}}
value={toDate}
timezone={timezone}
Expand Down
73 changes: 73 additions & 0 deletions src/components/graphs/registration-line-graph/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Copyright 2022 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

import React, {useMemo} from 'react'
import {Line} from "react-chartjs-2";
import Chart from 'chart.js/auto';
import {isMobile} from 'react-device-detect';
import styles from './index.module.less'


const LineGraph = ({title, data, labels, children}) => {
const graphSize = isMobile ? { width: 400, height: (400 + (labels.length * 60)) } : { width: 600, height: 600 };
const layoutPadding = isMobile ? { top: 10, left: 10, right: 10, bottom: 30 } : { top: 80, left: 80, right: 80, bottom: 80 };

const chartData = {
labels: labels,
datasets: [
{
label: 'Attendees checked-in',
data: data,
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.5)',
borderWidth: 1,
},
],
};

const chartOptions = {
maintainAspectRatio: false,
layout: {
padding: layoutPadding
},
responsive: true,
plugins: {
tooltip: {
callbacks: {
label: context => context.value
}
},
},
scales: {
y: {
min: 0,
max: Math.max(...data) + 2,
ticks: {
stepSize: 1
}
}
}
};

return (
<div className={styles.wrapper}>
<h5 className={styles.title}>{title}</h5>
{children}
<div>
<Line data={chartData} {...graphSize} options={chartOptions}/>
</div>
</div>
);
};

export default LineGraph;
Original file line number Diff line number Diff line change
Expand Up @@ -13,74 +13,21 @@

import React, {useMemo} from 'react'
import {Pie} from "react-chartjs-2";
import {Chart} from 'chart.js';
import Chart from 'chart.js/auto';
import {isMobile} from 'react-device-detect';
import {getRandomColors, createDonnutCanvas} from "../utils";
import styles from './index.module.less'


const starters = [[220,120,20],[120,220,20],[80,20,240],[220,20,120],[20,120,220],[20,210,90]];

const getRandomColors = (amount, starter) => {

const starterColors = starters[starter];

let incBase = starterColors[0] < 100 ? (255 - starterColors[0]) : starterColors[0];
const incRed = Math.ceil(incBase / amount);

incBase = starterColors[1] < 100 ? (255 - starterColors[1]) : starterColors[1];
const incGreen = Math.ceil(incBase / amount);

incBase = starterColors[2] < 100 ? (255 - starterColors[2]) : starterColors[2];
const incBlue = Math.ceil(incBase / amount);


return Array.apply(null, {length: amount}).map((v,i) => {
let multiplier = starterColors[0] < 100 ? -1 : 1;
const r = Math.ceil(starterColors[0] - (i * incRed * multiplier));
multiplier = starterColors[1] < 100 ? -1 : 1;
const g = Math.ceil(starterColors[1] - (i * incGreen * multiplier));
multiplier = starterColors[2] < 100 ? -1 : 1;
const b = Math.ceil(starterColors[2] - (i * incBlue * multiplier));

return `rgb(${r}, ${g}, ${b})`;
});
};

function createDonnutCanvas(arc, percent) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

canvas.width = 100;
canvas.height = 60;

ctx.font = "10px Arial";
ctx.textAlign = "center";
ctx.fillText(`${percent}%`, 32, 28);

ctx.beginPath()
ctx.fillStyle = arc?.options?.backgroundColor;
ctx.arc(32, 25, 20, 0, (2 * Math.PI * percent / 100), false); // outer (filled)
ctx.arc(32, 25, 14, (2 * Math.PI * percent / 100), 0, true); // inner (unfills it)
ctx.fill();

ctx.beginPath()
ctx.arc(32, 25, 20, 0, Math.PI * 2, true);
ctx.stroke();
ctx.beginPath()
ctx.arc(32, 25, 14, 0, Math.PI * 2, true);
ctx.stroke();

return canvas;
};

const Graph = ({title, subtitle = null, legendTitle= null, data, labels, colors = null, colorPalette = null}) => {
const PieGraph = ({title, subtitle = null, legendTitle= null, data, labels, colors = null, colorPalette = null}) => {
const fillColors = useMemo(() => colors || getRandomColors(data.length, colorPalette), [colors, data.length]);
const graphSize = isMobile ? { width: 400, height: (400 + (labels.length * 60)) } : { width: 600, height: 600 };
const height = Math.max(600, labels.length * 68);
const graphSize = isMobile ? { width: 400, height: height } : { width: 600, height: height };
const legendPos = isMobile ? 'bottom' : 'right';
const legendAlign = isMobile ? 'start' : 'center';
const layoutPadding = isMobile ? { top: 10, left: 10, right: 10, bottom: 30 } : { top: 80, left: 80, right: 80, bottom: 80 };
const layoutPadding = isMobile ? { top: 10, left: 10, right: 10, bottom: 30 } : { top: 80, left: 30, right: 30, bottom: 80 };
const titlePadding = isMobile ? { top: 10, left: 0, right: 0, bottom: 0 } : 0;

const chartData = {
labels: labels,
datasets: [
Expand Down Expand Up @@ -127,6 +74,9 @@ const Graph = ({title, subtitle = null, legendTitle= null, data, labels, colors
const color = dataset.backgroundColor[i];
const arc = chart.getDatasetMeta(0).data[i];
let percent = 0;
// we need this so that legend title is not cut off when labels are shorter that legend title
const labelText = dataItem.label || label;
const labelTextExt = legendTitle ? labelText.padEnd(legendTitle.length + 5) : labelText;

if (dataItem.total) {
percent = dataItem.total > 0 ? Math.round((dataItem.value / dataItem.total) * 100) : 100;
Expand All @@ -135,7 +85,7 @@ const Graph = ({title, subtitle = null, legendTitle= null, data, labels, colors
}

return {
text: dataItem.label || label,
text: labelTextExt,
fillStyle: color,
fontColor: color,
strokeStyle: color,
Expand All @@ -146,7 +96,7 @@ const Graph = ({title, subtitle = null, legendTitle= null, data, labels, colors
}, this);
}
}
}
},
}
};

Expand All @@ -159,10 +109,10 @@ const Graph = ({title, subtitle = null, legendTitle= null, data, labels, colors
</div>
}
<div>
<Pie data={chartData} {...graphSize} options={chartOptions}/>
<Pie data={chartData} {...graphSize} options={chartOptions} />
</div>
</div>
);
};

export default Graph;
export default PieGraph;
12 changes: 12 additions & 0 deletions src/components/graphs/registration-pie-graph/index.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.wrapper {
margin-top: 30px;

.title {
font-size: 18px;
font-weight: bold;
}

.subtitle {
font-size: 14px;
}
}
Loading

0 comments on commit a5da104

Please sign in to comment.