Столбчатая диаграмма
BarChart — столбчатая диаграмма на Svelte 5 без сторонних библиотек. Поддерживает шесть вариантов заливки, обычный и процентный стек, горизонтальную раскладку, свечение, подсветку столбца под курсором и «буферный» последний столбец для незавершённого периода.
Интерактивное демо
Пропсы
Заливка
Стек
Раскладка
2
Свечение
Подсветка наведения
Буферный столбец
Браш
Загрузка
<script lang="ts" module>
import type {
BackgroundVariant,
BarVariant,
BrushOptions,
BrushRange,
LegendOptions,
TooltipOptions
} from './types';
export type BarSeries = {
/** Series key — must exist on the data rows and in the config. */
dataKey: string;
/** Fill style of this bar series. */
variant?: BarVariant;
/** Corner radius — falls back to the chart `barRadius`. */
radius?: number;
/** Clicking a bar selects (or clears) the series. */
isClickable?: boolean;
/** Dims every bar except the hovered category. */
enableHoverHighlight?: boolean;
/** Soft outer glow around the bars. */
glowing?: boolean;
/** Renders the last data point as a hatched "buffer" bar. */
bufferBar?: boolean;
};
</script>
<script lang="ts">
import ChartContainer from './ChartContainer.svelte';
import CartesianFrame from './CartesianFrame.svelte';
import ChartTooltip from './ChartTooltip.svelte';
import ChartLegend from './ChartLegend.svelte';
import ChartBrush from './ChartBrush.svelte';
import ChartLoadingPattern from './ChartLoadingPattern.svelte';
import BarPatternDefs from './BarPatternDefs.svelte';
import SeriesColorGradient from './SeriesColorGradient.svelte';
import GlowFilter from './GlowFilter.svelte';
import {
computeBarSlots,
computeCartesianLayout,
computeValueTicks,
resolveOptions,
stackSeries,
type AxisOptions,
type GridOptions
} from './cartesian';
import {
axisValueToPercentFormatter,
roundedRectPath,
toNumber,
type ChartConfig,
type ChartRow,
type Margin,
toLabel
} from './chart-utils';
import { createLoadingData } from './loading.svelte';
type Props = {
data: ChartRow[];
config: ChartConfig;
/** Bars to draw. Defaults to every key of `config`. */
series?: BarSeries[];
/** Category key for the category axis, tooltip label and brush. */
xDataKey?: string;
class?: string;
/** `stacked` piles bars up, `percent` normalizes each stack to 100%. */
stackType?: 'default' | 'stacked' | 'percent';
/** `horizontal` lays bars out left-to-right with categories on the y-axis. */
layout?: 'vertical' | 'horizontal';
barRadius?: number;
barGap?: number;
barCategoryGap?: number | string;
/** Defaults for every series (overridden per series). */
variant?: BarVariant;
isClickable?: boolean;
enableHoverHighlight?: boolean;
glowing?: boolean;
bufferBar?: boolean;
grid?: boolean | GridOptions;
/** Bottom axis (categories, or values when horizontal). */
xAxis?: boolean | AxisOptions;
/** Left axis (values, or categories when horizontal). */
yAxis?: boolean | AxisOptions;
tooltip?: boolean | TooltipOptions;
legend?: boolean | LegendOptions;
brush?: boolean | BrushOptions;
backgroundVariant?: BackgroundVariant;
defaultSelectedDataKey?: string | null;
onSelectionChange?: (dataKey: string | null) => void;
isLoading?: boolean;
loadingBars?: number;
margin?: Margin;
ariaLabel?: string;
};
let {
data,
config,
series,
xDataKey,
class: className = '',
stackType = 'default',
layout: barLayout = 'vertical',
barRadius = 2,
barGap = 4,
barCategoryGap = '10%',
variant = 'default',
isClickable = false,
enableHoverHighlight = false,
glowing = false,
bufferBar = false,
grid = true,
xAxis,
yAxis,
tooltip = true,
legend = false,
brush = false,
backgroundVariant,
defaultSelectedDataKey = null,
onSelectionChange,
isLoading = false,
loadingBars = 12,
margin,
ariaLabel = 'Bar chart'
}: Props = $props();
const chartId = $props.id();
// svelte-ignore state_referenced_locally
let selectedDataKey = $state<string | null>(defaultSelectedDataKey);
let activeIndex = $state<number | null>(null);
let pointer = $state<{ x: number; y: number } | null>(null);
// Resets to the full range whenever the data changes; the brush overrides it.
let range = $derived<BrushRange>({ startIndex: 0, endIndex: Math.max(0, data.length - 1) });
const loading = createLoadingData(() => isLoading, {
points: () => loadingBars,
min: 20,
max: 80
});
const isHorizontal = $derived(barLayout === 'horizontal');
const isStacked = $derived(stackType !== 'default');
const isPercent = $derived(stackType === 'percent');
const gridOpts = $derived.by(() => {
const opts = resolveOptions<GridOptions>(grid, true);
if (!opts) return null;
return {
...opts,
vertical: opts.vertical ?? isHorizontal,
horizontal: opts.horizontal ?? !isHorizontal
};
});
const xAxisOpts = $derived(resolveOptions<AxisOptions>(xAxis, !isHorizontal));
const yAxisOpts = $derived(resolveOptions<AxisOptions>(yAxis, isHorizontal));
const categoryAxisOpts = $derived(isHorizontal ? yAxisOpts : xAxisOpts);
const valueAxisOpts = $derived(isHorizontal ? xAxisOpts : yAxisOpts);
const tooltipOpts = $derived(resolveOptions<TooltipOptions>(tooltip, true));
const legendOpts = $derived(resolveOptions<LegendOptions>(legend, false));
const brushOpts = $derived(resolveOptions<BrushOptions>(brush, false));
const categoryKey = $derived(xDataKey ?? categoryAxisOpts?.dataKey);
const resolvedSeries = $derived(
(series ?? Object.keys(config).map((dataKey): BarSeries => ({ dataKey }))).map((s) => ({
dataKey: s.dataKey,
variant: s.variant ?? variant,
radius: s.radius ?? barRadius,
isClickable: s.isClickable ?? isClickable,
enableHoverHighlight: s.enableHoverHighlight ?? enableHoverHighlight,
glowing: s.glowing ?? glowing,
bufferBar: s.bufferBar ?? bufferBar
}))
);
const keys = $derived(resolvedSeries.map((s) => s.dataKey));
const visibleData = $derived(
brushOpts && !isLoading ? data.slice(range.startIndex, range.endIndex + 1) : data
);
const rows = $derived(isLoading ? (loading.data as ChartRow[]) : visibleData);
const stacks = $derived(
isLoading ? stackSeries(rows, ['loading'], 'default') : stackSeries(rows, keys, stackType)
);
const valueTicks = $derived(
computeValueTicks(stacks, isLoading ? 'default' : stackType, valueAxisOpts)
);
function selectDataKey(key: string | null) {
selectedDataKey = key;
onSelectionChange?.(key);
}
function barOpacity(s: (typeof resolvedSeries)[number], index: number) {
const isSelectedKey = selectedDataKey === null || selectedDataKey === s.dataKey;
const clickOpacity = s.isClickable && selectedDataKey !== null ? (isSelectedKey ? 1 : 0.15) : 1;
if (s.enableHoverHighlight && activeIndex !== null) {
return activeIndex === index ? clickOpacity : clickOpacity * 0.3;
}
return clickOpacity;
}
function fillFor(s: (typeof resolvedSeries)[number], sid: string) {
return s.variant === 'default'
? `url(#${sid}-colors-${s.dataKey})`
: `url(#${sid}-${s.variant}-${s.dataKey})`;
}
function categoryLabel(index: number) {
const raw = categoryKey ? rows[index]?.[categoryKey] : index;
const text = toLabel(raw);
return config[text]?.label ?? text;
}
</script>
<ChartContainer id={chartId} {config} class={className} {isLoading} hasFooter={!!brushOpts}>
{#snippet header()}
{#if legendOpts && (legendOpts.verticalAlign ?? 'top') === 'top'}
<ChartLegend {config} {keys} selected={selectedDataKey} onSelectChange={selectDataKey} options={legendOpts} />
{/if}
{/snippet}
{#snippet bottom()}
{#if legendOpts && legendOpts.verticalAlign === 'bottom'}
<ChartLegend {config} {keys} selected={selectedDataKey} onSelectChange={selectDataKey} options={legendOpts} />
{/if}
{/snippet}
{#snippet footer()}
{#if brushOpts && !isLoading}
<ChartBrush
{data}
{config}
{keys}
xDataKey={categoryKey}
variant="bar"
{barRadius}
stacked={isStacked}
options={brushOpts}
bind:range
class="mt-1"
/>
{/if}
{/snippet}
{#snippet children({ width, height }: { width: number; height: number })}
{@const layout = computeCartesianLayout({
width,
height,
rows,
categoryKey,
valueTicks,
valueFormatter: isPercent ? axisValueToPercentFormatter : undefined,
categoryAxis: isLoading ? null : categoryAxisOpts,
valueAxis: isLoading ? null : valueAxisOpts,
horizontal: isHorizontal,
band: true,
margin
})}
{@const slots = computeBarSlots({
bandSize: layout.bandSize,
slots: isStacked || isLoading ? 1 : resolvedSeries.length,
barGap,
barCategoryGap
})}
{@const barRect = (pair: [number, number], index: number, slot: number) => {
const a = layout.valueToPx(pair[0]);
const b = layout.valueToPx(pair[1]);
const start = layout.categoryPositions[index] + slots.offsetFor(slot);
return isHorizontal
? { x: Math.min(a, b), y: start, width: Math.abs(b - a), height: slots.size }
: { x: start, y: Math.min(a, b), width: slots.size, height: Math.abs(b - a) };
}}
<CartesianFrame
{width}
{height}
{layout}
grid={gridOpts}
{backgroundVariant}
cursor={false}
defaultIndex={tooltipOpts?.defaultIndex}
bind:activeIndex
bind:pointer
{ariaLabel}
>
{#snippet defs()}
{#if isLoading}
<ChartLoadingPattern id={chartId} />
{:else}
{#each resolvedSeries as s, index (s.dataKey)}
{@const sid = `${chartId}-${index}`}
<SeriesColorGradient
id={sid}
dataKey={s.dataKey}
item={config[s.dataKey]}
direction="vertical"
/>
<BarPatternDefs
id={sid}
dataKey={s.dataKey}
item={config[s.dataKey]}
variant={s.variant}
buffer={s.bufferBar}
/>
{#if s.glowing}
<GlowFilter id="{sid}-bar-glow-{s.dataKey}" stdDeviation={8} alpha={0.5} region={100} />
{/if}
{/each}
{/if}
{/snippet}
{#if isLoading}
<g style="mask: url(#{chartId}-loading-mask)" class="text-foreground">
{#each stacks[0] ?? [] as pair, i (i)}
{#if pair}
{@const r = barRect(pair, i, 0)}
<path
d={roundedRectPath(r.x, r.y, r.width, r.height, barRadius)}
fill="currentColor"
fill-opacity="0.15"
/>
{/if}
{/each}
</g>
{:else}
{#each resolvedSeries as s, index (s.dataKey)}
{@const sid = `${chartId}-${index}`}
{@const isStripped = s.variant === 'stripped'}
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
<g
style:cursor={s.isClickable || s.enableHoverHighlight ? 'pointer' : undefined}
onclick={() => {
if (s.isClickable) selectDataKey(selectedDataKey === s.dataKey ? null : s.dataKey);
}}
>
{#each stacks[index] ?? [] as pair, i (i)}
{#if pair && toNumber(rows[i]?.[s.dataKey]) !== null}
{@const r = barRect(pair, i, isStacked ? 0 : index)}
{@const isLastBar = s.bufferBar && i === rows.length - 1}
{@const fill = isLastBar ? `url(#${sid}-buffer-hatched-${s.dataKey})` : fillFor(s, sid)}
<g>
<rect x={r.x} y={r.y} width={r.width} height={r.height} fill="transparent" />
<path
d={roundedRectPath(
r.x,
r.y,
r.width,
Math.max(0, r.height - 3),
isStripped ? [s.radius, s.radius, 0, 0] : s.radius
)}
opacity={barOpacity(s, i)}
{fill}
filter={s.glowing ? `url(#${sid}-bar-glow-${s.dataKey})` : undefined}
stroke={isLastBar ? `url(#${sid}-colors-${s.dataKey})` : undefined}
stroke-width={isLastBar ? 1 : undefined}
class="transition-opacity duration-200"
/>
{#if isStripped}
<path
d={roundedRectPath(r.x, r.y - 4, r.width, 2, 1)}
fill="url(#{sid}-colors-{s.dataKey})"
opacity={barOpacity(s, i)}
class="transition-opacity duration-200"
/>
{/if}
</g>
{/if}
{/each}
</g>
{/each}
{/if}
{#snippet tooltip({ index, x, y }: { index: number; x: number; y: number })}
{#if tooltipOpts && !isLoading}
<ChartTooltip
{x}
{y}
boundsWidth={width}
boundsHeight={height}
label={categoryLabel(index)}
options={tooltipOpts}
items={resolvedSeries
.filter((s) => rows[index]?.[s.dataKey] != null)
.map((s) => ({
key: s.dataKey,
name: config[s.dataKey]?.label ?? s.dataKey,
value: rows[index]?.[s.dataKey],
item: config[s.dataKey],
dimmed: selectedDataKey !== null && selectedDataKey !== s.dataKey
}))}
/>
{/if}
{/snippet}
</CartesianFrame>
{/snippet}
</ChartContainer>
<script lang="ts" module>
import type {
BackgroundVariant,
BarVariant,
BrushOptions,
BrushRange,
LegendOptions,
TooltipOptions
} from './types';
export type BarSeries = {
/** Series key — must exist on the data rows and in the config. */
dataKey: string;
/** Fill style of this bar series. */
variant?: BarVariant;
/** Corner radius — falls back to the chart `barRadius`. */
radius?: number;
/** Clicking a bar selects (or clears) the series. */
isClickable?: boolean;
/** Dims every bar except the hovered category. */
enableHoverHighlight?: boolean;
/** Soft outer glow around the bars. */
glowing?: boolean;
/** Renders the last data point as a hatched "buffer" bar. */
bufferBar?: boolean;
};
</script>
<script lang="ts">
import ChartContainer from './ChartContainer.svelte';
import CartesianFrame from './CartesianFrame.svelte';
import ChartTooltip from './ChartTooltip.svelte';
import ChartLegend from './ChartLegend.svelte';
import ChartBrush from './ChartBrush.svelte';
import ChartLoadingPattern from './ChartLoadingPattern.svelte';
import BarPatternDefs from './BarPatternDefs.svelte';
import SeriesColorGradient from './SeriesColorGradient.svelte';
import GlowFilter from './GlowFilter.svelte';
import {
computeBarSlots,
computeCartesianLayout,
computeValueTicks,
resolveOptions,
stackSeries,
type AxisOptions,
type GridOptions
} from './cartesian';
import {
axisValueToPercentFormatter,
roundedRectPath,
toNumber,
type ChartConfig,
type ChartRow,
type Margin,
toLabel
} from './chart-utils';
import { createLoadingData } from './loading.svelte';
type Props = {
data: ChartRow[];
config: ChartConfig;
/** Bars to draw. Defaults to every key of `config`. */
series?: BarSeries[];
/** Category key for the category axis, tooltip label and brush. */
xDataKey?: string;
class?: string;
/** `stacked` piles bars up, `percent` normalizes each stack to 100%. */
stackType?: 'default' | 'stacked' | 'percent';
/** `horizontal` lays bars out left-to-right with categories on the y-axis. */
layout?: 'vertical' | 'horizontal';
barRadius?: number;
barGap?: number;
barCategoryGap?: number | string;
/** Defaults for every series (overridden per series). */
variant?: BarVariant;
isClickable?: boolean;
enableHoverHighlight?: boolean;
glowing?: boolean;
bufferBar?: boolean;
grid?: boolean | GridOptions;
/** Bottom axis (categories, or values when horizontal). */
xAxis?: boolean | AxisOptions;
/** Left axis (values, or categories when horizontal). */
yAxis?: boolean | AxisOptions;
tooltip?: boolean | TooltipOptions;
legend?: boolean | LegendOptions;
brush?: boolean | BrushOptions;
backgroundVariant?: BackgroundVariant;
defaultSelectedDataKey?: string | null;
onSelectionChange?: (dataKey: string | null) => void;
isLoading?: boolean;
loadingBars?: number;
margin?: Margin;
ariaLabel?: string;
};
let {
data,
config,
series,
xDataKey,
class: className = '',
stackType = 'default',
layout: barLayout = 'vertical',
barRadius = 2,
barGap = 4,
barCategoryGap = '10%',
variant = 'default',
isClickable = false,
enableHoverHighlight = false,
glowing = false,
bufferBar = false,
grid = true,
xAxis,
yAxis,
tooltip = true,
legend = false,
brush = false,
backgroundVariant,
defaultSelectedDataKey = null,
onSelectionChange,
isLoading = false,
loadingBars = 12,
margin,
ariaLabel = 'Bar chart'
}: Props = $props();
const chartId = $props.id();
// svelte-ignore state_referenced_locally
let selectedDataKey = $state<string | null>(defaultSelectedDataKey);
let activeIndex = $state<number | null>(null);
let pointer = $state<{ x: number; y: number } | null>(null);
// Resets to the full range whenever the data changes; the brush overrides it.
let range = $derived<BrushRange>({ startIndex: 0, endIndex: Math.max(0, data.length - 1) });
const loading = createLoadingData(() => isLoading, {
points: () => loadingBars,
min: 20,
max: 80
});
const isHorizontal = $derived(barLayout === 'horizontal');
const isStacked = $derived(stackType !== 'default');
const isPercent = $derived(stackType === 'percent');
const gridOpts = $derived.by(() => {
const opts = resolveOptions<GridOptions>(grid, true);
if (!opts) return null;
return {
...opts,
vertical: opts.vertical ?? isHorizontal,
horizontal: opts.horizontal ?? !isHorizontal
};
});
const xAxisOpts = $derived(resolveOptions<AxisOptions>(xAxis, !isHorizontal));
const yAxisOpts = $derived(resolveOptions<AxisOptions>(yAxis, isHorizontal));
const categoryAxisOpts = $derived(isHorizontal ? yAxisOpts : xAxisOpts);
const valueAxisOpts = $derived(isHorizontal ? xAxisOpts : yAxisOpts);
const tooltipOpts = $derived(resolveOptions<TooltipOptions>(tooltip, true));
const legendOpts = $derived(resolveOptions<LegendOptions>(legend, false));
const brushOpts = $derived(resolveOptions<BrushOptions>(brush, false));
const categoryKey = $derived(xDataKey ?? categoryAxisOpts?.dataKey);
const resolvedSeries = $derived(
(series ?? Object.keys(config).map((dataKey): BarSeries => ({ dataKey }))).map((s) => ({
dataKey: s.dataKey,
variant: s.variant ?? variant,
radius: s.radius ?? barRadius,
isClickable: s.isClickable ?? isClickable,
enableHoverHighlight: s.enableHoverHighlight ?? enableHoverHighlight,
glowing: s.glowing ?? glowing,
bufferBar: s.bufferBar ?? bufferBar
}))
);
const keys = $derived(resolvedSeries.map((s) => s.dataKey));
const visibleData = $derived(
brushOpts && !isLoading ? data.slice(range.startIndex, range.endIndex + 1) : data
);
const rows = $derived(isLoading ? (loading.data as ChartRow[]) : visibleData);
const stacks = $derived(
isLoading ? stackSeries(rows, ['loading'], 'default') : stackSeries(rows, keys, stackType)
);
const valueTicks = $derived(
computeValueTicks(stacks, isLoading ? 'default' : stackType, valueAxisOpts)
);
function selectDataKey(key: string | null) {
selectedDataKey = key;
onSelectionChange?.(key);
}
function barOpacity(s: (typeof resolvedSeries)[number], index: number) {
const isSelectedKey = selectedDataKey === null || selectedDataKey === s.dataKey;
const clickOpacity = s.isClickable && selectedDataKey !== null ? (isSelectedKey ? 1 : 0.15) : 1;
if (s.enableHoverHighlight && activeIndex !== null) {
return activeIndex === index ? clickOpacity : clickOpacity * 0.3;
}
return clickOpacity;
}
function fillFor(s: (typeof resolvedSeries)[number], sid: string) {
return s.variant === 'default'
? `url(#${sid}-colors-${s.dataKey})`
: `url(#${sid}-${s.variant}-${s.dataKey})`;
}
function categoryLabel(index: number) {
const raw = categoryKey ? rows[index]?.[categoryKey] : index;
const text = toLabel(raw);
return config[text]?.label ?? text;
}
</script>
<ChartContainer id={chartId} {config} class={className} {isLoading} hasFooter={!!brushOpts}>
{#snippet header()}
{#if legendOpts && (legendOpts.verticalAlign ?? 'top') === 'top'}
<ChartLegend {config} {keys} selected={selectedDataKey} onSelectChange={selectDataKey} options={legendOpts} />
{/if}
{/snippet}
{#snippet bottom()}
{#if legendOpts && legendOpts.verticalAlign === 'bottom'}
<ChartLegend {config} {keys} selected={selectedDataKey} onSelectChange={selectDataKey} options={legendOpts} />
{/if}
{/snippet}
{#snippet footer()}
{#if brushOpts && !isLoading}
<ChartBrush
{data}
{config}
{keys}
xDataKey={categoryKey}
variant="bar"
{barRadius}
stacked={isStacked}
options={brushOpts}
bind:range
class="mt-1"
/>
{/if}
{/snippet}
{#snippet children({ width, height }: { width: number; height: number })}
{@const layout = computeCartesianLayout({
width,
height,
rows,
categoryKey,
valueTicks,
valueFormatter: isPercent ? axisValueToPercentFormatter : undefined,
categoryAxis: isLoading ? null : categoryAxisOpts,
valueAxis: isLoading ? null : valueAxisOpts,
horizontal: isHorizontal,
band: true,
margin
})}
{@const slots = computeBarSlots({
bandSize: layout.bandSize,
slots: isStacked || isLoading ? 1 : resolvedSeries.length,
barGap,
barCategoryGap
})}
{@const barRect = (pair: [number, number], index: number, slot: number) => {
const a = layout.valueToPx(pair[0]);
const b = layout.valueToPx(pair[1]);
const start = layout.categoryPositions[index] + slots.offsetFor(slot);
return isHorizontal
? { x: Math.min(a, b), y: start, width: Math.abs(b - a), height: slots.size }
: { x: start, y: Math.min(a, b), width: slots.size, height: Math.abs(b - a) };
}}
<CartesianFrame
{width}
{height}
{layout}
grid={gridOpts}
{backgroundVariant}
cursor={false}
defaultIndex={tooltipOpts?.defaultIndex}
bind:activeIndex
bind:pointer
{ariaLabel}
>
{#snippet defs()}
{#if isLoading}
<ChartLoadingPattern id={chartId} />
{:else}
{#each resolvedSeries as s, index (s.dataKey)}
{@const sid = `${chartId}-${index}`}
<SeriesColorGradient
id={sid}
dataKey={s.dataKey}
item={config[s.dataKey]}
direction="vertical"
/>
<BarPatternDefs
id={sid}
dataKey={s.dataKey}
item={config[s.dataKey]}
variant={s.variant}
buffer={s.bufferBar}
/>
{#if s.glowing}
<GlowFilter id="{sid}-bar-glow-{s.dataKey}" stdDeviation={8} alpha={0.5} region={100} />
{/if}
{/each}
{/if}
{/snippet}
{#if isLoading}
<g style="mask: url(#{chartId}-loading-mask)" class="text-foreground">
{#each stacks[0] ?? [] as pair, i (i)}
{#if pair}
{@const r = barRect(pair, i, 0)}
<path
d={roundedRectPath(r.x, r.y, r.width, r.height, barRadius)}
fill="currentColor"
fill-opacity="0.15"
/>
{/if}
{/each}
</g>
{:else}
{#each resolvedSeries as s, index (s.dataKey)}
{@const sid = `${chartId}-${index}`}
{@const isStripped = s.variant === 'stripped'}
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
<g
style:cursor={s.isClickable || s.enableHoverHighlight ? 'pointer' : undefined}
onclick={() => {
if (s.isClickable) selectDataKey(selectedDataKey === s.dataKey ? null : s.dataKey);
}}
>
{#each stacks[index] ?? [] as pair, i (i)}
{#if pair && toNumber(rows[i]?.[s.dataKey]) !== null}
{@const r = barRect(pair, i, isStacked ? 0 : index)}
{@const isLastBar = s.bufferBar && i === rows.length - 1}
{@const fill = isLastBar ? `url(#${sid}-buffer-hatched-${s.dataKey})` : fillFor(s, sid)}
<g>
<rect x={r.x} y={r.y} width={r.width} height={r.height} fill="transparent" />
<path
d={roundedRectPath(
r.x,
r.y,
r.width,
Math.max(0, r.height - 3),
isStripped ? [s.radius, s.radius, 0, 0] : s.radius
)}
opacity={barOpacity(s, i)}
{fill}
filter={s.glowing ? `url(#${sid}-bar-glow-${s.dataKey})` : undefined}
stroke={isLastBar ? `url(#${sid}-colors-${s.dataKey})` : undefined}
stroke-width={isLastBar ? 1 : undefined}
class="transition-opacity duration-200"
/>
{#if isStripped}
<path
d={roundedRectPath(r.x, r.y - 4, r.width, 2, 1)}
fill="url(#{sid}-colors-{s.dataKey})"
opacity={barOpacity(s, i)}
class="transition-opacity duration-200"
/>
{/if}
</g>
{/if}
{/each}
</g>
{/each}
{/if}
{#snippet tooltip({ index, x, y }: { index: number; x: number; y: number })}
{#if tooltipOpts && !isLoading}
<ChartTooltip
{x}
{y}
boundsWidth={width}
boundsHeight={height}
label={categoryLabel(index)}
options={tooltipOpts}
items={resolvedSeries
.filter((s) => rows[index]?.[s.dataKey] != null)
.map((s) => ({
key: s.dataKey,
name: config[s.dataKey]?.label ?? s.dataKey,
value: rows[index]?.[s.dataKey],
item: config[s.dataKey],
dimmed: selectedDataKey !== null && selectedDataKey !== s.dataKey
}))}
/>
{/if}
{/snippet}
</CartesianFrame>
{/snippet}
</ChartContainer>
Использование
<script lang="ts">
import BarChart from '$lib/components/charts/BarChart.svelte';
import type { ChartConfig } from '$lib/components/charts/chart-utils';
const config = {
desktop: { label: 'Десктоп', colors: { light: ['#047857'], dark: ['#10b981'] } },
mobile: { label: 'Мобильные', colors: { light: ['#be123c'], dark: ['#f43f5e'] } }
} satisfies ChartConfig;
</script>
<BarChart
{data}
{config}
xDataKey="month"
legend={{ isClickable: true }}
series={[
{ dataKey: 'desktop', variant: 'hatched', isClickable: true },
{ dataKey: 'mobile', variant: 'duotone', isClickable: true }
]}
class="h-[320px] w-full"
/><script lang="ts">
import BarChart from '$lib/components/charts/BarChart.svelte';
import type { ChartConfig } from '$lib/components/charts/chart-utils';
const config = {
desktop: { label: 'Десктоп', colors: { light: ['#047857'], dark: ['#10b981'] } },
mobile: { label: 'Мобильные', colors: { light: ['#be123c'], dark: ['#f43f5e'] } }
} satisfies ChartConfig;
</script>
<BarChart
{data}
{config}
xDataKey="month"
legend={{ isClickable: true }}
series={[
{ dataKey: 'desktop', variant: 'hatched', isClickable: true },
{ dataKey: 'mobile', variant: 'duotone', isClickable: true }
]}
class="h-[320px] w-full"
/>Примеры
Варианты заливки
variant="default"
variant="hatched"
variant="duotone"
variant="duotone-reverse"
variant="gradient"
variant="stripped"
<BarChart {data} {config} xDataKey="month" variant="duotone" /><BarChart {data} {config} xDataKey="month" variant="duotone" />Стек и процентный стек
Десктоп
Мобильные
Десктоп
Мобильные
<BarChart {data} {config} xDataKey="month" stackType="stacked" legend />
<BarChart {data} {config} xDataKey="month" stackType="percent" yAxis legend /><BarChart {data} {config} xDataKey="month" stackType="stacked" legend />
<BarChart {data} {config} xDataKey="month" stackType="percent" yAxis legend />Горизонтальная раскладка
Десктоп
Мобильные
<BarChart {data} {config} xDataKey="month" layout="horizontal" barRadius={4} legend /><BarChart {data} {config} xDataKey="month" layout="horizontal" barRadius={4} legend />Подсветка, свечение и буферный столбец
<BarChart {data} {config} xDataKey="month" enableHoverHighlight glowing />
<BarChart {data} {config} xDataKey="month" bufferBar isClickable legend={{ isClickable: true }} /><BarChart {data} {config} xDataKey="month" enableHoverHighlight glowing />
<BarChart {data} {config} xDataKey="month" bufferBar isClickable legend={{ isClickable: true }} />Градиентные цвета и тултип frosted-glass
<BarChart {data} {config} xDataKey="month" barRadius={4} backgroundVariant="dots"
tooltip={{ variant: 'frosted-glass', roundness: 'xl', defaultIndex: 7 }} /><BarChart {data} {config} xDataKey="month" barRadius={4} backgroundVariant="dots"
tooltip={{ variant: 'frosted-glass', roundness: 'xl', defaultIndex: 7 }} />Состояние загрузки
Загрузка
В markdown
:::chart
[type]bar
[title]Продажи по кварталам
[variant]hatched
[stack]stacked
| Квартал | Продукт А | Продукт Б |
|---------|-----------|-----------|
| Q1 | 4200 | 3100 |
| Q2 | 5800 | 4700 |
| Q3 | 5200 | 5400 |
| Q4 | 7100 | 6200 |
::::::chart
[type]bar
[title]Продажи по кварталам
[variant]hatched
[stack]stacked
| Квартал | Продукт А | Продукт Б |
|---------|-----------|-----------|
| Q1 | 4200 | 3100 |
| Q2 | 5800 | 4700 |
| Q3 | 5200 | 5400 |
| Q4 | 7100 | 6200 |
:::Продажи по кварталам
Пропсы
| Пропс | Тип | По умолчанию | Описание |
|---|---|---|---|
data | ChartRow[] | — | Строки данных |
config | ChartConfig | — | Подписи и цвета серий |
series | BarSeries[] | все ключи config | Серии и их настройки |
xDataKey | string | — | Ключ категории |
stackType | 'default' 'stacked' 'percent' | 'default' | Группы, стек или стек 100% |
layout | 'vertical' 'horizontal' | 'vertical' | Ориентация столбцов |
variant | 'default' 'hatched' 'duotone' 'duotone-reverse' 'gradient' 'stripped' | 'default' | Заливка по умолчанию |
barRadius | number | 2 | Скругление углов |
barGap | number | 4 | Зазор между столбцами одной категории |
barCategoryGap | number string | '10%' | Отступ категории с каждой стороны |
glowing | boolean | false | Свечение столбцов |
enableHoverHighlight | boolean | false | Приглушать всё, кроме категории под курсором |
bufferBar | boolean | false | Штрихованный последний столбец |
isClickable | boolean | false | Выбор серии кликом |
grid | boolean GridOptions | true | Сетка |
xAxis / yAxis | boolean AxisOptions | зависит от layout | Нижняя и левая оси |
tooltip | boolean TooltipOptions | true | Тултип |
legend | boolean LegendOptions | false | Легенда |
brush | boolean BrushOptions | false | Выбор диапазона |
backgroundVariant | BackgroundVariant | — | Декоративный фон |
onSelectionChange | (key: string null) => void | — | Смена выбранной серии |
isLoading | boolean | false | Скелетон загрузки |
loadingBars | number | 12 | Столбцов в скелетоне |
class | string | — | Классы контейнера |
BarSeries
| Поле | Тип | Описание |
|---|---|---|
dataKey | string | Ключ серии |
variant | BarVariant | Заливка серии |
radius | number | Скругление серии |
isClickable | boolean | Кликабельность |
enableHoverHighlight | boolean | Подсветка наведения |
glowing | boolean | Свечение |
bufferBar | boolean | Буферный последний столбец |
