mirror of
https://github.com/ershisan99/vacancies-trends-front.git
synced 2026-01-31 12:35:43 +00:00
add grouping and totals
This commit is contained in:
148
app/components/ui/chart-tooltip.tsx
Normal file
148
app/components/ui/chart-tooltip.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React from 'react'
|
||||
import { Color, colorPalette, getColorClassNames, tremorTwMerge } from '~/lib/tremor'
|
||||
import { CustomTooltipProps } from '@tremor/react'
|
||||
import { GroupByPeriod } from '~/services/vacancies/vacancies.types'
|
||||
import { endOfMonth, endOfWeek, endOfYear, isSameDay, setDefaultOptions } from 'date-fns'
|
||||
setDefaultOptions({ weekStartsOn: 1 })
|
||||
|
||||
export const ChartTooltipFrame = ({ children }: { children: React.ReactNode }) => (
|
||||
<div
|
||||
className={tremorTwMerge(
|
||||
// common
|
||||
'rounded-tremor-default text-tremor-default border',
|
||||
// light
|
||||
'bg-tremor-background shadow-tremor-dropdown border-tremor-border',
|
||||
// dark
|
||||
'dark:bg-dark-tremor-background dark:shadow-dark-tremor-dropdown dark:border-dark-tremor-border'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export interface ChartTooltipRowProps {
|
||||
value: string
|
||||
name: string
|
||||
color: Color | string
|
||||
}
|
||||
|
||||
export const ChartTooltipRow = ({ value, name, color }: ChartTooltipRowProps) => (
|
||||
<div className="flex items-center justify-between space-x-8">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span
|
||||
className={tremorTwMerge(
|
||||
// common
|
||||
'shrink-0 rounded-tremor-full border-2 h-3 w-3',
|
||||
// light
|
||||
'border-tremor-background shadow-tremor-card',
|
||||
// dark
|
||||
'dark:border-dark-tremor-background dark:shadow-dark-tremor-card',
|
||||
getColorClassNames(color, colorPalette.background).bgColor
|
||||
)}
|
||||
/>
|
||||
<p
|
||||
className={tremorTwMerge(
|
||||
// commmon
|
||||
'text-right whitespace-nowrap',
|
||||
// light
|
||||
'text-tremor-content',
|
||||
// dark
|
||||
'dark:text-dark-tremor-content'
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
className={tremorTwMerge(
|
||||
// common
|
||||
'font-medium tabular-nums text-right whitespace-nowrap',
|
||||
// light
|
||||
'text-tremor-content-emphasis',
|
||||
// dark
|
||||
'dark:text-dark-tremor-content-emphasis'
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
const ChartTooltip = ({
|
||||
active,
|
||||
payload,
|
||||
label,
|
||||
groupBy,
|
||||
}: CustomTooltipProps & {
|
||||
rawDate: string
|
||||
groupBy: GroupByPeriod
|
||||
}) => {
|
||||
if (active && payload) {
|
||||
const rawDate = payload[0]?.payload?.rawDate
|
||||
console.log(payload, rawDate)
|
||||
const filteredPayload = payload.filter((item: any) => item.type !== 'none')
|
||||
|
||||
return (
|
||||
<ChartTooltipFrame>
|
||||
<div
|
||||
className={tremorTwMerge(
|
||||
// light
|
||||
'border-tremor-border border-b px-4 py-2',
|
||||
// dark
|
||||
'dark:border-dark-tremor-border'
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={tremorTwMerge(
|
||||
// common
|
||||
'font-medium',
|
||||
// light
|
||||
'text-tremor-content-emphasis',
|
||||
// dark
|
||||
'dark:text-dark-tremor-content-emphasis'
|
||||
)}
|
||||
>
|
||||
{formatDate(rawDate ?? '', groupBy)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={tremorTwMerge('px-4 py-2 space-y-1')}>
|
||||
{filteredPayload.map(({ value, name, color }, idx: number) => (
|
||||
<ChartTooltipRow
|
||||
key={`id-${idx}`}
|
||||
value={value?.toString() ?? ''}
|
||||
name={name?.toString() ?? ''}
|
||||
color={color ?? ''}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ChartTooltipFrame>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export { ChartTooltip }
|
||||
function formatDate(date: string | number, groupBy: GroupByPeriod): string {
|
||||
let endDate: Date
|
||||
const startDate = new Date(date)
|
||||
switch (groupBy) {
|
||||
case 'day':
|
||||
endDate = new Date(startDate)
|
||||
break
|
||||
case 'week':
|
||||
endDate = endOfWeek(startDate)
|
||||
break
|
||||
case 'month':
|
||||
endDate = endOfMonth(startDate)
|
||||
break
|
||||
case 'year':
|
||||
endDate = new Date(endOfYear(startDate))
|
||||
break
|
||||
default:
|
||||
endDate = new Date(startDate)
|
||||
}
|
||||
return isSameDay(startDate, endDate)
|
||||
? startDate.toLocaleDateString('ru')
|
||||
: `${startDate.toLocaleDateString('ru')} - ${endDate.toLocaleDateString('ru')}`
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { KeywordsResponse, VacancyData } from '~/services/vacancies/vacancies.types'
|
||||
import { AreaChart, MultiSelect, MultiSelectItem, Select, SelectItem } from '@tremor/react'
|
||||
import { GroupByPeriod, KeywordsResponse, VacancyData } from '~/services/vacancies/vacancies.types'
|
||||
import { AreaChart, MultiSelect, MultiSelectItem, Select, SelectItem, Switch } from '@tremor/react'
|
||||
import { useSearchParams } from '@remix-run/react'
|
||||
import { capitalize } from 'remeda'
|
||||
import { ChartTooltip } from '~/components/ui/chart-tooltip'
|
||||
|
||||
type Props = {
|
||||
data?: VacancyData
|
||||
@@ -32,6 +34,11 @@ export function VacanciesChart({ data, keywords }: Props) {
|
||||
return searchParams.get('preset') ?? 'None'
|
||||
}, [searchParams])
|
||||
|
||||
const showTotal = useMemo(() => searchParams.get('showTotal') === 'true', [searchParams])
|
||||
const groupBy: GroupByPeriod = useMemo(() => {
|
||||
return (searchParams.get('groupBy') ?? 'day') as GroupByPeriod
|
||||
}, [searchParams])
|
||||
|
||||
const setPreset = useCallback(
|
||||
(value: string | null) => {
|
||||
if (!value || value === 'None') {
|
||||
@@ -44,10 +51,27 @@ export function VacanciesChart({ data, keywords }: Props) {
|
||||
},
|
||||
[searchParams, setSearchParams]
|
||||
)
|
||||
const setShowTotal = useCallback(
|
||||
(value: boolean) => {
|
||||
if (value) {
|
||||
searchParams.set('showTotal', 'true')
|
||||
} else {
|
||||
searchParams.delete('showTotal')
|
||||
}
|
||||
setSearchParams(searchParams)
|
||||
},
|
||||
[searchParams, setSearchParams]
|
||||
)
|
||||
|
||||
const categoriesForChart = useMemo(() => {
|
||||
return data?.categories.filter(category => selectedCategories.includes(category)) ?? []
|
||||
}, [selectedCategories, data?.categories])
|
||||
const filtered = [
|
||||
...(data?.categories.filter(category => selectedCategories.includes(category)) ?? []),
|
||||
]
|
||||
if (showTotal) {
|
||||
filtered.unshift('total')
|
||||
}
|
||||
return filtered
|
||||
}, [selectedCategories, data?.categories, showTotal])
|
||||
|
||||
const setSelectedCategories = useCallback(
|
||||
(value: string[]) => {
|
||||
@@ -62,17 +86,42 @@ export function VacanciesChart({ data, keywords }: Props) {
|
||||
[searchParams, setSearchParams]
|
||||
)
|
||||
|
||||
const setGroupBy = useCallback(
|
||||
(value: string) => {
|
||||
if (!value || value === 'day') {
|
||||
searchParams.delete('groupBy')
|
||||
} else {
|
||||
searchParams.set('groupBy', value)
|
||||
}
|
||||
|
||||
setSearchParams(searchParams)
|
||||
},
|
||||
[searchParams, setSearchParams]
|
||||
)
|
||||
|
||||
const filteredData = useMemo(
|
||||
() =>
|
||||
data?.data?.filter(row => {
|
||||
for (const category of selectedCategories) {
|
||||
const value = row[category]
|
||||
if (typeof value === 'number' && value > 0) {
|
||||
return true
|
||||
data?.data
|
||||
?.filter(row => {
|
||||
for (const category of selectedCategories) {
|
||||
const value = row[category]
|
||||
if (typeof value === 'number' && value > 0) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}) ?? [],
|
||||
[data?.data, selectedCategories]
|
||||
if (showTotal) {
|
||||
const value = row['total']
|
||||
if (typeof value === 'number' && value > 0) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
?.map(({ date, ...rest }) => ({
|
||||
...rest,
|
||||
date: new Date(date).toLocaleDateString('ru'),
|
||||
rawDate: date,
|
||||
})) ?? [],
|
||||
[data?.data, selectedCategories, showTotal, groupBy]
|
||||
)
|
||||
|
||||
const handlePresetChange = useCallback(
|
||||
@@ -103,6 +152,16 @@ export function VacanciesChart({ data, keywords }: Props) {
|
||||
))
|
||||
}, [presetsForSelect])
|
||||
|
||||
const groupBySelectItems = useMemo(
|
||||
() =>
|
||||
Object.entries(GroupByPeriod).map(([label, value]) => (
|
||||
<SelectItem key={'groupBy-select-item-' + label} value={value}>
|
||||
{capitalize(value)}
|
||||
</SelectItem>
|
||||
)),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex gap-4'}>
|
||||
@@ -121,6 +180,7 @@ export function VacanciesChart({ data, keywords }: Props) {
|
||||
{multiSelectItems}
|
||||
</MultiSelect>
|
||||
</div>
|
||||
|
||||
<div className="max-w-xl">
|
||||
<label
|
||||
htmlFor="presets"
|
||||
@@ -137,9 +197,31 @@ export function VacanciesChart({ data, keywords }: Props) {
|
||||
{presetSelectItems}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="max-w-xl">
|
||||
<label
|
||||
htmlFor="groupBy"
|
||||
className="text-tremor-default text-tremor-content dark:text-dark-tremor-content"
|
||||
>
|
||||
Group by
|
||||
</label>
|
||||
<Select value={groupBy} id={'groupBy'} defaultValue={'day'} onValueChange={setGroupBy}>
|
||||
{groupBySelectItems}
|
||||
</Select>
|
||||
</div>
|
||||
<label
|
||||
htmlFor="Show total"
|
||||
className="max-w-xl flex self-center mt-6 gap-2 text-tremor-default text-tremor-content dark:text-dark-tremor-content select-none"
|
||||
>
|
||||
Show total
|
||||
<Switch checked={showTotal} onChange={setShowTotal} id={'Show total'} />
|
||||
</label>
|
||||
</div>
|
||||
<AreaChart
|
||||
connectNulls={true}
|
||||
customTooltip={args => (
|
||||
<ChartTooltip {...args} groupBy={groupBy} rawDate={args.payload?.payload?.rawDate} />
|
||||
)}
|
||||
enableLegendSlider
|
||||
className="h-full"
|
||||
data={filteredData}
|
||||
index="date"
|
||||
|
||||
Reference in New Issue
Block a user