Bar List
A ranked list of horizontal bars with each value in mono on the right.
// Bar List: a ranked list of horizontal bars. Rank numbers are fixed slots down the left; rows
// slide between them on springs when the data re-ranks (FLIP), and every bar resizes on its own
// spring. A bar's leading edge glints while it is still moving, so you can see which values
// changed. Hover, a finger or the arrow keys light one row while the rest recede by subtraction.
//
// Built on viz-kit parts: the focus store (hover is imperative, no re-render per row), patterns,
// the tooltip panel view, formatters and the shared motion. Options: dot bars (a lattice of
// rounded units, unlit units read as "remaining"), hatched remaining tracks, pattern and gradient
// fills, a dashed reference line with a value pill, positive / negative rows diverging from zero,
// the selected-row grammar (one row in the accent), mass-by-value landing, and loading / empty.
import { useEffect, useLayoutEffect, useMemo, useRef, type CSSProperties, type KeyboardEvent, type PointerEvent } from 'react';
import { RollingNumber } from '../shared/RollingNumber';
import { resolveAccent, type Accent } from '../shared/accent';
import { useFocusEffect, useFocusStore } from '../viz-kit/focus';
import { Pattern, isPatternPreset, useSvgId, type PatternPreset } from '../viz-kit/patterns';
import { TipMotion, TooltipView } from '../viz-kit/Tooltip';
import { nearWeight, type Obstacle } from '../viz-kit/chart';
import { Spring, VIZ_SPRING, createLoop, prefersReducedMotion } from '../viz-kit/motion';
import { MINUS, fmtCompact, fmtNumber } from '../viz-kit/scale';
import { useSizeCallback } from '../viz-kit/size';
import { useTouchRead } from '../viz-kit/touch';
import '../viz-kit/viz-kit.css';
import './bar-list.css';
export interface BarListItem {
label: string;
value: number;
}
export interface BarListProps {
/** Your own rows, any order (they are ranked for you). Overrides `rows` and `seed`. */
data?: BarListItem[];
rows?: number;
seed?: number;
/** Tighter rows and smaller type. */
compact?: boolean;
/** Label inside the bar, or on its own line over a thin bar. */
labelPosition?: 'inside' | 'over';
/** Show each row's share of the total beside its value. */
showShare?: boolean;
unit?: string;
/** Accessible name. */
label?: string;
formatValue?: (value: number) => string;
/* --- viz-kit options --- */
/** Continuous bars, or a lattice of rounded units (unlit units are the remainder). */
shape?: 'bar' | 'dots';
/** Bar fill: the quiet translucent bar, or a left-to-right gradient. 'pattern' is the same as setting `pattern`. */
fill?: 'solid' | 'gradient' | 'pattern';
/** A texture for the bars ('none' by default). */
pattern?: PatternPreset | 'none';
/** The rest of each row, from the bar's end to the top value: hatched ("remaining") or faint. */
track?: 'none' | 'hatch' | 'solid';
/** A dashed vertical reference with a value pill: the average, the median or a value. */
reference?: 'none' | 'avg' | 'median' | number;
/** The selected row (by label): it runs in the accent; hovering moves the selection. */
selected?: string | null;
/** Hover look: a soft highlight (default) or the accent. */
hover?: 'soft' | 'hot';
/** A floating panel with rank, value and share. */
tooltip?: 'none' | 'panel';
/** Order: ranked high → low (default), low → high, or as given. */
sort?: 'desc' | 'asc' | 'none';
/** Long bars land later and heavier. */
mass?: boolean;
status?: 'ready' | 'loading' | 'empty';
accent?: Accent;
className?: string;
}
const STAGGER_MS = 45;
const DOT = 8;
const GREEK = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda', 'Mu'];
const clamp = (v: number, lo: number, hi: number) => (v < lo ? lo : v > hi ? hi : v);
function rand(seed: number) {
let a = (Math.floor(seed) | 0) + 0x6d2b79f5;
return () => {
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
/** Neutral seeded rows: Greek-letter names, whole-number values with a long tail. */
export function seededList(rows: number, seed: number): BarListItem[] {
const n = clamp(Math.round(rows) || 1, 1, GREEK.length);
const r = rand(seed);
return Array.from({ length: n }, (_, i) => {
const g = Math.sqrt(-2 * Math.log(Math.max(r(), 1e-9))) * Math.cos(2 * Math.PI * r());
return { label: GREEK[i], value: Math.max(12, Math.round(Math.exp(g * 0.75) * 4800 * (0.35 + r() * 0.4))) };
});
}
interface RowState {
y: Spring;
/** Signed width in % of the span. */
w: Spring;
released: boolean;
order: number;
el: HTMLDivElement | null;
bar: HTMLDivElement | null;
track: HTMLDivElement | null;
edgeA: number;
}
class ListEngine {
rows = new Map<string, RowState>();
stride = 40;
reduced = false;
riseAt = -1;
/** Zero position in % of the track (diverging rows). */
zero = 0;
trackW = 0;
dots = false;
loop = createLoop((dt, now) => this.tick(dt, now));
setData(ranked: { label: string; pct: number; mass: number }[], rise: boolean) {
const seen = new Set<string>();
ranked.forEach((r, rank) => {
seen.add(r.label);
const y = rank * this.stride;
let s = this.rows.get(r.label);
if (!s) {
s = { y: new Spring(y, VIZ_SPRING.land), w: new Spring(0, VIZ_SPRING.land), released: !rise, order: rank, el: null, bar: null, track: null, edgeA: -1 };
this.rows.set(r.label, s);
}
s.w.config = { ...VIZ_SPRING.land, mass: 1 + 1.2 * r.mass };
s.y.target = y;
s.w.target = r.pct;
if (this.reduced) {
s.y.set(y);
s.w.set(r.pct);
s.released = true;
}
});
for (const key of Array.from(this.rows.keys())) if (!seen.has(key)) this.rows.delete(key);
this.loop.wake();
}
setStride(stride: number) {
if (stride === this.stride) return;
const k = stride / this.stride;
this.stride = stride;
for (const s of this.rows.values()) s.y.set(s.y.target * k);
this.paint();
}
startRise(now: number) {
if (this.riseAt >= 0) return;
this.riseAt = now;
this.loop.wake();
}
tick(dt: number, now: number): boolean {
let moving = false;
for (const s of this.rows.values()) {
if (!s.released) {
if (this.riseAt >= 0 && now - this.riseAt >= s.order * STAGGER_MS) s.released = true;
else {
moving = true;
continue;
}
}
if (s.y.step(dt)) moving = true;
if (s.w.step(dt)) moving = true;
}
this.paint();
return moving;
}
paint() {
const z = this.zero;
for (const s of this.rows.values()) {
if (s.el) s.el.style.transform = `translate3d(0,${s.y.value.toFixed(2)}px,0)`;
let w = s.released ? s.w.value : 0;
// Dot bars snap to whole units so a unit is never cut.
if (this.dots && this.trackW > 0) {
const unitPct = (100 * DOT) / this.trackW;
w = Math.sign(w) * Math.round(Math.abs(w) / unitPct) * unitPct;
}
const a = Math.abs(w);
const left = w >= 0 ? z : z - a;
if (s.bar) {
s.bar.style.left = `${clamp(left, 0, 100).toFixed(3)}%`;
s.bar.style.width = `${clamp(a, 0, 100).toFixed(3)}%`;
s.bar.toggleAttribute('data-neg', w < 0);
const heat = s.released ? clamp(Math.abs(s.w.velocity) / 60, 0, 1) : 0;
const q = Math.round(heat * 50) / 50;
if (q !== s.edgeA) {
s.bar.style.setProperty('--heat', String(q));
s.edgeA = q;
}
}
if (s.track) {
// Remaining: from the bar's far end to the end of its side.
const t0 = w >= 0 ? z + a : 0;
const t1 = w >= 0 ? 100 : z - a;
s.track.style.left = `${clamp(t0, 0, 100).toFixed(3)}%`;
s.track.style.width = `${clamp(t1 - t0, 0, 100).toFixed(3)}%`;
}
}
}
}
export function BarList({
data,
rows = 8,
seed = 1,
compact = false,
labelPosition = 'inside',
showShare = true,
unit,
label = 'Ranked values',
formatValue: fmtProp,
shape = 'bar',
fill = 'solid',
pattern = 'none',
track = 'none',
reference = 'none',
selected = null,
hover = 'soft',
tooltip = 'none',
sort = 'desc',
mass = false,
status = 'ready',
accent: accentProp,
className,
}: BarListProps) {
const accent = resolveAccent(accentProp);
const store = useFocusStore();
const uid = useSvgId('bl');
const rootRef = useRef<HTMLDivElement>(null);
const bodyRef = useRef<HTMLDivElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const liveRef = useRef<HTMLDivElement>(null);
const tipHost = useRef<HTMLDivElement>(null);
const tip = useRef<TipMotion | null>(null);
const engineRef = useRef<ListEngine | null>(null);
if (!engineRef.current) engineRef.current = new ListEngine();
const engine = engineRef.current;
engine.reduced = prefersReducedMotion();
engine.dots = shape === 'dots';
const items = useMemo(() => (data && data.length ? data : seededList(rows, seed)), [data, rows, seed]);
const ranked = useMemo(() => {
const r = [...items];
if (sort === 'desc') r.sort((a, b) => b.value - a.value || a.label.localeCompare(b.label));
if (sort === 'asc') r.sort((a, b) => a.value - b.value || a.label.localeCompare(b.label));
return r;
}, [items, sort]);
const total = useMemo(() => ranked.reduce((s, r) => s + Math.abs(r.value), 0), [ranked]);
const posMax = Math.max(0, ...ranked.map((r) => r.value));
const negMax = Math.max(0, ...ranked.map((r) => -r.value));
const span = Math.max(1e-9, posMax + negMax);
const signed = negMax > 0;
engine.zero = (100 * negMax) / span;
const decimals = ranked.some((r) => Math.abs(r.value - Math.round(r.value)) > 1e-9) ? 1 : 0;
const fmt = fmtProp ?? ((v: number) => fmtNumber(v, decimals));
const share = (v: number) => (total > 0 ? `${((100 * Math.abs(v)) / total).toFixed(1)}%` : '—');
const refValue = useMemo(() => {
if (reference === 'none' || !ranked.length) return null;
if (typeof reference === 'number') return reference;
const vals = ranked.map((r) => r.value).sort((a, b) => a - b);
if (reference === 'median') {
const h = vals.length >> 1;
return vals.length % 2 ? vals[h] : (vals[h - 1] + vals[h]) / 2;
}
return vals.reduce((a, b) => a + b, 0) / vals.length;
}, [reference, ranked]);
const over = labelPosition === 'over' || signed || shape === 'dots';
const rowH = over ? (compact ? 26 : 30) : compact ? 26 : 34;
const gap = over ? (compact ? 10 : 14) : compact ? 4 : 6;
const stride = rowH + gap;
const height = Math.max(0, ranked.length * stride - gap);
const domOrder = useMemo(() => [...items].map((r) => r.label).sort(), [items]);
const byLabel = useMemo(() => new Map(items.map((r) => [r.label, r])), [items]);
const rankOf = useMemo(() => new Map(ranked.map((r, i) => [r.label, i])), [ranked]);
useLayoutEffect(() => engine.setStride(stride), [engine, stride]);
useLayoutEffect(() => {
const maxAbs = Math.max(1e-9, ...ranked.map((r) => Math.abs(r.value)));
engine.setData(
ranked.map((r) => ({ label: r.label, pct: (100 * r.value) / span, mass: mass ? Math.abs(r.value) / maxAbs : 0 })),
engine.riseAt < 0 && !engine.reduced,
);
for (const [key, s] of engine.rows) {
const el = listRef.current?.querySelector<HTMLDivElement>(`[data-row="${CSS.escape(key)}"]`) ?? null;
s.el = el;
s.bar = el?.querySelector<HTMLDivElement>('.ebl-bar') ?? null;
s.track = el?.querySelector<HTMLDivElement>('.ebl-rest') ?? null;
s.edgeA = -1;
}
engine.paint();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [engine, ranked, span, mass, status, track]);
useSizeCallback(listRef, (width) => {
const tw = Math.max(0, width - (rootRef.current?.querySelector<HTMLElement>('.ebl-value')?.offsetWidth ?? 0) - 16);
engine.trackW = tw;
rootRef.current?.style.setProperty('--ebl-track', `${tw}px`);
engine.paint();
});
// Rise once the list is on screen and ready.
useEffect(() => {
if (status !== 'ready') return;
const el = rootRef.current;
if (!el) return;
const io = new IntersectionObserver((es) => {
if (es.some((e) => e.isIntersecting)) {
engine.startRise(performance.now());
io.disconnect();
}
});
io.observe(el);
return () => io.disconnect();
}, [engine, status]);
useEffect(() => () => engine.loop.stop(), [engine]);
// Tooltip panel (plain DOM view).
useLayoutEffect(() => {
if (tooltip !== 'panel' || !tipHost.current) return;
tip.current = new TipMotion(new TooltipView(tipHost.current, { roll: true }), { tight: true });
return () => {
tip.current?.destroy();
tip.current = null;
};
}, [tooltip]);
/* ----- focus: imperative, no React render per row ----- */
const paintFocus = (key: string | null) => {
const body = bodyRef.current;
if (!body) return;
const lit = key ?? selected;
body.toggleAttribute('data-has-active', key !== null);
body.querySelectorAll<HTMLElement>('[data-row]').forEach((el) => {
const k = el.getAttribute('data-row');
el.classList.toggle('is-active', k === key);
el.toggleAttribute('data-hot', selected !== null ? k === lit : hover === 'hot' && k === key);
});
const rank = key ? rankOf.get(key) : undefined;
body.querySelectorAll<HTMLElement>('.ebl-rank').forEach((el, i) => el.classList.toggle('is-active', i === rank));
const t = tip.current;
if (t) {
const r = key ? byLabel.get(key) : null;
if (!r || rank === undefined) t.clear();
else {
t.update({
title: `${String(rank + 1).padStart(2, '0')} · ${r.label}`,
rows: [
{ label: unit ? `Value (${unit})` : 'Value', value: fmt(r.value), marker: 'square', color: 'var(--viz-2)', hot: selected === r.label || (selected !== null && key === r.label) },
{ label: 'Share', value: share(r.value), marker: 'dash', color: 'var(--viz-4)', muted: true },
],
});
// In the empty track past this row's bar where there is room, else above or below it;
// never over this row's bar or value, and clear of the other rows' bar ends and values.
const listW = listRef.current?.offsetWidth ?? 0;
const end = (v: number) => clamp(((engine.zero + Math.max(0, (100 * v) / span)) / 100) * engine.trackW + 34, 0, listW);
const valX = Math.min(listW, engine.trackW + 34);
const row = (k: number) => ({ y0: k * stride, y1: k * stride + stride - gap });
const obs: Obstacle[] = [];
ranked.forEach((o, k) => {
if (k === rank) return;
const w = nearWeight(Math.abs(k - rank));
obs.push({ x0: Math.min(34, end(o.value) - 80), x1: end(o.value) + 4, ...row(k), w });
obs.push({ x0: valX, x1: listW, ...row(k), w: w * 0.8 });
});
obs.push({ x0: valX, x1: listW, ...row(rank), w: Infinity });
t.show(
{
bounds: { x0: 0, y0: 0, x1: listW, y1: ranked.length * stride },
plot: { x0: 34, y0: 0, x1: valX, y1: ranked.length * stride },
anchor: { x0: 0, x1: end(r.value) + 2, ...row(rank) },
obstacles: obs,
home: 'r-mid',
gap: 10,
},
{ on: 1 },
);
}
}
};
useFocusEffect(store, (s, p) => {
if (s === p || s.series !== p.series) paintFocus(s.series);
}, [ranked, selected, hover, tooltip]);
useLayoutEffect(() => paintFocus(store.get().series));
const speak = (key: string) => {
const r = byLabel.get(key);
const rank = rankOf.get(key);
if (!r || rank === undefined || !liveRef.current) return;
liveRef.current.textContent = `${rank + 1} of ${ranked.length}. ${r.label}: ${fmt(r.value)}${unit ? ` ${unit}` : ''}${showShare ? `, ${share(r.value)}` : ''}`;
};
const rowAt = (clientY: number): string | null => {
const r = listRef.current!.getBoundingClientRect();
const i = Math.floor((clientY - r.top + gap / 2) / stride);
return ranked[clamp(i, 0, ranked.length - 1)]?.label ?? null;
};
const onPointerMove = (e: PointerEvent<HTMLDivElement>) => {
if (status !== 'ready') return;
if (e.pointerType === 'mouse' || e.buttons) store.set({ series: rowAt(e.clientY), source: 'chart' });
};
const onPointerDown = (e: PointerEvent<HTMLDivElement>) => {
if (status !== 'ready') return;
store.set({ series: rowAt(e.clientY), source: 'chart' });
};
// Mouse leave clears; a touch read stays up until a tap outside, a scroll or Escape.
const touchRead = useTouchRead(() => store.set({ series: null, source: 'chart' }));
const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
if (!ranked.length || status !== 'ready') return;
const last = ranked.length - 1;
const active = store.get().series;
const cur = active ? rankOf.get(active) ?? -1 : -1;
let next: number;
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') next = cur < 0 ? 0 : Math.min(last, cur + 1);
else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') next = cur < 0 ? last : Math.max(0, cur - 1);
else if (e.key === 'Home') next = 0;
else if (e.key === 'End') next = last;
else if (e.key === 'Escape') {
if (active) {
e.preventDefault();
store.set({ series: null, source: 'keyboard' });
}
return;
} else return;
e.preventDefault();
const key = ranked[next].label;
store.set({ series: key, source: 'keyboard' });
speak(key);
};
const shareW = showShare ? 6 : 0;
const valueW = Math.max(...ranked.map((r) => fmt(r.value).length + (signed && r.value > 0 ? 1 : 0)), 1);
const summary = `${label}: ${ranked.length} rows, highest ${ranked[0] ? `${ranked[0].label} ${fmt(ranked[0].value)}` : 'none'}. Arrow keys read rows.`;
const usePattern = (pattern !== 'none' && isPatternPreset(pattern)) || fill === 'pattern';
const patternPreset: PatternPreset = pattern !== 'none' && isPatternPreset(pattern) ? pattern : 'diagonal-up';
const refPct = refValue === null ? null : clamp(engine.zero + (100 * refValue) / span, 0, 100);
return (
<div
ref={rootRef}
className={['ember', 'ebl', compact ? 'ebl--compact' : '', over ? 'ebl--over' : '', className].filter(Boolean).join(' ')}
data-accent={accent}
data-shape={shape}
data-fill={usePattern ? 'pattern' : fill}
data-status={status}
data-signed={signed ? '' : undefined}
data-ref={refPct !== null ? '' : undefined}
style={
{
'--ebl-row': `${rowH}px`,
'--ebl-value-ch': valueW,
'--ebl-share-ch': shareW,
'--ebl-zero': `${engine.zero}%`,
} as CSSProperties
}
>
<svg className="ebl-defs" aria-hidden="true" width="0" height="0">
<defs>
{usePattern && <Pattern id={`${uid}-fill`} preset={patternPreset} color="var(--viz-2)" />}
{track === 'hatch' && <Pattern id={`${uid}-rest`} preset="diagonal-up" color="var(--viz-5)" />}
</defs>
</svg>
<div
ref={bodyRef}
className="ebl-body"
data-u2-plot=""
tabIndex={status === 'ready' ? 0 : -1}
role="list"
aria-label={summary}
aria-busy={status === 'loading' || undefined}
onPointerMove={onPointerMove}
onPointerDown={onPointerDown}
{...touchRead}
onKeyDown={onKeyDown}
onBlur={() => store.set({ series: null, source: 'keyboard' })}
>
<div className="ebl-ranks" aria-hidden="true">
{ranked.map((_, i) => (
<span key={i} className="ebl-rank" style={{ top: i * stride, height: over ? undefined : rowH }}>
{String(i + 1).padStart(2, '0')}
</span>
))}
</div>
<div ref={listRef} className="ebl-list" style={{ height }}>
{refPct !== null && refValue !== null && (
<div className="ebl-ref" aria-hidden="true" style={{ '--ebl-ref-f': refPct / 100 } as CSSProperties}>
<span className="ebl-ref-line" />
<span className="viz-pill viz-pill-ref ebl-ref-pill">
{typeof reference === 'string' ? `${reference} ` : ''}
{fmtCompact(refValue)}
</span>
</div>
)}
{domOrder.map((key) => {
const r = byLabel.get(key)!;
const rank = rankOf.get(key) ?? 0;
const text = signed && r.value > 0 ? `+${fmt(r.value)}` : r.value < 0 ? `${MINUS}${fmt(Math.abs(r.value))}` : fmt(r.value);
return (
<div key={key} data-row={key} role="listitem" aria-label={`${rank + 1}. ${r.label}: ${fmt(r.value)}${unit ? ` ${unit}` : ''}`} className="ebl-row">
<div className="ebl-track" aria-hidden="true">
{track !== 'none' && (
<div className="ebl-rest" data-kind={track}>
{track === 'hatch' && (
<svg width="100%" height="100%" preserveAspectRatio="none">
<rect width="100%" height="100%" fill={`url(#${uid}-rest)`} />
</svg>
)}
</div>
)}
<div className="ebl-bar">
{usePattern && (
<svg className="ebl-pat" width="100%" height="100%" preserveAspectRatio="none">
<rect width="100%" height="100%" fill={`url(#${uid}-fill)`} />
</svg>
)}
<div className="ebl-lit" />
<div className="ebl-edge" />
</div>
<span className="ebl-label">{r.label}</span>
</div>
<div className="ebl-value" aria-hidden="true">
<RollingNumber value={text} />
{showShare ? <span className="ebl-share">{share(r.value)}</span> : null}
</div>
</div>
);
})}
{status !== 'ready' && (
<div className="ebl-state" aria-hidden={status === 'loading'}>
{status === 'loading' ? (
<>
{ranked.map((_, i) => (
<span key={i} className="ebl-ph" style={{ top: i * stride, height: over ? 6 : rowH, width: `${88 - i * (60 / Math.max(1, ranked.length))}%`, animationDelay: `${i * 70}ms` }} />
))}
{/* Centred on the placeholder ladder (its longest bar is 88% wide), not the whole list. */}
<span className="viz-plot-center" style={{ left: 0, top: 0, width: '88%', height: '100%' }}>
<span className="ebl-state-chip">Loading</span>
</span>
</>
) : (
<span className="ebl-state-empty">Nothing ranked yet</span>
)}
</div>
)}
</div>
<div ref={tipHost} className="ebl-tip-host" />
</div>
<div ref={liveRef} className="em-sr" aria-live="polite" />
</div>
);
}
export default BarList;
Customize
012345678908
inside
over
bar
dots
solid
gradient
none
hatch
solid
none
avg
median
012345678900
desc
asc
none
soft
hot
none
panel
ready
loading
empty
012345678901
Rank numbers stay put while rows slide between them on springs when the data re-ranks, and a bar's leading edge glints while it is still resizing. Point at a row, or use the arrow keys, and the rest recede. Options on the viz-kit: dot bars on a unit lattice, hatched remaining tracks, gradient and pattern fills, a dashed reference line with a value pill, positive and negative rows diverging from zero, one selected row in the accent, a tooltip panel, sort order, mass-by-value landing, and loading and empty states.
Install
npx shadcn@latest add https://obsydeui.com/r/bar-list.jsonUsage
Import it from where the CLI puts it, or copy the files below by hand.
import { BarList } from '@/components/obsyde/ember/bar-list/BarList';
export function Example() {
return (
<BarList />
);
}
Files
Plain React, TypeScript and CSS. No dependencies. The download keeps these paths.
ember/bar-list/BarList.tsxcomponentember/bar-list/bar-list.cssstylesember/bar-list/BarListDemo.tsxdemoember/viz-kit/viz-kit.csssharedember/viz-kit/Tooltip.tsxsharedember/viz-kit/focus.tssharedember/viz-kit/patterns.tsxsharedember/viz-kit/roll.tssharedember/shared/rolling-number.csssharedember/shared/spring.tssharedember/shared/accent.tssharedember/viz-kit/scale.tssharedember/viz-kit/motion.tssharedember/viz-kit/chart.tsxsharedember/shared/ember.csssharedember/viz-kit/touch.tssharedember/viz-kit/keyboard.tssharedember/viz-kit/size.tssharedember/shared/RollingNumber.tsxshared
API
| Prop | Type | Default |
|---|---|---|
accentAccent | 'ember' | 'volt' | 'arc' | 'ember' |
rowsRows | number | 8 |
compactCompact | boolean | false |
labelPositionLabel position | 'inside' | 'over' | 'inside' |
shapeShape | 'bar' | 'dots' | 'bar' |
fillFill | 'solid' | 'gradient' | 'solid' |
trackTrack | 'none' | 'hatch' | 'solid' | 'none' |
referenceReference | 'none' | 'avg' | 'median' | 'none' |
selectedSelected rank | number | 0 |
signedPositive & negative | boolean | false |
sortSort | 'desc' | 'asc' | 'none' | 'desc' |
showShareShow share | boolean | true |
unitUnit | string | — |
autoShuffleAuto shuffle | boolean | false |
hoverHover | 'soft' | 'hot' | 'soft' |
tooltipTooltip | 'none' | 'panel' | 'none' |
patternPattern | 'none' | 'diagonal-up' | 'diagonal-down' | 'dots' | 'vertical' | 'crosshatch' | 'none' |
statusStatus | 'ready' | 'loading' | 'empty' | 'ready' |
seedSeed | number | 1 |