chore: change charts library (#1305)

* fix: dashboard charts

* fix: cycles new charts

* chore: sidebar burn down chart and calendar graph

* chore: update dashboard line and pie graph

* chore: update axes width of burndown chart

---------

Co-authored-by: Dakshesh Jain <dakshesh.jain14@gmail.com>
This commit is contained in:
Aaryan Khandelwal 2023-06-19 12:59:57 +05:30 committed by GitHub
parent 59c0de9b57
commit 1da86b80b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 211 additions and 584 deletions

View file

@ -1,12 +1,11 @@
import React from "react";
import { XAxis, YAxis, Tooltip, AreaChart, Area, ReferenceLine, TooltipProps} from "recharts";
// ui
import { LineGraph } from "components/ui";
// helpers
import { getDatesInRange, renderShortNumericDateFormat } from "helpers/date-time.helper";
//types
import { IIssue } from "types";
import { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent";
// helper
import { getDatesInRange, renderShortNumericDateFormat } from "helpers/date-time.helper";
type Props = {
issues: IIssue[];
@ -16,12 +15,40 @@ type Props = {
height?: number;
};
const ProgressChart: React.FC<Props> = ({ issues, start, end, width = 360, height = 160 }) => {
const styleById = {
ideal: {
strokeDasharray: "6, 3",
strokeWidth: 1,
},
default: {
strokeWidth: 1,
},
};
const DashedLine = ({ series, lineGenerator, xScale, yScale }: any) =>
series.map(({ id, data, color }: any) => (
<path
key={id}
d={lineGenerator(
data.map((d: any) => ({
x: xScale(d.data.x),
y: yScale(d.data.y),
}))
)}
fill="none"
stroke={color ?? "#ddd"}
style={styleById[id as keyof typeof styleById] || styleById.default}
/>
));
const ProgressChart: React.FC<Props> = ({ issues, start, end }) => {
const startDate = new Date(start);
const endDate = new Date(end);
const getChartData = () => {
const dateRangeArray = getDatesInRange(startDate, endDate);
let count = 0;
const dateWiseData = dateRangeArray.map((d) => {
const current = d.toISOString().split("T")[0];
const total = issues.length;
@ -39,56 +66,67 @@ const ProgressChart: React.FC<Props> = ({ issues, start, end, width = 360, heigh
return dateWiseData;
};
const CustomTooltip = ({ active, payload }: TooltipProps<ValueType, NameType>) => {
if (active && payload && payload.length) {
return (
<div className="rounded-sm bg-brand-surface-1 p-1 text-xs text-brand-base">
<p>{payload[0].payload.currentDate}</p>
</div>
);
}
return null;
};
const ChartData = getChartData();
const chartData = getChartData();
return (
<div className="absolute -left-4 flex h-full w-full items-center justify-center text-xs">
<AreaChart
width={width}
height={height}
data={ChartData}
margin={{
top: 12,
right: 12,
left: 0,
bottom: 12,
<div className="w-full flex justify-center items-center">
<LineGraph
animate
curve="monotoneX"
height="160px"
width="360px"
enableGridY={false}
lineWidth={1}
margin={{ top: 30, right: 30, bottom: 30, left: 30 }}
data={[
{
id: "pending",
color: "#3F76FF",
data: chartData.map((item, index) => ({
index,
x: item.currentDate,
y: item.pending,
color: "#3F76FF",
})),
enableArea: true,
},
{
id: "ideal",
color: "#a9bbd0",
fill: "transparent",
data: [
{
x: chartData[0].currentDate,
y: issues.length,
},
{
x: chartData[chartData.length - 1].currentDate,
y: 0,
},
],
},
]}
layers={["grid", "markers", "areas", DashedLine, "slices", "points", "axes", "legends"]}
axisBottom={{
tickValues: chartData.map((item, index) => (index % 2 === 0 ? item.currentDate : "")),
}}
>
<defs>
<linearGradient id="linearblue" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3F76FF" stopOpacity={0.7} />
<stop offset="50%" stopColor="#3F76FF" stopOpacity={0.1} />
<stop offset="100%" stopColor="#3F76FF" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis dataKey="currentDate" tickSize={10} minTickGap={10} />
<YAxis tickSize={10} minTickGap={10} allowDecimals={false} />
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="pending"
stroke="#8884d8"
fill="url(#linearblue)"
activeDot={{ r: 8 }}
/>
<ReferenceLine
stroke="#16a34a"
strokeDasharray="3 3"
segment={[
{ x: `${renderShortNumericDateFormat(endDate)}`, y: 0 },
{ x: `${renderShortNumericDateFormat(startDate)}`, y: issues.length },
]}
/>
</AreaChart>
enablePoints={false}
enableArea
colors={(datum) => datum.color ?? "#3F76FF"}
customYAxisTickValues={[0, issues.length]}
gridXValues={chartData.map((item, index) => (index % 2 === 0 ? item.currentDate : ""))}
theme={{
background: "rgb(var(--color-bg-sidebar))",
axis: {
domain: {
line: {
stroke: "rgb(var(--color-border))",
strokeWidth: 1,
},
},
},
}}
/>
</div>
);
};

View file

@ -485,7 +485,6 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</div>
</div>
</div>
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-brand-base p-6">
<Disclosure defaultOpen>
{({ open }) => (
@ -552,7 +551,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
</div>
</div>
</div>
<div className="relative h-40 w-80">
<div className="relative">
<ProgressChart
issues={issues ?? []}
start={cycle?.start_date ?? ""}
@ -569,7 +568,6 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
)}
</Disclosure>
</div>
<div className="flex w-full flex-col items-center justify-start gap-2 border-t border-brand-base p-6">
<Disclosure defaultOpen>
{({ open }) => (
@ -604,7 +602,7 @@ export const CycleDetailsSidebar: React.FC<Props> = ({
<Transition show={open}>
<Disclosure.Panel>
{cycle.total_issues > 0 ? (
<div className=" h-full w-full py-4">
<div className="h-full w-full py-4">
<SidebarProgressStats
issues={issues ?? []}
groupedIssues={{

View file

@ -63,7 +63,7 @@ export const ProgressBar: React.FC<Props> = ({
return (
<svg width={radius * 2} height={radius * 2}>
{renderOuterCircle()}
<circle r={radius - strokeWidth} cx={radius} cy={radius} className="progress-bar"/>
<circle r={radius - strokeWidth} cx={radius} cy={radius} className="progress-bar" />
</svg>
);
};

View file

@ -1,141 +1,34 @@
import { useEffect, useRef, useState } from "react";
// ui
import { Tooltip } from "components/ui";
import { CalendarGraph } from "components/ui";
// helpers
import { renderDateFormat, renderShortNumericDateFormat } from "helpers/date-time.helper";
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import { IUserActivity } from "types";
// constants
import { DAYS, MONTHS } from "constants/project";
type Props = {
activities: IUserActivity[] | undefined;
};
export const ActivityGraph: React.FC<Props> = ({ activities }) => {
const ref = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
const today = new Date();
const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1);
const twoMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 2, 1);
const threeMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 3, 1);
const fourMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 4, 1);
const fiveMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 5, 1);
const recentMonths = [
fiveMonthsAgo,
fourMonthsAgo,
threeMonthsAgo,
twoMonthsAgo,
lastMonth,
today,
];
const getDatesOfMonth = (dateOfMonth: Date) => {
const month = dateOfMonth.getMonth();
const year = dateOfMonth.getFullYear();
const dates = [];
const date = new Date(year, month, 1);
while (date.getMonth() === month && date < new Date()) {
dates.push(renderDateFormat(new Date(date)));
date.setDate(date.getDate() + 1);
export const ActivityGraph: React.FC<Props> = ({ activities }) => (
<CalendarGraph
data={
activities?.map((activity) => ({
day: activity.created_date,
value: activity.activity_count,
})) ?? []
}
return dates;
};
const recentDates = [
...getDatesOfMonth(recentMonths[0]),
...getDatesOfMonth(recentMonths[1]),
...getDatesOfMonth(recentMonths[2]),
...getDatesOfMonth(recentMonths[3]),
...getDatesOfMonth(recentMonths[4]),
...getDatesOfMonth(recentMonths[5]),
];
const activitiesIntensity = (activityCount: number) => {
if (activityCount <= 3) return "opacity-20";
else if (activityCount > 3 && activityCount <= 6) return "opacity-40";
else if (activityCount > 6 && activityCount <= 9) return "opacity-80";
else return "";
};
const addPaddingTiles = () => {
const firstDateDay = new Date(recentDates[0]).getDay();
for (let i = 0; i < firstDateDay; i++) recentDates.unshift("");
};
addPaddingTiles();
useEffect(() => {
if (!ref.current) return;
setWidth(ref.current.offsetWidth);
}, [ref]);
return (
<div className="grid place-items-center overflow-x-scroll">
<div className="flex items-start gap-4">
<div className="flex flex-col gap-2 pt-6">
{DAYS.map((day, index) => (
<h6 key={day} className="h-4 text-xs">
{index % 2 === 0 && day.substring(0, 3)}
</h6>
))}
</div>
<div>
<div className="flex items-center justify-between" style={{ width: `${width}px` }}>
{recentMonths.map((month, index) => (
<h6 key={index} className="w-full text-xs">
{MONTHS[month.getMonth()].substring(0, 3)}
</h6>
))}
</div>
<div
className="mt-2 grid w-full grid-flow-col gap-2"
style={{ gridTemplateRows: "repeat(7, minmax(0, 1fr))" }}
ref={ref}
>
{recentDates.map((date, index) => {
const isActive = activities?.find((a) => a.created_date === date);
return (
<Tooltip
key={`${date}-${index}`}
tooltipContent={`${
isActive ? isActive.activity_count : 0
} activities on ${renderShortNumericDateFormat(date)}`}
theme="dark"
>
<div
className={`${
date === "" ? "pointer-events-none opacity-0" : ""
} h-4 w-4 rounded ${
isActive
? `bg-brand-accent ${activitiesIntensity(isActive.activity_count)}`
: "bg-brand-surface-2"
}`}
/>
</Tooltip>
);
})}
</div>
<div className="mt-8 flex items-center gap-2 text-xs">
<span>Less</span>
<span className="h-4 w-4 rounded bg-brand-surface-2" />
<span className="h-4 w-4 rounded bg-brand-accent opacity-20" />
<span className="h-4 w-4 rounded bg-brand-accent opacity-40" />
<span className="h-4 w-4 rounded bg-brand-accent opacity-80" />
<span className="h-4 w-4 rounded bg-brand-accent" />
<span>More</span>
</div>
</div>
from={activities?.length ? activities[0].created_date : new Date()}
to={activities?.length ? activities[activities.length - 1].created_date : new Date()}
height="200px"
margin={{ bottom: 0, left: 10, right: 10, top: 0 }}
tooltip={(datum) => (
<div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
<span className="text-brand-secondary">{renderShortDateWithYearFormat(datum.day)}:</span>{" "}
{datum.value}
</div>
</div>
);
};
)}
theme={{
background: "rgb(var(--color-bg-base))",
}}
/>
);

View file

@ -1,15 +1,5 @@
// recharts
import {
CartesianGrid,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
// ui
import { CustomMenu } from "components/ui";
import { CustomMenu, LineGraph } from "components/ui";
// constants
import { MONTHS } from "constants/project";
@ -36,13 +26,6 @@ export const CompletedIssuesGraph: React.FC<Props> = ({ month, issues, setMonth
});
}
const CustomTooltip = ({ payload, label }: any) => (
<div className="space-y-1 rounded bg-brand-surface-1 p-3 text-sm shadow-md">
<h4 className="text-brand-secondary">{label}</h4>
<h5>Completed issues: {payload[0]?.value}</h5>
</div>
);
return (
<div>
<div className="mb-0.5 flex justify-between">
@ -56,25 +39,37 @@ export const CompletedIssuesGraph: React.FC<Props> = ({ month, issues, setMonth
</CustomMenu>
</div>
<div className="rounded-[10px] border border-brand-base bg-brand-base p-8 pl-4">
<ResponsiveContainer width="100%" height={250}>
<LineChart data={data}>
<CartesianGrid stroke="#858e9660" />
<XAxis dataKey="week_in_month" padding={{ left: 48, right: 48 }} />
<YAxis dataKey="completed_count" allowDecimals={false} />
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="completed_count"
stroke="#d687ff"
strokeWidth={3}
fill="#8e2de2"
{data.every((item) => item.completed_count === 0) ? (
<div className="flex items-center justify-center h-72">
<h4 className="text-[#d687ff]">No issues closed this month</h4>
</div>
) : (
<>
<LineGraph
height="250px"
data={[
{
id: "completed_issues",
color: "#d687ff",
data: data.map((item) => ({
x: item.week_in_month,
y: item.completed_count,
})),
},
]}
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
customYAxisTickValues={data.map((item) => item.completed_count)}
colors={(datum) => datum.color}
theme={{
background: "rgb(var(--color-bg-base))",
}}
/>
</LineChart>
</ResponsiveContainer>
<h4 className="mt-4 flex items-center justify-center gap-2 text-[#d687ff]">
<span className="h-2 w-2 bg-[#d687ff]" />
Completed Issues
</h4>
<h4 className="mt-4 flex items-center justify-center gap-2 text-[#d687ff]">
<span className="h-2 w-2 bg-[#d687ff]" />
Completed Issues
</h4>
</>
)}
</div>
</div>
);

View file

@ -1,7 +1,7 @@
import { useCallback, useState } from "react";
// recharts
import { Cell, Legend, Pie, PieChart, ResponsiveContainer, Sector } from "recharts";
// ui
import { PieGraph } from "components/ui";
// helpers
import { capitalizeFirstLetter } from "helpers/string.helper";
// types
import { IUserStateDistribution } from "types";
// constants
@ -11,113 +11,52 @@ type Props = {
groupedIssues: IUserStateDistribution[] | undefined;
};
export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => {
const [activeIndex, setActiveIndex] = useState(0);
const onPieEnter = useCallback(
(_: any, index: number) => {
setActiveIndex(index);
},
[setActiveIndex]
);
const renderActiveShape = ({
cx,
cy,
midAngle,
innerRadius,
outerRadius,
startAngle,
endAngle,
fill,
payload,
value,
}: any) => {
const RADIAN = Math.PI / 180;
const sin = Math.sin(-RADIAN * midAngle);
const cos = Math.cos(-RADIAN * midAngle);
const sx = cx + (outerRadius + 10) * cos;
const sy = cy + (outerRadius + 10) * sin;
const mx = cx + (outerRadius + 30) * cos;
const my = cy + (outerRadius + 30) * sin;
const ex = mx + (cos >= 0 ? 1 : -1) * 22;
const ey = my;
const textAnchor = cos >= 0 ? "start" : "end";
return (
<g>
<text x={cx} y={cy} dy={8} className="capitalize" textAnchor="middle" fill={fill}>
{payload.state_group}
</text>
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius}
outerRadius={outerRadius}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
/>
<Sector
cx={cx}
cy={cy}
startAngle={startAngle}
endAngle={endAngle}
innerRadius={outerRadius + 6}
outerRadius={outerRadius + 10}
fill={fill}
/>
<path d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`} stroke={fill} fill="none" />
<circle cx={ex} cy={ey} r={2} fill={fill} stroke="none" />
<text x={ex + (cos >= 0 ? 1 : -1) * 12} y={ey} textAnchor={textAnchor} fill="#858e96">
{value} issues
</text>
</g>
);
};
return (
<div>
<h3 className="mb-2 font-semibold">Issues by States</h3>
<div className="rounded-[10px] border border-brand-base bg-brand-base p-4">
<ResponsiveContainer width="100%" height={320}>
<PieChart>
<Pie
data={groupedIssues}
dataKey="state_count"
nameKey="state_group"
cx="50%"
cy="50%"
fill="#8884d8"
innerRadius={70}
outerRadius={90}
activeIndex={activeIndex}
activeShape={renderActiveShape}
onMouseEnter={onPieEnter}
>
{groupedIssues?.map((cell) => (
<Cell
key={cell.state_group}
fill={STATE_GROUP_COLORS[cell.state_group.toLowerCase()]}
/>
))}
</Pie>
<Legend
layout="vertical"
verticalAlign="middle"
align="right"
height={36}
payload={[
{ value: "Backlog", type: "square", color: STATE_GROUP_COLORS.backlog },
{ value: "Unstarted", type: "square", color: STATE_GROUP_COLORS.unstarted },
{ value: "Started", type: "square", color: STATE_GROUP_COLORS.started },
{ value: "Completed", type: "square", color: STATE_GROUP_COLORS.completed },
{ value: "Cancelled", type: "square", color: STATE_GROUP_COLORS.cancelled },
]}
/>
</PieChart>
</ResponsiveContainer>
</div>
export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => (
<div>
<h3 className="mb-2 font-semibold">Issues by States</h3>
<div className="rounded-[10px] border border-brand-base bg-brand-base p-4">
<PieGraph
data={
groupedIssues?.map((cell) => ({
id: cell.state_group,
label: cell.state_group,
value: cell.state_count,
color: STATE_GROUP_COLORS[cell.state_group.toLowerCase()],
})) ?? []
}
height="320px"
innerRadius={0.5}
arcLinkLabel={(cell) => `${capitalizeFirstLetter(cell.label.toString())} (${cell.value})`}
legends={[
{
anchor: "right",
direction: "column",
justify: false,
translateX: 0,
translateY: 56,
itemsSpacing: 10,
itemWidth: 100,
itemHeight: 18,
itemTextColor: "rgb(var(--color-text-secondary))",
itemDirection: "left-to-right",
itemOpacity: 1,
symbolSize: 12,
symbolShape: "square",
data:
groupedIssues?.map((cell) => ({
id: cell.state_group,
label: capitalizeFirstLetter(cell.state_group),
value: cell.state_count,
color: STATE_GROUP_COLORS[cell.state_group.toLowerCase()],
})) ?? [],
},
]}
activeInnerRadiusOffset={5}
colors={(datum) => datum.data.color}
theme={{
background: "rgb(var(--color-bg-base))",
}}
/>
</div>
);
};
</div>
);

View file

@ -1,8 +1,7 @@
// components
import { Loader } from "components/ui";
import { ActivityGraph } from "components/workspace";
// helpers
import { groupBy } from "helpers/array.helper";
// ui
import { Loader } from "components/ui";
// types
import { IUserWorkspaceDashboard } from "types";