TrendChart
시계열 값을 선 또는 막대로 그리고 기간 칩 · 요약 · 기준선을 함께 보여 주는 추이 차트
목차
시간에 따라 변하는 값 하나를 선(line) 또는 막대(bar)로 그리는 컴포넌트입니다. 기간 칩(7일 · 30일 · 12개월), 요약 슬롯(보통
Stat), 목표 · 예산 같은 점선 기준선을 한 덩어리로 배치합니다. 데이터와 모든 표시 문자열은 호출자가 넘기고, 차트는 그 위치만 정합니다.
기본 사용법 (Basic Usage)#
TrendChart(
semanticLabel: '월별 구독자 추이',
summary: const Stat(label: '구독자', value: '12,480명'),
periods: const ['7일', '30일', '12개월'],
selectedPeriodIndex: period,
onPeriodChanged: (index) => setState(() => period = index),
referenceValue: 12000,
referenceLabel: '목표 12,000명',
points: [
CoreTrendPoint(date: DateTime(2026, 8), value: 11200, label: '8월', valueLabel: '11,200명'),
CoreTrendPoint(date: DateTime(2026, 9), value: 12480, label: '9월', valueLabel: '12,480명'),
],
)
TrendChart(
semanticLabel: '월별 구독자 추이',
summary: const Stat(label: '구독자', value: '12,480명'),
periods: const ['7일', '30일', '12개월'],
selectedPeriodIndex: period,
onPeriodChanged: (index) => setState(() => period = index),
referenceValue: 12000,
referenceLabel: '목표 12,000명',
points: [
CoreTrendPoint(date: DateTime(2026, 8), value: 11200, label: '8월', valueLabel: '11,200명'),
CoreTrendPoint(date: DateTime(2026, 9), value: 12480, label: '9월', valueLabel: '12,480명'),
],
)
빠른 오버라이드 (Chain)#
이미 만든 TrendChart 인스턴스에 스타일을 덧붙이려면 withStyle 체인을 씁니다. trendChartStyle
슬롯에 넘기는 것과 같은 결과입니다.
class TrendChartChainExample extends StatefulWidget {
const TrendChartChainExample({super.key});
@override
State<TrendChartChainExample> createState() => _TrendChartChainExampleState();
}
class _TrendChartChainExampleState extends State<TrendChartChainExample> {
@override
Widget build(BuildContext context) {
return TrendChart(
semanticLabel: '월별 구독자 추이',
points: [
CoreTrendPoint(date: DateTime(2026, 4), value: 8200, label: '4월', valueLabel: '8,200명'),
CoreTrendPoint(date: DateTime(2026, 5), value: 9100, label: '5월', valueLabel: '9,100명'),
CoreTrendPoint(date: DateTime(2026, 6), value: 8800, label: '6월', valueLabel: '8,800명'),
CoreTrendPoint(date: DateTime(2026, 7), value: 10400, label: '7월', valueLabel: '10,400명'),
CoreTrendPoint(date: DateTime(2026, 8), value: 11200, label: '8월', valueLabel: '11,200명'),
CoreTrendPoint(date: DateTime(2026, 9), value: 12480, label: '9월', valueLabel: '12,480명'),
],
).withStyle(
const CoreTrendChartStyle(
plotHeight: CoreSpace.space120,
lineWidth: CoreStrokeWidth.stroke3,
seriesColor: CoreColor.token(CoreColors.secondary),
),
);
}
}
class TrendChartChainExample extends StatefulComponent {
const TrendChartChainExample({super.key});
@override
State<TrendChartChainExample> createState() => _TrendChartChainExampleState();
}
class _TrendChartChainExampleState extends State<TrendChartChainExample> {
@override
Component build(BuildContext context) {
return TrendChart(
semanticLabel: '월별 구독자 추이',
points: [
CoreTrendPoint(date: DateTime(2026, 4), value: 8200, label: '4월', valueLabel: '8,200명'),
CoreTrendPoint(date: DateTime(2026, 5), value: 9100, label: '5월', valueLabel: '9,100명'),
CoreTrendPoint(date: DateTime(2026, 6), value: 8800, label: '6월', valueLabel: '8,800명'),
CoreTrendPoint(date: DateTime(2026, 7), value: 10400, label: '7월', valueLabel: '10,400명'),
CoreTrendPoint(date: DateTime(2026, 8), value: 11200, label: '8월', valueLabel: '11,200명'),
CoreTrendPoint(date: DateTime(2026, 9), value: 12480, label: '9월', valueLabel: '12,480명'),
],
).withStyle(
const CoreTrendChartStyle(
plotHeight: CoreSpace.space120,
lineWidth: CoreStrokeWidth.stroke3,
seriesColor: CoreColor.token(CoreColors.secondary),
),
);
}
}
Live Preview#
코드 보기
class TrendChartDefaultExample extends StatefulComponent {
const TrendChartDefaultExample({super.key});
@override
State<TrendChartDefaultExample> createState() => _TrendChartDefaultExampleState();
}
class _TrendChartDefaultExampleState extends State<TrendChartDefaultExample> {
int _period = 1;
@override
Component build(BuildContext context) {
return TrendChart(
semanticLabel: '월별 구독자 추이',
summary: const Stat(label: '구독자', value: '12,480명', change: '+11.4%', changeType: CoreStatChangeType.positive),
periods: const ['7일', '30일', '12개월'],
selectedPeriodIndex: _period,
onPeriodChanged: (index) => setState(() => _period = index),
referenceValue: 12000,
referenceLabel: '목표 12,000명',
points: [
CoreTrendPoint(date: DateTime(2026, 4), value: 8200, label: '4월', valueLabel: '8,200명'),
CoreTrendPoint(date: DateTime(2026, 5), value: 9100, label: '5월', valueLabel: '9,100명'),
CoreTrendPoint(date: DateTime(2026, 6), value: 8800, label: '6월', valueLabel: '8,800명'),
CoreTrendPoint(date: DateTime(2026, 7), value: 10400, label: '7월', valueLabel: '10,400명'),
CoreTrendPoint(date: DateTime(2026, 8), value: 11200, label: '8월', valueLabel: '11,200명'),
CoreTrendPoint(date: DateTime(2026, 9), value: 12480, label: '9월', valueLabel: '12,480명'),
],
);
}
}
class TrendChartDefaultExample extends StatefulWidget {
const TrendChartDefaultExample({super.key});
@override
State<TrendChartDefaultExample> createState() => _TrendChartDefaultExampleState();
}
class _TrendChartDefaultExampleState extends State<TrendChartDefaultExample> {
int _period = 1;
@override
Widget build(BuildContext context) {
return TrendChart(
semanticLabel: '월별 구독자 추이',
summary: const Stat(label: '구독자', value: '12,480명', change: '+11.4%', changeType: CoreStatChangeType.positive),
periods: const ['7일', '30일', '12개월'],
selectedPeriodIndex: _period,
onPeriodChanged: (index) => setState(() => _period = index),
referenceValue: 12000,
referenceLabel: '목표 12,000명',
points: [
CoreTrendPoint(date: DateTime(2026, 4), value: 8200, label: '4월', valueLabel: '8,200명'),
CoreTrendPoint(date: DateTime(2026, 5), value: 9100, label: '5월', valueLabel: '9,100명'),
CoreTrendPoint(date: DateTime(2026, 6), value: 8800, label: '6월', valueLabel: '8,800명'),
CoreTrendPoint(date: DateTime(2026, 7), value: 10400, label: '7월', valueLabel: '10,400명'),
CoreTrendPoint(date: DateTime(2026, 8), value: 11200, label: '8월', valueLabel: '11,200명'),
CoreTrendPoint(date: DateTime(2026, 9), value: 12480, label: '9월', valueLabel: '12,480명'),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 구독자 · 사용량 · 환율 · 매출처럼 한 지표가 시간에 따라 어떻게 움직였는지 보여 줄 때
- 기간을 바꿔 가며 같은 지표를 다시 볼 때(기간 칩)
- 목표나 예산 대비 현재 위치를 함께 보여 줄 때(기준선)
대신 다른 컴포넌트를 사용하세요:
Stat: 추이 없이 현재 값과 변화율만 보여 줄 때Tracker: 기간별 상태(정상 · 경고 · 장애)를 색 구간으로 보여 줄 때Progress: 단일 작업의 완료 비율
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
points |
List<CoreTrendPoint> |
필수 | 표시할 시계열(오래된 것부터) |
variant |
CoreTrendChartVariant |
line |
선(line) 또는 막대(bar) |
referenceValue |
double? |
null |
점선 기준선 값(목표 · 예산) |
referenceLabel |
String? |
null |
기준선 범례 문구 — 값과 함께 있을 때만 범례가 보입니다 |
valueFormatter |
String Function(double)? |
null |
값 축 눈금 포맷. 없으면 라틴 숫자 기본 포맷 |
periods |
List<String> |
[] |
기간 칩 라벨. 비어 있으면 칩 줄을 그리지 않습니다 |
selectedPeriodIndex |
int? |
null |
선택된 기간 칩 인덱스 |
onPeriodChanged |
void Function(int)? |
null |
칩을 누르면 그 인덱스로 호출 |
summary |
Widget? / Component? |
null |
차트 위 요약 슬롯(보통 Stat) |
empty |
Widget? / Component? |
null |
points 가 비었을 때 그래프 대신 그릴 내용 |
semanticLabel |
String? |
null |
그래프 영역의 접근 가능한 이름 |
trendChartStyle |
CoreTrendChartStyle? |
null |
인스턴스 chrome / 치수 override |
CoreTrendPoint#
| 속성 | 타입 | 설명 |
|---|---|---|
date |
DateTime |
이 점이 가리키는 시점. 점은 목록 순서대로 같은 간격에 그려지고, 날짜는 대체 라벨에만 쓰입니다 |
value | double | 측정값(유한수, 음수 허용) |
label |
String? |
호출자가 로케일로 포맷한 날짜 라벨. 없으면 ISO yyyy-MM-dd |
valueLabel |
String? |
호출자가 포맷한 값 문자열. 툴팁과 스크린 리더가 읽습니다 |
CoreTrendChartStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
seriesColor |
CoreColor? |
Colour of the line, its end marker and the bars. |
gridColor |
CoreColor? |
Colour of the horizontal grid lines at each axis tick. |
referenceColor |
CoreColor? |
Colour of the dashed reference line and its legend swatch. |
lineWidth |
double? |
Stroke width of the series line (logical px). |
gridLineWidth |
double? |
Stroke width of each grid line (logical px). |
referenceLineWidth |
double? |
Stroke width of the reference line (logical px). |
referenceDashLength |
double? |
Length of one dash of the reference line (logical px). |
referenceDashSpacing |
double? |
Gap between two dashes of the reference line (logical px). |
markerSize |
double? |
Diameter of the marker on the last point of a line (logical px). |
markerBorderRadius |
CoreBorderRadius? |
Corner radius of the last-point marker. |
barSpacing |
double? |
Horizontal space between two adjacent bars (logical px). |
barBorderRadius |
CoreBorderRadius? |
Corner radius of a bar's far end — the end away from the zero baseline. The baseline end stays square. |
plotHeight |
double? |
Minimum height of the plot area (logical px). The plot grows when the axis labels need more room (large text). |
sectionSpacing |
double? |
Vertical space between the summary, the period chips, the plot and the legend (logical px). Flows into
Column.spacing
/ CSS
gap
.
|
axisLabelSpacing |
double? |
Space between the value axis labels and the plot, and between the plot and the date labels (logical px). |
axisTickCount |
int? |
Target number of value-axis intervals. The axis rounds to a nice step, so the drawn count can differ by one. |
axisLabelStyle |
CoreTextStyle? |
Typography of the value-axis and date-axis labels. |
legendLabelStyle |
CoreTextStyle? |
Typography of the reference-line legend text. |
legendSpacing |
double? |
Space between the legend swatch and its text (logical px). |
legendSwatchWidth |
double? |
Width of the dashed legend swatch (logical px). |
periodSpacing |
double? |
Space between period chips, both along a row and between wrapped rows (logical px). |
periodChipStyle |
CoreChipStyle? |
Chrome forwarded to every period Chip. |
스타일 시스템 (Style System)#
모든 chrome 은 trendChartStyle 슬롯 하나로 들어갑니다. 프로젝트 공통 값은 CoreComponentTheme.trendChart
의 style / variantStyles 에 둡니다.
CoreTrendChartStyle.default*
→ CoreTrendChartTheme.style // 프로젝트 공통
→ CoreTrendChartTheme.variantStyles[variant] // variant 별
→ widget.trendChartStyle // 인스턴스별
기간 칩의 모양은 periodChipStyle(CoreChipStyle)로 Chip 에 그대로 전달됩니다.
변형 (Variants)#
line#
점마다 같은 폭의 구간 중앙을 지나는 선을 그리고, 마지막 점에 표식을 둡니다. 값 축은 데이터 범위에 맞춰 좁게 잡혀 작은 움직임도 보입니다.
bar#
점마다 막대 하나를 0 기준선에서 그립니다. 막대 길이가 곧 값이므로 값 축은 항상 0 을 포함합니다. 음수 막대는 아래로 자라고, 둥근 모서리는 기준선 반대쪽 끝에만 붙습니다.
동작 스펙 (Behavior)#
인터랙션#
각 점은 그래프 높이 전체를 차지하는 구간을 갖고, 그 구간에 마우스를 올리면 Tooltip 이 「날짜, 값」을 보여 줍니다. 기간 칩을 누르면 onPeriodChanged
가 그 인덱스로 호출됩니다 — 선택 상태와 데이터 교체는 호출자가 합니다.
상태 전환#
points 가 비면 그래프 자리에 empty 슬롯을 그립니다(없으면 아무것도 그리지 않음). 요약 슬롯과 기간 칩은 그대로 남아 다른 기간을 고를 수 있습니다. 로딩 중에는 호출자가 같은 자리에
Skeleton 을 넣습니다.
축#
값 축은 데이터와 기준선을 모두 담도록 1 · 2 · 5 × 10ⁿ 간격으로 반올림되고 눈금 수는 axisTickCount(기본 4 구간)를 목표로 합니다. 날짜 축은 첫 · 가운데 · 마지막 점의 라벨만 시작 · 가운데 · 끝에 붙입니다 — 어떤 폭과 글자 크기에서도 라벨이 겹치지 않고, 나머지 날짜는 툴팁과 스크린 리더로 읽을 수 있습니다. 값 축 라벨이
plotHeight 보다 더 많은 높이를 필요로 하면(큰 글자) 그래프가 그만큼 커집니다.
애니메이션#
전환 애니메이션이 없습니다. 데이터가 바뀌면 바로 다시 그립니다.
사용 가이드라인 (Usage Guidelines)#
✅ Do#
label · valueLabel 을 호출자 로케일로 포맷해서 넘기기
CoreTrendPoint(date: date, value: 12480, label: '9월', valueLabel: '12,480명')
이유: 차트는 숫자 · 날짜를 포맷하지 않습니다. 생략하면 ISO 날짜와 그룹 구분 없는 숫자가 툴팁과 스크린 리더에 그대로 나옵니다.
❌ Don't#
막대 차트에 0 에서 먼 값만 넣어 변화를 키워 보이게 하기
// ❌ 막대는 항상 0 에서 자라므로 9,980 과 10,020 의 차이가 거의 안 보인다
TrendChart(variant: CoreTrendChartVariant.bar, points: nearlyFlat)
이유: 막대의 길이는 값 자체를 뜻합니다. 작은 변동을 보여 주려면 축을 좁게 잡는 line 을 쓰세요.
접근성 (Accessibility)#
역할#
그래프 영역은 semanticLabel 을 이름으로 갖는 그룹입니다(Flutter Semantics(container: true, label:), Web
role="group" + aria-label). 그 안에 점마다 하나씩 「날짜, 값」 이름을 가진 항목이 있습니다(Flutter 점별
Semantics(label:), Web role="list" 안의 role="listitem" + aria-label). 이 목록이 그래프의 데이터 표 대체 보기 역할을 합니다.
키보드#
처리하는 키가 없습니다. 기간 칩은 Chip 의 키보드 동작을 따릅니다.
스크린 리더#
그래프 이름을 읽은 뒤 점을 순서대로 「1월 1일, 3건」처럼 읽습니다. 값 축 · 날짜 축 라벨과 선 · 막대 · 격자는 장식이라 보조 기술에서 숨깁니다(Flutter ExcludeSemantics, Web
aria-hidden). 기준선은 범례 문장으로 읽힙니다.
포커스 관리#
그래프 자체는 포커스를 받지 않습니다. 점별 툴팁은 hover 로 열리므로 키보드로는 열 수 없지만, 같은 내용이 각 점의 접근 가능한 이름에 있습니다.
알려진 제약#
- 시리즈는 하나입니다. 여러 시리즈를 비교하려면 차트를 나란히 둡니다.
- 점은 시간 간격과 무관하게 목록 순서대로 같은 간격에 놓입니다.
감속 모션 · 고대비 · 강제 색상 · 최소 터치 타겟처럼 전 컴포넌트에 공통으로 걸리는 축은 전역 접근성 축에서 다룹니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | TrendChart | TrendChart |
| 그래프 | CustomPaint 한 장(격자 · 기준선 · 선 · 막대) |
절대 배치 <div> + 인라인 <svg>(선 · 점선) |
| 위치 계산 | CoreTrendChartGeometry → 픽셀 |
CoreTrendChartGeometry → 퍼센트 |
| 오른쪽→왼쪽 | 페인터가 x 를 뒤집고 구간 Row 가 저절로 뒤집힘 |
inset-inline-start 와 rtl:-scale-x-100 |
| 툴팁 | Tooltip 위젯 | Tooltip 컴포넌트 |