Skip to content

Commit

Permalink
TimeSeries support groupBy (#231)
Browse files Browse the repository at this point in the history
  • Loading branch information
felipecadavid authored Sep 19, 2024
1 parent cd975a6 commit 8de412c
Show file tree
Hide file tree
Showing 18 changed files with 1,424 additions and 136 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-gorillas-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@propeldata/ui-kit': minor
---

added support for groupBy on TimeSeries component
96 changes: 69 additions & 27 deletions packages/ui-kit/src/components/PieChart/PieChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Chart as ChartJS, ChartConfiguration, Plugin } from 'chart.js/auto'
import classnames from 'classnames'
import React from 'react'
import * as radixColors from '@radix-ui/colors'
import {
customCanvasBackgroundColor,
getCustomChartLabelsPlugin,
Expand All @@ -20,7 +21,15 @@ import { useSetupTheme } from '../ThemeProvider'
import { withContainer } from '../withContainer'
import componentStyles from './PieChart.module.scss'
import { PieChartData, PieChartProps, PieChartVariant } from './PieChart.types'
import { useParsedComponentProps } from '../../themes'
import {
AccentColors,
grayColors,
GrayColors,
handleArbitraryColor,
palette,
PaletteColor,
useParsedComponentProps
} from '../../themes'
import { emptyStatePlugin } from './plugins/empty'

let idCounter = 0
Expand All @@ -46,6 +55,8 @@ export const PieChartComponent = React.forwardRef<HTMLDivElement, PieChartProps>
chartProps,
labelListClassName,
chartConfigProps,
accentColors,
otherColor,
...rest
}: PieChartProps,
forwardedRef: React.ForwardedRef<HTMLDivElement>
Expand Down Expand Up @@ -115,21 +126,37 @@ export const PieChartComponent = React.forwardRef<HTMLDivElement, PieChartProps>

const showValues = chartProps?.showValues ?? false

const defaultChartColorPalette = React.useMemo(
() => [
theme?.getVar('--propel-accent-3'),
theme?.getVar('--propel-accent-4'),
theme?.getVar('--propel-accent-5'),
theme?.getVar('--propel-accent-6'),
theme?.getVar('--propel-accent-7'),
theme?.getVar('--propel-accent-8'),
theme?.getVar('--propel-accent-9'),
theme?.getVar('--propel-accent-10'),
theme?.getVar('--propel-accent-11'),
theme?.getVar('--propel-accent-12')
],
[theme]
)
const defaultChartColorPalette = React.useMemo(() => {
let customColors: PaletteColor[] = []

const accentColor = accentColors?.[0] ?? theme?.accentColor

let colorPos = palette.findIndex((value) => value?.name === accentColor)

if (accentColors != null) {
const isCustomColors = (accentColors?.length ?? 0) > 0

if (isCustomColors) {
customColors = accentColors.map(
(color) =>
palette.find(({ name }) => name === color) ?? {
primary: handleArbitraryColor(color),
secondary: handleArbitraryColor(color),
name: color as AccentColors
}
)

const lastColorName = customColors[customColors.length - 1]?.name
const lastColorIndex = palette.findIndex((color) => color.name === lastColorName)

colorPos = lastColorIndex + 1
}
}

const additionalColors = palette.slice(colorPos)

return [...customColors, ...additionalColors, ...palette].map(({ primary }) => primary)
}, [accentColors, theme?.accentColor])

const totalValue = isStatic ? rows?.reduce((a, b) => a + Number(b[1]), 0) ?? 0 : Number(counterData?.counter?.value)

Expand Down Expand Up @@ -202,6 +229,24 @@ export const PieChartComponent = React.forwardRef<HTMLDivElement, PieChartProps>

const datasets = isDoughnut ? { cutout: '75%' } : { cutout: '0' }

const otherIndex = labels.findIndex((label) => label === 'Other')

if (otherIndex !== -1 && otherColor != null) {
const isArbitraryGray = otherColor != null && !grayColors.includes(otherColor as GrayColors)

const grayColor: PaletteColor = {
name: 'gray',
primary: isArbitraryGray
? handleArbitraryColor(otherColor ?? '')
: theme.tokens[`${otherColor ?? theme.grayColor}8`] ?? radixColors.gray.gray8,
secondary: isArbitraryGray
? handleArbitraryColor(otherColor ?? '')
: theme.tokens[`${otherColor ?? theme.grayColor}10`] ?? radixColors.gray.gray10
}

chartColorPalette[otherIndex] = grayColor.primary
}

let config: ChartConfiguration<PieChartVariant> = {
...chartConfig,
type: variant,
Expand Down Expand Up @@ -269,16 +314,17 @@ export const PieChartComponent = React.forwardRef<HTMLDivElement, PieChartProps>
canvasRef.current.style.borderRadius = '0px'
},
[
variant,
theme,
card,
chartProps,
chartConfig,
isDoughnut,
showValues,
defaultChartColorPalette,
chartProps,
card,
isPie,
totalValue,
defaultChartColorPalette,
showValues,
isDoughnut,
otherColor,
variant,
chartConfigProps
]
)
Expand Down Expand Up @@ -330,11 +376,7 @@ export const PieChartComponent = React.forwardRef<HTMLDivElement, PieChartProps>

if (
!isStatic &&
(hasError?.name === 'AccessTokenError' ||
!query.metric ||
!query.timeRange ||
!query.dimension ||
!query.rowLimit)
(hasError?.name === 'AccessTokenError' || !query.metric || !query.dimension || !query.rowLimit)
) {
// console.error(
// 'InvalidPropsError: When opting for fetching data you must pass at least `accessToken`, `metric`, `dimensions`, `rowLimit` and `timeRange` in the `query` prop'
Expand Down
13 changes: 11 additions & 2 deletions packages/ui-kit/src/components/PieChart/PieChart.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ChartConfiguration } from 'chart.js'
import { DimensionInput, Sort } from '../../graphql'
import { ThemeSettingProps } from '../../themes'
import { AccentColors, GrayColors, ThemeSettingProps } from '../../themes'
import { DataComponentProps, QueryProps } from '../shared.types'

export type PieChartVariant = 'pie' | 'doughnut'
Expand Down Expand Up @@ -29,7 +29,10 @@ export type ChartProps = {
*/
showValues?: boolean

/** Sets the chart color palette */
/**
* Sets the chart color palette
* @deprecated This property is deprecated and will be removed in a future version. Use `accentColors` instead.
*/
chartColorPalette?: string[]

/** Hides the total value on chart if it is set the true
Expand Down Expand Up @@ -90,6 +93,12 @@ export interface PieChartProps extends ThemeSettingProps, DataComponentProps<'di
/** Provides className to style label list of chart. */
labelListClassName?: string

/** A list of accent colors the PieChart component will use, those will be picked in order */
accentColors?: (AccentColors | string)[]

/** Color that will be used for `other` */
otherColor?: GrayColors | string

/** An optional prop that provides access to the Chart.js API, allowing for further customization of chart settings. */
chartConfigProps?: (config: ChartConfiguration<'pie' | 'doughnut'>) => ChartConfiguration<'pie' | 'doughnut'>
}
4 changes: 4 additions & 0 deletions packages/ui-kit/src/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Tabs as TabsBase } from '@radix-ui/themes'
import '@radix-ui/themes/styles.css'

export const Tabs = TabsBase
1 change: 1 addition & 0 deletions packages/ui-kit/src/components/Tabs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Tabs'
18 changes: 18 additions & 0 deletions packages/ui-kit/src/components/TimeSeries/TimeSeries.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,24 @@ export const ChartFormatXLabelsStory: Story = {
render: (args) => <TimeSeries {...args} />
}

export const GroupedStory: Story = {
name: 'Grouped',
args: {
variant: 'bar',
query: {
...connectedParams,
groupBy: ['restaurant_name']
},
showGroupByOther: true,
maxGroupBy: 5,
stacked: true,
accentColors: ['red', 'blue'],
card: true,
otherColor: 'gray'
},
render: (args) => <TimeSeries {...args} />
}

export const StaticStory: Story = {
name: 'Static',
parameters: { imports: 'TimeSeries' },
Expand Down
76 changes: 42 additions & 34 deletions packages/ui-kit/src/components/TimeSeries/TimeSeries.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
'use client'

import { Chart as ChartJS, ChartConfiguration, ChartDataset, ChartOptions, Color, LineController } from 'chart.js'
import { Chart as ChartJS, ChartConfiguration, ChartOptions, Color, LineController } from 'chart.js'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as chartJsAdapterLuxon from 'chartjs-adapter-luxon'
import classnames from 'classnames'
import { startOfDay } from 'date-fns'
import React from 'react'
import {
convertHexToRGBA,
customCanvasBackgroundColor,
formatLabels,
getPixelFontSizeAsNumber,
getTimeZone,
useEmptyableData,
useForwardedRefCallback,
Expand All @@ -26,7 +26,7 @@ import { useSetupTheme } from '../ThemeProvider'
import { withContainer } from '../withContainer'
import componentStyles from './TimeSeries.module.scss'
import type { TimeSeriesChartVariant, TimeSeriesData, TimeSeriesProps } from './TimeSeries.types'
import { getDefaultGranularity, getNumericValues, getScales, tooltipTitleCallback } from './utils'
import { buildDatasets, getDefaultGranularity, getScales, tooltipTitleCallback } from './utils'

let idCounter = 0

Expand Down Expand Up @@ -76,6 +76,11 @@ export const TimeSeriesComponent = React.forwardRef<HTMLDivElement, TimeSeriesPr
renderEmpty,
card = false,
chartProps,
maxGroupBy = 5,
showGroupByOther = true,
accentColors = [],
stacked = false,
otherColor,
...rest
},
forwardedRef
Expand Down Expand Up @@ -163,7 +168,6 @@ export const TimeSeriesComponent = React.forwardRef<HTMLDivElement, TimeSeriesPr

const { grid = false, fillArea = false } = chartProps ?? {}
const labels = formatLabels({ labels: data.labels, formatter: labelFormatter }) ?? []
const values = getNumericValues(data.values ?? [], log)

const plugins = [customCanvasBackgroundColor]

Expand Down Expand Up @@ -194,27 +198,16 @@ export const TimeSeriesComponent = React.forwardRef<HTMLDivElement, TimeSeriesPr
}
}

const borderRadius = Math.max(
getPixelFontSizeAsNumber(theme?.getVar('--propel-radius-2')),
getPixelFontSizeAsNumber(theme?.getVar('--propel-radius-full'))
const datasets = buildDatasets(
data,
theme,
{ fill, maxGroupBy, showGroupByOther, accentColors, otherColor },
log
)

const dataset = {
labels,
datasets: [
{
data: values,
backgroundColor: backgroundColor,
borderColor: theme?.getVar('--propel-accent-8'),
borderRadius,
hoverBackgroundColor: theme?.getVar('--propel-accent-10'),
pointBackgroundColor: theme?.getVar('--propel-accent-10'),
pointHoverBackgroundColor: theme?.getVar('--propel-accent-10'),
pointHoverBorderWidth: 2,
pointHoverBorderColor: theme?.getVar('--propel-accent-contrast'),
fill
} as ChartDataset<TimeSeriesChartVariant>
]
labels: labels.length > 0 ? labels : [startOfDay(new Date().toISOString()).toISOString()], // workaround for groupBy not returning labels
datasets
}

// @TODO: need to refactor this logic
Expand All @@ -225,7 +218,8 @@ export const TimeSeriesComponent = React.forwardRef<HTMLDivElement, TimeSeriesPr
chart: chartRef.current,
variant,
grid,
theme
theme,
stacked
})

const options: ChartOptions<TimeSeriesChartVariant> = {
Expand Down Expand Up @@ -270,19 +264,24 @@ export const TimeSeriesComponent = React.forwardRef<HTMLDivElement, TimeSeriesPr
chartRef.current = new ChartJS(canvasRef.current, config)
},
[
granularity,
hasError,
isFormatted,
variant,
timeZone,
theme,
card,
chartProps,
log,
labelFormatter,
chartConfigProps,
card,
variant,
maxGroupBy,
showGroupByOther,
accentColors,
otherColor,
log,
granularity,
isFormatted,
timeZone,
stacked,
chartConfig,
type,
chartConfig
chartConfigProps
]
)

Expand Down Expand Up @@ -338,7 +337,12 @@ export const TimeSeriesComponent = React.forwardRef<HTMLDivElement, TimeSeriesPr
const labels = serverData.timeSeries?.labels ?? []
const values = (serverData.timeSeries?.values ?? []).map((value) => (value == null ? null : Number(value)))

setData({ labels, values })
const groups = serverData.timeSeries?.groups?.map((group) => ({
...group,
values: group.values.map((value) => (value == null ? null : Number(value)))
}))

setData({ labels, values, groups })
}
}, [serverData, variant, isStatic, setData])

Expand Down Expand Up @@ -378,8 +382,12 @@ export const TimeSeriesComponent = React.forwardRef<HTMLDivElement, TimeSeriesPr
return <Loader ref={setRef} {...loaderProps} />
}

if (isEmptyState && renderEmptyComponent) {
return themeWrapper(renderEmptyComponent({ theme }))
if (isEmptyState) {
if (renderEmptyComponent != null) {
return themeWrapper(renderEmptyComponent({ theme }))
} else {
data != null && renderChart(data) // render chart with empty data in case no empty state component is provided
}
}

return (
Expand Down
Loading

0 comments on commit 8de412c

Please sign in to comment.