@nethru/kit 1.0.7 → 1.1.1

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.
@@ -0,0 +1,379 @@
1
+ import React, { useCallback, useEffect, useState } from "react";
2
+ import Highcharts from 'highcharts';
3
+ import HighchartsReactModule from 'highcharts-react-official';
4
+ import highchartsAccessibility from 'highcharts/modules/accessibility.js';
5
+ import highchartsAnnotations from 'highcharts/modules/annotations.js';
6
+ import { format, formatToPercent } from "./Number";
7
+ import * as ChartColors from "./ChartColors";
8
+ import { grey } from "@nethru/ui/base/colors";
9
+
10
+ // Handle both ES module and CommonJS exports
11
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
12
+ const HighchartsReact = HighchartsReactModule.default || HighchartsReactModule;
13
+ highchartsAccessibility(Highcharts);
14
+ highchartsAnnotations(Highcharts);
15
+ export default function Chart(props) {
16
+ const [legendLimitEnabled, setLegendLimitEnabled] = useState(props.legendLimit > 0);
17
+ const [options, setOptions] = useState(toOptions(props, legendLimitEnabled));
18
+ useEffect(() => {
19
+ if (props.delay) setTimeout(() => setOptions(toOptions(props, legendLimitEnabled, handleLegendItemClick)), props.delay);else setOptions(toOptions(props, legendLimitEnabled, handleLegendItemClick));
20
+ // eslint-disable-next-line
21
+ }, [props]);
22
+ const handleLegendItemClick = useCallback(() => setLegendLimitEnabled(false), []);
23
+ return /*#__PURE__*/_jsx(_Fragment, {
24
+ children: options && /*#__PURE__*/_jsx(HighchartsReact, {
25
+ highcharts: Highcharts,
26
+ options: options
27
+ })
28
+ });
29
+ }
30
+ function toOptions(props, legendLimitEnabled, handleLegendItemClick) {
31
+ const {
32
+ title = '',
33
+ type,
34
+ categorize,
35
+ stacking,
36
+ showLegend = true,
37
+ showDataLabel,
38
+ xAxisType,
39
+ xAxisLabel = true,
40
+ xAxisLabelFormat,
41
+ xAxisTickInterval,
42
+ yAxisLabelEnabled = true,
43
+ yAxisLabelFormat,
44
+ yAxisTickInterval = null,
45
+ yAxisPlotLines = [],
46
+ opacity = 1,
47
+ innerSize = 0,
48
+ colors: customColors,
49
+ backgroundColor = undefined,
50
+ xPlotIndex,
51
+ dimension,
52
+ metrics,
53
+ records = [],
54
+ metas,
55
+ height = 200,
56
+ legendLimit,
57
+ legendAlign = 'center',
58
+ tooltip = {},
59
+ secondaryXAxis = []
60
+ } = props;
61
+ const {
62
+ outside
63
+ } = tooltip;
64
+ const pie = type === 'pie';
65
+ const centers = pieCenters(metrics.length);
66
+ const size = pieSize(metrics.length);
67
+ const colors = customColors ? customColors : ChartColors.preset;
68
+ const categories = records.map(record => record[dimension]);
69
+ const mainXAxis = records.map(r => r.time);
70
+ const chartColorHandlers = {
71
+ line: (colors, index) => ({
72
+ color: colors[index],
73
+ mainColor: colors[index]
74
+ }),
75
+ area: (colors, index) => ({
76
+ color: colors[index][0],
77
+ mainColor: colors[index][1]
78
+ }),
79
+ gradient: (colors, index) => ({
80
+ color: {
81
+ linearGradient: {
82
+ x1: 0,
83
+ y1: 0,
84
+ x2: 0,
85
+ y2: 1
86
+ },
87
+ stops: [[0, `${colors[index][0]}`], [0.95, `${colors[index][1]}`]]
88
+ },
89
+ mainColor: colors[index][2]
90
+ })
91
+ };
92
+ const series = metrics.map(({
93
+ chartType,
94
+ metric
95
+ }, index) => {
96
+ const type = chartType === 'gradient' ? 'area' : chartType;
97
+ const {
98
+ color,
99
+ mainColor
100
+ } = chartColorHandlers[chartType](colors, index);
101
+ const meta = metas[metric];
102
+ let option = {
103
+ type: type,
104
+ name: meta ? meta.name : '',
105
+ data: records.map(record => {
106
+ const data = record.metric[metric] ?? null;
107
+ return pie ? {
108
+ name: record[dimension],
109
+ y: data
110
+ } : data;
111
+ }),
112
+ custom: {
113
+ type: meta ? meta.type : '',
114
+ pointerNames: meta?.pointerNames ?? null
115
+ },
116
+ center: centers[index] || [null, null],
117
+ size: size,
118
+ point: {
119
+ events: {
120
+ render: function () {
121
+ this.graphic.attr({
122
+ zIndex: this.y === 0 ? -1 : ZIndex.COMPARE_CHART_BASE - index
123
+ });
124
+ }
125
+ }
126
+ },
127
+ showInLegend: pie && index > 0 ? false : true,
128
+ color: color,
129
+ lineColor: mainColor,
130
+ lineWidth: 2,
131
+ marker: {
132
+ fillColor: mainColor
133
+ },
134
+ metric: metric
135
+ };
136
+ if (legendLimitEnabled) option = {
137
+ ...option,
138
+ visible: index < legendLimit
139
+ };
140
+ return option;
141
+ });
142
+ const yPlotLines = yAxisPlotLines.map((line, index) => {
143
+ return {
144
+ value: line.value,
145
+ width: 1,
146
+ color: line.color,
147
+ dashStyle: "Dash",
148
+ zIndex: ZIndex.AVERAGE_LINE_BASE + index,
149
+ label: {
150
+ text: line.label,
151
+ align: line.labelAlign,
152
+ style: {
153
+ color: line.labelColor,
154
+ opacity: 0.7,
155
+ fontSize: "12px",
156
+ padding: '1px 5px'
157
+ },
158
+ y: -8,
159
+ useHTML: true
160
+ }
161
+ };
162
+ });
163
+ const xPlotBands = [{
164
+ from: xPlotIndex,
165
+ to: categories.length - 1,
166
+ color: 'rgba(0, 0, 0, 0.03)',
167
+ zIndex: ZIndex.NOW_BAND
168
+ }];
169
+ const xPlotLines = [{
170
+ color: "#aaa",
171
+ width: 1,
172
+ value: xPlotIndex,
173
+ zIndex: ZIndex.NOW_BORDER,
174
+ label: {
175
+ text: "현재",
176
+ rotation: 0,
177
+ x: 5,
178
+ y: 13,
179
+ style: {
180
+ fontWeight: 500,
181
+ lineHeight: "16.8px",
182
+ color: "#333",
183
+ fontSize: "12px"
184
+ }
185
+ }
186
+ }];
187
+ return {
188
+ chart: {
189
+ type: type.toLowerCase(),
190
+ height: height,
191
+ backgroundColor,
192
+ events: {
193
+ render: secondaryXAxis.length === 0 ? undefined : function () {
194
+ const chart = this;
195
+ const visibleSeries = chart.series.filter(s => s.visible);
196
+ let newCategories = null;
197
+ if (visibleSeries.length === 1 && visibleSeries[0].options.metric === 'prev') {
198
+ newCategories = secondaryXAxis;
199
+ } else {
200
+ newCategories = mainXAxis;
201
+ }
202
+ const isSame = JSON.stringify(chart.xAxis[0].categories) === JSON.stringify(newCategories);
203
+ if (!isSame) {
204
+ chart.xAxis[0].setCategories(newCategories);
205
+ }
206
+ }
207
+ }
208
+ },
209
+ title: {
210
+ text: ''
211
+ },
212
+ subtitle: {
213
+ text: title
214
+ },
215
+ colors: colors,
216
+ xAxis: {
217
+ type: xAxisType,
218
+ labels: {
219
+ enabled: xAxisLabel,
220
+ autoRotation: false,
221
+ format: xAxisLabelFormat ? `{value:${xAxisLabelFormat}}` : undefined,
222
+ style: {
223
+ color: grey[600]
224
+ }
225
+ },
226
+ categories: categorize ? categories : undefined,
227
+ tickWidth: 1,
228
+ tickInterval: xAxisTickInterval ?? (xAxisType === 'datetime' ? records.length / 20 : 1),
229
+ lineColor: grey[900],
230
+ tickColor: grey[900],
231
+ crosshair: true,
232
+ startOnTick: true,
233
+ plotBands: xPlotIndex !== undefined ? xPlotBands : undefined,
234
+ plotLines: xPlotIndex !== undefined ? xPlotLines : undefined
235
+ },
236
+ yAxis: {
237
+ title: {
238
+ text: ''
239
+ },
240
+ labels: {
241
+ enabled: yAxisLabelEnabled,
242
+ formatter: yAxisLabelFormat,
243
+ style: {
244
+ color: grey[600]
245
+ }
246
+ },
247
+ tickInterval: yAxisTickInterval,
248
+ gridLineColor: '#f8f8f8',
249
+ plotLines: yPlotLines
250
+ },
251
+ plotOptions: {
252
+ series: {
253
+ opacity: opacity,
254
+ stacking: stackingType(stacking),
255
+ showInLegend: showLegend,
256
+ dataLabels: {
257
+ enabled: showDataLabel
258
+ },
259
+ colorByPoint: pie,
260
+ lineWidth: 2,
261
+ marker: {
262
+ enabled: false,
263
+ symbol: 'circle'
264
+ },
265
+ cursor: 'pointer',
266
+ point: {
267
+ events: {}
268
+ },
269
+ events: {
270
+ legendItemClick: handleLegendItemClick
271
+ }
272
+ },
273
+ pie: {
274
+ allowPointSelect: true,
275
+ innerSize: `${innerSize}%`
276
+ }
277
+ },
278
+ legend: {
279
+ align: legendAlign,
280
+ verticalAlign: 'top',
281
+ enabled: showLegend
282
+ },
283
+ tooltip: {
284
+ followPointer: false,
285
+ shared: true,
286
+ shadow: false,
287
+ useHTML: true,
288
+ formatter: function () {
289
+ return tooltipFormatter(this, tooltip);
290
+ },
291
+ outside: outside,
292
+ style: {
293
+ zIndex: 2000
294
+ }
295
+ },
296
+ series: series,
297
+ time: {
298
+ useUTC: false,
299
+ getTimezoneOffset: function () {
300
+ return new Date().getTimezoneOffset();
301
+ }
302
+ },
303
+ credits: {
304
+ enabled: false
305
+ }
306
+ };
307
+ }
308
+ const tooltipFormatter = (_this, props) => {
309
+ const {
310
+ headerVisible = true,
311
+ dateFormat,
312
+ headerTime,
313
+ legendColors = []
314
+ } = props;
315
+ let tooltip = '<div style="font-size: 14px;padding: 12px; min-width: 200px;">';
316
+ if (headerVisible) {
317
+ const xDate = new Date(_this.x);
318
+ const date = formatDate(xDate, dateFormat);
319
+ tooltip += `<div style="display: flex; justify-content: space-between; font-weight: bold; font-size: 12px;">
320
+ <span style="font-size: 12px; color: #333;">${date}</span>`;
321
+ if (headerTime) {
322
+ const time = formatDate(xDate, '%H:%M');
323
+ tooltip += `<span style="font-size: 12px; color: #333;">${time}</span>`;
324
+ }
325
+ tooltip += '</div>';
326
+ }
327
+ _this.points.forEach(point => {
328
+ const type = point.series.options.custom.type;
329
+ const value = type !== 'TO_PERCENT' ? format(point.y, type) : formatToPercent(_this.percentage / 100);
330
+ const name = point.series.name;
331
+ let color = type !== 'TO_PERCENT' ? point.series.color : point.color;
332
+ if (legendColors.length !== 0) {
333
+ color = legendColors[point.series.index];
334
+ }
335
+ tooltip += `<div style="display: flex; flex-direction: column; gap: 8px;">
336
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 15px;">
337
+ <span style="width: 10px; height: 10px; border-radius: 2px; margin-right: 8px; background-color: ${color}"></span>
338
+ <span style="flex-grow: 1; font-size: 14px; padding-right: 20px;">${name}</span>
339
+ <span style="font-weight: bold; font-size: 14px;">${value}</span>
340
+ </div>
341
+ </div>`;
342
+ });
343
+ tooltip += '</div>';
344
+ return tooltip;
345
+ };
346
+ function formatDate(data, format) {
347
+ return Highcharts.dateFormat(format, data.getTime() - data.getTimezoneOffset() * 60000);
348
+ }
349
+ const ZIndex = {
350
+ AVERAGE_LINE_BASE: 10,
351
+ COMPARE_CHART_BASE: 5,
352
+ NOW_BORDER: 2,
353
+ NOW_BAND: 1
354
+ };
355
+ function stackingType(stacking) {
356
+ return stacking ? stacking.toLowerCase() : '';
357
+ }
358
+ function pieCenters(count) {
359
+ switch (count) {
360
+ case 2:
361
+ return [['25%', '50%'], ['75%', '50%']];
362
+ case 3:
363
+ return [['25%', '25%'], ['75%', '25%'], ['50%', '75%']];
364
+ case 4:
365
+ return [['25%', '25%'], ['75%', '25%'], ['25%', '75%'], ['75%', '75%']];
366
+ default:
367
+ return [];
368
+ }
369
+ }
370
+ function pieSize(count) {
371
+ switch (count) {
372
+ case 3:
373
+ return '50%';
374
+ case 4:
375
+ return '25%';
376
+ default:
377
+ return null;
378
+ }
379
+ }
@@ -0,0 +1,11 @@
1
+ import { blue, green, lime, orange, purple, red, yellow } from "@nethru/ui/base/colors";
2
+ import { alpha } from "@mui/material";
3
+ export const all = [[blue[500]], [purple[500], '#4168a6', '#81a7e1'], [green[500], '#debb7f', '#ecdbbe'], [yellow[500], '#1b8286', '#4ab3b6'], [red[500]], [orange[500]], [lime[500]], ['#54AAF9'], ['#BB4ECD'], ['#6DDBBA'], ['#FFE81C'], ['#FA7EBA'], ['#F55713'], ['#51B304'], ['#1559B2'], ['#733FAB'], ['#1C7B5F'], ['#D87500'], ['#C91E48'], ['#2F862F']];
4
+ export const colors = [[blue[400], blue[500]], [purple[400], purple[500]], [green[400], green[500]], [yellow[400], yellow[500]], [red[400], red[500]], [orange[400], orange[500]], [lime[400], lime[500]]];
5
+ export const gradient = [[alpha(blue[500], 0.2), 'transparent', blue[300]], [alpha(purple[500], 0.2), 'transparent', purple[500]], [alpha(green[500], 0.2), 'transparent', green[500]], [alpha(yellow[500], 0.2), 'transparent', yellow[500]], [alpha(red[500], 0.2), 'transparent', red[500]], [alpha(orange[500], 0.2), 'transparent', orange[500]], [alpha(lime[500], 0.2), 'transparent', lime[500]]];
6
+ export const grey = ['#333', '#666', '#999'];
7
+ export const preset = ['#003f5c', '#a05195', '#665191', '#2f4b7c', '#d45087', '#f95d6a', '#ff7c43', '#ffa600'];
8
+ export const perRatio = ['#df5645', '#9fcc2e', '#333'];
9
+ export const compare = '#fa9f1b';
10
+ export const backgroundColor = '#fff';
11
+ export const main = all.map(color => color[0]);
@@ -0,0 +1,118 @@
1
+ import React, { useMemo } from 'react';
2
+ import Highcharts from 'highcharts';
3
+ import HighchartsReactModule from 'highcharts-react-official';
4
+ import 'highcharts/modules/pattern-fill.js';
5
+
6
+ // Handle both ES module and CommonJS exports
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ const HighchartsReact = HighchartsReactModule.default || HighchartsReactModule;
9
+ const ColumnChart = ({
10
+ data,
11
+ width,
12
+ height = 230
13
+ }) => {
14
+ const series = useMemo(() => {
15
+ return data.series.map((item, sindex) => ({
16
+ ...item,
17
+ data: item.data.map((value, index) => ({
18
+ y: value,
19
+ color: data.colors[index % data.colors.length] + alphas[sindex % alphas.length]
20
+ }))
21
+ }));
22
+ }, [data]);
23
+ const colors = useMemo(() => {
24
+ return data.series.map((_, index) => data.colors[0] + alphas[index % alphas.length]);
25
+ }, [data]);
26
+ const options = useMemo(() => {
27
+ return {
28
+ chart: {
29
+ type: 'column',
30
+ backgroundColor: 'transparent',
31
+ width: width,
32
+ height: height,
33
+ style: {
34
+ fontFamily: fontFamily
35
+ }
36
+ },
37
+ title: {
38
+ text: undefined
39
+ },
40
+ tooltip: {
41
+ pointFormat: '<span><b>{point.y}</b></span>',
42
+ style: {
43
+ fontFamily: fontFamily,
44
+ fontSize: '13px',
45
+ fontWeight: '100'
46
+ },
47
+ useHTML: true
48
+ },
49
+ plotOptions: {
50
+ column: {
51
+ //size: size
52
+ }
53
+ },
54
+ xAxis: {
55
+ categories: data.categories,
56
+ crosshair: true,
57
+ labels: {
58
+ style: {
59
+ color: '#777',
60
+ fontFamily: fontFamily,
61
+ fontWeight: '400'
62
+ }
63
+ },
64
+ lineColor: '#aaa'
65
+ },
66
+ yAxis: {
67
+ title: {
68
+ text: undefined
69
+ },
70
+ labels: {
71
+ style: {
72
+ fontFamily: fontFamily,
73
+ fontSize: '12px',
74
+ fontWeight: '100'
75
+ }
76
+ },
77
+ gridLineColor: '#f9f9f9'
78
+ },
79
+ legend: {
80
+ layout: 'vertical',
81
+ itemStyle: {
82
+ fontFamily: fontFamily,
83
+ fontSize: '12px',
84
+ fontWeight: '100'
85
+ },
86
+ useHTML: true
87
+ },
88
+ series: series,
89
+ colors: colors,
90
+ credits: {
91
+ enabled: false
92
+ },
93
+ accessibility: {
94
+ enabled: false
95
+ }
96
+ };
97
+ }, [data, width, height, series, colors]);
98
+ return /*#__PURE__*/_jsx("div", {
99
+ style: styles.chartWrapper,
100
+ children: /*#__PURE__*/_jsx(HighchartsReact, {
101
+ highcharts: Highcharts,
102
+ options: options
103
+ })
104
+ });
105
+ };
106
+ export default ColumnChart;
107
+ const fontFamily = 'Pretendard, -apple-system, BlinkMacSystemFont, sans-serif';
108
+ const styles = {
109
+ chartWrapper: {
110
+ background: 'transparent',
111
+ textAlign: 'center',
112
+ display: 'flex',
113
+ flexDirection: 'column',
114
+ justifyContent: 'center',
115
+ alignItems: 'center'
116
+ }
117
+ };
118
+ const alphas = ['ff', 'bb', '77', '33'];
@@ -0,0 +1,48 @@
1
+ export function toValue(value) {
2
+ return value && typeof value === 'object' ? value.value : value;
3
+ }
4
+ export function toPercent(value) {
5
+ return Math.round(value * 100 * 100) / 100;
6
+ }
7
+ export function formatNumber(value) {
8
+ return value !== undefined ? value.toLocaleString() : '-';
9
+ }
10
+ export function formatToPercent(value) {
11
+ return formatPercent(toPercent(value));
12
+ }
13
+ export function formatPercent(value) {
14
+ return value !== undefined ? `${value.toLocaleString()}%` : '-';
15
+ }
16
+ export function formatDiffPercent(value) {
17
+ return value !== undefined ? `${value > 0 ? '+' : ''}${formatToPercent(value)}` : '-';
18
+ }
19
+ export function formatDuration(value) {
20
+ value = toValue(value);
21
+ if (!value) return '-';
22
+ let day = Math.floor(value / (60 * 60 * 24));
23
+ let hour = Math.floor(value / (60 * 60)) - day * 24;
24
+ let minute = Math.floor(value / 60) - (day * 24 + hour) * 60;
25
+ let second = Math.round(value % 60);
26
+ day = day > 0 ? `${day.toLocaleString()}일 ` : '';
27
+ hour = hour > 0 ? `${hour >= 10 ? hour : '0' + hour}시간 ` : '';
28
+ minute = `${minute >= 10 ? minute : '0' + minute}분 `;
29
+ second = `${second >= 10 ? second : '0' + second}초`;
30
+ return `${day}${hour}${minute}${second}`;
31
+ }
32
+ export function formatBytes(value) {
33
+ return value !== undefined ? `${value.toLocaleString()} bytes` : '-';
34
+ }
35
+ export function format(value, type) {
36
+ switch (type) {
37
+ case 'TO_PERCENT':
38
+ return formatToPercent(value);
39
+ case 'DURATION':
40
+ return formatDuration(value);
41
+ case 'BYTES':
42
+ return formatBytes(value);
43
+ case 'PERCENT':
44
+ return formatPercent(value);
45
+ default:
46
+ return formatNumber(value);
47
+ }
48
+ }