FreeReportv0.13.0한국어

Region map

Sales by province on a map of Korea: rows keyed by name or code, summed per region, colored on a scale.

Report definition (JSON)

{
  "name": "시도별 매출 지도",
  "page": {
    "size": "A4",
    "margins": [
      14,
      15,
      14,
      15
    ]
  },
  "font": {
    "family": "Nanum Gothic",
    "size": 8.5
  },
  "sort": [
    "-sales"
  ],
  "bands": {
    "title": {
      "height": 150,
      "elements": [
        {
          "type": "text",
          "x": 0,
          "y": 0,
          "w": 180,
          "h": 10,
          "text": "2026년 시도별 매출",
          "style": {
            "fontSize": 16,
            "bold": true
          }
        },
        {
          "type": "map",
          "x": 0,
          "y": 14,
          "w": 104,
          "h": 128,
          "style": {
            "border": "0.2 solid #d5dbe3",
            "padding": 2
          },
          "title": "시도별 매출 (백만 원)",
          "region": "kr-sido",
          "source": "allRows",
          "key": "sido",
          "value": "sales",
          "labels": "both",
          "format": "#,##0",
          "colors": [
            "#EFF6FF",
            "#1D4ED8"
          ]
        },
        {
          "type": "map",
          "x": 108,
          "y": 14,
          "w": 72,
          "h": 92,
          "style": {
            "border": "0.2 solid #d5dbe3",
            "padding": 2
          },
          "title": "전년 대비 성장률",
          "source": "allRows",
          "key": "code",
          "value": "(sales - last) / last",
          "aggregate": "avg",
          "scale": "diverging",
          "format": "0%",
          "labels": "value"
        },
        {
          "type": "text",
          "x": 108,
          "y": 110,
          "w": 72,
          "h": 30,
          "text": "합계 {sum(sales):#,##0}백만 원\n가장 큰 지역: {first(sido)}\n지역 키는 이름(서울특별시·서울)이나 코드(11, 법정동 코드) 모두 됩니다.",
          "style": {
            "color": "#4a5568",
            "fontSize": 8
          }
        }
      ]
    },
    "columnHeader": {
      "height": 7,
      "elements": [
        {
          "type": "text",
          "x": 0,
          "y": 0,
          "w": 60,
          "h": 7,
          "text": "시도",
          "style": {
            "bold": true,
            "align": "center",
            "background": "#eef1f6",
            "border": "0.2 solid #8a94a6"
          }
        },
        {
          "type": "text",
          "x": 60,
          "y": 0,
          "w": 30,
          "h": 7,
          "text": "코드",
          "style": {
            "bold": true,
            "align": "center",
            "background": "#eef1f6",
            "border": "0.2 solid #8a94a6"
          }
        },
        {
          "type": "text",
          "x": 90,
          "y": 0,
          "w": 45,
          "h": 7,
          "text": "매출",
          "style": {
            "bold": true,
            "align": "center",
            "background": "#eef1f6",
            "border": "0.2 solid #8a94a6"
          }
        },
        {
          "type": "text",
          "x": 135,
          "y": 0,
          "w": 45,
          "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": 60,
          "h": 6,
          "expr": "sido",
          "style": {
            "border": "0.15 solid #b9c1cd",
            "align": "left"
          }
        },
        {
          "type": "field",
          "x": 60,
          "y": 0,
          "w": 30,
          "h": 6,
          "expr": "code",
          "style": {
            "border": "0.15 solid #b9c1cd",
            "align": "center"
          }
        },
        {
          "type": "field",
          "x": 90,
          "y": 0,
          "w": 45,
          "h": 6,
          "expr": "sales",
          "format": "#,##0",
          "style": {
            "border": "0.15 solid #b9c1cd",
            "align": "right"
          }
        },
        {
          "type": "field",
          "x": 135,
          "y": 0,
          "w": 45,
          "h": 6,
          "expr": "(sales - last) / last",
          "format": "0.0%",
          "style": {
            "border": "0.15 solid #b9c1cd",
            "align": "right"
          }
        }
      ]
    }
  }
}

Data

Long lists show their first three items.

{
  "rows": [
    {
      "sido": "서울특별시",
      "code": "11",
      "sales": 4120,
      "last": 3583
    },
    {
      "sido": "부산광역시",
      "code": "26",
      "sales": 1748,
      "last": 1840
    },
    {
      "sido": "대구광역시",
      "code": "27",
      "sales": 965,
      "last": 949
    },
    "… 17 items"
  ]
}

Code for this demo

import type { MapElement, ReportDefinition, Style } from '@freereportjs/core';
import { mountViewer, select, tr, type DemoContext } from '../ui';

/**
 * 시도별 매출 on a map of Korea: rows keyed by 시도 name, code or 법정동 code are summed per region and
 * colored on a scale, next to a table of the same rows and a growth map on a diverging scale.
 */
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 salesMap = (labels: MapElement['labels'], steps?: number): MapElement => ({
  type: 'map', x: 0, y: 14, w: 104, h: 128, style: box, title: '시도별 매출 (백만 원)', region: 'kr-sido', source: 'allRows',
  key: 'sido', value: 'sales', labels, format: '#,##0', colors: ['#EFF6FF', '#1D4ED8'], ...(steps ? { steps } : {}),
});

const growthMap: MapElement = {
  type: 'map', x: 108, y: 14, w: 72, h: 92, style: box, title: '전년 대비 성장률', source: 'allRows',
  key: 'code', value: '(sales - last) / last', aggregate: 'avg', scale: 'diverging', format: '0%', labels: 'value',
};

export const definition: ReportDefinition = {
  name: '시도별 매출 지도',
  page: { size: 'A4', margins: [14, 15, 14, 15] },
  font: { family: 'Nanum Gothic', size: 8.5 },
  sort: ['-sales'],
  bands: {
    title: {
      height: 150,
      elements: [
        { type: 'text', x: 0, y: 0, w: 180, h: 10, text: '2026년 시도별 매출', style: { fontSize: 16, bold: true } },
        salesMap('both'),
        growthMap,
        { type: 'text', x: 108, y: 110, w: 72, h: 30, text: '합계 {sum(sales):#,##0}백만 원\n가장 큰 지역: {first(sido)}\n지역 키는 이름(서울특별시·서울)이나 코드(11, 법정동 코드) 모두 됩니다.', style: { color: '#4a5568', fontSize: 8 } },
      ],
    },
    columnHeader: {
      height: 7,
      elements: [
        { type: 'text', x: 0, y: 0, w: 60, h: 7, text: '시도', style: th },
        { type: 'text', x: 60, y: 0, w: 30, h: 7, text: '코드', style: th },
        { type: 'text', x: 90, y: 0, w: 45, h: 7, text: '매출', style: th },
        { type: 'text', x: 135, y: 0, w: 45, h: 7, text: '성장률', style: th },
      ],
    },
    detail: {
      height: 6,
      elements: [
        { type: 'field', x: 0, y: 0, w: 60, h: 6, expr: 'sido', style: { ...td, align: 'left' } },
        { type: 'field', x: 60, y: 0, w: 30, h: 6, expr: 'code', style: { ...td, align: 'center' } },
        { type: 'field', x: 90, y: 0, w: 45, h: 6, expr: 'sales', format: '#,##0', style: td },
        { type: 'field', x: 135, y: 0, w: 45, h: 6, expr: '(sales - last) / last', format: '0.0%', style: td },
      ],
    },
  },
};

const SIDO: [string, string, number][] = [
  ['서울특별시', '11', 4120], ['부산광역시', '26', 1650], ['대구광역시', '27', 980], ['인천광역시', '28', 1210], ['광주광역시', '29', 760],
  ['대전광역시', '30', 690], ['울산광역시', '31', 540], ['세종특별자치시', '36', 210], ['경기도', '41', 3380], ['강원특별자치도', '51', 430],
  ['충청북도', '43', 520], ['충청남도', '44', 700], ['전북특별자치도', '52', 480], ['전라남도', '46', 510], ['경상북도', '47', 820],
  ['경상남도', '48', 1080], ['제주특별자치도', '50', 330],
];

/** One row per 시도: this year's and last year's sales (millions of won). */
export function makeSido(year: number) {
  const k = year === 2025 ? 0.88 : 1;
  return {
    rows: SIDO.map(([sido, code, s], i) => {
      const sales = Math.round(s * k * (1 + Math.sin(i * 1.7) * 0.06));
      return { sido, code, sales, last: Math.round(sales / (1 + Math.cos(i * 2.3) * 0.12 + 0.03)) };
    }),
  };
}

export const sampleData = makeSido(2026);

export default function mount(ctx: DemoContext) {
  const viewer = mountViewer(ctx, { definition, data: sampleData, fileName: '시도별 매출' });
  let labels: MapElement['labels'] = 'both';
  let steps = 0;
  const apply = () => {
    const els = definition.bands.title!.elements.map(e => (e.type === 'map' && e.key === 'sido' ? salesMap(labels, steps || undefined) : e));
    void viewer.setDefinition({ ...definition, bands: { ...definition.bands, title: { ...definition.bands.title!, elements: els } } });
  };
  select(ctx, tr(ctx.lang, { en: 'Year', ko: '연도' }), [['2026', '2026'], ['2025', '2025']], '2026', y => void viewer.setData(makeSido(Number(y))));
  select(ctx, tr(ctx.lang, { en: 'Labels', ko: '이름표' }), [['both', tr(ctx.lang, { en: 'Name and value', ko: '이름과 값' })], ['name', tr(ctx.lang, { en: 'Name', ko: '이름' })], ['value', tr(ctx.lang, { en: 'Value', ko: '값' })], ['none', tr(ctx.lang, { en: 'None', ko: '없음' })]], 'both', v => { labels = v; apply(); });
  select(ctx, tr(ctx.lang, { en: 'Colors', ko: '색' }), [['0', tr(ctx.lang, { en: 'Smooth', ko: '연속' })], ['5', tr(ctx.lang, { en: '5 classes', ko: '5단계' })]], '0', v => { steps = Number(v); apply(); });
}