Skip to content

Commit

Permalink
feat: New CategoryBar (#48)
Browse files Browse the repository at this point in the history
* update category bar

* update animation

* update chart values (#47)
  • Loading branch information
severinlandolt authored Jun 24, 2024
1 parent 3ac4c0d commit cb31e4f
Show file tree
Hide file tree
Showing 10 changed files with 420 additions and 7 deletions.
7 changes: 4 additions & 3 deletions src/components/AreaChart/AreaChart.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Tremor Raw AreaChart [v0.2.2]
// Tremor Raw AreaChart [v0.2.3]

"use client"

Expand Down Expand Up @@ -554,7 +554,8 @@ const AreaChart = React.forwardRef<HTMLDivElement, AreaChartProps>(
fill = "gradient",
...other
} = props
const paddingValue = !showXAxis && !showYAxis ? 0 : 20
const paddingValue =
(!showXAxis && !showYAxis) || (startEndOnly && !showYAxis) ? 0 : 20
const [legendHeight, setLegendHeight] = React.useState(60)
const [activeDot, setActiveDot] = React.useState<ActiveDot | undefined>(
undefined,
Expand All @@ -581,7 +582,7 @@ const AreaChart = React.forwardRef<HTMLDivElement, AreaChartProps>(
category: string
}) => {
const stopOpacity =
activeDot || (activeLegend && activeLegend !== category) ? 0.15 : 0.4
activeDot || (activeLegend && activeLegend !== category) ? 0.1 : 0.3

switch (fillType) {
case "none":
Expand Down
7 changes: 7 additions & 0 deletions src/components/AreaChart/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Tremor Raw AreaChart Changelog

## 0.2.3

### Changes

- Chore: Gradient fill values
- Chore: Padding if no y-axis and startEndOnly

## 0.2.2

### Changes
Expand Down
5 changes: 3 additions & 2 deletions src/components/BarChart/BarChart.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Tremor Raw BarChart [v0.1.1]
// Tremor Raw BarChart [v0.1.2]

"use client"

Expand Down Expand Up @@ -606,7 +606,8 @@ const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
tooltipCallback,
...other
} = props
const paddingValue = !showXAxis && !showYAxis ? 0 : 20
const paddingValue =
(!showXAxis && !showYAxis) || (startEndOnly && !showYAxis) ? 0 : 20
const [legendHeight, setLegendHeight] = React.useState(60)
const [activeLegend, setActiveLegend] = React.useState<string | undefined>(
undefined,
Expand Down
6 changes: 6 additions & 0 deletions src/components/BarChart/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Tremor Raw BarChart Changelog

## 0.1.2

### Changes

- Chore: Padding if no y-axis and startEndOnly

## 0.1.1

### Changes
Expand Down
219 changes: 219 additions & 0 deletions src/components/CategoryBar/CategoryBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
// Tremor Raw CategoryBar [v0.0.0]

"use client"

import React from "react"

import {
AvailableChartColors,
AvailableChartColorsKeys,
getColorClassName,
} from "../../utils/chartColors"
import { cx } from "../../utils/cx"
import { Tooltip } from "../Tooltip/Tooltip"

const getMarkerBgColor = (
marker: number | undefined,
values: number[],
colors: AvailableChartColorsKeys[],
): string => {
if (marker === undefined) return ""

if (marker === 0) {
for (let index = 0; index < values.length; index++) {
if (values[index] > 0) {
return getColorClassName(colors[index], "bg")
}
}
}

let prefixSum = 0
for (let index = 0; index < values.length; index++) {
prefixSum += values[index]
if (prefixSum >= marker) {
return getColorClassName(colors[index], "bg")
}
}

return getColorClassName(colors[values.length - 1], "bg")
}

const getPositionLeft = (
value: number | undefined,
maxValue: number,
): number => (value ? (value / maxValue) * 100 : 0)

const sumNumericArray = (arr: number[]) =>
arr.reduce((prefixSum, num) => prefixSum + num, 0)

const BarLabels = ({ values }: { values: number[] }) => {
const sumValues = React.useMemo(() => sumNumericArray(values), [values])
let prefixSum = 0
let sumConsecutiveHiddenLabels = 0

return (
<div
className={cx(
// base
"relative mb-2 flex h-5 w-full text-sm font-medium",
// text color
"text-gray-700 dark:text-gray-300",
)}
>
{values.map((widthPercentage, index) => {
prefixSum += widthPercentage

const showLabel =
(widthPercentage >= 0.1 * sumValues ||
sumConsecutiveHiddenLabels >= 0.09 * sumValues) &&
sumValues - prefixSum >= 0.1 * sumValues &&
prefixSum >= 0.1 * sumValues &&
prefixSum < 0.9 * sumValues

sumConsecutiveHiddenLabels = showLabel
? 0
: (sumConsecutiveHiddenLabels += widthPercentage)

const widthPositionLeft = getPositionLeft(widthPercentage, sumValues)

return (
<div
key={`item-${index}`}
className="flex items-center justify-end pr-0.5"
style={{ width: `${widthPositionLeft}%` }}
>
<span
className={cx(
showLabel ? "block" : "hidden",
"translate-x-1/2 text-sm tabular-nums",
)}
>
{prefixSum}
</span>
</div>
)
})}
<div className="absolute bottom-0 left-0 flex items-center">0</div>
<div className="absolute bottom-0 right-0 flex items-center">
{sumValues}
</div>
</div>
)
}

interface CategoryBarProps extends React.HTMLAttributes<HTMLDivElement> {
values: number[]
colors?: AvailableChartColorsKeys[]
marker?: { value: number; tooltip?: string; showAnimation?: boolean }
showLabels?: boolean
}

const CategoryBar = React.forwardRef<HTMLDivElement, CategoryBarProps>(
(
{
values = [],
colors = AvailableChartColors,
marker,
showLabels = true,
className,
...props
},
forwardedRef,
) => {
const markerBgColor = React.useMemo(
() => getMarkerBgColor(marker?.value, values, colors),
[marker, values, colors],
)

const maxValue = React.useMemo(() => sumNumericArray(values), [values])

const adjustedMarkerValue = React.useMemo(() => {
if (marker === undefined) return undefined
if (marker.value < 0) return 0
if (marker.value > maxValue) return maxValue
return marker.value
}, [marker, maxValue])

const markerPositionLeft: number = React.useMemo(
() => getPositionLeft(adjustedMarkerValue, maxValue),
[adjustedMarkerValue, maxValue],
)

return (
<div
ref={forwardedRef}
className={cx(className)}
aria-label="category bar"
aria-valuenow={marker?.value}
{...props}
>
{showLabels ? <BarLabels values={values} /> : null}
<div className="relative flex h-2 w-full items-center">
<div className="flex h-full flex-1 items-center gap-0.5 overflow-hidden rounded-full">
{values.map((value, index) => {
const barColor = colors[index] ?? "gray"
const percentage = (value / maxValue) * 100
return (
<div
key={`item-${index}`}
className={cx(
"h-full",
getColorClassName(
barColor as AvailableChartColorsKeys,
"bg",
),
percentage === 0 && "hidden",
)}
style={{ width: `${percentage}%` }}
/>
)
})}
</div>

{marker !== undefined ? (
<div
className={cx(
"absolute w-2 -translate-x-1/2",
marker.showAnimation &&
"transform-gpu transition-all duration-300 ease-in-out",
)}
style={{
left: `${markerPositionLeft}%`,
}}
>
{marker.tooltip ? (
<Tooltip triggerAsChild content={marker.tooltip}>
<div
aria-hidden="true"
className={cx(
"relative mx-auto h-4 w-1 rounded-full ring-2",
"ring-white dark:ring-gray-950",
markerBgColor,
)}
>
<div
aria-hidden
className="absolute size-7 -translate-x-[45%] -translate-y-[15%]"
></div>
</div>
</Tooltip>
) : (
<div
className={cx(
"mx-auto h-4 w-1 rounded-full ring-2",
"ring-white dark:ring-gray-950",
markerBgColor,
)}
/>
)}
</div>
) : null}
</div>
</div>
)
},
)

CategoryBar.displayName = "CategoryBar"

export { CategoryBar, type CategoryBarProps }
71 changes: 71 additions & 0 deletions src/components/CategoryBar/categorybar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { expect, test } from "@playwright/test"

test.describe("Expect progressbar default", () => {
test("to be rendered", async ({ page }) => {
await page.goto(
"http://localhost:6006/?path=/story/visualization-progressbar--default",
)
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByTestId("progressbar"),
).toBeVisible()
})

test("to have a background bar", async ({ page }) => {
await page.goto(
"http://localhost:6006/?path=/story/visualization-progressbar--default",
)
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByTestId("progressbar"),
).toBeVisible()
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByLabel("progress bar"),
).toBeVisible()
})

test("to have a background and indicator bar", async ({ page }) => {
await page.goto(
"http://localhost:6006/?path=/story/visualization-progressbar--default",
)
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByTestId("progressbar"),
).toBeVisible()
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByLabel("progress bar"),
).toBeVisible()
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByLabel("progress bar")
.locator("div"),
).toBeVisible()
})

test("to have a label", async ({ page }) => {
await page.goto(
"http://localhost:6006/?path=/story/visualization-progressbar--default",
)
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByTestId("progressbar"),
).toBeVisible()
await page.goto(
"http://localhost:6006/?path=/story/visualization-progressbar--default",
)
await expect(
page
.frameLocator('iframe[title="storybook-preview-iframe"]')
.getByText("%"),
).toBeVisible()
})
})
Loading

0 comments on commit cb31e4f

Please sign in to comment.