
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
waffle-chartuiReact mirror of @uipkge/waffle-chart — see the Vue registry item for the canonical description.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/waffle-chart.json$npx shadcn@latest add https://uipkge.dev/r/react/waffle-chart.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/waffle-chart.json$bunx shadcn@latest add https://uipkge.dev/r/react/waffle-chart.jsonnpx shadcn@latest add @uipkge-react/waffle-chartInstalls to:components/ui/charts/waffle-chart/| Name | Type / Values | Default | Required |
|---|---|---|---|
data | WaffleSlice[] | — | required |
height | number | string | — | optional |
sizeCells per side (total = size²). Default 10. | number | — | optional |
radiusCell corner radius. Default 2. | number | — | optional |
showLegend | boolean | — | optional |
colors | string[] | — | optional |
className | string | — | optional |
ariaLabelAccessible name announced for the chart image. Defaults to "Chart". | string | — | optional |
Type aliases from this item's source — use them to shape the data you pass in.
WaffleSliceinterface WaffleSlice {
name: string
value: number
color?: string
}'use client'
import * as React from 'react'
import { cn } from '@/lib/utils'
import { ChartFrame } from '../shared'
// WaffleChart — dependency-free SVG part-to-whole grid
// ─────────────────────────────────────────────────────────────────────────
export interface WaffleSlice {
name: string
value: number
color?: string
}
export interface WaffleChartProps {
data: WaffleSlice[]
height?: number | string
/** Cells per side (total = size²). Default 10. */
size?: number
/** Cell corner radius. Default 2. */
radius?: number
showLegend?: boolean
colors?: string[]
className?: string
/** Accessible name announced for the chart image. Defaults to "Chart". */
ariaLabel?: string
}
const DEFAULT_COLORS = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)']
export const WaffleChart = React.forwardRef<HTMLDivElement, WaffleChartProps>(
(
{ data, height = 260, size = 10, radius = 2, showLegend = true, colors = DEFAULT_COLORS, className, ariaLabel },
ref,
) => {
const total = data.reduce((s, d) => s + d.value, 0) || 1
const cells = React.useMemo(() => {
const n = size * size
const counts = data.map((d) => Math.floor((d.value / total) * n))
let rest = n - counts.reduce((s, c) => s + c, 0)
const remainders = data.map((d, i) => ({ i, r: (d.value / total) * n - counts[i]! })).sort((a, b) => b.r - a.r)
for (const { i } of remainders) {
if (rest <= 0) break
counts[i]!++
rest--
}
const out: { color: string; name: string }[] = []
data.forEach((d, i) => {
for (let k = 0; k < counts[i]!; k++) out.push({ color: d.color ?? colors[i % colors.length]!, name: d.name })
})
return out.reverse()
}, [data, size, total, colors])
return (
<ChartFrame
ref={ref}
height={height}
className={cn('flex items-center justify-center gap-5', className)}
ariaLabel={
ariaLabel ||
`Waffle chart: ${data.map((d) => `${d.name} ${Math.round((d.value / total) * 100)}%`).join(', ')}`
}
>
<svg viewBox={`0 0 ${size * 12} ${size * 12}`} className="aspect-square h-full max-h-full" role="presentation">
{cells.map((c, i) => (
<rect
key={i}
x={(i % size) * 12 + 1}
y={Math.floor(i / size) * 12 + 1}
width="10"
height="10"
rx={radius}
fill={c.color}
>
<title>{c.name}</title>
</rect>
))}
</svg>
{showLegend && (
<ul className="space-y-1.5 text-xs">
{data.map((d, i) => (
<li key={d.name} className="flex items-center gap-2">
<span className="size-2.5 rounded-[3px]" style={{ background: d.color ?? colors[i % colors.length] }} />
<span className="text-foreground font-medium">{d.name}</span>
<span className="text-muted-foreground tabular-nums">{Math.round((d.value / total) * 100)}%</span>
</li>
))}
</ul>
)}
</ChartFrame>
)
},
)
WaffleChart.displayName = 'WaffleChart'
export { WaffleChart, type WaffleChartProps, type WaffleSlice } from './WaffleChart'
'use client'
import * as React from 'react'
import * as echartsCore from 'echarts/core'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import {
LineChart as EChartsLineChart,
BarChart as EChartsBarChart,
PieChart as EChartsPieChart,
ScatterChart as EChartsScatterChart,
EffectScatterChart as EChartsEffectScatter,
PictorialBarChart as EChartsPictorialBar,
RadarChart as EChartsRadarChart,
GaugeChart as EChartsGaugeChart,
HeatmapChart as EChartsHeatmap,
TreemapChart as EChartsTreemapChart,
FunnelChart as EChartsFunnelChart,
BoxplotChart as EChartsBoxplotChart,
CandlestickChart as EChartsCandlestickChart,
CustomChart as EChartsCustomChart,
ChordChart as EChartsChordChart,
MapChart as EChartsMapChart,
GraphChart as EChartsGraphChart,
ParallelChart as EChartsParallelChart,
SankeyChart as EChartsSankeyChart,
SunburstChart as EChartsSunburstChart,
ThemeRiverChart,
TreeChart as EChartsTreeChart,
} from 'echarts/charts'
import {
GridComponent,
PolarComponent,
TooltipComponent,
LegendComponent,
MarkLineComponent,
MarkAreaComponent,
MarkPointComponent,
GraphicComponent,
RadarComponent,
VisualMapComponent,
CalendarComponent,
DataZoomComponent,
ParallelComponent,
SingleAxisComponent,
TitleComponent,
} from 'echarts/components'
import ReactECharts from 'echarts-for-react/esm/core'
import { cn } from '@/lib/utils'
export function heightToStyle(height: number | string): string {
return /^\d+$/.test(String(height)) ? `${height}px` : String(height)
}
interface ChartFrameProps {
height: number | string
className?: string
/** Apply the accessible role/tabindex/focus-ring chrome. Some chart
* wrappers ship a bare `w-full` frame -- pass `false` for those. */
focusable?: boolean
/** Accessible name for role="img". Defaults to "Chart". */
ariaLabel?: string
}
/** Shared `<div>` wrapper around the ECharts canvas. */
export const ChartFrame = React.forwardRef<HTMLDivElement, ChartFrameProps & { children: React.ReactNode }>(
({ height, className, focusable = true, ariaLabel, children }, ref) => (
<div
ref={ref}
role="img"
aria-label={ariaLabel || 'Chart'}
tabIndex={focusable ? 0 : undefined}
style={{ height: heightToStyle(height) }}
className={
focusable
? cn('focus-visible:ring-ring w-full focus-visible:ring-2 focus-visible:outline-none', className)
: cn('w-full', className)
}
>
{children}
</div>
),
)
ChartFrame.displayName = 'ChartFrame'
/** ECharts canvas filling its parent frame. */
export function EChart({ option }: { option: any }) {
return (
<ReactECharts
echarts={echartsCore as any}
option={option}
notMerge
lazyUpdate
style={{ width: '100%', height: '100%' }}
/>
)
}
export const echartsCoreModule = echartsCore
// Register every chart type the wrappers need once at module load.
use([
CanvasRenderer,
EChartsLineChart,
EChartsBarChart,
EChartsPieChart,
EChartsScatterChart,
EChartsEffectScatter,
EChartsPictorialBar,
EChartsRadarChart,
EChartsGaugeChart,
EChartsHeatmap,
EChartsTreemapChart,
EChartsFunnelChart,
EChartsBoxplotChart,
EChartsCandlestickChart,
EChartsCustomChart,
EChartsChordChart,
EChartsMapChart,
EChartsGraphChart,
EChartsParallelChart,
EChartsSankeyChart,
EChartsSunburstChart,
ThemeRiverChart,
EChartsTreeChart,
GridComponent,
PolarComponent,
TooltipComponent,
LegendComponent,
MarkLineComponent,
MarkAreaComponent,
MarkPointComponent,
GraphicComponent,
RadarComponent,
VisualMapComponent,
CalendarComponent,
DataZoomComponent,
ParallelComponent,
SingleAxisComponent,
TitleComponent,
])
Raw manifest:https://uipkge.dev/r/react/waffle-chart.json