Skip to content

Commit

Permalink
feat: component optimizations, add custom classes support
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisUser committed Nov 25, 2023
1 parent f6a86ab commit 3687fd3
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 92 deletions.
20 changes: 14 additions & 6 deletions src/UsageBar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as React from "react"
import UsageBar from "./UsageBar"

export default {
export default {
title: "Usage Bar",
component: UsageBar,
parameters: {}
parameters: {},
}

const items = [
Expand Down Expand Up @@ -33,13 +33,21 @@ const items = [

export const lightMode = () => <UsageBar items={items} total={100} />

export const withoutLabels = () => <UsageBar removeLabels items={items} total={100} />
export const withoutLabels = () => (
<UsageBar showLabels={false} showFallbackColors items={items} total={100} />
)

export const withPercentages = () => <UsageBar showPercentage items={items} total={100} />
export const withPercentages = () => (
<UsageBar showPercentage showFallbackColors items={items} total={100} />
)

export const compactLayout = () => <UsageBar showPercentage compactLayout items={items} total={100} />
export const compactLayout = () => (
<UsageBar showPercentage compactLayout items={items} total={100} />
)

export const compactLayoutWithoutLabels = () => <UsageBar removeLabels compactLayout items={items} total={100} />
export const compactLayoutWithoutLabels = () => (
<UsageBar showLabels={false} compactLayout items={items} total={100} />
)

export const error = () => (
<>
Expand Down
226 changes: 145 additions & 81 deletions src/UsageBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "./index.css"

interface Item {
color?: string
className?: string
value: number
name: string
}
Expand All @@ -11,9 +12,17 @@ interface Props {
items: Item[]
total: number
darkMode?: boolean
removeLabels?: boolean
showLabels?: boolean
showPercentage?: boolean
compactLayout?: boolean
showFallbackColors?: boolean
errorMessage?: string

usageBarContainerClassName?: string
usageBarClassName?: string
tooltipClassName?: string
dotElementClassName?: string
errorMessageClassName?: string
}

const lightColors: string[] = [
Expand Down Expand Up @@ -44,16 +53,39 @@ const darkColors: string[] = [
const getPercentageValue = (value: number, total: number): string =>
`${((value / total) * 100).toFixed(0)}%`

const shuffleArray = (a: string[]) => {
let j, x, i
for (i = a.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1))
x = a[i]
a[i] = a[j]
a[j] = x
}
return a
}

const appendCustomClass = (customClass?: string) => {
if (customClass) {
return customClass[0] === " " ? customClass : ` ${customClass}`
}
return ""
}

const UsageBar: React.FC<Props> = ({
darkMode = false,
removeLabels = false,
showLabels = true,
showPercentage = false,
compactLayout = false,
showFallbackColors = false,
total,
items,
errorMessage = "ERROR: Total elements values exceed 100%.",
usageBarContainerClassName,
usageBarClassName,
tooltipClassName,
dotElementClassName,
errorMessageClassName,
}) => {
const [formattedItems, setFormattedItems] = React.useState<Item[]>([])

/**
* Checks if the total value is equal or greater than the sum of all the elements values.
*/
Expand All @@ -65,82 +97,84 @@ const UsageBar: React.FC<Props> = ({
)

/**
* Formats the items prop array providing a color to
* elements without a defined one.
* Returns an array of colors based on the value of `darkMode`.
* The colors are either from the `darkColors` array or the `lightColors` array.
* The chosen array is shuffled before being returned.
*/
const formatItemsArray = React.useCallback(() => {
const selectedColors: string[] = []
const fallbackColors = React.useMemo(() => {
const colorsToPickFrom = darkMode ? [...darkColors] : [...lightColors]
shuffleArray(colorsToPickFrom)
return colorsToPickFrom
}, [])

// For each element a random index is generated and then used to pick a value
// from the colorsToPickFrom array; the selected value is removed by its original array
// and it's pushed into the selectedColors one.
for (let i = 0; i < items.length; i++) {
const randIndex = Math.floor(Math.random() * colorsToPickFrom.length)
const color = colorsToPickFrom[randIndex]
selectedColors.push(color)
colorsToPickFrom.splice(randIndex, 1)
}

// Each element from the items array is formatted correctly
// with a defined and valid color property.
setFormattedItems(
items.map((item: Item, index: number) => {
return item.color ? item : { ...item, color: selectedColors[index] }
})
)
}, [items, darkMode])

React.useEffect(() => {
if (itemsValuesAreCorrect) {
formatItemsArray()
}
}, [itemsValuesAreCorrect, formatItemsArray])
/**
* Returns the color of an element based on certain conditions.
*
* @param element - The item for which the color needs to be determined.
* @param index - The index of the item in the list.
* @returns The color of the element, either from the `color` property or a fallback
* color from the `fallbackColors` array. If the `element` does not have a `color`
* property and `showFallbackColors` is false, null is returned.
*/
const getElementColor = React.useCallback(
(element: Item, index: number) => {
if (element.color) return element.color
return showFallbackColors
? fallbackColors[index % fallbackColors.length]
: null
},
[showFallbackColors, fallbackColors]
)

if (!itemsValuesAreCorrect)
return (
<span className="u-UsageBar__error">
ERROR: Elements values exceed the total.
<span
className={`u-UsageBar__error${appendCustomClass(
errorMessageClassName
)}`}
>
{errorMessage}
</span>
)

if (formattedItems.length === 0) return null
if (items.length === 0) return null

if (compactLayout) {
return (
<div
className={`c-UsageBar c-UsageBar__compact ${
darkMode ? "u-UsageBar-dark" : "u-UsageBar-light"
}`}
}${appendCustomClass(usageBarContainerClassName)}`}
>
<div className="o-UsageBar__bar o-UsageBar__compact__bar">
{formattedItems.map((element: Item, index: number) => {
return (
<div
key={index}
className="o-UsageBar__bar__element"
style={{
width: getPercentageValue(element.value, total),
backgroundColor: element.color,
}}
/>
)
})}
<div
className={`o-UsageBar__bar o-UsageBar__compact__bar${appendCustomClass(
usageBarClassName
)}`}
>
{items.map((element: Item, index: number) => (
<UsageBarElement
element={element}
color={getElementColor(element, index)}
total={total}
key={index}
/>
))}
</div>
{!removeLabels && (
{showLabels && (
<div className="o-UsageBar__bar__elements__labels__container">
{formattedItems.map((element: Item, index: number) => {
{items.map((element: Item, index: number) => {
const color = getElementColor(element, index)
return (
<div key={index} className="o-UsageBar__bar__elements__label">
<div
className="o-UsageBar__bar__elements__label--dot"
style={{ backgroundColor: element.color }}
className={`o-UsageBar__bar__elements__label--dot ${appendCustomClass(
dotElementClassName
)}`}
style={color ? { backgroundColor: color } : {}}
/>
<span>{element.name}</span>
{showPercentage && (
<span className="o-UsageBar__bar__tooltip__percentage">
{getPercentageValue(element.value, total)}
</span>
<UsageBarPercentageLabel element={element} total={total} />
)}
</div>
)
Expand All @@ -155,35 +189,65 @@ const UsageBar: React.FC<Props> = ({
<div
className={`c-UsageBar ${
darkMode ? "u-UsageBar-dark" : "u-UsageBar-light"
}`}
}${appendCustomClass(usageBarContainerClassName)}`}
>
<div className="o-UsageBar__bar">
{formattedItems.map((element: Item, index: number) => {
return (
<div
key={index}
className="o-UsageBar__bar__element"
style={{
width: getPercentageValue(element.value, total),
backgroundColor: element.color,
}}
>
{!removeLabels && (
<div className="o-UsageBar__bar__tooltip">
<span>{element.name}</span>
{showPercentage && (
<span className="o-UsageBar__bar__tooltip__percentage">
{getPercentageValue(element.value, total)}
</span>
)}
</div>
)}
</div>
)
})}
<div className={`o-UsageBar__bar${appendCustomClass(usageBarClassName)}`}>
{items.map((element: Item, index: number) => (
<UsageBarElement
element={element}
color={getElementColor(element, index)}
total={total}
key={index}
>
{showLabels && (
<div
className={`o-UsageBar__bar__tooltip${appendCustomClass(
tooltipClassName
)}`}
>
<span>{element.name}</span>
{showPercentage && (
<UsageBarPercentageLabel element={element} total={total} />
)}
</div>
)}
</UsageBarElement>
))}
</div>
</div>
)
}

const UsageBarElement: React.FC<{
element: Item
color: string | null
total: number
children?: React.ReactNode
}> = ({ element, color, total, children }) => {
return (
<div
className={`o-UsageBar__bar__element${appendCustomClass(
element.className
)}`}
style={{
width: getPercentageValue(element.value, total),
...(color ? { backgroundColor: color } : null),
}}
>
{children}
</div>
)
}

const UsageBarPercentageLabel: React.FC<{ element: Item; total: number }> = ({
element,
total,
}) => {
return (
<span className="o-UsageBar__bar__tooltip__percentage">
{getPercentageValue(element.value, total)}
</span>
)
}

export default UsageBar
10 changes: 8 additions & 2 deletions src/UsageBarDark.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const darkMode = () => (

export const darkModeWithoutLabels = () => (
<div style={mainDarkModeContainerStyle}>
<UsageBar removeLabels darkMode items={items} total={100} />
<UsageBar showLabels={false} darkMode items={items} total={100} />
</div>
)

Expand All @@ -59,7 +59,13 @@ export const darkModeCompact = () => (

export const darkModeCompactWithoutLabels = () => (
<div style={mainDarkModeContainerStyle}>
<UsageBar removeLabels compactLayout darkMode items={items} total={100} />
<UsageBar
showLabels={false}
compactLayout
darkMode
items={items}
total={100}
/>
</div>
)

Expand Down
7 changes: 5 additions & 2 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
}
.o-UsageBar__bar__tooltip__percentage {
opacity: 0.78;
font-size: 0.9em;
font-size: 0.85em;
margin: 0 0 0 3pt;
}
.o-UsageBar__bar__tooltip::after {
Expand All @@ -128,20 +128,22 @@
.o-UsageBar__bar__elements__label {
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: row;
}
.o-UsageBar__bar__elements__labels__container {
margin-top: 12pt;
position: relative;
width: 100%;
max-width: 480px;
align-items: center;
flex-wrap: wrap;
gap: 12pt;
font-size: 0.9em;
}
.o-UsageBar__bar__elements__label {
align-items: flex-end;
flex-wrap: nowrap;
line-height: 1;
}
.o-UsageBar__bar__elements__label > span,
.o-UsageBar__bar__elements__label > .o-UsageBar__bar__tooltip__percentage {
Expand All @@ -152,4 +154,5 @@
margin-right: 6pt;
height: 8pt;
width: 8pt;
border: 1px solid var(--background-bar-color);
}
Loading

0 comments on commit 3687fd3

Please sign in to comment.