forked from github/plane
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:
parent
512b8c104d
commit
37bb183bf0
@ -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>
|
||||||
|
@ -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,7 +10,18 @@ type Props = {
|
|||||||
params: IAnalyticsParams;
|
params: IAnalyticsParams;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CustomTooltip: React.FC<Props> = ({ datum, params }) => (
|
export const CustomTooltip: React.FC<Props> = ({ datum, params }) => {
|
||||||
|
let tooltipValue: string | number = "";
|
||||||
|
|
||||||
|
if (params.segment) {
|
||||||
|
if (DATE_KEYS.includes(params.segment)) tooltipValue = renderMonthAndYear(datum.id);
|
||||||
|
else tooltipValue = datum.id;
|
||||||
|
} else {
|
||||||
|
if (DATE_KEYS.includes(params.x_axis)) tooltipValue = datum.indexValue;
|
||||||
|
else tooltipValue = datum.id === "count" ? "Issue count" : "Effort";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="flex items-center gap-2 rounded-md border border-brand-base bg-brand-base p-2 text-xs">
|
<div className="flex items-center gap-2 rounded-md border border-brand-base bg-brand-base p-2 text-xs">
|
||||||
<span
|
<span
|
||||||
className="h-3 w-3 rounded"
|
className="h-3 w-3 rounded"
|
||||||
@ -27,8 +40,9 @@ export const CustomTooltip: React.FC<Props> = ({ datum, params }) => (
|
|||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{params.segment ? datum.id : datum.id === "count" ? "Issue count" : "Effort"}:
|
{tooltipValue}:
|
||||||
</span>
|
</span>
|
||||||
<span>{datum.value}</span>
|
<span>{datum.value}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
@ -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 }}
|
||||||
|
@ -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" ? (
|
||||||
|
@ -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 />
|
||||||
|
@ -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 (
|
||||||
|
@ -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";
|
||||||
|
@ -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>
|
||||||
|
);
|
@ -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,
|
project: projectId ? projectId.toString() : null,
|
||||||
cycle: cycleId ? cycleId.toString() : null,
|
cycle: cycleId ? cycleId.toString() : null,
|
||||||
module: moduleId ? moduleId.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>
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
// 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();
|
|
||||||
const startMonth = Math.floor(currentMonth / 3) * 3 + 1;
|
|
||||||
const quarterMonthsList = [startMonth, startMonth + 1, startMonth + 2];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-[10px] border border-brand-base">
|
<div className="rounded-[10px] border border-brand-base">
|
||||||
<h5 className="p-3 text-xs text-green-500">SCOPE</h5>
|
<h5 className="p-3 text-xs text-green-500">SCOPE</h5>
|
||||||
<div className="divide-y divide-brand-base">
|
<div className="divide-y divide-brand-base">
|
||||||
@ -26,6 +19,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => {
|
|||||||
keys={["count"]}
|
keys={["count"]}
|
||||||
height="250px"
|
height="250px"
|
||||||
colors={() => `#f97316`}
|
colors={() => `#f97316`}
|
||||||
|
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)}
|
||||||
tooltip={(datum) => (
|
tooltip={(datum) => (
|
||||||
<div className="rounded-md border border-brand-base bg-brand-base p-2 text-xs">
|
<div className="rounded-md border border-brand-base bg-brand-base p-2 text-xs">
|
||||||
<span className="font-medium text-brand-secondary">
|
<span className="font-medium text-brand-secondary">
|
||||||
@ -35,64 +29,37 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
axisBottom={{
|
axisBottom={{
|
||||||
tickValues: [],
|
renderTick: (datum) => {
|
||||||
|
const avatar =
|
||||||
|
defaultAnalytics.pending_issue_user[datum.tickIndex].assignees__avatar ?? "";
|
||||||
|
|
||||||
|
if (avatar && avatar !== "")
|
||||||
|
return (
|
||||||
|
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||||
|
<image
|
||||||
|
x={-8}
|
||||||
|
y={10}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
xlinkHref={avatar}
|
||||||
|
style={{ clipPath: "circle(50%)" }}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||||
|
<circle cy={18} r={8} fill="#374151" />
|
||||||
|
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
||||||
|
{(`${datum.value}` ?? "No assignee").toUpperCase().charAt(0)}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
margin={{ top: 20 }}
|
margin={{ top: 20 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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 />
|
||||||
|
@ -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,
|
||||||
|
@ -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}
|
||||||
|
@ -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))";
|
|
||||||
};
|
|
||||||
|
91
apps/app/helpers/analytics.helper.ts
Normal file
91
apps/app/helpers/analytics.helper.ts
Normal 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] ?? ""
|
||||||
|
);
|
||||||
|
};
|
24
apps/app/helpers/graph.helper.ts
Normal file
24
apps/app/helpers/graph.helper.ts
Normal 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;
|
||||||
|
};
|
20
apps/app/types/analytics.d.ts
vendored
20
apps/app/types/analytics.d.ts
vendored
@ -57,26 +57,24 @@ export interface IExportAnalyticsFormData {
|
|||||||
project?: string[];
|
project?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDefaultAnalyticsResponse {
|
export interface IDefaultAnalyticsUser {
|
||||||
issue_completed_month_wise: { month: number; count: number }[];
|
|
||||||
most_issue_closed_user: {
|
|
||||||
assignees__avatar: string | null;
|
assignees__avatar: string | null;
|
||||||
assignees__email: string;
|
assignees__email: string;
|
||||||
count: number;
|
count: number;
|
||||||
}[];
|
}
|
||||||
|
|
||||||
|
export interface IDefaultAnalyticsResponse {
|
||||||
|
issue_completed_month_wise: { month: number; count: number }[];
|
||||||
|
most_issue_closed_user: IDefaultAnalyticsUser[];
|
||||||
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 }[];
|
||||||
|
Loading…
Reference in New Issue
Block a user