FreeReportv0.1.0한국어

Charts

Bar, horizontal bar, line, area, pie and doughnut charts inside a report.

Report definition (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"
          }
        }
      ]
    }
  }
}

Data

Long lists show their first three items.

{
  "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
    }
  ]
}

Code for this demo

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)) } });
  });
}