차트
막대, 가로 막대, 꺾은선, 영역, 원형, 도넛 차트를 보고서 안에.
보고서 정의 (JSON)
{
"name": "월별 실적 차트",
"page": {
"size": "A4",
"margins": [
14,
15,
14,
15
]
},
"font": {
"family": "Nanum Gothic",
"size": 8.5
},
"bands": {
"title": {
"height": 222,
"elements": [
{
"type": "text",
"x": 0,
"y": 0,
"w": 180,
"h": 10,
"text": "2026년 월별 실적",
"style": {
"fontSize": 16,
"bold": true
}
},
{
"type": "text",
"x": 0,
"y": 10,
"w": 180,
"h": 6,
"text": "매출 합계 {sum(sales):#,##0}백만 원 · 영업이익 {sum(sales - cost):#,##0}백만 원",
"style": {
"color": "#4a5568"
}
},
{
"type": "chart",
"x": 0,
"y": 20,
"w": 88,
"h": 64,
"style": {
"border": "0.2 solid #d5dbe3",
"padding": 2
},
"chartType": "bar",
"title": "매출과 원가 (막대)",
"category": "month",
"series": [
{
"expr": "sales",
"label": "매출"
},
{
"expr": "cost",
"label": "원가"
}
],
"legend": true
},
{
"type": "chart",
"x": 92,
"y": 20,
"w": 88,
"h": 64,
"style": {
"border": "0.2 solid #d5dbe3",
"padding": 2
},
"chartType": "line",
"title": "영업이익 (꺾은선)",
"category": "month",
"series": [
{
"expr": "sales - cost",
"label": "영업이익"
}
],
"showValues": true
},
{
"type": "chart",
"x": 0,
"y": 88,
"w": 88,
"h": 64,
"style": {
"border": "0.2 solid #d5dbe3",
"padding": 2
},
"chartType": "area",
"title": "채널별 매출 (누적 영역)",
"category": "month",
"series": [
{
"expr": "online",
"label": "온라인"
},
{
"expr": "sales - online",
"label": "오프라인"
}
],
"stacked": true,
"legend": true
},
{
"type": "chart",
"x": 92,
"y": 88,
"w": 88,
"h": 64,
"style": {
"border": "0.2 solid #d5dbe3",
"padding": 2
},
"chartType": "hbar",
"title": "지역별 매출 (가로 막대)",
"source": "data.regions",
"category": "region",
"series": [
{
"expr": "sales",
"label": "매출"
}
],
"showValues": true,
"format": "#,##0"
},
{
"type": "chart",
"x": 0,
"y": 156,
"w": 88,
"h": 64,
"style": {
"border": "0.2 solid #d5dbe3",
"padding": 2
},
"chartType": "pie",
"title": "제품군 비중 (원형)",
"source": "data.categories",
"category": "name",
"series": [
{
"expr": "share"
}
],
"legend": true
},
{
"type": "chart",
"x": 92,
"y": 156,
"w": 88,
"h": 64,
"style": {
"border": "0.2 solid #d5dbe3",
"padding": 2
},
"chartType": "doughnut",
"title": "채널 비중 (도넛)",
"source": "data.channels",
"category": "channel",
"series": [
{
"expr": "sales"
}
],
"legend": true
}
]
},
"columnHeader": {
"height": 7,
"elements": [
{
"type": "text",
"x": 0,
"y": 0,
"w": 30,
"h": 7,
"text": "월",
"style": {
"bold": true,
"align": "center",
"background": "#eef1f6",
"border": "0.2 solid #8a94a6"
}
},
{
"type": "text",
"x": 30,
"y": 0,
"w": 38,
"h": 7,
"text": "매출",
"style": {
"bold": true,
"align": "center",
"background": "#eef1f6",
"border": "0.2 solid #8a94a6"
}
},
{
"type": "text",
"x": 68,
"y": 0,
"w": 38,
"h": 7,
"text": "원가",
"style": {
"bold": true,
"align": "center",
"background": "#eef1f6",
"border": "0.2 solid #8a94a6"
}
},
{
"type": "text",
"x": 106,
"y": 0,
"w": 38,
"h": 7,
"text": "영업이익",
"style": {
"bold": true,
"align": "center",
"background": "#eef1f6",
"border": "0.2 solid #8a94a6"
}
},
{
"type": "text",
"x": 144,
"y": 0,
"w": 36,
"h": 7,
"text": "이익률",
"style": {
"bold": true,
"align": "center",
"background": "#eef1f6",
"border": "0.2 solid #8a94a6"
}
}
]
},
"detail": {
"height": 6,
"elements": [
{
"type": "field",
"x": 0,
"y": 0,
"w": 30,
"h": 6,
"expr": "month",
"style": {
"border": "0.15 solid #b9c1cd",
"align": "center"
}
},
{
"type": "field",
"x": 30,
"y": 0,
"w": 38,
"h": 6,
"expr": "sales",
"format": "#,##0",
"style": {
"border": "0.15 solid #b9c1cd",
"align": "right"
}
},
{
"type": "field",
"x": 68,
"y": 0,
"w": 38,
"h": 6,
"expr": "cost",
"format": "#,##0",
"style": {
"border": "0.15 solid #b9c1cd",
"align": "right"
}
},
{
"type": "field",
"x": 106,
"y": 0,
"w": 38,
"h": 6,
"expr": "sales - cost",
"format": "#,##0",
"style": {
"border": "0.15 solid #b9c1cd",
"align": "right"
}
},
{
"type": "field",
"x": 144,
"y": 0,
"w": 36,
"h": 6,
"expr": "(sales - cost) / sales",
"format": "0.0%",
"style": {
"border": "0.15 solid #b9c1cd",
"align": "right"
}
}
]
},
"summary": {
"height": 7,
"elements": [
{
"type": "text",
"x": 0,
"y": 0,
"w": 30,
"h": 7,
"text": "합계",
"style": {
"bold": true,
"align": "center",
"background": "#eef1f6",
"border": "0.2 solid #8a94a6"
}
},
{
"type": "field",
"x": 30,
"y": 0,
"w": 38,
"h": 7,
"expr": "sum(sales)",
"format": "#,##0",
"style": {
"bold": true,
"align": "right",
"background": "#eef1f6",
"border": "0.2 solid #8a94a6"
}
},
{
"type": "field",
"x": 68,
"y": 0,
"w": 38,
"h": 7,
"expr": "sum(cost)",
"format": "#,##0",
"style": {
"bold": true,
"align": "right",
"background": "#eef1f6",
"border": "0.2 solid #8a94a6"
}
},
{
"type": "field",
"x": 106,
"y": 0,
"w": 38,
"h": 7,
"expr": "sum(sales - cost)",
"format": "#,##0",
"style": {
"bold": true,
"align": "right",
"background": "#eef1f6",
"border": "0.2 solid #8a94a6"
}
},
{
"type": "field",
"x": 144,
"y": 0,
"w": 36,
"h": 7,
"expr": "sum(sales - cost) / sum(sales)",
"format": "0.0%",
"style": {
"bold": true,
"align": "right",
"background": "#eef1f6",
"border": "0.2 solid #8a94a6"
}
}
]
}
}
}데이터
긴 목록은 처음 3건만 보여 줍니다.
{
"rows": [
{
"month": "1월",
"sales": 820,
"cost": 549,
"online": 262
},
{
"month": "2월",
"sales": 945,
"cost": 611,
"online": 317
},
{
"month": "3월",
"sales": 942,
"cost": 564,
"online": 330
},
"… 12 items"
],
"regions": [
{
"region": "서울",
"sales": 4120
},
{
"region": "경기",
"sales": 3380
},
{
"region": "부산",
"sales": 1650
},
"… 6 items"
],
"categories": [
{
"name": "전자",
"share": 46
},
{
"name": "가구",
"share": 21
},
{
"name": "주변기기",
"share": 18
},
{
"name": "소모품",
"share": 15
}
],
"channels": [
{
"channel": "온라인",
"sales": 5098
},
{
"channel": "오프라인",
"sales": 7362
}
]
}이 데모의 코드
import type { ChartElement, ReportDefinition, Style } from '@freereportjs/core';
import { mountViewer, select, tr, type DemoContext } from '../ui';
/** Monthly figures charted six ways; the table under the charts lists the same rows. */
const box: Style = { border: '0.2 solid #d5dbe3', padding: 2 };
const th: Style = { bold: true, align: 'center', background: '#eef1f6', border: '0.2 solid #8a94a6' };
const td: Style = { border: '0.15 solid #b9c1cd', align: 'right' };
const chart = (x: number, y: number, el: Omit<ChartElement, 'type' | 'x' | 'y' | 'w' | 'h'>): ChartElement => ({ type: 'chart', x, y, w: 88, h: 64, style: box, ...el });
/** The title band of a year (the year is part of the heading). */
const titleBand = (year: number): NonNullable<ReportDefinition['bands']['title']> => ({
height: 222,
elements: [
{ type: 'text', x: 0, y: 0, w: 180, h: 10, text: `${year}년 월별 실적`, style: { fontSize: 16, bold: true } },
{ type: 'text', x: 0, y: 10, w: 180, h: 6, text: '매출 합계 {sum(sales):#,##0}백만 원 · 영업이익 {sum(sales - cost):#,##0}백만 원', style: { color: '#4a5568' } },
chart(0, 20, { chartType: 'bar', title: '매출과 원가 (막대)', category: 'month', series: [{ expr: 'sales', label: '매출' }, { expr: 'cost', label: '원가' }], legend: true }),
chart(92, 20, { chartType: 'line', title: '영업이익 (꺾은선)', category: 'month', series: [{ expr: 'sales - cost', label: '영업이익' }], showValues: true }),
chart(0, 88, { chartType: 'area', title: '채널별 매출 (누적 영역)', category: 'month', series: [{ expr: 'online', label: '온라인' }, { expr: 'sales - online', label: '오프라인' }], stacked: true, legend: true }),
chart(92, 88, { chartType: 'hbar', title: '지역별 매출 (가로 막대)', source: 'data.regions', category: 'region', series: [{ expr: 'sales', label: '매출' }], showValues: true, format: '#,##0' }),
chart(0, 156, { chartType: 'pie', title: '제품군 비중 (원형)', source: 'data.categories', category: 'name', series: [{ expr: 'share' }], legend: true }),
chart(92, 156, { chartType: 'doughnut', title: '채널 비중 (도넛)', source: 'data.channels', category: 'channel', series: [{ expr: 'sales' }], legend: true }),
],
});
export const definition: ReportDefinition = {
name: '월별 실적 차트',
page: { size: 'A4', margins: [14, 15, 14, 15] },
font: { family: 'Nanum Gothic', size: 8.5 },
bands: {
title: titleBand(2026),
columnHeader: {
height: 7,
elements: [
{ type: 'text', x: 0, y: 0, w: 30, h: 7, text: '월', style: th },
{ type: 'text', x: 30, y: 0, w: 38, h: 7, text: '매출', style: th },
{ type: 'text', x: 68, y: 0, w: 38, h: 7, text: '원가', style: th },
{ type: 'text', x: 106, y: 0, w: 38, h: 7, text: '영업이익', style: th },
{ type: 'text', x: 144, y: 0, w: 36, h: 7, text: '이익률', style: th },
],
},
detail: {
height: 6,
elements: [
{ type: 'field', x: 0, y: 0, w: 30, h: 6, expr: 'month', style: { ...td, align: 'center' } },
{ type: 'field', x: 30, y: 0, w: 38, h: 6, expr: 'sales', format: '#,##0', style: td },
{ type: 'field', x: 68, y: 0, w: 38, h: 6, expr: 'cost', format: '#,##0', style: td },
{ type: 'field', x: 106, y: 0, w: 38, h: 6, expr: 'sales - cost', format: '#,##0', style: td },
{ type: 'field', x: 144, y: 0, w: 36, h: 6, expr: '(sales - cost) / sales', format: '0.0%', style: td },
],
},
summary: {
height: 7,
elements: [
{ type: 'text', x: 0, y: 0, w: 30, h: 7, text: '합계', style: th },
{ type: 'field', x: 30, y: 0, w: 38, h: 7, expr: 'sum(sales)', format: '#,##0', style: { ...th, align: 'right' } },
{ type: 'field', x: 68, y: 0, w: 38, h: 7, expr: 'sum(cost)', format: '#,##0', style: { ...th, align: 'right' } },
{ type: 'field', x: 106, y: 0, w: 38, h: 7, expr: 'sum(sales - cost)', format: '#,##0', style: { ...th, align: 'right' } },
{ type: 'field', x: 144, y: 0, w: 36, h: 7, expr: 'sum(sales - cost) / sum(sales)', format: '0.0%', style: { ...th, align: 'right' } },
],
},
},
};
/** Twelve months of figures (millions of won) plus region, category and channel totals for the other charts. */
export function makeFigures(year: number) {
const k = year === 2025 ? 0.86 : 1;
const rows = Array.from({ length: 12 }, (_, i) => {
const sales = Math.round((820 + i * 38 + Math.sin(i * 1.3) * 90) * k);
const cost = Math.round(sales * (0.62 + Math.cos(i) * 0.05));
return { month: `${i + 1}월`, sales, cost, online: Math.round(sales * (0.32 + i * 0.015)) };
});
const online = rows.reduce((a, r) => a + r.online, 0);
const total = rows.reduce((a, r) => a + r.sales, 0);
return {
rows,
regions: ([['서울', 4120], ['경기', 3380], ['부산', 1650], ['대구', 980], ['광주', 760], ['대전', 690]] as const).map(([region, s]) => ({ region, sales: Math.round(s * k) })),
categories: [{ name: '전자', share: 46 }, { name: '가구', share: 21 }, { name: '주변기기', share: 18 }, { name: '소모품', share: 15 }],
channels: [{ channel: '온라인', sales: online }, { channel: '오프라인', sales: total - online }],
};
}
export const sampleData = makeFigures(2026);
export default function mount(ctx: DemoContext) {
const viewer = mountViewer(ctx, { definition, data: sampleData, fileName: '월별 실적' });
select(ctx, tr(ctx.lang, { en: 'Year', ko: '연도' }), [['2026', '2026'], ['2025', '2025']], '2026', y => {
viewer.report.setData(makeFigures(Number(y)));
void viewer.setDefinition({ ...definition, bands: { ...definition.bands, title: titleBand(Number(y)) } });
});
}