Площадная диаграмма
AreaChart — площадная диаграмма на Svelte 5 без сторонних библиотек: чистый SVG, цвета через ChartConfig и CSS-переменные, поддержка светлой и тёмной темы. График рисуется сразу, без вступительных анимаций.
Каждая серия сама генерирует свои градиенты и паттерны, поэтому в одном графике можно смешивать заливки, стили линий и кликабельность.
Интерактивное демо
Кликните по легенде или по области, чтобы выделить серию. Браш внизу сужает видимый диапазон.
Пропсы
<script lang="ts" module>
import type {
AreaVariant,
BackgroundVariant,
BrushOptions,
BrushRange,
DotVariant,
LegendOptions,
TooltipOptions
} from './types';
import type { CurveType } from './curves';
import type { StrokeVariant } from './chart-utils';
export type AreaSeries = {
/** Series key — must exist on the data rows and in the config. */
dataKey: string;
/** Fill style of this area. */
variant?: AreaVariant;
strokeVariant?: StrokeVariant;
strokeWidth?: number;
curveType?: CurveType;
connectNulls?: boolean;
/** Clicking the area selects (or clears) the series. */
isClickable?: boolean;
/** Resting point marker, `false` hides it. */
dot?: DotVariant | false;
/** Hovered point marker, `false` hides it. */
activeDot?: DotVariant | false;
};
</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 ChartDot from './ChartDot.svelte';
import ChartLoadingPattern from './ChartLoadingPattern.svelte';
import AreaPatternDefs from './AreaPatternDefs.svelte';
import SeriesColorGradient from './SeriesColorGradient.svelte';
import { areaPath, linePath } from './curves';
import {
computeCartesianLayout,
computeValueTicks,
resolveOptions,
stackSeries,
type AxisOptions,
type GridOptions
} from './cartesian';
import {
axisValueToPercentFormatter,
toNumber,
type ChartConfig,
type ChartRow,
type Margin,
toLabel
} from './chart-utils';
import { createLoadingData } from './loading.svelte';
type Props = {
data: ChartRow[];
config: ChartConfig;
/** Areas to draw. Defaults to every key of `config`. */
series?: AreaSeries[];
/** Category key for the x-axis, tooltip label and brush. */
xDataKey?: string;
class?: string;
curveType?: CurveType;
/** `stacked` piles areas up, `expanded` normalizes the stack to 100%. */
stackType?: 'default' | 'stacked' | 'expanded';
/** Defaults for every series (overridden per series). */
variant?: AreaVariant;
strokeVariant?: StrokeVariant;
dot?: DotVariant | false;
activeDot?: DotVariant | false;
isClickable?: boolean;
grid?: boolean | GridOptions;
xAxis?: boolean | AxisOptions;
yAxis?: boolean | AxisOptions;
tooltip?: boolean | TooltipOptions;
legend?: boolean | LegendOptions;
brush?: boolean | BrushOptions;
defaultSelectedDataKey?: string | null;
onSelectionChange?: (dataKey: string | null) => void;
/** Decorative pattern drawn behind the plot. */
backgroundVariant?: BackgroundVariant;
isLoading?: boolean;
loadingPoints?: number;
margin?: Margin;
ariaLabel?: string;
};
let {
data,
config,
series,
xDataKey,
class: className = '',
curveType = 'linear',
stackType = 'default',
variant = 'gradient',
strokeVariant = 'dashed',
dot = false,
activeDot = false,
isClickable = false,
grid = true,
xAxis = true,
yAxis = false,
tooltip = true,
legend = false,
brush = false,
defaultSelectedDataKey = null,
onSelectionChange,
backgroundVariant,
isLoading = false,
loadingPoints = 14,
margin,
ariaLabel = 'Area chart'
}: Props = $props();
const STROKE_WIDTH = 0.8;
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: () => loadingPoints });
const gridOpts = $derived(resolveOptions<GridOptions>(grid, true));
const xAxisOpts = $derived(resolveOptions<AxisOptions>(xAxis, true));
const yAxisOpts = $derived(resolveOptions<AxisOptions>(yAxis, false));
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 ?? xAxisOpts?.dataKey);
const isExpanded = $derived(stackType === 'expanded');
const resolvedSeries = $derived(
(series ?? Object.keys(config).map((dataKey): AreaSeries => ({ dataKey }))).map((s) => ({
dataKey: s.dataKey,
variant: s.variant ?? variant,
strokeVariant: s.strokeVariant ?? strokeVariant,
strokeWidth: s.strokeWidth ?? STROKE_WIDTH,
curveType: s.curveType ?? curveType,
connectNulls: s.connectNulls ?? false,
isClickable: s.isClickable ?? isClickable,
dot: s.dot ?? dot,
activeDot: s.activeDot ?? activeDot
}))
);
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, yAxisOpts));
function selectDataKey(key: string | null) {
selectedDataKey = key;
onSelectionChange?.(key);
}
function opacityFor(key: string) {
if (selectedDataKey === null || selectedDataKey === key) return { fill: 0.8, stroke: 1, dot: 1 };
return { fill: 0.1, stroke: 0.3, dot: 0.3 };
}
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="area"
{curveType}
stacked={stackType !== 'default'}
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: isExpanded ? axisValueToPercentFormatter : undefined,
categoryAxis: isLoading ? null : xAxisOpts,
valueAxis: isLoading ? null : yAxisOpts,
margin
})}
<CartesianFrame
{width}
{height}
{layout}
grid={gridOpts}
{backgroundVariant}
cursor={!isLoading && (tooltipOpts?.cursor ?? true) && tooltipOpts !== null}
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]} />
<AreaPatternDefs
id={sid}
dataKey={s.dataKey}
variant={s.variant}
unselected={selectedDataKey !== null && selectedDataKey !== s.dataKey}
/>
{/each}
{/if}
{/snippet}
{#if isLoading}
{@const points = rows.map((row, i) => {
const v = toNumber(row.loading) ?? 0;
return { x: layout.categoryPositions[i], y: layout.valueToPx(v), y0: layout.valueToPx(0) };
})}
<g style="mask: url(#{chartId}-loading-mask)">
<path d={areaPath(points, curveType)} fill="currentColor" fill-opacity="0.05" class="text-foreground" />
<path
d={linePath(points, curveType)}
fill="none"
stroke="currentColor"
stroke-opacity="0.5"
class="text-foreground"
/>
</g>
{:else}
{#each resolvedSeries as s, index (s.dataKey)}
{@const sid = `${chartId}-${index}`}
{@const opacity = opacityFor(s.dataKey)}
{@const showUnselected = selectedDataKey !== null && selectedDataKey !== s.dataKey}
{@const points = rows.map((row, i) => {
const pair = stacks[index]?.[i];
if (!pair || toNumber(row[s.dataKey]) === null) return null;
return {
x: layout.categoryPositions[i],
y: layout.valueToPx(pair[1]),
y0: layout.valueToPx(pair[0])
};
})}
{@const isAnimatedDashed = s.strokeVariant === 'animated-dashed'}
{@const isDashed = s.strokeVariant === 'dashed' || isAnimatedDashed}
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
<g
style:cursor={s.isClickable ? 'pointer' : undefined}
onclick={() => {
if (s.isClickable) selectDataKey(selectedDataKey === s.dataKey ? null : s.dataKey);
}}
>
<path
d={areaPath(points, s.curveType, s.connectNulls)}
fill={showUnselected ? `url(#${sid}-unselected)` : `url(#${sid}-${s.variant})`}
fill-opacity={opacity.fill}
stroke="none"
/>
<path
d={linePath(points, s.curveType, s.connectNulls)}
fill="none"
stroke="url(#{sid}-colors-{s.dataKey})"
stroke-opacity={opacity.stroke}
stroke-width={s.strokeWidth}
stroke-dasharray={isDashed ? '3 3' : undefined}
>
{#if isAnimatedDashed && selectedDataKey === null}
<animate
attributeName="stroke-dasharray"
values="3 3; 0 3; 3 3"
dur="1s"
repeatCount="indefinite"
keyTimes="0;0.5;1"
/>
<animate
attributeName="stroke-dashoffset"
values="0; -6"
dur="1s"
repeatCount="indefinite"
keyTimes="0;1"
/>
{/if}
</path>
{#if s.dot}
{#each points as p, i (i)}
{#if p}
<ChartDot
cx={p.x}
cy={p.y}
variant={s.dot}
gradientUrl="url(#{sid}-colors-{s.dataKey})"
fillOpacity={opacity.dot}
/>
{/if}
{/each}
{/if}
</g>
{/each}
{#if activeIndex !== null}
{#each resolvedSeries as s, index (s.dataKey)}
{@const pair = stacks[index]?.[activeIndex]}
{#if s.activeDot && pair && toNumber(rows[activeIndex]?.[s.dataKey]) !== null}
<ChartDot
cx={layout.categoryPositions[activeIndex]}
cy={layout.valueToPx(pair[1])}
variant={s.activeDot}
gradientUrl="url(#{chartId}-{index}-colors-{s.dataKey})"
fillOpacity={opacityFor(s.dataKey).dot}
class="pointer-events-none"
/>
{/if}
{/each}
{/if}
{/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 {
AreaVariant,
BackgroundVariant,
BrushOptions,
BrushRange,
DotVariant,
LegendOptions,
TooltipOptions
} from './types';
import type { CurveType } from './curves';
import type { StrokeVariant } from './chart-utils';
export type AreaSeries = {
/** Series key — must exist on the data rows and in the config. */
dataKey: string;
/** Fill style of this area. */
variant?: AreaVariant;
strokeVariant?: StrokeVariant;
strokeWidth?: number;
curveType?: CurveType;
connectNulls?: boolean;
/** Clicking the area selects (or clears) the series. */
isClickable?: boolean;
/** Resting point marker, `false` hides it. */
dot?: DotVariant | false;
/** Hovered point marker, `false` hides it. */
activeDot?: DotVariant | false;
};
</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 ChartDot from './ChartDot.svelte';
import ChartLoadingPattern from './ChartLoadingPattern.svelte';
import AreaPatternDefs from './AreaPatternDefs.svelte';
import SeriesColorGradient from './SeriesColorGradient.svelte';
import { areaPath, linePath } from './curves';
import {
computeCartesianLayout,
computeValueTicks,
resolveOptions,
stackSeries,
type AxisOptions,
type GridOptions
} from './cartesian';
import {
axisValueToPercentFormatter,
toNumber,
type ChartConfig,
type ChartRow,
type Margin,
toLabel
} from './chart-utils';
import { createLoadingData } from './loading.svelte';
type Props = {
data: ChartRow[];
config: ChartConfig;
/** Areas to draw. Defaults to every key of `config`. */
series?: AreaSeries[];
/** Category key for the x-axis, tooltip label and brush. */
xDataKey?: string;
class?: string;
curveType?: CurveType;
/** `stacked` piles areas up, `expanded` normalizes the stack to 100%. */
stackType?: 'default' | 'stacked' | 'expanded';
/** Defaults for every series (overridden per series). */
variant?: AreaVariant;
strokeVariant?: StrokeVariant;
dot?: DotVariant | false;
activeDot?: DotVariant | false;
isClickable?: boolean;
grid?: boolean | GridOptions;
xAxis?: boolean | AxisOptions;
yAxis?: boolean | AxisOptions;
tooltip?: boolean | TooltipOptions;
legend?: boolean | LegendOptions;
brush?: boolean | BrushOptions;
defaultSelectedDataKey?: string | null;
onSelectionChange?: (dataKey: string | null) => void;
/** Decorative pattern drawn behind the plot. */
backgroundVariant?: BackgroundVariant;
isLoading?: boolean;
loadingPoints?: number;
margin?: Margin;
ariaLabel?: string;
};
let {
data,
config,
series,
xDataKey,
class: className = '',
curveType = 'linear',
stackType = 'default',
variant = 'gradient',
strokeVariant = 'dashed',
dot = false,
activeDot = false,
isClickable = false,
grid = true,
xAxis = true,
yAxis = false,
tooltip = true,
legend = false,
brush = false,
defaultSelectedDataKey = null,
onSelectionChange,
backgroundVariant,
isLoading = false,
loadingPoints = 14,
margin,
ariaLabel = 'Area chart'
}: Props = $props();
const STROKE_WIDTH = 0.8;
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: () => loadingPoints });
const gridOpts = $derived(resolveOptions<GridOptions>(grid, true));
const xAxisOpts = $derived(resolveOptions<AxisOptions>(xAxis, true));
const yAxisOpts = $derived(resolveOptions<AxisOptions>(yAxis, false));
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 ?? xAxisOpts?.dataKey);
const isExpanded = $derived(stackType === 'expanded');
const resolvedSeries = $derived(
(series ?? Object.keys(config).map((dataKey): AreaSeries => ({ dataKey }))).map((s) => ({
dataKey: s.dataKey,
variant: s.variant ?? variant,
strokeVariant: s.strokeVariant ?? strokeVariant,
strokeWidth: s.strokeWidth ?? STROKE_WIDTH,
curveType: s.curveType ?? curveType,
connectNulls: s.connectNulls ?? false,
isClickable: s.isClickable ?? isClickable,
dot: s.dot ?? dot,
activeDot: s.activeDot ?? activeDot
}))
);
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, yAxisOpts));
function selectDataKey(key: string | null) {
selectedDataKey = key;
onSelectionChange?.(key);
}
function opacityFor(key: string) {
if (selectedDataKey === null || selectedDataKey === key) return { fill: 0.8, stroke: 1, dot: 1 };
return { fill: 0.1, stroke: 0.3, dot: 0.3 };
}
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="area"
{curveType}
stacked={stackType !== 'default'}
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: isExpanded ? axisValueToPercentFormatter : undefined,
categoryAxis: isLoading ? null : xAxisOpts,
valueAxis: isLoading ? null : yAxisOpts,
margin
})}
<CartesianFrame
{width}
{height}
{layout}
grid={gridOpts}
{backgroundVariant}
cursor={!isLoading && (tooltipOpts?.cursor ?? true) && tooltipOpts !== null}
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]} />
<AreaPatternDefs
id={sid}
dataKey={s.dataKey}
variant={s.variant}
unselected={selectedDataKey !== null && selectedDataKey !== s.dataKey}
/>
{/each}
{/if}
{/snippet}
{#if isLoading}
{@const points = rows.map((row, i) => {
const v = toNumber(row.loading) ?? 0;
return { x: layout.categoryPositions[i], y: layout.valueToPx(v), y0: layout.valueToPx(0) };
})}
<g style="mask: url(#{chartId}-loading-mask)">
<path d={areaPath(points, curveType)} fill="currentColor" fill-opacity="0.05" class="text-foreground" />
<path
d={linePath(points, curveType)}
fill="none"
stroke="currentColor"
stroke-opacity="0.5"
class="text-foreground"
/>
</g>
{:else}
{#each resolvedSeries as s, index (s.dataKey)}
{@const sid = `${chartId}-${index}`}
{@const opacity = opacityFor(s.dataKey)}
{@const showUnselected = selectedDataKey !== null && selectedDataKey !== s.dataKey}
{@const points = rows.map((row, i) => {
const pair = stacks[index]?.[i];
if (!pair || toNumber(row[s.dataKey]) === null) return null;
return {
x: layout.categoryPositions[i],
y: layout.valueToPx(pair[1]),
y0: layout.valueToPx(pair[0])
};
})}
{@const isAnimatedDashed = s.strokeVariant === 'animated-dashed'}
{@const isDashed = s.strokeVariant === 'dashed' || isAnimatedDashed}
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
<g
style:cursor={s.isClickable ? 'pointer' : undefined}
onclick={() => {
if (s.isClickable) selectDataKey(selectedDataKey === s.dataKey ? null : s.dataKey);
}}
>
<path
d={areaPath(points, s.curveType, s.connectNulls)}
fill={showUnselected ? `url(#${sid}-unselected)` : `url(#${sid}-${s.variant})`}
fill-opacity={opacity.fill}
stroke="none"
/>
<path
d={linePath(points, s.curveType, s.connectNulls)}
fill="none"
stroke="url(#{sid}-colors-{s.dataKey})"
stroke-opacity={opacity.stroke}
stroke-width={s.strokeWidth}
stroke-dasharray={isDashed ? '3 3' : undefined}
>
{#if isAnimatedDashed && selectedDataKey === null}
<animate
attributeName="stroke-dasharray"
values="3 3; 0 3; 3 3"
dur="1s"
repeatCount="indefinite"
keyTimes="0;0.5;1"
/>
<animate
attributeName="stroke-dashoffset"
values="0; -6"
dur="1s"
repeatCount="indefinite"
keyTimes="0;1"
/>
{/if}
</path>
{#if s.dot}
{#each points as p, i (i)}
{#if p}
<ChartDot
cx={p.x}
cy={p.y}
variant={s.dot}
gradientUrl="url(#{sid}-colors-{s.dataKey})"
fillOpacity={opacity.dot}
/>
{/if}
{/each}
{/if}
</g>
{/each}
{#if activeIndex !== null}
{#each resolvedSeries as s, index (s.dataKey)}
{@const pair = stacks[index]?.[activeIndex]}
{#if s.activeDot && pair && toNumber(rows[activeIndex]?.[s.dataKey]) !== null}
<ChartDot
cx={layout.categoryPositions[activeIndex]}
cy={layout.valueToPx(pair[1])}
variant={s.activeDot}
gradientUrl="url(#{chartId}-{index}-colors-{s.dataKey})"
fillOpacity={opacityFor(s.dataKey).dot}
class="pointer-events-none"
/>
{/if}
{/each}
{/if}
{/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 AreaChart from '$lib/components/charts/AreaChart.svelte';
import type { ChartConfig } from '$lib/components/charts/chart-utils';
const data = [
{ month: 'Январь', desktop: 342, mobile: 245 },
{ month: 'Февраль', desktop: 876, mobile: 654 },
{ month: 'Март', desktop: 512, mobile: 387 }
];
const config = {
desktop: { label: 'Десктоп', colors: { light: ['#047857'], dark: ['#10b981'] } },
mobile: { label: 'Мобильные', colors: { light: ['#be123c'], dark: ['#f43f5e'] } }
} satisfies ChartConfig;
</script>
<AreaChart
{data}
{config}
xDataKey="month"
stackType="stacked"
legend={{ isClickable: true }}
series={[
{ dataKey: 'desktop', variant: 'gradient', dot: 'border', activeDot: 'colored-border', isClickable: true },
{ dataKey: 'mobile', variant: 'hatched', strokeVariant: 'solid', isClickable: true }
]}
class="h-[320px] w-full"
/><script lang="ts">
import AreaChart from '$lib/components/charts/AreaChart.svelte';
import type { ChartConfig } from '$lib/components/charts/chart-utils';
const data = [
{ month: 'Январь', desktop: 342, mobile: 245 },
{ month: 'Февраль', desktop: 876, mobile: 654 },
{ month: 'Март', desktop: 512, mobile: 387 }
];
const config = {
desktop: { label: 'Десктоп', colors: { light: ['#047857'], dark: ['#10b981'] } },
mobile: { label: 'Мобильные', colors: { light: ['#be123c'], dark: ['#f43f5e'] } }
} satisfies ChartConfig;
</script>
<AreaChart
{data}
{config}
xDataKey="month"
stackType="stacked"
legend={{ isClickable: true }}
series={[
{ dataKey: 'desktop', variant: 'gradient', dot: 'border', activeDot: 'colored-border', isClickable: true },
{ dataKey: 'mobile', variant: 'hatched', strokeVariant: 'solid', isClickable: true }
]}
class="h-[320px] w-full"
/>Без series график рисует все ключи из config, а общие пропсы (variant, strokeVariant, dot, activeDot, isClickable) применяются ко всем сериям.
Примеры
Варианты заливки
<AreaChart {data} {config} xDataKey="month" variant="hatched" curveType="monotone" /><AreaChart {data} {config} xDataKey="month" variant="hatched" curveType="monotone" />Градиентные цвета
Несколько цветов в colors строят горизонтальный градиент — он красит заливку, линию и точки.
<script lang="ts">
const config = {
desktop: {
label: 'Десктоп',
colors: { light: ['#047857', '#0891b2', '#7c3aed'], dark: ['#10b981', '#22d3ee', '#a78bfa'] }
},
mobile: { label: 'Мобильные', colors: { light: ['#be123c', '#ea580c'], dark: ['#f43f5e', '#fbbf24'] } }
} satisfies ChartConfig;
</script>
<AreaChart {data} {config} xDataKey="month" curveType="bump" strokeVariant="solid" dot="colored-border" legend /><script lang="ts">
const config = {
desktop: {
label: 'Десктоп',
colors: { light: ['#047857', '#0891b2', '#7c3aed'], dark: ['#10b981', '#22d3ee', '#a78bfa'] }
},
mobile: { label: 'Мобильные', colors: { light: ['#be123c', '#ea580c'], dark: ['#f43f5e', '#fbbf24'] } }
} satisfies ChartConfig;
</script>
<AreaChart {data} {config} xDataKey="month" curveType="bump" strokeVariant="solid" dot="colored-border" legend />Нормализованный стек
stackType="expanded" приводит каждую точку к 100%, ось Y форматируется в процентах.
<AreaChart {data} {config} xDataKey="month" stackType="expanded" variant="lines" yAxis curveType="natural" /><AreaChart {data} {config} xDataKey="month" stackType="expanded" variant="lines" yAxis curveType="natural" />Ступенчатая кривая и анимированный пунктир
<AreaChart {data} {config} xDataKey="month" curveType="step" strokeVariant="animated-dashed" variant="solid" /><AreaChart {data} {config} xDataKey="month" curveType="step" strokeVariant="animated-dashed" variant="solid" />Состояние загрузки
<AreaChart data={isLoading ? [] : data} {config} {isLoading} curveType="bump" /><AreaChart data={isLoading ? [] : data} {config} {isLoading} curveType="bump" />В markdown
:::chart
[type]area
[title]Посещаемость
[stack]stacked
[variant]gradient
[dots]border
[brush]true
| Месяц | Десктоп | Мобильные |
|-------|---------|-----------|
| Янв | 342 | 245 |
| Фев | 876 | 654 |
| Мар | 512 | 387 |
| Апр | 629 | 521 |
::::::chart
[type]area
[title]Посещаемость
[stack]stacked
[variant]gradient
[dots]border
[brush]true
| Месяц | Десктоп | Мобильные |
|-------|---------|-----------|
| Янв | 342 | 245 |
| Фев | 876 | 654 |
| Мар | 512 | 387 |
| Апр | 629 | 521 |
:::Все параметры блока описаны на странице Основы и markdown.
Пропсы
| Пропс | Тип | По умолчанию | Описание |
|---|---|---|---|
data | ChartRow[] | — | Строки данных |
config | ChartConfig | — | Подписи и цвета серий |
series | AreaSeries[] | все ключи config | Серии и их индивидуальные настройки |
xDataKey | string | — | Ключ категории (ось X, тултип, браш) |
curveType | 'linear' 'monotone' 'natural' 'step' 'bump' ... | 'linear' | Интерполяция кривой |
stackType | 'default' 'stacked' 'expanded' | 'default' | Наложение, стек или стек 100% |
variant | 'gradient' 'gradient-reverse' 'solid' 'dotted' 'lines' 'hatched' | 'gradient' | Заливка по умолчанию |
strokeVariant | 'solid' 'dashed' 'animated-dashed' | 'dashed' | Стиль линии по умолчанию |
dot / activeDot | 'default' 'border' 'colored-border' false | false | Точки и точка под курсором |
isClickable | boolean | false | Выбор серии кликом |
grid | boolean GridOptions | true | Сетка |
xAxis / yAxis | boolean AxisOptions | true / false | Оси |
tooltip | boolean TooltipOptions | true | Тултип |
legend | boolean LegendOptions | false | Легенда |
brush | boolean BrushOptions | false | Мини-график для выбора диапазона |
backgroundVariant | BackgroundVariant | — | Декоративный фон |
defaultSelectedDataKey | string null | null | Выбранная серия при старте |
onSelectionChange | (key: string null) => void | — | Смена выбранной серии |
isLoading | boolean | false | Скелетон загрузки |
loadingPoints | number | 14 | Точек в скелетоне |
class | string | — | Классы контейнера (задайте высоту) |
AreaSeries
| Поле | Тип | Описание |
|---|---|---|
dataKey | string | Ключ серии |
variant | AreaVariant | Заливка серии |
strokeVariant | StrokeVariant | Стиль линии |
strokeWidth | number | Толщина линии (по умолчанию 0.8) |
curveType | CurveType | Кривая серии |
connectNulls | boolean | Соединять пропуски |
isClickable | boolean | Кликабельность серии |
dot / activeDot | DotVariant false | Точки серии |
