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