chore: update analytics x-axis, tooltip and table heading values (#1040)

* chore: global bar graph component

* chore: global pie graph component

* chore: global line graph component

* chore: removed unnecessary file

* chore: refactored global chart components to accept all props

* chore: function to convert response to chart data

* chore: global calendar graph component added

* chore: global scatter plot graph component

* feat: analytics boilerplate created

* chore: null value for segment and project

* chore: clean up file

* chore: change project query param key

* chore: export, refresh buttons, analytics table

* fix: analytics fetch key error

* chore: show only integer values in the y-axis

* chore: custom x-axis tick values and bar colors

* refactor: divide analytics page into granular components

* chore: convert analytics page to modal, save analytics modal

* fix: build error

* fix: modal overflow issues, analytics loading screen

* chore: custom tooltip, refactor: graphs folder structure

* refactor: folder structure, chore: x-axis tick values for larger data

* chore: code cleanup

* chore: remove unnecessary files

* fix: refresh analytics button on error

* feat: scope and demand analytics

* refactor: scope and demand and custom analytics folder structure

* fix: dynamic import type

* chore: minor updates

* feat: project, cycle and module level analytics

* style: project analytics modal

* fix: merge conflicts

* style: new scope and demand ui, user avatars in bar graph

* fix: default analytics types

* chore: new values when dimensioned by date

* chore: update table and tooltip content when dimensioned or segmented by dates
This commit is contained in:
Aaryan Khandelwal 2023-05-15 11:22:06 +05:30 committed by GitHub
parent 512b8c104d
commit 37bb183bf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 401 additions and 271 deletions

View File

@ -17,8 +17,8 @@ import {
} from "components/analytics"; } from "components/analytics";
// ui // ui
import { Loader, PrimaryButton } from "components/ui"; import { Loader, PrimaryButton } from "components/ui";
// types // helpers
import { convertResponseToBarGraphData } from "constants/analytics"; import { convertResponseToBarGraphData } from "helpers/analytics.helper";
// types // types
import { IAnalyticsParams } from "types"; import { IAnalyticsParams } from "types";
// fetch-keys // fetch-keys
@ -63,11 +63,7 @@ export const CustomAnalytics: React.FC<Props> = ({ isProjectLevel = false, fullS
); );
const yAxisKey = params.y_axis === "issue_count" ? "count" : "effort"; const yAxisKey = params.y_axis === "issue_count" ? "count" : "effort";
const barGraphData = convertResponseToBarGraphData( const barGraphData = convertResponseToBarGraphData(analytics?.distribution, params);
analytics?.distribution,
watch("segment") ? true : false,
watch("y_axis")
);
return ( return (
<> <>
@ -125,7 +121,7 @@ export const CustomAnalytics: React.FC<Props> = ({ isProjectLevel = false, fullS
<div className="space-y-4 text-brand-secondary"> <div className="space-y-4 text-brand-secondary">
<p className="text-sm">There was some error in fetching the data.</p> <p className="text-sm">There was some error in fetching the data.</p>
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<PrimaryButton onClick={mutateAnalytics}>Refresh</PrimaryButton> <PrimaryButton onClick={() => mutateAnalytics()}>Refresh</PrimaryButton>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,7 @@
// nivo // nivo
import { BarTooltipProps } from "@nivo/bar"; import { BarTooltipProps } from "@nivo/bar";
import { DATE_KEYS } from "constants/analytics";
import { renderMonthAndYear } from "helpers/analytics.helper";
// types // types
import { IAnalyticsParams } from "types"; import { IAnalyticsParams } from "types";
@ -8,27 +10,39 @@ type Props = {
params: IAnalyticsParams; params: IAnalyticsParams;
}; };
export const CustomTooltip: React.FC<Props> = ({ datum, params }) => ( export const CustomTooltip: React.FC<Props> = ({ datum, params }) => {
<div className="flex items-center gap-2 rounded-md border border-brand-base bg-brand-base p-2 text-xs"> let tooltipValue: string | number = "";
<span
className="h-3 w-3 rounded" if (params.segment) {
style={{ if (DATE_KEYS.includes(params.segment)) tooltipValue = renderMonthAndYear(datum.id);
backgroundColor: datum.color, else tooltipValue = datum.id;
}} } else {
/> if (DATE_KEYS.includes(params.x_axis)) tooltipValue = datum.indexValue;
<span else tooltipValue = datum.id === "count" ? "Issue count" : "Effort";
className={`font-medium text-brand-secondary ${ }
params.segment
? params.segment === "priority" || params.segment === "state__group" return (
<div className="flex items-center gap-2 rounded-md border border-brand-base bg-brand-base p-2 text-xs">
<span
className="h-3 w-3 rounded"
style={{
backgroundColor: datum.color,
}}
/>
<span
className={`font-medium text-brand-secondary ${
params.segment
? params.segment === "priority" || params.segment === "state__group"
? "capitalize"
: ""
: params.x_axis === "priority" || params.x_axis === "state__group"
? "capitalize" ? "capitalize"
: "" : ""
: params.x_axis === "priority" || params.x_axis === "state__group" }`}
? "capitalize" >
: "" {tooltipValue}:
}`} </span>
> <span>{datum.value}</span>
{params.segment ? datum.id : datum.id === "count" ? "Issue count" : "Effort"}: </div>
</span> );
<span>{datum.value}</span> };
</div>
);

View File

@ -6,10 +6,10 @@ import { CustomTooltip } from "./custom-tooltip";
import { BarGraph } from "components/ui"; import { BarGraph } from "components/ui";
// helpers // helpers
import { findStringWithMostCharacters } from "helpers/array.helper"; import { findStringWithMostCharacters } from "helpers/array.helper";
import { generateBarColor } from "helpers/analytics.helper";
// types // types
import { IAnalyticsParams, IAnalyticsResponse } from "types"; import { IAnalyticsParams, IAnalyticsResponse } from "types";
// constants // constants
import { generateBarColor } from "constants/analytics";
type Props = { type Props = {
analytics: IAnalyticsResponse; analytics: IAnalyticsResponse;
@ -47,28 +47,7 @@ export const AnalyticsGraph: React.FC<Props> = ({
}); });
else data = barGraphData.data.map((d) => d[yAxisKey] as number); else data = barGraphData.data.map((d) => d[yAxisKey] as number);
const minValue = 0; return data;
const maxValue = Math.max(...data);
const valueRange = maxValue - minValue;
let tickInterval = 1;
if (valueRange > 10) tickInterval = 2;
if (valueRange > 50) tickInterval = 5;
if (valueRange > 100) tickInterval = 10;
if (valueRange > 200) tickInterval = 50;
if (valueRange > 300) tickInterval = (Math.ceil(valueRange / 100) * 100) / 10;
const tickValues = [];
let tickValue = minValue;
while (tickValue <= maxValue) {
tickValues.push(tickValue);
tickValue += tickInterval;
}
if (!tickValues.includes(maxValue)) tickValues.push(maxValue);
return tickValues;
}; };
const longestXAxisLabel = findStringWithMostCharacters(barGraphData.data.map((d) => `${d.name}`)); const longestXAxisLabel = findStringWithMostCharacters(barGraphData.data.map((d) => `${d.name}`));
@ -78,11 +57,6 @@ export const AnalyticsGraph: React.FC<Props> = ({
data={barGraphData.data} data={barGraphData.data}
indexBy="name" indexBy="name"
keys={barGraphData.xAxisKeys} keys={barGraphData.xAxisKeys}
axisLeft={{
tickSize: 0,
tickPadding: 10,
tickValues: generateYAxisTickValues(),
}}
colors={(datum) => colors={(datum) =>
generateBarColor( generateBarColor(
params.segment ? `${datum.id}` : `${datum.indexValue}`, params.segment ? `${datum.id}` : `${datum.indexValue}`,
@ -91,6 +65,7 @@ export const AnalyticsGraph: React.FC<Props> = ({
params.segment ? "segment" : "x_axis" params.segment ? "segment" : "x_axis"
) )
} }
customYAxisTickValues={generateYAxisTickValues()}
tooltip={(datum) => <CustomTooltip datum={datum} params={params} />} tooltip={(datum) => <CustomTooltip datum={datum} params={params} />}
height={fullScreen ? "400px" : "300px"} height={fullScreen ? "400px" : "300px"}
margin={{ right: 20, bottom: longestXAxisLabel.length * 5 + 20 }} margin={{ right: 20, bottom: longestXAxisLabel.length * 5 + 20 }}

View File

@ -4,14 +4,13 @@ import { BarDatum } from "@nivo/bar";
import { getPriorityIcon } from "components/icons"; import { getPriorityIcon } from "components/icons";
// helpers // helpers
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
// helpers
import { generateBarColor, renderMonthAndYear } from "helpers/analytics.helper";
// types // types
import { IAnalyticsParams, IAnalyticsResponse } from "types"; import { IAnalyticsParams, IAnalyticsResponse } from "types";
// constants // constants
import { import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, DATE_KEYS } from "constants/analytics";
ANALYTICS_X_AXIS_VALUES, import { MONTHS_LIST } from "constants/calendar";
ANALYTICS_Y_AXIS_VALUES,
generateBarColor,
} from "constants/analytics";
type Props = { type Props = {
analytics: IAnalyticsResponse; analytics: IAnalyticsResponse;
@ -55,7 +54,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
}} }}
/> />
)} )}
{key} {DATE_KEYS.includes(params.segment ?? "") ? renderMonthAndYear(key) : key}
</div> </div>
</th> </th>
)) ))
@ -74,7 +73,9 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
> >
<td <td
className={`flex items-center gap-2 whitespace-nowrap py-2 px-2.5 font-medium ${ className={`flex items-center gap-2 whitespace-nowrap py-2 px-2.5 font-medium ${
params.x_axis === "priority" ? "capitalize" : "" params.x_axis === "priority" || params.x_axis === "state__group"
? "capitalize"
: ""
}`} }`}
> >
{params.x_axis === "priority" ? ( {params.x_axis === "priority" ? (

View File

@ -38,7 +38,7 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
> >
<div <div
className={`flex items-center justify-between gap-2 border-b border-b-brand-base bg-brand-sidebar p-3 text-sm ${ className={`flex items-center justify-between gap-2 border-b border-b-brand-base bg-brand-sidebar p-3 text-sm ${
fullScreen ? "" : "py-[1.3rem]" fullScreen ? "" : "py-[1.275rem]"
}`} }`}
> >
<h3>Project Analytics</h3> <h3>Project Analytics</h3>
@ -80,7 +80,7 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
</Tab.List> </Tab.List>
<Tab.Panels as={Fragment}> <Tab.Panels as={Fragment}>
<Tab.Panel as={Fragment}> <Tab.Panel as={Fragment}>
<ScopeAndDemand fullScreen={fullScreen} /> <ScopeAndDemand fullScreen={fullScreen} isProjectLevel />
</Tab.Panel> </Tab.Panel>
<Tab.Panel as={Fragment}> <Tab.Panel as={Fragment}>
<CustomAnalytics fullScreen={fullScreen} isProjectLevel /> <CustomAnalytics fullScreen={fullScreen} isProjectLevel />

View File

@ -10,14 +10,14 @@ type Props = {
}; };
export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => ( export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
<div className="space-y-3 self-start rounded-[10px] border border-brand-base p-3"> <div className="space-y-3 rounded-[10px] border border-brand-base p-3">
<h5 className="text-xs text-red-500">DEMAND</h5> <h5 className="text-xs text-red-500">DEMAND</h5>
<div> <div>
<h4 className="text-brand-bas text-base font-medium">Total open tasks</h4> <h4 className="text-brand-bas text-base font-medium">Total open tasks</h4>
<h3 className="mt-1 text-xl font-semibold">{defaultAnalytics.open_issues}</h3> <h3 className="mt-1 text-xl font-semibold">{defaultAnalytics.open_issues}</h3>
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
{defaultAnalytics.open_issues_classified.map((group) => { {defaultAnalytics?.open_issues_classified.map((group) => {
const percentage = ((group.state_count / defaultAnalytics.total_issues) * 100).toFixed(0); const percentage = ((group.state_count / defaultAnalytics.total_issues) * 100).toFixed(0);
return ( return (

View File

@ -1,3 +1,5 @@
export * from "./demand"; export * from "./demand";
export * from "./leaderboard";
export * from "./scope-and-demand"; export * from "./scope-and-demand";
export * from "./scope"; export * from "./scope";
export * from "./year-wise-issues";

View File

@ -0,0 +1,41 @@
import Image from "next/image";
type Props = {
users: {
avatar: string | null;
email: string | null;
count: number;
}[];
title: string;
};
export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
<div className="p-3 border border-brand-base rounded-[10px]">
<h6 className="text-base font-medium">{title}</h6>
<div className="mt-3 space-y-3">
{users.map((user) => (
<div key={user.email} className="flex items-start justify-between gap-4 text-xs">
<div className="flex items-center gap-2">
{user && user.avatar && user.avatar !== "" ? (
<div className="rounded-full h-4 w-4 flex-shrink-0">
<Image
src={user.avatar}
height="100%"
width="100%"
className="rounded-full"
alt={user.email ?? "No assignee"}
/>
</div>
) : (
<div className="grid place-items-center flex-shrink-0 rounded-full bg-gray-700 text-[11px] capitalize text-white h-4 w-4">
{(user.email ?? "No assignee").charAt(0)}
</div>
)}
<span className="break-all text-brand-secondary">{user.email ?? "No assignee"}</span>
</div>
<span className="flex-shrink-0">{user.count}</span>
</div>
))}
</div>
</div>
);

View File

@ -5,7 +5,12 @@ import useSWR from "swr";
// services // services
import analyticsService from "services/analytics.service"; import analyticsService from "services/analytics.service";
// components // components
import { AnalyticsDemand, AnalyticsScope } from "components/analytics"; import {
AnalyticsDemand,
AnalyticsLeaderboard,
AnalyticsScope,
AnalyticsYearWiseIssues,
} from "components/analytics";
// ui // ui
import { Loader, PrimaryButton } from "components/ui"; import { Loader, PrimaryButton } from "components/ui";
// fetch-keys // fetch-keys
@ -13,17 +18,20 @@ import { DEFAULT_ANALYTICS } from "constants/fetch-keys";
type Props = { type Props = {
fullScreen?: boolean; fullScreen?: boolean;
isProjectLevel?: boolean;
}; };
export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => { export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true, isProjectLevel = true }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const params = { const params = isProjectLevel
project: projectId ? projectId.toString() : null, ? {
cycle: cycleId ? cycleId.toString() : null, project: projectId ? projectId.toString() : null,
module: moduleId ? moduleId.toString() : null, cycle: cycleId ? cycleId.toString() : null,
}; module: moduleId ? moduleId.toString() : null,
}
: undefined;
const { const {
data: defaultAnalytics, data: defaultAnalytics,
@ -41,15 +49,36 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
{!defaultAnalyticsError ? ( {!defaultAnalyticsError ? (
defaultAnalytics ? ( defaultAnalytics ? (
<div className="h-full overflow-y-auto p-5 text-sm"> <div className="h-full overflow-y-auto p-5 text-sm">
<div className={`grid grid-cols-1 gap-5 ${fullScreen ? "lg:grid-cols-2" : ""}`}> <div className={`grid grid-cols-1 gap-5 ${fullScreen ? "md:grid-cols-2" : ""}`}>
<AnalyticsDemand defaultAnalytics={defaultAnalytics} /> <AnalyticsDemand defaultAnalytics={defaultAnalytics} />
<AnalyticsScope defaultAnalytics={defaultAnalytics} /> <AnalyticsScope defaultAnalytics={defaultAnalytics} />
<AnalyticsLeaderboard
users={defaultAnalytics.most_issue_created_user.map((user) => ({
avatar: user.created_by__avatar,
email: user.created_by__email,
count: user.count,
}))}
title="Most issues created"
/>
<AnalyticsLeaderboard
users={defaultAnalytics.most_issue_closed_user.map((user) => ({
avatar: user.assignees__avatar,
email: user.assignees__email,
count: user.count,
}))}
title="Most issues closed"
/>
<div className={fullScreen ? "md:col-span-2" : ""}>
<AnalyticsYearWiseIssues defaultAnalytics={defaultAnalytics} />
</div>
</div> </div>
</div> </div>
) : ( ) : (
<Loader className="grid grid-cols-1 gap-5 p-5 lg:grid-cols-2"> <Loader className="grid grid-cols-1 gap-5 p-5 lg:grid-cols-2">
<Loader.Item height="300px" /> <Loader.Item height="250px" />
<Loader.Item height="300px" /> <Loader.Item height="250px" />
<Loader.Item height="250px" />
<Loader.Item height="250px" />
</Loader> </Loader>
) )
) : ( ) : (
@ -57,7 +86,7 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
<div className="space-y-4 text-brand-secondary"> <div className="space-y-4 text-brand-secondary">
<p className="text-sm">There was some error in fetching the data.</p> <p className="text-sm">There was some error in fetching the data.</p>
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<PrimaryButton onClick={mutateDefaultAnalytics}>Refresh</PrimaryButton> <PrimaryButton onClick={() => mutateDefaultAnalytics()}>Refresh</PrimaryButton>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,98 +1,65 @@
// ui // ui
import { BarGraph, LineGraph } from "components/ui"; import { BarGraph } from "components/ui";
// types // types
import { IDefaultAnalyticsResponse } from "types"; import { IDefaultAnalyticsResponse } from "types";
// constants
import { MONTHS_LIST } from "constants/calendar";
type Props = { type Props = {
defaultAnalytics: IDefaultAnalyticsResponse; defaultAnalytics: IDefaultAnalyticsResponse;
}; };
export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => { export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
const currentMonth = new Date().getMonth(); <div className="rounded-[10px] border border-brand-base">
const startMonth = Math.floor(currentMonth / 3) * 3 + 1; <h5 className="p-3 text-xs text-green-500">SCOPE</h5>
const quarterMonthsList = [startMonth, startMonth + 1, startMonth + 2]; <div className="divide-y divide-brand-base">
<div>
<h6 className="px-3 text-base font-medium">Pending issues</h6>
<BarGraph
data={defaultAnalytics.pending_issue_user}
indexBy="assignees__email"
keys={["count"]}
height="250px"
colors={() => `#f97316`}
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)}
tooltip={(datum) => (
<div className="rounded-md border border-brand-base bg-brand-base p-2 text-xs">
<span className="font-medium text-brand-secondary">
Issue count- {datum.indexValue ?? "No assignee"}:{" "}
</span>
{datum.value}
</div>
)}
axisBottom={{
renderTick: (datum) => {
const avatar =
defaultAnalytics.pending_issue_user[datum.tickIndex].assignees__avatar ?? "";
return ( if (avatar && avatar !== "")
<div className="rounded-[10px] border border-brand-base"> return (
<h5 className="p-3 text-xs text-green-500">SCOPE</h5> <g transform={`translate(${datum.x},${datum.y})`}>
<div className="divide-y divide-brand-base"> <image
<div> x={-8}
<h6 className="px-3 text-base font-medium">Pending issues</h6> y={10}
<BarGraph width={16}
data={defaultAnalytics.pending_issue_user} height={16}
indexBy="assignees__email" xlinkHref={avatar}
keys={["count"]} style={{ clipPath: "circle(50%)" }}
height="250px" />
colors={() => `#f97316`} </g>
tooltip={(datum) => ( );
<div className="rounded-md border border-brand-base bg-brand-base p-2 text-xs"> else
<span className="font-medium text-brand-secondary"> return (
Issue count- {datum.indexValue ?? "No assignee"}:{" "} <g transform={`translate(${datum.x},${datum.y})`}>
</span> <circle cy={18} r={8} fill="#374151" />
{datum.value} <text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
</div> {(`${datum.value}` ?? "No assignee").toUpperCase().charAt(0)}
)} </text>
axisBottom={{ </g>
tickValues: [], );
}} },
margin={{ top: 20 }} }}
/> margin={{ top: 20 }}
</div> />
<div className="grid grid-cols-1 divide-y divide-brand-base sm:grid-cols-2 sm:divide-x">
<div className="p-3">
<h6 className="text-base font-medium">Most issues created</h6>
<div className="mt-3 space-y-3">
{defaultAnalytics.most_issue_created_user.map((user) => (
<div
key={user.assignees__email}
className="flex items-start justify-between gap-4 text-xs"
>
<span className="break-all text-brand-secondary">{user.assignees__email}</span>
<span className="flex-shrink-0">{user.count}</span>
</div>
))}
</div>
</div>
<div className="p-3">
<h6 className="text-base font-medium">Most issues closed</h6>
<div className="mt-3 space-y-3">
{defaultAnalytics.most_issue_closed_user.map((user) => (
<div
key={user.assignees__email}
className="flex items-start justify-between gap-4 text-xs"
>
<span className="break-all text-brand-secondary">{user.assignees__email}</span>
<span className="flex-shrink-0">{user.count}</span>
</div>
))}
</div>
</div>
</div>
<div className="py-3">
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
<LineGraph
data={[
{
id: "issues_closed",
color: "rgb(var(--color-accent))",
data: quarterMonthsList.map((month) => ({
x: MONTHS_LIST.find((m) => m.value === month)?.label.substring(0, 3),
y:
defaultAnalytics.issue_completed_month_wise.find((data) => data.month === month)
?.count || 0,
})),
},
]}
height="300px"
colors={(datum) => datum.color}
curve="monotoneX"
margin={{ top: 20 }}
enableArea
/>
</div>
</div> </div>
</div> </div>
); </div>
}; );

View File

@ -0,0 +1,47 @@
// ui
import { LineGraph } from "components/ui";
// types
import { IDefaultAnalyticsResponse } from "types";
// constants
import { MONTHS_LIST } from "constants/calendar";
type Props = {
defaultAnalytics: IDefaultAnalyticsResponse;
};
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => {
const currentMonth = new Date().getMonth();
const startMonth = Math.floor(currentMonth / 3) * 3 + 1;
const quarterMonthsList = [startMonth, startMonth + 1, startMonth + 2];
return (
<div className="py-3 border border-brand-base rounded-[10px]">
<h1 className="px-3 text-base font-medium">Issues closed in a year</h1>
<LineGraph
data={[
{
id: "issues_closed",
color: "rgb(var(--color-accent))",
data: MONTHS_LIST.map((month) => ({
x: month.label.substring(0, 3),
y:
defaultAnalytics.issue_completed_month_wise.find(
(data) => data.month === month.value
)?.count || 0,
})),
},
]}
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => {
if (quarterMonthsList.includes(data.month)) return data.count;
return 0;
})}
height="300px"
colors={(datum) => datum.color}
curve="monotoneX"
margin={{ top: 20 }}
enableArea
/>
</div>
);
};

View File

@ -22,7 +22,7 @@ export const AnalyticsWorkspaceModal: React.FC<Props> = ({ isOpen, onClose }) =>
return ( return (
<> <>
<div <div
className={`absolute z-20 h-full w-full bg-brand-surface-1 p-2 ${ className={`absolute z-40 h-full w-full bg-brand-surface-1 p-2 ${
isOpen ? "block" : "hidden" isOpen ? "block" : "hidden"
}`} }`}
> >
@ -56,7 +56,7 @@ export const AnalyticsWorkspaceModal: React.FC<Props> = ({ isOpen, onClose }) =>
</Tab.List> </Tab.List>
<Tab.Panels as={Fragment}> <Tab.Panels as={Fragment}>
<Tab.Panel as={Fragment}> <Tab.Panel as={Fragment}>
<ScopeAndDemand /> <ScopeAndDemand isProjectLevel={false} />
</Tab.Panel> </Tab.Panel>
<Tab.Panel as={Fragment}> <Tab.Panel as={Fragment}>
<CustomAnalytics /> <CustomAnalytics />

View File

@ -1,5 +1,7 @@
// nivo // nivo
import { ResponsiveBar, BarSvgProps } from "@nivo/bar"; import { ResponsiveBar, BarSvgProps } from "@nivo/bar";
// helpers
import { generateYAxisTickValues } from "helpers/graph.helper";
// types // types
import { TGraph } from "./types"; import { TGraph } from "./types";
// constants // constants
@ -8,11 +10,13 @@ import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph";
type Props = { type Props = {
indexBy: string; indexBy: string;
keys: string[]; keys: string[];
customYAxisTickValues?: number[];
}; };
export const BarGraph: React.FC<Props & TGraph & Omit<BarSvgProps<any>, "height" | "width">> = ({ export const BarGraph: React.FC<Props & TGraph & Omit<BarSvgProps<any>, "height" | "width">> = ({
indexBy, indexBy,
keys, keys,
customYAxisTickValues,
height = "400px", height = "400px",
width = "100%", width = "100%",
margin, margin,
@ -25,6 +29,13 @@ export const BarGraph: React.FC<Props & TGraph & Omit<BarSvgProps<any>, "height"
keys={keys} keys={keys}
margin={{ ...DEFAULT_MARGIN, ...(margin ?? {}) }} margin={{ ...DEFAULT_MARGIN, ...(margin ?? {}) }}
padding={rest.padding ?? rest.data.length > 7 ? 0.8 : 0.9} padding={rest.padding ?? rest.data.length > 7 ? 0.8 : 0.9}
axisLeft={{
tickSize: 0,
tickPadding: 10,
tickValues: customYAxisTickValues
? generateYAxisTickValues(customYAxisTickValues)
: undefined,
}}
axisBottom={{ axisBottom={{
tickSize: 0, tickSize: 0,
tickPadding: 10, tickPadding: 10,

View File

@ -1,11 +1,18 @@
// nivo // nivo
import { ResponsiveLine, LineSvgProps } from "@nivo/line"; import { ResponsiveLine, LineSvgProps } from "@nivo/line";
// helpers
import { generateYAxisTickValues } from "helpers/graph.helper";
// types // types
import { TGraph } from "./types"; import { TGraph } from "./types";
// constants // constants
import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph"; import { CHARTS_THEME, DEFAULT_MARGIN } from "constants/graph";
export const LineGraph: React.FC<TGraph & LineSvgProps> = ({ type Props = {
customYAxisTickValues?: number[];
};
export const LineGraph: React.FC<Props & TGraph & LineSvgProps> = ({
customYAxisTickValues,
height = "400px", height = "400px",
width = "100%", width = "100%",
margin, margin,
@ -15,6 +22,13 @@ export const LineGraph: React.FC<TGraph & LineSvgProps> = ({
<div style={{ height, width }}> <div style={{ height, width }}>
<ResponsiveLine <ResponsiveLine
margin={{ ...DEFAULT_MARGIN, ...(margin ?? {}) }} margin={{ ...DEFAULT_MARGIN, ...(margin ?? {}) }}
axisLeft={{
tickSize: 0,
tickPadding: 10,
tickValues: customYAxisTickValues
? generateYAxisTickValues(customYAxisTickValues)
: undefined,
}}
theme={{ ...CHARTS_THEME, ...(theme ?? {}) }} theme={{ ...CHARTS_THEME, ...(theme ?? {}) }}
animate={true} animate={true}
{...rest} {...rest}

View File

@ -1,23 +1,14 @@
// nivo
import { BarDatum, ComputedDatum } from "@nivo/bar";
// types // types
import { import { TXAxisValues, TYAxisValues } from "types";
IAnalyticsData,
IAnalyticsParams,
IAnalyticsResponse,
TXAxisValues,
TYAxisValues,
} from "types";
import { STATE_GROUP_COLORS } from "./state";
export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] = [ export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] = [
{ {
value: "state__name", value: "state__name",
label: "State Name", label: "State name",
}, },
{ {
value: "state__group", value: "state__group",
label: "State Group", label: "State group",
}, },
{ {
value: "priority", value: "priority",
@ -33,7 +24,7 @@ export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
}, },
{ {
value: "estimate_point", value: "estimate_point",
label: "Estimate", label: "Estimate point",
}, },
{ {
value: "issue_cycle__cycle__name", value: "issue_cycle__cycle__name",
@ -51,10 +42,10 @@ export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
value: "target_date", value: "target_date",
label: "Due date", label: "Due date",
}, },
{ // {
value: "start_date", // value: "start_date",
label: "Start Date", // label: "Start date",
}, // },
{ {
value: "created_at", value: "created_at",
label: "Created date", label: "Created date",
@ -72,75 +63,4 @@ export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] =
}, },
]; ];
export const convertResponseToBarGraphData = ( export const DATE_KEYS = ["completed_at", "target_date", "start_date", "created_at"];
response: IAnalyticsData | undefined,
segmented: boolean,
yAxis: TYAxisValues
): { data: BarDatum[]; xAxisKeys: string[] } => {
if (!response || !(typeof response === "object") || Object.keys(response).length === 0)
return { data: [], xAxisKeys: [] };
const data: BarDatum[] = [];
let xAxisKeys: string[] = [];
const yAxisKey = yAxis === "issue_count" ? "count" : "effort";
Object.keys(response).forEach((key) => {
const segments: { [key: string]: number } = {};
if (segmented) {
response[key].map((item: any) => {
segments[item.segment ?? "None"] = item[yAxisKey] ?? 0;
// store the segment in the xAxisKeys array
if (!xAxisKeys.includes(item.segment ?? "None")) xAxisKeys.push(item.segment ?? "None");
});
data.push({
name: key,
...segments,
});
} else {
xAxisKeys = [yAxisKey];
const item = response[key][0];
data.push({
name: item.dimension ?? "None",
[yAxisKey]: item[yAxisKey] ?? 0,
});
}
});
return { data, xAxisKeys };
};
export const generateBarColor = (
value: string,
analytics: IAnalyticsResponse,
params: IAnalyticsParams,
type: "x_axis" | "segment"
): string => {
let color: string | undefined = "rgb(var(--color-accent))";
if (!analytics) return color;
if (params[type] === "state__name" || params[type] === "labels__name")
color = analytics?.extras?.colors.find((c) => c.name === value)?.color;
if (params[type] === "state__group") color = STATE_GROUP_COLORS[value];
if (params[type] === "priority")
color =
value === "urgent"
? "#ef4444"
: value === "high"
? "#f97316"
: value === "medium"
? "#eab308"
: value === "low"
? "#22c55e"
: "#ced4da";
return color ?? "rgb(var(--color-accent))";
};

View File

@ -0,0 +1,91 @@
// nivo
import { BarDatum } from "@nivo/bar";
// types
import { IAnalyticsData, IAnalyticsParams, IAnalyticsResponse } from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
import { MONTHS_LIST } from "constants/calendar";
import { DATE_KEYS } from "constants/analytics";
export const convertResponseToBarGraphData = (
response: IAnalyticsData | undefined,
params: IAnalyticsParams
): { data: BarDatum[]; xAxisKeys: string[] } => {
if (!response || !(typeof response === "object") || Object.keys(response).length === 0)
return { data: [], xAxisKeys: [] };
const data: BarDatum[] = [];
let xAxisKeys: string[] = [];
const yAxisKey = params.y_axis === "issue_count" ? "count" : "effort";
Object.keys(response).forEach((key) => {
const segments: { [key: string]: number } = {};
if (params.segment) {
response[key].map((item: any) => {
segments[item.segment ?? "None"] = item[yAxisKey] ?? 0;
// store the segment in the xAxisKeys array
if (!xAxisKeys.includes(item.segment ?? "None")) xAxisKeys.push(item.segment ?? "None");
});
data.push({
name: DATE_KEYS.includes(params.x_axis) ? renderMonthAndYear(key) : key,
...segments,
});
} else {
xAxisKeys = [yAxisKey];
const item = response[key][0];
data.push({
name: DATE_KEYS.includes(params.x_axis)
? renderMonthAndYear(item.dimension)
: item.dimension ?? "None",
[yAxisKey]: item[yAxisKey] ?? 0,
});
}
});
return { data, xAxisKeys };
};
export const generateBarColor = (
value: string,
analytics: IAnalyticsResponse,
params: IAnalyticsParams,
type: "x_axis" | "segment"
): string => {
let color: string | undefined = "rgb(var(--color-accent))";
if (!analytics) return color;
if (params[type] === "state__name" || params[type] === "labels__name")
color = analytics?.extras?.colors.find((c) => c.name === value)?.color;
if (params[type] === "state__group") color = STATE_GROUP_COLORS[value];
if (params[type] === "priority")
color =
value === "urgent"
? "#ef4444"
: value === "high"
? "#f97316"
: value === "medium"
? "#eab308"
: value === "low"
? "#22c55e"
: "#ced4da";
return color ?? "rgb(var(--color-accent))";
};
export const renderMonthAndYear = (date: string | number | null): string => {
if (!date || date === "") return "";
return (
(MONTHS_LIST.find((m) => `${m.value}` === `${date}`.split("-")[1])?.label.substring(0, 3) ??
"None") + ` ${date}`.split("-")[0] ?? ""
);
};

View File

@ -0,0 +1,24 @@
export const generateYAxisTickValues = (data: number[]) => {
if (!data || !Array.isArray(data) || data.length === 0) return [];
const minValue = 0;
const maxValue = Math.max(...data);
const valueRange = maxValue - minValue;
let tickInterval = 1;
if (valueRange < 10) tickInterval = 1;
else if (valueRange < 20) tickInterval = 2;
else if (valueRange < 50) tickInterval = 5;
else tickInterval = (Math.ceil(valueRange / 100) * 100) / 10;
const tickValues: number[] = [];
let tickValue = minValue;
while (tickValue <= maxValue) {
tickValues.push(tickValue);
tickValue += tickInterval;
}
return tickValues;
};

View File

@ -57,26 +57,24 @@ export interface IExportAnalyticsFormData {
project?: string[]; project?: string[];
} }
export interface IDefaultAnalyticsUser {
assignees__avatar: string | null;
assignees__email: string;
count: number;
}
export interface IDefaultAnalyticsResponse { export interface IDefaultAnalyticsResponse {
issue_completed_month_wise: { month: number; count: number }[]; issue_completed_month_wise: { month: number; count: number }[];
most_issue_closed_user: { most_issue_closed_user: IDefaultAnalyticsUser[];
assignees__avatar: string | null;
assignees__email: string;
count: number;
}[];
most_issue_created_user: { most_issue_created_user: {
assignees__avatar: string | null; created_by__avatar: string | null;
assignees__email: string; created_by__email: string;
count: number; count: number;
}[]; }[];
open_estimate_sum: number; open_estimate_sum: number;
open_issues: number; open_issues: number;
open_issues_classified: { state_group: string; state_count: number }[]; open_issues_classified: { state_group: string; state_count: number }[];
pending_issue_user: { pending_issue_user: IDefaultAnalyticsUser[];
assignees__avatar: string | null;
assignees__email: string;
count: number;
}[];
total_estimate_sum: number; total_estimate_sum: number;
total_issues: number; total_issues: number;
total_issues_classified: { state_group: string; state_count: number }[]; total_issues_classified: { state_group: string; state_count: number }[];