Smooth Funnel
Vue chartSmoothly tapering SVG funnel with cubic-bezier transitions between stages. Pure SVG (no ECharts). Each stage is colored independently and shows its own percent pill; a minHeight floor keeps tail stages visible at tiny percents.
Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/smooth-funnel.json$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/smooth-funnel.json$ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/smooth-funnel.json$ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/smooth-funnel.json
Or with the named registry:
npx shadcn-vue@latest add @uipkge/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 | 240 | optional |
showLabels Show the percent pill on each stage. Defaults to true. | boolean | true | optional |
minHeight Minimum segment height in px so tail stages stay visible at tiny percents. Default 18. | number | 18 | optional |
colors Optional fallback palette when `color` is omitted on a stage. | string[] | () => ['#3b82f6' | optional |
class | HTMLAttributes['class'] | — | 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 (2)
-
app/components/ui/charts/smooth-funnel/SmoothFunnel.vue 3.8 kB
<script setup lang="ts"> import { computed } from 'vue' import type { HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' interface FunnelStage { name: string value: number /** Optional override; defaults to chart-1..N from the registry palette. */ color?: string } interface Props { 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[] class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { height: 240, showLabels: true, minHeight: 18, colors: () => ['#3b82f6', '#a855f7', '#34d399', '#facc15', '#fb7185', '#06b6d4'], }) // 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 W = 720 const H = 180 const cy = H / 2 const segments = computed(() => { const stages = props.data const n = stages.length if (n === 0) return [] const segW = 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) * H, props.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 = cy - h0 / 2 const yTop1 = cy - h1 / 2 const yBot0 = cy + h0 / 2 const yBot1 = 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 ?? props.colors[i % props.colors.length], percent: startPct, name: s.name, value: s.value, labelX: x0 + segW * 0.42, labelY: cy, } }) }) const heightStyle = computed(() => (/^\d+$/.test(String(props.height)) ? `${props.height}px` : String(props.height))) </script> <template> <div data-uipkge data-slot="smooth-funnel" tabindex="0" :style="{ height: heightStyle }" :class="cn('focus-visible:ring-ring w-full focus-visible:ring-2 focus-visible:outline-none', props.class)" > <svg :viewBox="`0 0 ${W} ${H}`" class="block h-full w-full" preserveAspectRatio="none" role="img"> <g v-for="(seg, i) in segments" :key="i"> <path :d="seg.d" :fill="seg.color" /> <foreignObject v-if="props.showLabels" :x="seg.labelX - 28" :y="seg.labelY - 12" width="56" height="24"> <div class="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> </template> -
app/components/ui/charts/smooth-funnel/index.ts 0.1 kB
export { default as SmoothFunnel } from './SmoothFunnel.vue'
Raw manifest: https://uipkge.dev/r/vue/smooth-funnel.json