dashboardcn
blockgoal-chart-cardsource: ok · hit
Goal Chart Card
A card with a total, a period switcher, and a bar chart with a dashed goal line that colors the bars reaching it.
npx shadcn add "https://dashboardcn.com/r/goal-chart-card.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumchartsrecharts
iconslucide-react
Registry dependencies
cardcharttabstooltip
Files6 · source cached 10/9/2026, 8:11:26 AM
registry/dashboardcn/blocks/goal-chart-card.tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { formatNumber, type NumberFormat } from "@/registry/dashboardcn/lib/format"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { BarChart, type BarChartProps, type BarRow } from "@/registry/dashboardcn/ui/bar-chart"
import { DeltaBadge } from "@/registry/dashboardcn/ui/delta-badge"
import { MetricValue } from "@/registry/dashboardcn/ui/metric-value"
import { PeriodTabs, type PeriodOption } from "@/registry/dashboardcn/ui/period-tabs"
export interface GoalChartCardProps extends React.ComponentProps<typeof Card> {
title: string
/** Headline number. Defaults to the sum of the rows. */
value?: number
format?: NumberFormat
currency?: string
/** Caption after the value, e.g. "total steps". */
unit?: string
/** Period text, e.g. "This week". Shown in the top-right corner unless periods is set. */
caption?: string
data: BarRow[]
/** Key of the x-axis value in each row. */
xKey: string
/** Key of the bar value in each row. */
yKey: string
/** Target drawn as a dashed line. Bars at or above it are drawn in color. */
goal: number
goalLabel?: string
/** Any CSS color for bars that reach the goal. Defaults to chart-1. */
color?: string
/** Any CSS color for bars below the goal. Defaults to the muted foreground. */
mutedColor?: string
variant?: BarChartProps["variant"]
grid?: BarChartProps["grid"]
xFormatter?: BarChartProps["xFormatter"]
yFormatter?: BarChartProps["yFormatter"]
chartClassName?: string
/** Fractional change vs the prior period, e.g. 0.052 for +5.2%. */
delta?: number
/** Text after the delta, e.g. "vs last week". */
deltaLabel?: string
/** Treat a decrease as good. */
invertDelta?: boolean
/** Period options for the switcher. Omit to hide it. */
periods?: PeriodOption[]
period?: string
defaultPeriod?: string
onPeriodChange?: (period: string) => void
}
function sum(rows: BarRow[], key: string) {
return rows.reduce((total, row) => total + (Number(row[key]) || 0), 0)
}
/** A card with a total, a period switcher, and a bar chart with a dashed goal line that colors the bars reaching it. */
function GoalChartCard({
title,
value,
format = "number",
currency,
unit,
caption,
data,
xKey,
yKey,
goal,
goalLabel = "Goal",
color,
mutedColor,
variant = "solid",
grid = "none",
xFormatter,
yFormatter: yFormatterProp,
chartClassName,
delta,
deltaLabel,
invertDelta,
periods,
period,
defaultPeriod,
onPeriodChange,
className,
...props
}: GoalChartCardProps) {
const total = value ?? sum(data, yKey)
const yFormatter = React.useCallback(
(v: number) => formatNumber(v, { format, currency }),
[format, currency]
)
const highlight = React.useCallback(
(row: BarRow) => Number(row[yKey]) >= goal,
[yKey, goal]
)
const showCaptionInline = Boolean(caption && periods)
return (
<Card data-slot="goal-chart-card" className={cn("@container/card gap-4", className)} {...props}>
<CardHeader className="@max-sm/card:[grid-template-columns:minmax(0,1fr)]!">
<CardDescription>{title}</CardDescription>
<CardTitle className="flex flex-wrap items-baseline gap-x-2 gap-y-1 text-3xl font-semibold tabular-nums tracking-tight">
<MetricValue value={total} format={format} currency={currency} />
{unit ? (
<span className="text-muted-foreground text-sm font-normal">{unit}</span>
) : null}
</CardTitle>
{delta !== undefined || showCaptionInline ? (
<div className="flex flex-wrap items-center gap-1.5 text-sm">
{delta !== undefined ? (
<DeltaBadge delta={delta} invert={invertDelta} variant="text" />
) : null}
{deltaLabel || showCaptionInline ? (
<span className="text-muted-foreground">
{[deltaLabel, showCaptionInline ? caption : undefined]
.filter(Boolean)
.join(" · ")}
</span>
) : null}
</div>
) : null}
{periods ? (
<CardAction className="@max-sm/card:[grid-area:auto]! @max-sm/card:justify-self-start!">
<PeriodTabs
options={periods}
value={period}
defaultValue={defaultPeriod ?? periods[0]?.value}
onValueChange={onPeriodChange}
/>
</CardAction>
) : caption ? (
<CardAction className="@max-sm/card:[grid-area:auto]! @max-sm/card:justify-self-start!">
<span className="text-muted-foreground text-sm tabular-nums">{caption}</span>
</CardAction>
) : null}
</CardHeader>
<CardContent>
<BarChart
data={data}
xKey={xKey}
yKey={yKey}
variant={variant}
color={color}
mutedColor={mutedColor}
grid={grid}
highlight={highlight}
referenceLines={[{ y: goal, label: goalLabel, dashed: true }]}
xFormatter={xFormatter}
yFormatter={yFormatterProp ?? yFormatter}
className={cn("h-56", chartClassName)}
/>
</CardContent>
</Card>
)
}
export { GoalChartCard }
registry/dashboardcn/ui/bar-chart.tsx
"use client"
import * as React from "react"
import {
Bar,
BarChart as RechartsBarChart,
CartesianGrid,
Cell,
Rectangle,
ReferenceLine,
XAxis,
YAxis,
} from "recharts"
import { cn } from "@/lib/utils"
import { ChartContainer, ChartTooltip } from "@/components/ui/chart"
export type BarRow = Record<string, unknown>
export interface BarReferenceLine {
/** Horizontal line at this y value. */
y: number
label?: string
/** Any CSS color. Defaults to the muted foreground. */
color?: string
/** Defaults to dashed. */
dashed?: boolean
}
export interface BarChartProps
extends Omit<React.ComponentProps<typeof ChartContainer>, "config" | "children"> {
data: BarRow[]
/** Key of the x-axis value in each row. */
xKey: string
/** Key of the bar value in each row. */
yKey: string
/**
* Key of a coarser label, e.g. "month" for weekly rows. When set, the x-axis
* shows one tick per group centered under its bars instead of a tick per bar.
*/
groupKey?: string
/** "gradient" fades each bar toward the bottom; "striped" fills it with diagonal bands. */
variant?: "gradient" | "striped" | "solid"
/** Any CSS color for highlighted bars. Defaults to chart-1. */
color?: string
/** Any CSS color for bars that are not highlighted. Defaults to the muted foreground. */
mutedColor?: string
/** Return true for bars drawn in color. Every bar is highlighted when omitted. */
highlight?: (row: BarRow, index: number) => boolean
grid?: "dashed" | "solid" | "none"
showYAxis?: boolean
showTooltip?: boolean
/** Draw a hollow ring on top of the hovered bar. */
showActiveMarker?: boolean
barRadius?: number
/** Animate bars after hydration and when data changes. Respects reduced motion. */
animate?: boolean
animationDuration?: number
/** Horizontal lines, e.g. a goal or an average. The y-axis extends to fit them. */
referenceLines?: BarReferenceLine[]
xFormatter?: (value: unknown) => string
yFormatter?: (value: number) => string
/** Secondary line of the tooltip. Defaults to the formatted x value. */
tooltipLabel?: (row: BarRow, index: number) => React.ReactNode
onBarClick?: (row: BarRow, index: number) => void
}
const reducedMotionQuery = "(prefers-reduced-motion: reduce)"
function subscribeToReducedMotion(onChange: () => void) {
const query = window.matchMedia(reducedMotionQuery)
query.addEventListener("change", onChange)
return () => query.removeEventListener("change", onChange)
}
function getReducedMotionSnapshot() {
return window.matchMedia(reducedMotionQuery).matches
}
function getReducedMotionServerSnapshot() {
return true
}
function defaultXFormatter(value: unknown) {
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value)) {
const [y, m, d] = value.split("-").map(Number)
return new Date(y!, m! - 1, d).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
}
return String(value)
}
/** One tick per group, placed under the middle bar of the group. */
function groupTicks(data: BarRow[], groupKey: string) {
const labels: string[] = Array.from({ length: data.length }, () => "")
let start = 0
for (let i = 1; i <= data.length; i++) {
if (i === data.length || data[i]![groupKey] !== data[start]![groupKey]) {
labels[Math.floor((start + i - 1) / 2)] = String(data[start]![groupKey] ?? "")
start = i
}
}
return labels
}
interface ShapeProps {
x?: number
y?: number
width?: number
height?: number
index?: number
}
/** A single-series bar chart with gradient or striped fills, highlighted bars, and a hover marker. */
function BarChart({
data,
xKey,
yKey,
groupKey,
variant = "gradient",
color = "var(--chart-1)",
mutedColor = "var(--muted-foreground)",
highlight,
grid = "dashed",
showYAxis = false,
showTooltip = true,
showActiveMarker = true,
barRadius = 6,
animate = true,
animationDuration = 500,
referenceLines = [],
xFormatter = defaultXFormatter,
yFormatter,
tooltipLabel,
onBarClick,
className,
...props
}: BarChartProps) {
const id = React.useId()
const reducedMotion = React.useSyncExternalStore(
subscribeToReducedMotion,
getReducedMotionSnapshot,
getReducedMotionServerSnapshot
)
const isHighlighted = React.useCallback(
(index: number) => (highlight ? highlight(data[index]!, index) : true),
[data, highlight]
)
const tickLabels = React.useMemo(
() => (groupKey ? groupTicks(data, groupKey) : null),
[data, groupKey]
)
const fillFor = (index: number) =>
`url(#${id}-${isHighlighted(index) ? "on" : "off"})`
const activeBar = (shape: ShapeProps) => {
const { x = 0, y = 0, width = 0, height = 0, index = 0 } = shape
const on = isHighlighted(index)
return (
<g>
<Rectangle {...shape} fill={fillFor(index)} />
{showActiveMarker && height > 0 ? (
<circle
cx={x + width / 2}
cy={y}
r={4}
fill="var(--background)"
stroke={on ? color : mutedColor}
strokeWidth={2}
/>
) : null}
</g>
)
}
return (
<ChartContainer
data-slot="bar-chart"
config={{ [yKey]: { label: yKey, color } }}
className={cn("aspect-auto h-64 w-full", className)}
{...props}
>
<RechartsBarChart
data={data}
margin={{ top: 12, right: 8, bottom: 0, left: showYAxis ? 0 : 8 }}
barCategoryGap="28%"
>
<defs>
{variant === "gradient" ? (
<>
<linearGradient id={`${id}-on`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity={1} />
<stop offset="100%" stopColor={color} stopOpacity={0.6} />
</linearGradient>
<linearGradient id={`${id}-off`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={mutedColor} stopOpacity={0.35} />
<stop offset="100%" stopColor={mutedColor} stopOpacity={0.18} />
</linearGradient>
</>
) : variant === "striped" ? (
<>
{(["on", "off"] as const).map((state) => {
const fill = state === "on" ? color : mutedColor
const opacity = state === "on" ? 1 : 0.3
return (
<pattern
key={state}
id={`${id}-${state}`}
patternUnits="userSpaceOnUse"
width={10}
height={10}
patternTransform="rotate(45)"
>
<rect width={10} height={10} fill={fill} fillOpacity={opacity} />
<rect
width={5}
height={10}
fill="var(--background)"
fillOpacity={0.25}
/>
</pattern>
)
})}
</>
) : (
<>
<linearGradient id={`${id}-on`}>
<stop stopColor={color} />
</linearGradient>
<linearGradient id={`${id}-off`}>
<stop stopColor={mutedColor} stopOpacity={0.3} />
</linearGradient>
</>
)}
</defs>
{grid !== "none" ? (
<CartesianGrid
vertical={false}
strokeDasharray={grid === "dashed" ? "6 6" : undefined}
/>
) : null}
<XAxis
dataKey={xKey}
tickLine={false}
axisLine={false}
tickMargin={10}
interval={tickLabels ? 0 : "preserveStartEnd"}
minTickGap={tickLabels ? 0 : 24}
tickFormatter={
tickLabels ? (_, index) => tickLabels[index] ?? "" : (v) => xFormatter(v)
}
/>
<YAxis
hide={!showYAxis}
tickLine={false}
axisLine={false}
tickMargin={8}
width={44}
tickFormatter={yFormatter}
/>
{showTooltip ? (
<ChartTooltip
cursor={false}
isAnimationActive={false}
content={({ active, payload }) => {
const entry = payload?.[0]
if (!active || !entry) return null
const row = entry.payload as BarRow
const index = data.indexOf(row)
const value = Number(row[yKey])
return (
<div className="bg-background rounded-lg border px-3 py-2 text-xs shadow-md">
<div className="text-sm font-semibold tabular-nums">
{yFormatter ? yFormatter(value) : String(value)}
</div>
<div className="text-muted-foreground">
{tooltipLabel ? tooltipLabel(row, index) : xFormatter(row[xKey])}
</div>
</div>
)
}}
/>
) : null}
<Bar
dataKey={yKey}
radius={barRadius}
isAnimationActive={animate && !reducedMotion}
animationDuration={animationDuration}
animationEasing="ease-out"
activeBar={activeBar}
onClick={
onBarClick
? (_, index) => onBarClick(data[index]!, index)
: undefined
}
className={onBarClick ? "cursor-pointer" : undefined}
>
{data.map((_, index) => (
<Cell key={index} fill={fillFor(index)} />
))}
</Bar>
{referenceLines.map((line, index) => (
<ReferenceLine
key={index}
y={line.y}
ifOverflow="extendDomain"
stroke={line.color ?? "var(--muted-foreground)"}
strokeDasharray={line.dashed === false ? undefined : "4 4"}
label={
line.label
? {
value: line.label,
position: "insideTopRight",
fill: line.color ?? "var(--muted-foreground)",
fontSize: 11,
}
: undefined
}
/>
))}
</RechartsBarChart>
</ChartContainer>
)
}
export { BarChart }
registry/dashboardcn/ui/delta-badge.tsx
import * as React from "react"
import { Minus, TrendingDown, TrendingUp } from "lucide-react"
import { cn } from "@/lib/utils"
import { formatDelta } from "@/registry/dashboardcn/lib/format"
export type DeltaDirection = "up" | "down" | "flat"
export function getDeltaDirection(delta: number | undefined): DeltaDirection {
if (delta === undefined || delta === 0 || !Number.isFinite(delta)) return "flat"
return delta > 0 ? "up" : "down"
}
const directionIcon: Record<DeltaDirection, React.ElementType> = {
up: TrendingUp,
down: TrendingDown,
flat: Minus,
}
export interface DeltaBadgeProps extends React.ComponentProps<"span"> {
/** Fractional change, e.g. 0.124 for +12.4%. */
delta: number
/** Treat a decrease as good and an increase as bad (churn, latency, errors). */
invert?: boolean
variant?: "outline" | "soft" | "text"
showIcon?: boolean
}
function DeltaBadge({
delta,
invert = false,
variant = "outline",
showIcon = true,
className,
children,
...props
}: DeltaBadgeProps) {
const direction = getDeltaDirection(delta)
const positive = direction === "flat" ? null : (direction === "up") !== invert
const Icon = directionIcon[direction]
return (
<span
data-slot="delta-badge"
data-direction={direction}
data-positive={positive}
className={cn(
"inline-flex items-center gap-1 text-xs font-medium tabular-nums",
variant === "outline" && "rounded-md border px-1.5 py-0.5",
variant === "soft" && "rounded-md px-1.5 py-0.5",
variant === "soft" && positive === true && "bg-emerald-500/10",
variant === "soft" && positive === false && "bg-red-500/10",
variant === "soft" && positive === null && "bg-muted",
positive === true && "text-emerald-600 dark:text-emerald-400",
positive === false && "text-red-600 dark:text-red-400",
positive === null && "text-muted-foreground",
className
)}
{...props}
>
{showIcon ? <Icon className="size-3" aria-hidden="true" /> : null}
{children ?? formatDelta(delta)}
</span>
)
}
export { DeltaBadge }
registry/dashboardcn/ui/metric-value.tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { formatNumber, type NumberFormat } from "@/registry/dashboardcn/lib/format"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
/** Values at or above this are abbreviated by default. */
export const DEFAULT_COMPACT_FROM = 100_000
export interface MetricValueProps extends Omit<React.ComponentProps<"span">, "children"> {
/** Strings render as-is. */
value: number | string
format?: NumberFormat
currency?: string
maximumFractionDigits?: number
/** Values at or above this are abbreviated (e.g. $158K) with the full value in a tooltip. Set to Infinity to always show the full value. */
compactFrom?: number
/** Text after the number, e.g. a unit. */
suffix?: React.ReactNode
}
/** A formatted number. Large values are abbreviated and reveal the full value on hover. */
function MetricValue({
value,
format,
currency,
maximumFractionDigits,
compactFrom = DEFAULT_COMPACT_FROM,
suffix,
className,
...props
}: MetricValueProps) {
const classes = cn("tabular-nums", className)
if (typeof value !== "number") {
return (
<span data-slot="metric-value" className={classes} {...props}>
{value}
{suffix}
</span>
)
}
const full = formatNumber(value, { format, currency, maximumFractionDigits })
const abbreviate = format !== "percent" && Math.abs(value) >= compactFrom
if (!abbreviate) {
return (
<span data-slot="metric-value" className={classes} {...props}>
{full}
{suffix}
</span>
)
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
data-slot="metric-value"
data-compact=""
className={cn(classes, "cursor-default")}
{...props}
>
{formatNumber(value, { format, currency, compact: true })}
{suffix}
</span>
</TooltipTrigger>
<TooltipContent className="tabular-nums">
{full}
{suffix}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
export { MetricValue }
registry/dashboardcn/ui/period-tabs.tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
export interface PeriodOption {
value: string
label: string
}
export const defaultPeriods: PeriodOption[] = [
{ value: "week", label: "Week" },
{ value: "month", label: "Month" },
{ value: "year", label: "Year" },
]
export interface PeriodTabsProps
extends Omit<React.ComponentProps<typeof Tabs>, "children"> {
options?: PeriodOption[]
size?: "sm" | "default"
/** Slide a single pill between tabs instead of swapping backgrounds. */
animated?: boolean
}
interface IndicatorRect {
left: number
top: number
width: number
height: number
}
/** Small segmented control for switching a chart's time range. */
function PeriodTabs({
options = defaultPeriods,
size = "sm",
animated = false,
className,
value,
defaultValue,
onValueChange,
...props
}: PeriodTabsProps) {
const listRef = React.useRef<HTMLDivElement>(null)
const [internalValue, setInternalValue] = React.useState(
defaultValue ?? options[0]?.value
)
const active = value ?? internalValue
const [rect, setRect] = React.useState<IndicatorRect | null>(null)
const measure = React.useCallback(() => {
const list = listRef.current
if (!list) return
const trigger = list.querySelector<HTMLElement>('[data-state="active"]')
if (!trigger) return
setRect({
left: trigger.offsetLeft,
top: trigger.offsetTop,
width: trigger.offsetWidth,
height: trigger.offsetHeight,
})
}, [])
React.useLayoutEffect(() => {
if (!animated) return
measure()
}, [animated, active, options, size, measure])
React.useEffect(() => {
if (!animated || !listRef.current) return
const observer = new ResizeObserver(measure)
observer.observe(listRef.current)
return () => observer.disconnect()
}, [animated, measure])
return (
<Tabs
data-slot="period-tabs"
className={cn("w-fit", className)}
value={value}
defaultValue={defaultValue}
onValueChange={(next) => {
setInternalValue(next)
onValueChange?.(next)
}}
{...props}
>
<TabsList
ref={listRef}
className={cn(
"relative",
size === "sm" && "group-data-[orientation=horizontal]/tabs:h-8 p-1"
)}
>
{animated && rect ? (
<span
aria-hidden
data-slot="period-tabs-indicator"
className="pointer-events-none absolute rounded-md bg-background shadow-sm transition-[left,top,width,height] duration-300 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none dark:border dark:border-input dark:bg-input/30"
style={rect}
/>
) : null}
{options.map((option) => (
<TabsTrigger
key={option.value}
value={option.value}
className={cn(
size === "sm" && "h-6 px-2.5 text-xs",
animated &&
"relative z-10 data-[state=active]:bg-transparent group-data-[variant=default]/tabs-list:data-[state=active]:shadow-none dark:data-[state=active]:border-transparent dark:data-[state=active]:bg-transparent"
)}
>
{option.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
)
}
export { PeriodTabs }
registry/dashboardcn/lib/format.ts
export type NumberFormat = "number" | "compact" | "currency" | "percent"
export interface FormatNumberOptions {
format?: NumberFormat
/** ISO 4217 code, used when format is "currency". Defaults to USD. */
currency?: string
locale?: string
maximumFractionDigits?: number
/** Abbreviate large values, e.g. "$158K" instead of "$158,143". */
compact?: boolean
}
const formatterCache = new Map<string, Intl.NumberFormat>()
function getFormatter(locale: string, options: Intl.NumberFormatOptions) {
const key = locale + JSON.stringify(options)
let formatter = formatterCache.get(key)
if (!formatter) {
formatter = new Intl.NumberFormat(locale, options)
formatterCache.set(key, formatter)
}
return formatter
}
/**
* Format a metric value for display.
*
* formatNumber(1234567) -> "1,234,567"
* formatNumber(1234567, { format: "compact" }) -> "1.2M"
* formatNumber(48.2, { format: "currency" }) -> "$48.20"
* formatNumber(158143, { format: "currency", compact: true }) -> "$158K"
* formatNumber(0.124, { format: "percent" }) -> "12.4%"
*/
export function formatNumber(
value: number,
{
format = "number",
currency = "USD",
locale = "en-US",
maximumFractionDigits,
compact = false,
}: FormatNumberOptions = {}
): string {
if (!Number.isFinite(value)) return "—"
// Compact values keep three significant digits: $158K, $1.23M, 41.2K.
const compactOptions: Intl.NumberFormatOptions = compact
? maximumFractionDigits === undefined
? { notation: "compact", maximumSignificantDigits: 3 }
: { notation: "compact", maximumFractionDigits }
: {}
switch (format) {
case "compact":
return getFormatter(locale, {
notation: "compact",
maximumFractionDigits: maximumFractionDigits ?? 1,
}).format(value)
case "currency":
return getFormatter(locale, {
style: "currency",
currency,
maximumFractionDigits: maximumFractionDigits ?? 2,
...compactOptions,
}).format(value)
case "percent":
return getFormatter(locale, {
style: "percent",
maximumFractionDigits: maximumFractionDigits ?? 1,
}).format(value)
default:
return getFormatter(locale, {
maximumFractionDigits: maximumFractionDigits ?? 0,
...compactOptions,
}).format(value)
}
}
/**
* Format a fractional change as a signed percentage.
*
* formatDelta(0.124) -> "+12.4%"
* formatDelta(-0.03) -> "-3.0%"
* formatDelta(0) -> "0.0%"
*/
export function formatDelta(delta: number, locale = "en-US"): string {
if (!Number.isFinite(delta)) return "—"
return getFormatter(locale, {
style: "percent",
signDisplay: "exceptZero",
maximumFractionDigits: 1,
minimumFractionDigits: 1,
}).format(delta)
}
/**
* Fractional change between two values. Returns 0 when there is no baseline.
*/
export function computeDelta(current: number, previous: number): number {
if (!previous) return 0
return (current - previous) / Math.abs(previous)
}