Smooth Funnel
smooth-funnel ui React mirror of @uipkge/smooth-funnel.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/smooth-funnel.json $ npx shadcn@latest add https://uipkge.dev/r/react/smooth-funnel.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/smooth-funnel.json $ bunx shadcn@latest add https://uipkge.dev/r/react/smooth-funnel.json Named registry:
npx shadcn@latest add @uipkge-react/smooth-funnel Installs to: components/ui/charts/smooth-funnel/ Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
data | FunnelStage[] | — | required |
height Container height (px when numeric, raw CSS when string). Defaults to 240. | number | string | — | optional |
showLabels Show the percent pill on each stage. Defaults to true. | boolean | — | optional |
minHeight Minimum segment height in px so tail stages stay visible at tiny percents. Default 18. | number | — | optional |
colors Optional fallback palette when `color` is omitted on a stage. | string[] | — | optional |
className | string | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
FunnelStage interface FunnelStage {
name: string
value: number
/** Optional override; defaults to chart-1..N from the registry palette. */
color?: string
} Used by
Files installed (2)
-
components/ui/charts/smooth-funnel/SmoothFunnel.tsx 4.4 kB
'use client' import * as React from 'react' import { cn } from '@/lib/utils' function heightToStyle(height: number | string): string { return /^\d+$/.test(String(height)) ? `${height}px` : String(height) } // ───────────────────────────────────────────────────────────────────────── // SmoothFunnel — pure SVG, no ECharts // ───────────────────────────────────────────────────────────────────────── interface FunnelStage { name: string value: number /** Optional override; defaults to chart-1..N from the registry palette. */ color?: string } export interface SmoothFunnelProps { data: FunnelStage[] /** Container height (px when numeric, raw CSS when string). Defaults to 240. */ height?: number | string /** Show the percent pill on each stage. Defaults to true. */ showLabels?: boolean /** Minimum segment height in px so tail stages stay visible at tiny percents. Default 18. */ minHeight?: number /** Optional fallback palette when `color` is omitted on a stage. */ colors?: string[] className?: string } // SVG geometry. Width/height are virtual (the SVG fits to container // via viewBox). The aspect ratio (W:H ≈ 4:1) matches the horizontal // "flow" layout consumers typically want for a 4-6 stage funnel; if // you need taller bands, override `height` and the curves stretch // vertically without distorting horizontally. const SF_W = 720 const SF_H = 180 const SF_CY = SF_H / 2 export const SmoothFunnel = React.forwardRef<HTMLDivElement, SmoothFunnelProps>( ( { data, height = 240, showLabels = true, minHeight = 18, colors = ['#3b82f6', '#a855f7', '#34d399', '#facc15', '#fb7185', '#06b6d4'], className, }, ref, ) => { const segments = React.useMemo(() => { const stages = data const n = stages.length if (n === 0) return [] const segW = SF_W / n const max = Math.max(...stages.map((s) => s.value)) const pctOf = (v: number) => (max > 0 ? (v / max) * 100 : 0) const heightFor = (pct: number) => Math.max((pct / 100) * SF_H, minHeight) return stages.map((s, i) => { const next = stages[i + 1] ?? s const startPct = pctOf(s.value) const endPct = pctOf(next.value) const h0 = heightFor(startPct) const h1 = heightFor(endPct) const x0 = i * segW const x1 = x0 + segW const yTop0 = SF_CY - h0 / 2 const yTop1 = SF_CY - h1 / 2 const yBot0 = SF_CY + h0 / 2 const yBot1 = SF_CY + h1 / 2 // Cubic bezier control points at 38% / 62% of segment width produce // a soft S-curve transition between stages rather than the // trapezoidal default of an ECharts funnel. const cx1 = x0 + segW * 0.38 const cx2 = x0 + segW * 0.62 const d = [ `M ${x0.toFixed(1)} ${yTop0.toFixed(1)}`, `C ${cx1.toFixed(1)} ${yTop0.toFixed(1)}, ${cx2.toFixed(1)} ${yTop1.toFixed(1)}, ${x1.toFixed(1)} ${yTop1.toFixed(1)}`, `L ${x1.toFixed(1)} ${yBot1.toFixed(1)}`, `C ${cx2.toFixed(1)} ${yBot1.toFixed(1)}, ${cx1.toFixed(1)} ${yBot0.toFixed(1)}, ${x0.toFixed(1)} ${yBot0.toFixed(1)}`, 'Z', ].join(' ') return { d, color: s.color ?? colors[i % colors.length], percent: startPct, name: s.name, value: s.value, labelX: x0 + segW * 0.42, labelY: SF_CY, } }) }, [data, minHeight, colors]) return ( <div ref={ref} data-uipkge="" data-slot="smooth-funnel" tabIndex={0} style={{ height: heightToStyle(height) }} className={cn('focus-visible:ring-ring w-full focus-visible:ring-2 focus-visible:outline-none', className)} > <svg viewBox={`0 0 ${SF_W} ${SF_H}`} className="block h-full w-full" preserveAspectRatio="none" role="img"> {segments.map((seg, i) => ( <g key={i}> <path d={seg.d} fill={seg.color} /> {showLabels && ( <foreignObject x={seg.labelX - 28} y={seg.labelY - 12} width={56} height={24}> <div className="bg-background text-foreground inline-flex h-6 items-center rounded-full border px-2 text-[11px] font-semibold shadow-sm"> {Math.round(seg.percent * 10) / 10}% </div> </foreignObject> )} </g> ))} </svg> </div> ) }, ) SmoothFunnel.displayName = 'SmoothFunnel' -
components/ui/charts/smooth-funnel/index.ts 0.1 kB
export { SmoothFunnel, type SmoothFunnelProps } from './SmoothFunnel'
Raw manifest: https://uipkge.dev/r/react/smooth-funnel.json