Chart container and lightweight sparkline helpers
Bar Chart - Interactive
Showing total visitors for the last 3 months
import { BarSparkline, ChartContainer } from '@/components/ui/chart'
...import { BarSparkline, ChartContainer } from '@/components/ui/chart'
const visitors = [
[222, 150], [97, 180], [167, 120], [242, 260], [373, 290], [301, 340], [245, 180], [409, 320], [59, 110], [261, 190],
[327, 350], [292, 210], [342, 380], [137, 220], [120, 170], [138, 190], [446, 360], [364, 410], [243, 180], [89, 150],
[137, 200], [224, 170], [138, 230], [387, 290], [215, 250], [75, 130], [383, 420], [122, 180], [315, 240], [454, 380],
]
export default function ChartDemoExample() {
let series = $state<'desktop' | 'mobile'>('desktop')
const data = () => visitors.map((values, index) => ({ label: `Apr ${index + 1}`, value: values[series === 'desktop' ? 0 : 1] }))
return (
<ChartContainer class="w-full max-w-2xl">
<header class="flex items-start justify-between gap-4"><div><h3>Bar Chart - Interactive</h3><p>Showing total visitors for the last 3 months</p></div><div class="flex"><button type="button" aria-pressed={series === 'desktop'} onClick={() => { series = 'desktop' }}>Desktop<strong>7,324</strong></button><button type="button" aria-pressed={series === 'mobile'} onClick={() => { series = 'mobile' }}>Mobile<strong>7,250</strong></button></div></header>
<BarSparkline data={data} showGrid showAxis showTooltip primaryLabel={series === 'desktop' ? 'Desktop' : 'Mobile'} />
</ChartContainer>
)
}
Installation
npx @fictjs/shadcn@latest add chartCopy the Fict shadcn component into your project.
Source
components/ui/chart.tsx
src/components/ui/chart.tsx
import { cn } from '@/lib/cn'
export interface ChartPoint {
label: string
value: number
secondaryValue?: number
}
export interface ChartLegendItem {
label: string
colorClass?: string
}
type DivProps = JSX.IntrinsicElements['div']
type SparklineProps = {
data: ChartPoint[] | (() => ChartPoint[])
class?: string
dir?: 'ltr' | 'rtl'
showGrid?: boolean
showAxis?: boolean
showTooltip?: boolean
primaryLabel?: string
secondaryLabel?: string
}
type LegendProps = DivProps & {
items: ChartLegendItem[]
}
type TooltipContentProps = DivProps & {
label?: string
items: Array<{ label: string; value: string | number; colorClass?: string }>
}
function readData(data: SparklineProps['data']): ChartPoint[] {
return typeof data === 'function' ? data() : data
}
function maxValue(data: ChartPoint[]): number {
const values = data.map(point => point.value)
const next = Math.max(...values, 1)
return Number.isFinite(next) ? next : 1
}
export function ChartContainer(props: DivProps) {
const { class: className, ...rest } = props
return <div data-slot='chart-container' class={cn('rounded-lg border bg-card p-4 text-card-foreground', className)} {...rest} />
}
export function BarSparkline(props: SparklineProps) {
return () => {
const data = readData(props.data)
const max = maxValue(data.flatMap(point => [point, { ...point, value: point.secondaryValue ?? 0 }]))
return (
<div data-slot='bar-sparkline' class={cn('relative flex h-40 items-end gap-2', props.showGrid && 'bg-[linear-gradient(to_bottom,var(--border)_1px,transparent_1px)] bg-[size:100%_25%]', props.class)} dir={props.dir}>
{data.map(point => (
<div class='group relative flex h-full flex-1 items-end justify-center gap-1' title={props.showTooltip ? point.label + ': ' + (props.primaryLabel ?? 'Value') + ' ' + String(point.value) + (point.secondaryValue === undefined ? '' : ', ' + (props.secondaryLabel ?? 'Secondary') + ' ' + String(point.secondaryValue)) : undefined}>
<div class='w-full max-w-8 rounded-t-sm bg-primary transition-[height]' style={{ height: String(Math.max((point.value / max) * 128, 4)) + 'px' }} />
{point.secondaryValue === undefined ? null : <div class='w-full max-w-8 rounded-t-sm bg-primary/45 transition-[height]' style={{ height: String(Math.max((point.secondaryValue / max) * 128, 4)) + 'px' }} />}
{props.showAxis ? <span class='absolute -bottom-6 text-xs text-muted-foreground'>{point.label}</span> : null}
</div>
))}
</div>
)
}
}
export function ChartLegend(props: LegendProps) {
const { class: className, items, ...rest } = props
return (
<div class={cn('mt-4 flex flex-wrap items-center gap-4 text-xs text-muted-foreground', className)} {...rest}>
{items.map(item => (
<span class='inline-flex items-center gap-2'>
<span class={cn('h-2.5 w-2.5 rounded-full bg-primary', item.colorClass)} />
{item.label}
</span>
))}
</div>
)
}
export function ChartTooltipContent(props: TooltipContentProps) {
const { class: className, label, items, ...rest } = props
return (
<div role='tooltip' class={cn('grid min-w-32 gap-1.5 rounded-lg border bg-background px-3 py-2 text-xs shadow-xl', className)} {...rest}>
{label ? <strong>{label}</strong> : null}
{items.map(item => <span class='flex items-center justify-between gap-4'><span class='inline-flex items-center gap-2'><i class={cn('h-2 w-2 rounded-sm bg-primary', item.colorClass)} />{item.label}</span><b>{item.value}</b></span>)}
</div>
)
}
Usage
BarSparkline,
ChartContainer,
ChartLegend,
ChartTooltipContent,
} from "@/components/ui/chart"Examples
Example
import { BarSparkline, ChartContainer } from '@/components/ui/chart'
...import { BarSparkline, ChartContainer } from '@/components/ui/chart'
const data = [
{ label: 'Jan', value: 186, secondaryValue: 80 }, { label: 'Feb', value: 305, secondaryValue: 200 },
{ label: 'Mar', value: 237, secondaryValue: 120 }, { label: 'Apr', value: 73, secondaryValue: 190 },
{ label: 'May', value: 209, secondaryValue: 130 }, { label: 'Jun', value: 214, secondaryValue: 140 },
]
export default function ChartExampleExample() {
return <ChartContainer><BarSparkline data={data} primaryLabel="Desktop" secondaryLabel="Mobile" /></ChartContainer>
}
Example Grid
import { BarSparkline, ChartContainer } from '@/components/ui/chart'
...import { BarSparkline, ChartContainer } from '@/components/ui/chart'
const data = [
{ label: 'Jan', value: 186, secondaryValue: 80 }, { label: 'Feb', value: 305, secondaryValue: 200 },
{ label: 'Mar', value: 237, secondaryValue: 120 }, { label: 'Apr', value: 73, secondaryValue: 190 },
{ label: 'May', value: 209, secondaryValue: 130 }, { label: 'Jun', value: 214, secondaryValue: 140 },
]
export default function ChartExampleGridExample() {
return <ChartContainer><BarSparkline data={data} showGrid primaryLabel="Desktop" secondaryLabel="Mobile" /></ChartContainer>
}
Example Axis
import { BarSparkline, ChartContainer } from '@/components/ui/chart'
...import { BarSparkline, ChartContainer } from '@/components/ui/chart'
const data = [
{ label: 'Jan', value: 186, secondaryValue: 80 }, { label: 'Feb', value: 305, secondaryValue: 200 },
{ label: 'Mar', value: 237, secondaryValue: 120 }, { label: 'Apr', value: 73, secondaryValue: 190 },
{ label: 'May', value: 209, secondaryValue: 130 }, { label: 'Jun', value: 214, secondaryValue: 140 },
]
export default function ChartExampleAxisExample() {
return <ChartContainer><BarSparkline data={data} showGrid showAxis primaryLabel="Desktop" secondaryLabel="Mobile" /></ChartContainer>
}
Example Tooltip
import { BarSparkline, ChartContainer } from '@/components/ui/chart'
...import { BarSparkline, ChartContainer } from '@/components/ui/chart'
const data = [
{ label: 'Jan', value: 186, secondaryValue: 80 }, { label: 'Feb', value: 305, secondaryValue: 200 },
{ label: 'Mar', value: 237, secondaryValue: 120 }, { label: 'Apr', value: 73, secondaryValue: 190 },
{ label: 'May', value: 209, secondaryValue: 130 }, { label: 'Jun', value: 214, secondaryValue: 140 },
]
export default function ChartExampleTooltipExample() {
return <ChartContainer><BarSparkline data={data} showGrid showAxis showTooltip primaryLabel="Desktop" secondaryLabel="Mobile" /></ChartContainer>
}
Example Legend
DesktopMobile
import { BarSparkline, ChartContainer, ChartLegend } from '@/components/ui/chart'
...import { BarSparkline, ChartContainer, ChartLegend } from '@/components/ui/chart'
const data = [
{ label: 'Jan', value: 186, secondaryValue: 80 }, { label: 'Feb', value: 305, secondaryValue: 200 },
{ label: 'Mar', value: 237, secondaryValue: 120 }, { label: 'Apr', value: 73, secondaryValue: 190 },
{ label: 'May', value: 209, secondaryValue: 130 }, { label: 'Jun', value: 214, secondaryValue: 140 },
]
export default function ChartExampleLegendExample() {
return <ChartContainer><BarSparkline data={data} showGrid showAxis showTooltip primaryLabel="Desktop" secondaryLabel="Mobile" /><ChartLegend items={[{ label: 'Desktop', colorClass: 'bg-primary' }, { label: 'Mobile', colorClass: 'bg-primary/45' }]} /></ChartContainer>
}
Tooltip
Page ViewsDesktop 186Mobile 80
Chrome 1,286Firefox 1,000
Page ViewsDesktop 12,486
Chrome 1,286
import { ChartTooltipContent } from '@/components/ui/chart'
...import { ChartTooltipContent } from '@/components/ui/chart'
export default function ChartTooltipExample() {
return (
<div class="grid grid-cols-2 gap-4">
<ChartTooltipContent label="Page Views" items={[{ label: 'Desktop', value: 186 }, { label: 'Mobile', value: 80 }]} />
<ChartTooltipContent items={[{ label: 'Chrome', value: '1,286' }, { label: 'Firefox', value: '1,000' }]} />
<ChartTooltipContent label="Page Views" items={[{ label: 'Desktop', value: '12,486' }]} />
<ChartTooltipContent items={[{ label: 'Chrome', value: '1,286' }]} />
</div>
)
}
Rtl
سطح المكتبالجوال
import { BarSparkline, ChartContainer, ChartLegend } from '@/components/ui/chart'
...import { BarSparkline, ChartContainer, ChartLegend } from '@/components/ui/chart'
const translations = {
ar: { dir: 'rtl', months: ['ينا', 'فبر', 'مار', 'أبر', 'ماي', 'يون'], desktop: 'سطح المكتب', mobile: 'الجوال' },
he: { dir: 'rtl', months: ['ינו', 'פבר', 'מרץ', 'אפר', 'מאי', 'יונ'], desktop: 'שולחן עבודה', mobile: 'נייד' },
en: { dir: 'ltr', months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], desktop: 'Desktop', mobile: 'Mobile' },
} as const
const values = [[186, 80], [305, 200], [237, 120], [73, 190], [209, 130], [214, 140]]
export default function ChartRtlExample() {
let language = $state<keyof typeof translations>('ar')
const text = () => translations[language]
const data = () => values.map((value, index) => ({ label: text().months[index], value: value[0], secondaryValue: value[1] })).reverse()
return (
<div class="grid gap-4"><select value={language} onChange={event => { language = event.currentTarget.value as keyof typeof translations }}><option value="ar">Arabic (العربية)</option><option value="he">Hebrew (עברית)</option><option value="en">English</option></select><ChartContainer dir={text().dir}><BarSparkline data={data} dir={text().dir} showGrid showAxis showTooltip primaryLabel={text().desktop} secondaryLabel={text().mobile} /><ChartLegend items={[{ label: text().desktop }, { label: text().mobile, colorClass: 'bg-primary/45' }]} /></ChartContainer></div>
)
}