csat-chart.js 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +381 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/dist/csat-chart.cjs +6171 -0
- package/dist/csat-chart.cjs.map +1 -0
- package/dist/csat-chart.d.cts +914 -0
- package/dist/csat-chart.d.mts +914 -0
- package/dist/csat-chart.mjs +6097 -0
- package/dist/csat-chart.mjs.map +1 -0
- package/dist/csat-chart.umd.min.js +4 -0
- package/dist/csat-chart.umd.min.js.map +1 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# csat-chart.js
|
|
2
|
+
|
|
3
|
+
수능·모의고사 시험지 양식의 그래프를 Canvas 2D로 그리는 라이브러리입니다.
|
|
4
|
+
축·범례·각주·출처의 배치, 명조 글꼴, 흑백 인쇄를 전제한 해칭 패턴까지
|
|
5
|
+
시험지 관습을 그대로 따릅니다. **런타임 의존성이 없습니다.**
|
|
6
|
+
|
|
7
|
+
- 데모: https://yhk1m.github.io/csat-chart.js/
|
|
8
|
+
- AI 참고 문서: [docs/ai-reference.md](docs/ai-reference.md) — 이 라이브러리를 모르는 AI 어시스턴트에게 붙여넣는 용도
|
|
9
|
+
- 라이선스: MIT
|
|
10
|
+
|
|
11
|
+
## 시작하기
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<script src="https://yhk1m.github.io/csat-chart.js/lib/csat-chart.umd.min.js"></script>
|
|
15
|
+
<canvas id="c" width="800" height="600"></canvas>
|
|
16
|
+
<script>
|
|
17
|
+
CsatChart.ensureFonts().then(function () {
|
|
18
|
+
new CsatChart('c', {
|
|
19
|
+
type: 'ternary',
|
|
20
|
+
data: CsatChart.createDefaultTernaryData(),
|
|
21
|
+
options: { title: '토지 이용 구성', source: '통계청' },
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
</script>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
이 패키지가 npm에 올라온 뒤에는 `https://cdn.jsdelivr.net/npm/csat-chart.js` 로
|
|
28
|
+
더 짧게 쓸 수 있습니다.
|
|
29
|
+
|
|
30
|
+
`ensureFonts()` 를 부르지 않으면 대체 글꼴로 그려져 시험지 양식이 재현되지 않습니다.
|
|
31
|
+
던지지 않습니다 — 글꼴을 못 받아도, 제한 시간(기본 5초)을 넘겨도 조용히 `false` 로
|
|
32
|
+
끝납니다.
|
|
33
|
+
|
|
34
|
+
`<canvas id="c"></canvas>` 처럼 크기를 적지 않으면 800×600 으로 채웁니다. HTML
|
|
35
|
+
기본값인 300×150 으로 그리면 여백·글자 크기가 절대 픽셀이라 제목·눈금·각주가
|
|
36
|
+
한 덩어리로 겹칩니다. 크기를 직접 적었다면 그 크기를 그대로 씁니다. 다만 대략
|
|
37
|
+
500×400 보다 작으면 글자가 겹쳐 읽기 어려워집니다.
|
|
38
|
+
|
|
39
|
+
## 내 자료 넣기
|
|
40
|
+
|
|
41
|
+
`createDefault○○Data()` 가 주는 것은 **뼈대**입니다. 기후·인구 피라미드처럼 값이 전부
|
|
42
|
+
0인 것도 있어서, 그대로 그리면 빈 그림이 나옵니다. 필요한 칸만 덮어 쓰세요.
|
|
43
|
+
|
|
44
|
+
```html
|
|
45
|
+
<script>
|
|
46
|
+
CsatChart.ensureFonts().then(function () {
|
|
47
|
+
const data = CsatChart.createDefaultClimateData();
|
|
48
|
+
// 1월부터 12월까지, 월마다 { temp, precip } 하나씩 — 열두 개를 다 채운다.
|
|
49
|
+
data.months = [
|
|
50
|
+
{ temp: -1.9, precip: 16.8 }, { temp: 0.7, precip: 28.2 },
|
|
51
|
+
{ temp: 6.1, precip: 36.9 }, { temp: 12.6, precip: 72.9 },
|
|
52
|
+
{ temp: 18.2, precip: 103.6 },{ temp: 22.7, precip: 129.5 },
|
|
53
|
+
{ temp: 25.3, precip: 414.4 },{ temp: 26.1, precip: 348.2 },
|
|
54
|
+
{ temp: 21.2, precip: 141.5 },{ temp: 14.8, precip: 52.2 },
|
|
55
|
+
{ temp: 7.2, precip: 51.1 }, { temp: 0.4, precip: 22.6 },
|
|
56
|
+
];
|
|
57
|
+
const chart = new CsatChart('c', {
|
|
58
|
+
type: 'climate',
|
|
59
|
+
data: data,
|
|
60
|
+
options: { title: '서울의 기후', source: '기상청', footnotes: ['1991~2020년의 평년값임.'] },
|
|
61
|
+
});
|
|
62
|
+
// 인쇄용으로 뽑을 때는 아래 「인쇄용으로 뽑기」 처럼 chart.download(…) 를 쓴다.
|
|
63
|
+
});
|
|
64
|
+
</script>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`tempRange`·`precipRange` 는 `auto: true` 라 값에 맞춰 알아서 잡힙니다. 눈금을 고정하고
|
|
68
|
+
싶으면 `auto: false` 로 두고 `min`·`max` 를 적으세요.
|
|
69
|
+
|
|
70
|
+
## 그래프 16종
|
|
71
|
+
|
|
72
|
+
| `type` | 그래프 | 기본 데이터 | 저수준 렌더러 | 데이터 타입 |
|
|
73
|
+
|---|---|---|---|---|
|
|
74
|
+
| `absbar` | 절댓값 막대 | `createDefaultAbsBarData()` | `renderAbsBarGraph` | `AbsBarGraphData` |
|
|
75
|
+
| `category-dot` | 범주 점 | `createDefaultCategoryDotData()` | `renderCategoryDotGraph` | `CategoryDotGraphData` |
|
|
76
|
+
| `climate` | 기후 그래프 | `createDefaultClimateData()` | `renderClimateGraph` | `ClimateGraphData` |
|
|
77
|
+
| `cube` | 정육면체 | `createDefaultCubeData()` | `renderCubeGraph` | `CubeGraphData` |
|
|
78
|
+
| `data-table` | 항목×지역 표 | `createDefaultDataTableData()` | `renderDataTable` | `DataTableData` |
|
|
79
|
+
| `deviation-a` | 월별 편차 | `createDefaultDeviationAData()` | `renderDeviationAGraph` | `DeviationAData` |
|
|
80
|
+
| `deviation-b` | 지역별 편차 | `createDefaultDeviationBData()` | `renderDeviationBGraph` | `DeviationBData` |
|
|
81
|
+
| `hythergraph` | 하이서그래프 | `createDefaultHythergraphData()` | `renderHythergraph` | `HythergraphData` |
|
|
82
|
+
| `line` | 꺾은선 | `createDefaultLineData()` | `renderLineGraph` | `LineGraphData` |
|
|
83
|
+
| `matrix-table` | 계단식 행렬표 | `createDefaultMatrixTableData()` | `renderMatrixTable` | `MatrixTableData` |
|
|
84
|
+
| `pyramid` | 인구 피라미드 | `createDefaultPyramidData()` | `renderPyramidGraph` | `PyramidGraphData` |
|
|
85
|
+
| `radar` | 방사형 | `createDefaultRadarData()` | `renderRadarChart` | `RadarGraphData` |
|
|
86
|
+
| `scatter` | 산점도·버블 | `createDefaultScatterData()` | `renderScatterGraph` | `ScatterGraphData` |
|
|
87
|
+
| `stacked` | 100% 막대·원 | `createDefaultStackedData()` | `renderStackedGraph` | `StackedGraphData` |
|
|
88
|
+
| `ternary` | 삼각 그래프 | `createDefaultTernaryData()` | `renderTernaryGraph` | `TernaryGraphData` |
|
|
89
|
+
| `treemap` | 트리맵 | `createDefaultTreemapData()` | `renderTreemapGraph` | `TreemapGraphData` |
|
|
90
|
+
|
|
91
|
+
기본 데이터는 표의 `createDefault○○Data()` 로 얻어 고쳐 씁니다 — 어느 종류든 이
|
|
92
|
+
이름 규칙을 따릅니다. 저수준 렌더러 이름은 그렇지 않습니다 — `renderDataTable`·
|
|
93
|
+
`renderHythergraph`·`renderMatrixTable`·`renderRadarChart` 넷은 `render○○Graph`
|
|
94
|
+
를 따르지 않으니 표에서 확인하세요. 데이터 모양이 어긋나면 한국어 메시지로
|
|
95
|
+
알려줍니다 — [오류 가려내기](#오류-가려내기) 참고.
|
|
96
|
+
|
|
97
|
+
## 옵션
|
|
98
|
+
|
|
99
|
+
`options` 는 다음 13개 필드를 받습니다. 필요한 것만 적으면 나머지는 기본값을 씁니다.
|
|
100
|
+
|
|
101
|
+
| 필드 | 기본값 | 하는 일 |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `title` | `''` | 제목 |
|
|
104
|
+
| `source` | `''` | 출처. 각주 위(또는 `sourceInline` 이면 각주와 같은 줄)에 오른쪽 정렬로 적힙니다 |
|
|
105
|
+
| `sourceLeft` | 없음 | 출처 줄 왼쪽에 함께 적을 글(예: 자료 연도 `(2024)`). **지금은 `stacked` 에서만 동작합니다** |
|
|
106
|
+
| `sourceInline` | 없음(꺼짐) | 출처를 마지막 각주와 같은 줄 오른쪽 끝에 붙입니다(시험지 관습). **지금은 `scatter` 에서만 동작합니다** |
|
|
107
|
+
| `footnotes` | `['']` | 각주 목록. 앞에 `* ` 를 자동으로 붙이므로 직접 적지 않습니다. 빈 문자열은 무시됩니다 |
|
|
108
|
+
| `fontFamily` | `'serif'` | `'serif'`(명조)·`'sans'`(고딕)·`'custom'` 중 하나 |
|
|
109
|
+
| `customFont` | `''` | `fontFamily` 가 `'custom'` 일 때 쓸 글꼴 이름 |
|
|
110
|
+
| `fontSize` | `{ title: 36, axisLabel: 28, tick: 26, dataLabel: 22 }` | 제목·축 이름·눈금·데이터 값 글자 크기(px). 각주·출처는 `dataLabel` 을 따릅니다 |
|
|
111
|
+
| `showDataLabels` | `false` | 막대·점에 값을 함께 표시할지 |
|
|
112
|
+
| `showLegend` | `true` | 범례를 보여줄지 |
|
|
113
|
+
| `legendPosition` | `'bottom'` | `'bottom'`(아래)·`'right'`(오른쪽) 중 하나 |
|
|
114
|
+
| `legendLabel1` | `''` | 두 계열을 쓰는 종류(기후·편차·인구 피라미드)의 첫 계열 범례 이름. 비워 두면 데이터가 준 이름을 씁니다 |
|
|
115
|
+
| `legendLabel2` | `''` | 같은 종류의 두 번째 계열 범례 이름 |
|
|
116
|
+
|
|
117
|
+
`fontSize` 는 하나만 부분 지정해도 됩니다 — TypeScript·JavaScript 모두 마찬가지입니다.
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
chart.update({ options: { fontSize: { title: 44 } } });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
나머지 세 값은 그대로 유지됩니다(준 항목만 갈아 끼웁니다). TypeScript 에서 이 모양의
|
|
124
|
+
옵션 타입 이름은 `PartialGraphOptions`, 한 종류의 부분 갱신 전체는 `UpdateFor<T>` 입니다.
|
|
125
|
+
|
|
126
|
+
## 그림이 이상할 때
|
|
127
|
+
|
|
128
|
+
| 증상 | 원인 | 할 일 |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| 글꼴이 시험지 같지 않습니다 | `ensureFonts()` 를 안 불렀거나, 차트를 만든 **뒤에** 불렀습니다 | `await CsatChart.ensureFonts()` 를 먼저 부르고 그 안에서 차트를 만드세요 |
|
|
131
|
+
| 제목·눈금·각주가 한 덩어리로 겹칩니다 | 캔버스가 너무 작습니다 | 800×600 안팎으로. 500×400 아래로는 내려가지 않습니다 |
|
|
132
|
+
| 축은 그려지는데 자료가 없습니다 | 기본 데이터를 그대로 썼습니다 (값이 전부 0인 종류가 있습니다) | [내 자료 넣기](#내-자료-넣기) |
|
|
133
|
+
| 레티나에서 흐릿합니다 | 화면 캔버스는 1배입니다 | `toDataURL({ scale: 2 })` 로 뽑아 `<img>` 로 거세요 |
|
|
134
|
+
| 아무것도 안 그려집니다 | `CsatChartError` 가 던져졌습니다 | 브라우저 콘솔(F12)을 열면 한국어로 이유가 적혀 있습니다 |
|
|
135
|
+
| 축 이름이 두 줄로 나뉘거나 그림이 조금 작아졌습니다 | 이름이 길어 한 줄로 두면 캔버스를 벗어납니다 | 그대로 두셔도 됩니다 — **이름을 자르는 대신** 자리를 비우고 줄을 늘립니다. 그림을 키우려면 이름을 줄이거나 캔버스를 넓히세요 |
|
|
136
|
+
|
|
137
|
+
## 인쇄용으로 뽑기
|
|
138
|
+
|
|
139
|
+
`toDataURL()`·`download()` 의 `scale` 은 **글자·선까지 함께 키우는** 배율입니다.
|
|
140
|
+
캔버스만 키우는 `resize(1600, 1200)` 과는 다릅니다 — 이 라이브러리의 글꼴 크기와
|
|
141
|
+
여백이 절대 픽셀이라서, 캔버스를 두 배로 하면 «두 배로 선명한 같은 그림» 이 아니라
|
|
142
|
+
**«글자가 절반으로 작아진 다른 그림»** 이 나옵니다. 인쇄용으로 뽑을 때는 이렇게 씁니다.
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
const printUrl = chart.toDataURL({ scale: 2 }); // 화면과 같은 구도, 두 배 해상도
|
|
146
|
+
chart.download('시험지그림.png', { scale: 2 });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
화면에 그려지는 캔버스 자체는 항상 1배입니다. `devicePixelRatio` 를 따로 다루지
|
|
150
|
+
않으므로 **레티나 디스플레이에서는 글자와 선이 조금 흐릿하게 보입니다.** 화면으로
|
|
151
|
+
훑어보는 데는 지장이 없지만, 또렷한 그림이 필요하면 `scale: 2` 로 뽑은 PNG 를
|
|
152
|
+
`<img>` 에 넣고 CSS 로 원래 크기까지 줄여 거세요. 인쇄·투사도 같은 방법입니다.
|
|
153
|
+
|
|
154
|
+
## 지원 환경
|
|
155
|
+
|
|
156
|
+
- Chrome 99, Firefox 112, Safari 16.4(iOS 16.4) 이상은 아무것도 하지 않아도
|
|
157
|
+
16종 전부가 그대로 그려집니다 (빌드 타깃 ES2020).
|
|
158
|
+
- 그 아래 — Chrome 80·Firefox 74·Safari 13.1 까지 — 도 그려집니다. **다만
|
|
159
|
+
`absbar`·`climate`·`deviation-a`·`deviation-b`·`hythergraph`·`pyramid`·
|
|
160
|
+
`scatter`·`stacked` 여덟 종류는 범례 박스를 그릴 때 `ctx.roundRect()` 를
|
|
161
|
+
쓰는데, 그 메서드가 Chrome 99·Firefox 112·Safari 16.4 미만에는 없습니다.** 이
|
|
162
|
+
라이브러리는 그 자리를 폴리필로 메웁니다 — `new CsatChart(...)` 를 쓰면
|
|
163
|
+
생성자가 첫 렌더 전에 자동으로 불러 주므로 신경 쓸 일이 없습니다. 저수준
|
|
164
|
+
렌더러(`renderClimateGraph` 등)를 파사드 없이 직접 부른다면 그리기 전에
|
|
165
|
+
`installRoundRectPolyfill()` 을 스스로 한 번 불러야 합니다.
|
|
166
|
+
- 학교 PC·구형 iPad 가 이 라이브러리의 실제 관객입니다. iPad 5세대·Air 2 처럼
|
|
167
|
+
iOS 15 에서 멈춘 기기는 iOS 16.4 를 영영 받을 수 없어, 폴리필이 없으면
|
|
168
|
+
위 여덟 종류가 흰 캔버스로만 보입니다.
|
|
169
|
+
- 런타임 의존성 0. `<script>` 한 줄이면 됩니다
|
|
170
|
+
- Node 는 캔버스 구현체를 직접 고릅니다 (아래 「Node.js 에서 PNG 뽑기」)
|
|
171
|
+
|
|
172
|
+
## 번들러
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npm install csat-chart.js
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { CsatChart, createDefaultClimateData } from 'csat-chart.js';
|
|
180
|
+
|
|
181
|
+
await CsatChart.ensureFonts();
|
|
182
|
+
|
|
183
|
+
// 기본 자료는 값이 전부 0이다 — 채우는 법은 위 「내 자료 넣기」.
|
|
184
|
+
const chart = new CsatChart(document.querySelector('canvas'), {
|
|
185
|
+
type: 'climate',
|
|
186
|
+
data: createDefaultClimateData(),
|
|
187
|
+
options: { title: '기후 그래프', source: '기상청' },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
chart.update({ options: { title: '부산의 기후' } });
|
|
191
|
+
chart.download('기후그래프.png');
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Node.js 에서 PNG 뽑기
|
|
195
|
+
|
|
196
|
+
캔버스 구현체는 직접 고릅니다. 이 패키지의 의존성이 아닙니다.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
npm install @napi-rs/canvas
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
import { writeFileSync } from 'node:fs';
|
|
204
|
+
import { createCanvas } from '@napi-rs/canvas';
|
|
205
|
+
import { renderClimateGraph, createDefaultClimateData, createDefaultGraphOptions } from 'csat-chart.js';
|
|
206
|
+
|
|
207
|
+
const canvas = createCanvas(800, 600);
|
|
208
|
+
const ctx = canvas.getContext('2d');
|
|
209
|
+
renderClimateGraph(ctx, 800, 600, createDefaultClimateData(), createDefaultGraphOptions());
|
|
210
|
+
writeFileSync('out.png', canvas.toBuffer('image/png'));
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Node 에는 `document` 가 없으므로 `ensureFonts()` 를 불러도 아무 일도 하지 않고
|
|
214
|
+
`false` 만 돌아옵니다 — 시스템에 깔린 대체 글꼴로 그려집니다. 시험지 서체가 꼭
|
|
215
|
+
필요하면 그 글꼴을 서버에 직접 설치하세요.
|
|
216
|
+
|
|
217
|
+
## API 자세히
|
|
218
|
+
|
|
219
|
+
### `new CsatChart(target, config)`
|
|
220
|
+
|
|
221
|
+
`target` 은 `<canvas>` 요소 또는 그 `id` 문자열. `config` 는 `{ type, data, options? }`.
|
|
222
|
+
TypeScript 에서는 `type` 을 적는 순간 `data` 타입이 그 종류로 좁혀집니다 — 다른 종류의
|
|
223
|
+
데이터를 넣으면 컴파일이 막힙니다.
|
|
224
|
+
|
|
225
|
+
`target` 이 잘못되면 알아보기 쉬운 한국어 오류를 던집니다. 특히 `id` 를 잘못 적어
|
|
226
|
+
`document.getElementById()` 가 `null` 을 돌려준 경우 — 브라우저 콘솔을 잘 열지
|
|
227
|
+
않는 사용자를 겨냥해, «그 id 를 찾지 못한 것은 아닌지 보라» 는 안내까지 붙습니다.
|
|
228
|
+
|
|
229
|
+
| 메서드 | 하는 일 |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `update(next: UpdateFor<T>)` | `data`·`options` 중 준 것만 덮고 다시 그립니다. 어긋나면 던지고 이전 상태를 지킵니다 |
|
|
232
|
+
| `resize(width, height)` | 캔버스 픽셀 크기를 바꾸고 다시 그립니다. 둘 다 0보다 커야 합니다 |
|
|
233
|
+
| `toDataURL(options?: { scale?: number })` | PNG data URL |
|
|
234
|
+
| `download(filename?, options?: { scale?: number })` | 내려받기 (브라우저 전용) |
|
|
235
|
+
| `destroy()` | 캔버스를 흰 바탕으로 지우고 더는 그리지 못하게 합니다 |
|
|
236
|
+
|
|
237
|
+
`update()`·`resize()` 는 `this` 를 돌려주므로 이어 쓸 수 있습니다. `chart.canvas` 로
|
|
238
|
+
넘겨준 캔버스 자체에도 접근할 수 있습니다.
|
|
239
|
+
|
|
240
|
+
### `CsatChart.ensureFonts(options?)`
|
|
241
|
+
|
|
242
|
+
`Noto Serif KR`·`Noto Sans KR` 을 확보합니다. 준비되면 `true`, 못 받거나 Node 이면
|
|
243
|
+
`false` 를 돌려줍니다. **던지지 않습니다.**
|
|
244
|
+
|
|
245
|
+
한 페이지에서 여러 번 불러도 실제 작업은 한 번뿐입니다. 그래서 **차트를 만들기 전에,
|
|
246
|
+
가장 먼저** 부르세요 — 나중 호출에 넘긴 옵션은 조용히 버려지고, 차트를 먼저 만들면
|
|
247
|
+
늦게 도착한 글꼴이 반영되지 않을 수 있습니다.
|
|
248
|
+
|
|
249
|
+
옵션은 셋입니다. `href`(글꼴 CSS 주소), `families`(확인할 글꼴 이름), `timeoutMs`
|
|
250
|
+
(기본 5000). 교내망·오프라인이라 `href` 를 바꾼다면 **`families` 도 함께 바꾸세요**
|
|
251
|
+
— `href` 를 그대로 둔 채 `families` 만 바꾸면 그 글꼴이 없어도 `true` 가 나옵니다.
|
|
252
|
+
|
|
253
|
+
### 오류 가려내기
|
|
254
|
+
|
|
255
|
+
데이터가 어긋나면 `CsatChartError` 를 던집니다. 가려낼 때는 `instanceof` 말고
|
|
256
|
+
**`err.name` 을 보세요.**
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
try {
|
|
260
|
+
new CsatChart(c, { type, data });
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (err.name === 'CsatChartError') showHint(err.message);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
같은 페이지에 ESM 판과 CDN 판이 함께 올라오면 클래스가 두 벌이 되어
|
|
267
|
+
`instanceof` 가 조용히 `false` 가 됩니다. `name` 은 그런 일이 없습니다.
|
|
268
|
+
|
|
269
|
+
메시지는 무엇이 왜 잘못됐는지까지 말해 줍니다. 필수 항목이 빠졌으면 그 이름을,
|
|
270
|
+
값의 자료형이 다르면 무엇이었어야 하는지를, `type` 을 잘못 적었으면 가까운
|
|
271
|
+
후보를 댑니다.
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
csat-chart: 알 수 없는 type "climat" — 혹시 "climate"?
|
|
275
|
+
csat-chart: type "pyramid" 의 data 에 ages 항목이 없습니다
|
|
276
|
+
csat-chart: type "climate" 의 data.months[0]: 객체여야 합니다 (지금 숫자)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
마지막 예는 흔한 실수 하나를 잡습니다 — 열두 달 자료를 `months: [1, 2, …, 12]`
|
|
280
|
+
처럼 숫자만 늘어놓은 배열로 적는 것. 배열이고 길이도 12라 겉모양만 보면
|
|
281
|
+
통과할 법하지만, 그 상태로 그리면 브라우저에서는 좌표가 어긋나 빈 그림이
|
|
282
|
+
나오고 Node 캔버스에서는 프로세스가 죽습니다. 배열의 첫 원소까지 한 겹 더 보고
|
|
283
|
+
막습니다.
|
|
284
|
+
|
|
285
|
+
### 상수
|
|
286
|
+
|
|
287
|
+
기호·눈금 순서를 정하는 상수 7개를 내보냅니다 — `AGE_GROUPS`·`DOT_MARKER_ORDER`·
|
|
288
|
+
`LINE_MARKER_ORDER`·`LINE_STYLE_ORDER`·`MONTH_LABELS_EN`·`MONTH_LABELS_NUM`·
|
|
289
|
+
`LINE_DASH`. 모두 **얼려서** 내보냅니다. 렌더러가 기본값으로 읽는 바로 그 객체라서,
|
|
290
|
+
얼지 않으면 `DOT_MARKER_ORDER.reverse()` 한 번에 이후 모든 그림의 기호 배정이
|
|
291
|
+
조용히 어긋납니다. `CHART_TYPES`(그래프 16종 목록)도 같은 이유로 따로 얼려서
|
|
292
|
+
내보냅니다.
|
|
293
|
+
|
|
294
|
+
ESM/CJS 로 쓸 때는 각각 이름으로 가져옵니다.
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import { CHART_TYPES, AGE_GROUPS } from 'csat-chart.js';
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
CDN 판에서는 전역 `CsatChart` 에도 같이 붙어 있어 `CsatChart.CHART_TYPES` 로도
|
|
301
|
+
쓸 수 있습니다 — UMD 번들이 전역 `CsatChart` 에 한 번 더 얹어 둔 것뿐이라, 번들러로
|
|
302
|
+
쓸 때 `CsatChart.CHART_TYPES` 라고 쓰면 `undefined` 입니다.
|
|
303
|
+
|
|
304
|
+
## 저수준 렌더러
|
|
305
|
+
|
|
306
|
+
`CsatChart` 를 거치지 않고 그래프 하나만 직접 그릴 수 있습니다. 모두 같은 꼴입니다.
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
render○○(ctx, width, height, data, options): void
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
번들러를 쓴다면 렌더러 하나만 가져오는 편이 실제로 훨씬 가볍습니다. 실측(esbuild
|
|
313
|
+
0.27.7, `--bundle --minify --format=esm`, `csat-chart.js` 를 패키지로 설치한
|
|
314
|
+
상태 기준):
|
|
315
|
+
|
|
316
|
+
| 무엇을 가져오나 | 크기 |
|
|
317
|
+
|---|---|
|
|
318
|
+
| 라이브러리 전부 | 95.9 KB |
|
|
319
|
+
| `CsatChart` 만 | 94.6 KB |
|
|
320
|
+
| `renderClimateGraph` 만 | **10.0 KB** |
|
|
321
|
+
|
|
322
|
+
`CsatChart` 는 `type` 을 문자열로 받아 그때그때 렌더러를 고르므로 16종을 전부
|
|
323
|
+
붙들고 있어야 합니다. 기후 그래프 하나만 필요한 앱이라면 저수준 렌더러를 직접
|
|
324
|
+
부르는 편이 아홉 배 가볍습니다. CDN 으로 쓰면 어차피 한 벌을 통째로 받으므로 이
|
|
325
|
+
이야기는 해당하지 않습니다.
|
|
326
|
+
|
|
327
|
+
저수준 렌더러는 번들 크기 말고 다른 이유로도 씁니다 — `CsatChart` 의 수명주기
|
|
328
|
+
(캔버스 자동 크기 보정, 글꼴이 늦게 도착했을 때 다시 그리기)가 필요 없을 때입니다.
|
|
329
|
+
위 [Node.js 에서 PNG 뽑기](#nodejs-에서-png-뽑기)가 그런 경우입니다.
|
|
330
|
+
|
|
331
|
+
렌더러가 눈금 간격을 잡을 때 쓰는 두 축 계산 유틸도 함께 내보냅니다 —
|
|
332
|
+
`niceStep(range, maxTicks = 8)` 은 범위를 주면 1·2·5 배수 규칙으로 보기 좋은
|
|
333
|
+
눈금 간격 하나를 돌려주고(범위가 0 이하거나 유한하지 않으면 1을 돌려줍니다 —
|
|
334
|
+
간격이 0이면 눈금을 그리는 루프가 멎습니다), `autoRange(values, maxTicks = 8)`
|
|
335
|
+
는 값 배열을 주면 그 값들을 담는 `{ min, max, step }` 을 한 번에 잡아 줍니다.
|
|
336
|
+
직접 축을 그리는 커스텀 렌더러를 만들 때 유용합니다.
|
|
337
|
+
|
|
338
|
+
## 만든 배경
|
|
339
|
+
|
|
340
|
+
지리 교사가 수업·평가 자료를 만들려고 쓰던 렌더러를 떼어내 공개한 것입니다.
|
|
341
|
+
[GeoTester](https://geotester-v2.vercel.app) 와 GeoGrapher 에서 쓰이던 코드입니다.
|
|
342
|
+
|
|
343
|
+
## 기여
|
|
344
|
+
|
|
345
|
+
버그 제보와 새 그래프 종류 제안을 환영합니다. 렌더 결과를 바꾸는 변경은
|
|
346
|
+
골든 이미지 기준을 함께 갱신해야 합니다.
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
npm install
|
|
350
|
+
npm run verify # 타입 → 린트 → 빌드 → 테스트 순
|
|
351
|
+
UPDATE_GOLDEN=1 npx vitest run test/core/golden.test.ts # 기준 갱신
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
골든 이미지는 시스템 글꼴 대체 결과에 의존하므로 기계마다 다를 수 있습니다.
|
|
355
|
+
CI 에서는 `SKIP_GOLDEN=1` 로 건너뜁니다.
|
|
356
|
+
|
|
357
|
+
`src/core/**` 는 이 저장소의 ESLint 대상에서도 제외됩니다(`eslint.config.mjs` 참고) —
|
|
358
|
+
이 층을 지키는 수단이 린트가 아니라 골든 이미지이기 때문입니다. 1.0.0 은 원본
|
|
359
|
+
렌더러를 한 글자도 고치지 않고 옮긴 판이었고 골든 이미지 31장이 그 증거였습니다.
|
|
360
|
+
지금은 필요하면 고칩니다 — 대신 렌더 결과가 달라진 내역은 CHANGELOG 에 남깁니다.
|
|
361
|
+
그리고 CI 는 `SKIP_GOLDEN=1` 로 골든 이미지 비교 자체를
|
|
362
|
+
건너뛰므로, 렌더 결과가 기준 이미지와 실제로 같은지는 CI 가 확인하지 않습니다 —
|
|
363
|
+
CI 가 렌더러에 대해 돌리는 자동 검사는 「캔버스가 비어 있지 않다」 하나뿐입니다.
|
|
364
|
+
렌더러를 바꾸는 PR 을 보낸다면, 위 명령으로 로컬에서 골든 이미지 비교까지
|
|
365
|
+
통과하는지 직접 확인한 결과를 함께 적어 주면 리뷰가 빨라집니다.
|
|
366
|
+
|
|
367
|
+
## 라이선스
|
|
368
|
+
|
|
369
|
+
MIT © 2026 김용현
|