Skip to content
Snippets Groups Projects
Commit fec75d6c authored by Henrik Brun Fevang's avatar Henrik Brun Fevang
Browse files

Merge branch 'Feature/4-bar-chart-component' into 'master'

Resolve "Bar chart component"

Closes #4

See merge request it2810-h21/team-11/gitlab-visualization!8
parents 4972345c 20e761da
No related branches found
No related tags found
No related merge requests found
Showing
with 1381 additions and 28 deletions
...@@ -3,25 +3,38 @@ ...@@ -3,25 +3,38 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@date-io/date-fns": "^2.11.0",
"@emotion/styled": "^11.3.0",
"@material-ui/core": "^4.12.3",
"@material-ui/pickers": "^3.3.10",
"@mui/lab": "^5.0.0-alpha.49",
"@mui/material": "^5.0.2",
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15", "@types/jest": "^26.0.15",
"@types/node": "^12.0.0", "@types/node": "^12.0.0",
"@types/react": "^17.0.0", "@types/react": "^17.0.0",
"@types/react-datepicker": "^4.1.7",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.0",
"@types/react-loader-spinner": "^4.0.0",
"axios": "^0.21.4", "axios": "^0.21.4",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-datepicker": "^4.2.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-dotenv": "^0.1.3", "react-dotenv": "^0.1.3",
"react-loader-spinner": "^4.0.0",
"react-router": "^5.2.1", "react-router": "^5.2.1",
"react-router-dom": "^5.3.0", "react-router-dom": "^5.3.0",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"react-select": "^5.0.0", "react-select": "^5.0.0",
"react-spinners": "^0.11.0", "react-spinners": "^0.11.0",
"react-switch": "^6.0.0",
"react-toastify": "^8.0.3", "react-toastify": "^8.0.3",
"sass": "^1.42.1",
"typescript": "^4.1.2", "typescript": "^4.1.2",
"victory": "^36.0.1",
"web-vitals": "^1.0.1", "web-vitals": "^1.0.1",
"yarn": "^1.22.11" "yarn": "^1.22.11"
}, },
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
--> -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link href="https://fonts.googleapis.com/css?family=Work+Sans:400,700&display=swap" rel="stylesheet">
<!-- <!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.
......
...@@ -5,6 +5,8 @@ import OverviewPage from "./pages/overviewPage/Overview"; ...@@ -5,6 +5,8 @@ import OverviewPage from "./pages/overviewPage/Overview";
import { GlobalCommitContext } from './context/commitPageContext'; import { GlobalCommitContext } from './context/commitPageContext';
import { useState } from 'react'; import { useState } from 'react';
import IssuePage from './pages/issuePage'; import IssuePage from './pages/issuePage';
import { CommitPage } from './pages/commitGraphPage/CommitPage';
import { IssueGraphPage } from './pages/issueGraphPage/issueGraphPage';
function App() { function App() {
const [testContext, setTestContext] = useState<string>('Admin'); const [testContext, setTestContext] = useState<string>('Admin');
...@@ -26,18 +28,17 @@ function App() { ...@@ -26,18 +28,17 @@ function App() {
<Route exact path={"/issuelist"}> <Route exact path={"/issuelist"}>
<IssuePage /> <IssuePage />
</Route> </Route>
{/* <Route exact path={"/issuegraph"}> <Route exact path={"/issuegraph"}>
<IssueGraphPage />
</GlobalCommitContext.Provider> </Route>
</Route> */} <Route exact path={"/commitlist"}>
{/* <Route exact path={"/commitlist"}>
</Route> */} </Route>
{/* <Route exact path={"/commitgraph"}> <Route exact path={"/commitgraph"}>
<GlobalCommitContext.Provider value = {{testContext, setTestContext}}> <GlobalCommitContext.Provider value = {{testContext, setTestContext}}>
<CommitPage />
</GlobalCommitContext.Provider> </GlobalCommitContext.Provider>
</Route> */} </Route>
<Redirect to={"/"}/> <Redirect to={"/"}/>
</Switch> </Switch>
</div> </div>
......
.wrapper {
width: 70%;
align-self: center;
}
@media only screen and (max-width: 500px){
[class*="wrapper"] {
width: 100%;
}
}
.container {
width: 100%;
height: fit-content;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
}
// .datePickerContainer {
// display: flex;
// flex-direction: column;
// justify-content: space-evenly;
// height: 30%;
// width: 50%;
// }
export const wrapper: string;
export const container: string;
export const datePickerContainer: string;
import { useEffect, useState } from "react";
import { VictoryBar, VictoryChart, VictoryTheme } from "victory";
import { useVictory } from "../../../utils/victory/useVictory";
import styles from './barChart.module.scss';
import AdapterDateFns from '@mui/lab/AdapterDateFns';
import LocalizationProvider from '@mui/lab/LocalizationProvider';
import { ICommitsPerDay } from "../../../utils/victory/types";
import { Commit, Issue } from '../../../utils/queryType';
import { TextField } from "@mui/material";
import { DatePicker } from "@mui/lab";
interface IBarChartProps {
data: Commit[] | Issue[],
title: string,
}
export const BarChart = (props: IBarChartProps) => {
const { getEntriesPerDayBarChartData } = useVictory(props.data);
const [currentData, setCurrentData] = useState<ICommitsPerDay[]>();
const [dateIntervalStart, setStartDate] = useState<Date>(new Date("2021-09-26"));
const [dateIntervalEnd, setEndDate] = useState<Date>(new Date("2021-10-03"));
useEffect(() => {
let commitsPerDay = getEntriesPerDayBarChartData(dateIntervalStart, dateIntervalEnd);
setCurrentData(commitsPerDay);
}, [dateIntervalStart, dateIntervalEnd])
return (
<div className={styles.container}>
<h2>{props.title}</h2>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<div>
<DatePicker
label="Start date"
value={dateIntervalStart}
onChange={(newValue) => {
setStartDate(newValue ?? new Date("2021-09-26"));
}}
renderInput={(params) => <TextField {...params} sx={{ marginBottom: "10px"}} />}
/>
</div>
<div>
<DatePicker
label="End date"
value={dateIntervalEnd}
onChange={(newValue) => {
setEndDate(newValue ?? new Date());
}}
renderInput={(params) => <TextField {...params} />}
/>
</div>
</LocalizationProvider>
<div className={styles.wrapper}>
<VictoryChart
domainPadding={20}
theme={VictoryTheme.material}
style={{ background: { fill: '#3d3d3d'}}}>
<VictoryBar
data={currentData}
x="date"
y="amount"
style={{ data: {fill: 'orange'}}}/>
</VictoryChart>
</div>
</div>
)
}
\ No newline at end of file
.pieChartContainer {
display: flex;
flex-direction: column;
align-items: center;
width: 70%;
}
@media only screen and (max-width: 500px){
[class*="pieChartContainer"] {
width: 100%;
}
}
.inputContainer {
display: flex;
flex-direction: column;
justify-content: space-evenly;
height: fit-content;
width: 80%;
max-width: 300px;
}
.checkboxContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
background-color: #F5CA7B;
padding: 4%;
font-size: 1.2rem;
font-weight: 600;
border-radius: 5px;
margin-bottom: 10px;
color: #2a2a2a;
}
export const pieChartContainer: string;
export const inputContainer: string;
export const checkboxContainer: string;
import { useEffect, useState } from 'react';
import { VictoryContainer, VictoryPie } from 'victory';
import { Commit, Issue } from '../../../utils/queryType';
import { useVictory } from '../../../utils/victory/useVictory';
import styles from './pieChart.module.scss';
import Switch from "react-switch";
interface IPieChartProps {
data: Commit[] | Issue[],
title: string,
}
export const PieChart = (props: IPieChartProps) => {
const { getEntriesPerMemberPieChartData, getAnonAnimals, colorScaleAnimals } = useVictory(props.data);
const [selectedAnimals, setSelectedAnimals] = useState(
getAnonAnimals().map(animal => { return {animal: animal, selected: true}}))
const [currentData, setCurrentData] = useState(getEntriesPerMemberPieChartData(selectedAnimals));
const changeSelectedAnimals = (animalToChange: string) => {
setSelectedAnimals(prevState =>
prevState.map(prevEntry => {
if (prevEntry.animal === animalToChange) {
prevEntry.selected = !prevEntry.selected
}
return prevEntry
}))
}
const animalCheckboxes = () : JSX.Element[] => {
let checkboxes = getAnonAnimals().map(animal => {
return (
<div className={styles.checkboxContainer} key={animal}>
<label>{animal}</label>
<Switch
onColor="#fae7c2"
checked={selectedAnimals.find(sa => sa.animal === animal)?.selected ?? false}
onChange={() => changeSelectedAnimals(animal)}
/>
</div>
);
})
return checkboxes;
}
useEffect(() => {
setCurrentData(getEntriesPerMemberPieChartData(selectedAnimals))
}, [selectedAnimals])
return (
<div className={styles.pieChartContainer}>
<h2>{props.title}</h2>
<div className={styles.inputContainer}>
{animalCheckboxes()}
</div>
<VictoryPie
data={currentData[0]}
containerComponent={<VictoryContainer responsive={true}/>}
x="member"
y="amount"
colorScale={colorScaleAnimals}
/>
</div>
);
}
...@@ -11,3 +11,13 @@ code { ...@@ -11,3 +11,13 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; monospace;
} }
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Work Sans";
text-align: center;
}
.pageContainer {
display: flex;
flex-direction: column;
height: fit-content;
justify-content: space-evenly;
align-items: center;
width: 80%;
margin-left: 10%;
}
import { BarChart } from '../../components/graphs/barChart/barChart';
import { PieChart } from '../../components/graphs/pieChart/pieChart';
import { useGitlabApi } from '../../utils/gitlab_api_service';
import styles from './CommitPage.module.scss';
import { queryTypes, Commit } from '../../utils/queryType'
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
import Loader from "react-loader-spinner";
export const CommitPage = () => {
const commitData = useGitlabApi(queryTypes.AllCommits);
if (commitData.isLoading) {
return (
<Loader
type="Puff"
color="#00BFFF"
height={100}
width={100}
timeout={3000}
/>
);
}
return (
<div className={styles.pageContainer}>
<h1>Charts for issues</h1>
<BarChart data={commitData.data as Commit[]} title="Commits per day"/>
<PieChart data={commitData.data as Commit[]} title={"Commits per member"}/>
</div>
)
}
\ No newline at end of file
.pageContainer {
display: flex;
flex-direction: column;
height: fit-content;
justify-content: space-evenly;
align-items: center;
width: 80%;
margin-left: 10%;
}
import { useEffect } from 'react';
import Loader from 'react-loader-spinner';
import { BarChart } from '../../components/graphs/barChart/barChart';
import { PieChart } from '../../components/graphs/pieChart/pieChart';
import { useGitlabApi } from '../../utils/gitlab_api_service';
import { Issue, queryTypes } from '../../utils/queryType';
import styles from './issueGraphPage.module.scss';
export const IssueGraphPage = () => {
const commitData = useGitlabApi(queryTypes.AllIssuesWithoutPagination);
if (commitData.isLoading) {
return (
<Loader
type="Puff"
color="#00BFFF"
height={100}
width={100}
timeout={3000}
/>
);
}
return (
<div className={styles.pageContainer}>
<h1>Charts for issues</h1>
<BarChart data={commitData.data as Issue[]} title="Issues authored per day"/>
<PieChart data={commitData.data as Issue[]} title={"Issues authored per member"}/>
</div>
)
}
...@@ -5,12 +5,14 @@ export enum queryTypes { ...@@ -5,12 +5,14 @@ export enum queryTypes {
AllIssues = "issues/", AllIssues = "issues/",
Languages = "languages", Languages = "languages",
Access="access_requests", Access="access_requests",
AllIssuesWithoutPagination = "issues/?scope=all",
AllCommitsWithoutPagination = "commits/?scope=all",
} }
export type User = { export type User = {
id: String, id: String,
name: String, name: String,
userName: String, username: String,
} }
export type Issue = { export type Issue = {
...@@ -32,7 +34,8 @@ export type Commit = { ...@@ -32,7 +34,8 @@ export type Commit = {
title: string, title: string,
message: string, message: string,
author_name: string, author_name: string,
committer_name: string committer_name: string,
committed_date: Date,
} }
export type Branch = { export type Branch = {
......
interface IGraphData {
name: string,
date: Date,
}
interface ICommitsPerDay {
date: string,
amount: number,
}
interface ICommitsPerMember {
member: string,
amount: number,
}
export type {IGraphData, ICommitsPerDay, ICommitsPerMember};
\ No newline at end of file
import { useState } from "react";
import { Commit, Issue } from "../queryType";
import { IGraphData, ICommitsPerDay, ICommitsPerMember } from "./types";
const anonymousAnimals = [
"Tiger",
"Lion",
"Giraffe",
"Rhino",
"Monkey",
"Donkey",
"Zebra",
"Alligator",
"Baboon",
"Gorilla",
"Lemur",
]
const colorScaleAnimals = [
"#f0ad34",
"#f1b74c",
"#f3c063",
"#f5ca7b",
"#f7d493",
"#f9ddaa",
"#fae7c2",
]
export const useVictory = (initData: Commit[] | Issue[]) => {
const [data, setData] = useState(anonymizeData(initData));
function anonymizeData(currentData: Commit[] | Issue[]) {
let emails: string[] = []
let animals: string[] = []
let standardizedData = currentData?.map(entry => {
return {
name: (entry as Commit).committer_name ?? (entry as Issue).author.username,
date: (entry as Commit).committed_date ?? (entry as Issue).created_at,
}
})
standardizedData?.forEach(entry => {
if (!emails.includes(entry.name)) {
emails.push(entry.name)
}
});
let anonData : IGraphData[] = []
standardizedData?.forEach(entry => {
let memberNumber = emails.indexOf(entry.name);
let anonName = memberNumber < anonymousAnimals.length
? anonymousAnimals[memberNumber]
: memberNumber.toString();
if (!animals.includes(anonName)) animals.push(anonName);
anonData.push({ name: anonName, date: new Date(entry.date)});
})
return { anonData, animals };
}
function getEntriesPerDayBarChartData(startDate: Date, endDate: Date) : ICommitsPerDay[]{
let commitsPerDayData = []
let anon = anonymizeData(initData);
startDate = startDate ?? new Date();
endDate = endDate ?? new Date();
for (let day = startDate; day <= endDate; day.setDate(day.getDate() + 1)) {
let amount: number = anon.anonData.filter(entry => entry.date.getDate() === day.getDate()).length;
commitsPerDayData.push({ date: `${day.getDate()}.${day.getMonth()}`, amount: amount});
}
return commitsPerDayData;
};
function getEntriesPerMemberPieChartData(animals: { animal: string, selected: boolean}[]) {
let commitsPerMemberData: ICommitsPerMember[] = []
animals.filter(animal => animal.selected).forEach(animal => {
let amount: number = data.anonData.filter(entry => entry.name === animal.animal).length;
commitsPerMemberData.push({ member: animal.animal, amount: amount})
})
return [commitsPerMemberData, animals];
}
function getAnonAnimals() {
return data.animals;
}
return { getEntriesPerDayBarChartData, anonymizeData, getEntriesPerMemberPieChartData, getAnonAnimals, colorScaleAnimals };
}
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment