@nethru/kit 1.0.0 → 1.0.2

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/dist/index.esm.js CHANGED
@@ -1,38 +1,512 @@
1
- import React from 'react';
1
+ import { useState, useEffect, useCallback, useMemo } from 'react';
2
+ import Highcharts from 'highcharts';
3
+ import HighchartsReact from 'highcharts-react-official';
4
+ import highchartsAccessibility from 'highcharts/modules/accessibility';
5
+ import highchartsAnnotations from 'highcharts/modules/annotations';
6
+ import { blue, purple, green, yellow, red, orange, lime, grey } from '@nethru/ui/base/colors';
7
+ import { alpha } from '@mui/material';
2
8
 
3
- const Button = ({
4
- children,
5
- onClick,
6
- variant = 'primary',
7
- disabled = false
8
- }) => {
9
- const baseStyles = {
10
- padding: '10px 20px',
11
- fontSize: '16px',
12
- border: 'none',
13
- borderRadius: '4px',
14
- cursor: disabled ? 'not-allowed' : 'pointer',
15
- opacity: disabled ? 0.6 : 1,
16
- transition: 'all 0.2s ease'
9
+ function _extends() {
10
+ return _extends = Object.assign ? Object.assign.bind() : function (n) {
11
+ for (var e = 1; e < arguments.length; e++) {
12
+ var t = arguments[e];
13
+ for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
14
+ }
15
+ return n;
16
+ }, _extends.apply(null, arguments);
17
+ }
18
+
19
+ function toValue(value) {
20
+ return value && typeof value === 'object' ? value.value : value;
21
+ }
22
+ function toPercent(value) {
23
+ return Math.round(value * 100 * 100) / 100;
24
+ }
25
+ function formatNumber(value) {
26
+ return value !== undefined ? value.toLocaleString() : '-';
27
+ }
28
+ function formatToPercent(value) {
29
+ return formatPercent(toPercent(value));
30
+ }
31
+ function formatPercent(value) {
32
+ return value !== undefined ? `${value.toLocaleString()}%` : '-';
33
+ }
34
+ function formatDuration(value) {
35
+ value = toValue(value);
36
+ if (!value) return '-';
37
+ let day = Math.floor(value / (60 * 60 * 24));
38
+ let hour = Math.floor(value / (60 * 60)) - day * 24;
39
+ let minute = Math.floor(value / 60) - (day * 24 + hour) * 60;
40
+ let second = Math.round(value % 60);
41
+ day = day > 0 ? `${day.toLocaleString()}일 ` : '';
42
+ hour = hour > 0 ? `${hour >= 10 ? hour : '0' + hour}시간 ` : '';
43
+ minute = `${minute >= 10 ? minute : '0' + minute}분 `;
44
+ second = `${second >= 10 ? second : '0' + second}초`;
45
+ return `${day}${hour}${minute}${second}`;
46
+ }
47
+ function formatBytes(value) {
48
+ return value !== undefined ? `${value.toLocaleString()} bytes` : '-';
49
+ }
50
+ function format(value, type) {
51
+ switch (type) {
52
+ case 'TO_PERCENT':
53
+ return formatToPercent(value);
54
+ case 'DURATION':
55
+ return formatDuration(value);
56
+ case 'BYTES':
57
+ return formatBytes(value);
58
+ case 'PERCENT':
59
+ return formatPercent(value);
60
+ default:
61
+ return formatNumber(value);
62
+ }
63
+ }
64
+
65
+ 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']];
66
+ [[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]]];
67
+ [[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]]];
68
+ const preset = ['#003f5c', '#a05195', '#665191', '#2f4b7c', '#d45087', '#f95d6a', '#ff7c43', '#ffa600'];
69
+ all.map(color => color[0]);
70
+
71
+ highchartsAccessibility(Highcharts);
72
+ highchartsAnnotations(Highcharts);
73
+ function Chart(props) {
74
+ const [legendLimitEnabled, setLegendLimitEnabled] = useState(props.legendLimit > 0);
75
+ const [options, setOptions] = useState(toOptions(props, legendLimitEnabled));
76
+ useEffect(() => {
77
+ if (props.delay) setTimeout(() => setOptions(toOptions(props, legendLimitEnabled, handleLegendItemClick)), props.delay);else setOptions(toOptions(props, legendLimitEnabled, handleLegendItemClick));
78
+ // eslint-disable-next-line
79
+ }, [props]);
80
+ const handleLegendItemClick = useCallback(() => setLegendLimitEnabled(false), []);
81
+ return /*#__PURE__*/React.createElement(React.Fragment, null, options && /*#__PURE__*/React.createElement(HighchartsReact, {
82
+ highcharts: Highcharts,
83
+ options: options
84
+ }));
85
+ }
86
+ function toOptions(props, legendLimitEnabled, handleLegendItemClick) {
87
+ const {
88
+ title = '',
89
+ type,
90
+ categorize,
91
+ stacking,
92
+ showLegend = true,
93
+ showDataLabel,
94
+ xAxisType,
95
+ xAxisLabel = true,
96
+ xAxisLabelFormat,
97
+ xAxisTickInterval,
98
+ yAxisLabelEnabled = true,
99
+ yAxisLabelFormat,
100
+ yAxisTickInterval = null,
101
+ yAxisPlotLines = [],
102
+ opacity = 1,
103
+ innerSize = 0,
104
+ colors: customColors,
105
+ backgroundColor = undefined,
106
+ xPlotIndex,
107
+ dimension,
108
+ metrics,
109
+ records = [],
110
+ metas,
111
+ height = 200,
112
+ legendLimit,
113
+ legendAlign = 'center',
114
+ tooltip = {},
115
+ secondaryXAxis = []
116
+ } = props;
117
+ const {
118
+ outside
119
+ } = tooltip;
120
+ const pie = type === 'pie';
121
+ const centers = pieCenters(metrics.length);
122
+ const size = pieSize(metrics.length);
123
+ const colors = customColors ? customColors : preset;
124
+ const categories = records.map(record => record[dimension]);
125
+ const mainXAxis = records.map(r => r.time);
126
+ const chartColorHandlers = {
127
+ line: (colors, index) => ({
128
+ color: colors[index],
129
+ mainColor: colors[index]
130
+ }),
131
+ area: (colors, index) => ({
132
+ color: colors[index][0],
133
+ mainColor: colors[index][1]
134
+ }),
135
+ gradient: (colors, index) => ({
136
+ color: {
137
+ linearGradient: {
138
+ x1: 0,
139
+ y1: 0,
140
+ x2: 0,
141
+ y2: 1
142
+ },
143
+ stops: [[0, `${colors[index][0]}`], [0.95, `${colors[index][1]}`]]
144
+ },
145
+ mainColor: colors[index][2]
146
+ })
17
147
  };
18
- const variantStyles = {
19
- primary: {
20
- backgroundColor: '#007bff',
21
- color: 'white'
22
- },
23
- secondary: {
24
- backgroundColor: '#6c757d',
25
- color: 'white'
148
+ const series = metrics.map(({
149
+ chartType,
150
+ metric
151
+ }, index) => {
152
+ const type = chartType === 'gradient' ? 'area' : chartType;
153
+ const {
154
+ color,
155
+ mainColor
156
+ } = chartColorHandlers[chartType](colors, index);
157
+ const meta = metas[metric];
158
+ let option = {
159
+ type: type,
160
+ name: meta ? meta.name : '',
161
+ data: records.map(record => {
162
+ const data = record.metric[metric] ?? null;
163
+ return pie ? {
164
+ name: record[dimension],
165
+ y: data
166
+ } : data;
167
+ }),
168
+ custom: {
169
+ type: meta ? meta.type : '',
170
+ pointerNames: meta?.pointerNames ?? null
171
+ },
172
+ center: centers[index] || [null, null],
173
+ size: size,
174
+ point: {
175
+ events: {
176
+ render: function () {
177
+ this.graphic.attr({
178
+ zIndex: this.y === 0 ? -1 : ZIndex.COMPARE_CHART_BASE - index
179
+ });
180
+ }
181
+ }
182
+ },
183
+ showInLegend: pie && index > 0 ? false : true,
184
+ color: color,
185
+ lineColor: mainColor,
186
+ lineWidth: 2,
187
+ marker: {
188
+ fillColor: mainColor
189
+ },
190
+ metric: metric
191
+ };
192
+ if (legendLimitEnabled) option = {
193
+ ...option,
194
+ visible: index < legendLimit
195
+ };
196
+ return option;
197
+ });
198
+ const yPlotLines = yAxisPlotLines.map((line, index) => {
199
+ return {
200
+ value: line.value,
201
+ width: 1,
202
+ color: line.color,
203
+ dashStyle: "Dash",
204
+ zIndex: ZIndex.AVERAGE_LINE_BASE + index,
205
+ label: {
206
+ text: line.label,
207
+ align: line.labelAlign,
208
+ style: {
209
+ color: line.labelColor,
210
+ opacity: 0.7,
211
+ fontSize: "12px",
212
+ padding: '1px 5px'
213
+ },
214
+ y: -8,
215
+ useHTML: true
216
+ }
217
+ };
218
+ });
219
+ const xPlotBands = [{
220
+ from: xPlotIndex,
221
+ to: categories.length - 1,
222
+ color: 'rgba(0, 0, 0, 0.03)',
223
+ zIndex: ZIndex.NOW_BAND
224
+ }];
225
+ const xPlotLines = [{
226
+ color: "#aaa",
227
+ width: 1,
228
+ value: xPlotIndex,
229
+ zIndex: ZIndex.NOW_BORDER,
230
+ label: {
231
+ text: "현재",
232
+ rotation: 0,
233
+ x: 5,
234
+ y: 13,
235
+ style: {
236
+ fontWeight: 500,
237
+ lineHeight: "16.8px",
238
+ color: "#333",
239
+ fontSize: "12px"
240
+ }
241
+ }
242
+ }];
243
+ return {
244
+ chart: {
245
+ type: type.toLowerCase(),
246
+ height: height,
247
+ backgroundColor,
248
+ events: {
249
+ render: secondaryXAxis.length === 0 ? undefined : function () {
250
+ const chart = this;
251
+ const visibleSeries = chart.series.filter(s => s.visible);
252
+ let newCategories = null;
253
+ if (visibleSeries.length === 1 && visibleSeries[0].options.metric === 'prev') {
254
+ newCategories = secondaryXAxis;
255
+ } else {
256
+ newCategories = mainXAxis;
257
+ }
258
+ const isSame = JSON.stringify(chart.xAxis[0].categories) === JSON.stringify(newCategories);
259
+ if (!isSame) {
260
+ chart.xAxis[0].setCategories(newCategories);
261
+ }
262
+ }
263
+ }
264
+ },
265
+ title: {
266
+ text: ''
267
+ },
268
+ subtitle: {
269
+ text: title
270
+ },
271
+ colors: colors,
272
+ xAxis: {
273
+ type: xAxisType,
274
+ labels: {
275
+ enabled: xAxisLabel,
276
+ autoRotation: false,
277
+ format: xAxisLabelFormat ? `{value:${xAxisLabelFormat}}` : undefined,
278
+ style: {
279
+ color: grey[600]
280
+ }
281
+ },
282
+ categories: categorize ? categories : undefined,
283
+ tickWidth: 1,
284
+ tickInterval: xAxisTickInterval ?? (xAxisType === 'datetime' ? records.length / 20 : 1),
285
+ lineColor: grey[900],
286
+ tickColor: grey[900],
287
+ crosshair: true,
288
+ startOnTick: true,
289
+ plotBands: xPlotIndex !== undefined ? xPlotBands : undefined,
290
+ plotLines: xPlotIndex !== undefined ? xPlotLines : undefined
291
+ },
292
+ yAxis: {
293
+ title: {
294
+ text: ''
295
+ },
296
+ labels: {
297
+ enabled: yAxisLabelEnabled,
298
+ formatter: yAxisLabelFormat,
299
+ style: {
300
+ color: grey[600]
301
+ }
302
+ },
303
+ tickInterval: yAxisTickInterval,
304
+ gridLineColor: '#f8f8f8',
305
+ plotLines: yPlotLines
306
+ },
307
+ plotOptions: {
308
+ series: {
309
+ opacity: opacity,
310
+ stacking: stackingType(stacking),
311
+ showInLegend: showLegend,
312
+ dataLabels: {
313
+ enabled: showDataLabel
314
+ },
315
+ colorByPoint: pie,
316
+ lineWidth: 2,
317
+ marker: {
318
+ enabled: false,
319
+ symbol: 'circle'
320
+ },
321
+ cursor: 'pointer',
322
+ point: {
323
+ events: {}
324
+ },
325
+ events: {
326
+ legendItemClick: handleLegendItemClick
327
+ }
328
+ },
329
+ pie: {
330
+ allowPointSelect: true,
331
+ innerSize: `${innerSize}%`
332
+ }
333
+ },
334
+ legend: {
335
+ align: legendAlign,
336
+ verticalAlign: 'top',
337
+ enabled: showLegend
338
+ },
339
+ tooltip: {
340
+ followPointer: false,
341
+ shared: true,
342
+ shadow: false,
343
+ useHTML: true,
344
+ formatter: function () {
345
+ return tooltipFormatter(this, tooltip);
346
+ },
347
+ outside: outside,
348
+ style: {
349
+ zIndex: 2000
350
+ }
351
+ },
352
+ series: series,
353
+ time: {
354
+ useUTC: false,
355
+ getTimezoneOffset: function () {
356
+ return new Date().getTimezoneOffset();
357
+ }
358
+ },
359
+ credits: {
360
+ enabled: false
26
361
  }
27
362
  };
28
- return /*#__PURE__*/React.createElement("button", {
29
- onClick: onClick,
30
- disabled: disabled,
31
- style: {
32
- ...baseStyles,
33
- ...variantStyles[variant]
363
+ }
364
+ const tooltipFormatter = (_this, props) => {
365
+ const {
366
+ headerVisible = true,
367
+ dateFormat,
368
+ headerTime,
369
+ legendColors = []
370
+ } = props;
371
+ let tooltip = '<div style="font-size: 14px;padding: 12px; min-width: 200px;">';
372
+ if (headerVisible) {
373
+ const xDate = new Date(_this.x);
374
+ const date = formatDate(xDate, dateFormat);
375
+ tooltip += `<div style="display: flex; justify-content: space-between; font-weight: bold; font-size: 12px;">
376
+ <span style="font-size: 12px; color: #333;">${date}</span>`;
377
+ if (headerTime) {
378
+ const time = formatDate(xDate, '%H:%M');
379
+ tooltip += `<span style="font-size: 12px; color: #333;">${time}</span>`;
34
380
  }
35
- }, children);
381
+ tooltip += '</div>';
382
+ }
383
+ _this.points.forEach(point => {
384
+ const type = point.series.options.custom.type;
385
+ const value = type !== 'TO_PERCENT' ? format(point.y, type) : formatToPercent(_this.percentage / 100);
386
+ const name = point.series.name;
387
+ let color = type !== 'TO_PERCENT' ? point.series.color : point.color;
388
+ if (legendColors.length !== 0) {
389
+ color = legendColors[point.series.index];
390
+ }
391
+ tooltip += `<div style="display: flex; flex-direction: column; gap: 8px;">
392
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 15px;">
393
+ <span style="width: 10px; height: 10px; border-radius: 2px; margin-right: 8px; background-color: ${color}"></span>
394
+ <span style="flex-grow: 1; font-size: 14px; padding-right: 20px;">${name}</span>
395
+ <span style="font-weight: bold; font-size: 14px;">${value}</span>
396
+ </div>
397
+ </div>`;
398
+ });
399
+ tooltip += '</div>';
400
+ return tooltip;
401
+ };
402
+ function formatDate(data, format) {
403
+ return Highcharts.dateFormat(format, data.getTime() - data.getTimezoneOffset() * 60000);
404
+ }
405
+ const ZIndex = {
406
+ AVERAGE_LINE_BASE: 10,
407
+ COMPARE_CHART_BASE: 5,
408
+ NOW_BORDER: 2,
409
+ NOW_BAND: 1
36
410
  };
411
+ function stackingType(stacking) {
412
+ return stacking ? stacking.toLowerCase() : '';
413
+ }
414
+ function pieCenters(count) {
415
+ switch (count) {
416
+ case 2:
417
+ return [['25%', '50%'], ['75%', '50%']];
418
+ case 3:
419
+ return [['25%', '25%'], ['75%', '25%'], ['50%', '75%']];
420
+ case 4:
421
+ return [['25%', '25%'], ['75%', '25%'], ['25%', '75%'], ['75%', '75%']];
422
+ default:
423
+ return [];
424
+ }
425
+ }
426
+ function pieSize(count) {
427
+ switch (count) {
428
+ case 3:
429
+ return '50%';
430
+ case 4:
431
+ return '25%';
432
+ default:
433
+ return null;
434
+ }
435
+ }
436
+
437
+ function TrendChart({
438
+ colorIndex = 0,
439
+ legendColors,
440
+ isDaily,
441
+ isRealtime,
442
+ ...props
443
+ }) {
444
+ const colors = useMemo(() => {
445
+ return all[colorIndex % all.length];
446
+ }, [colorIndex]);
447
+ const tooltip = useMemo(() => ({
448
+ dateFormat: isRealtime ? '%H:%M' : '%Y-%m-%d',
449
+ headerTime: !isDaily && !isRealtime,
450
+ outside: props.tooltipOutSide,
451
+ legendColors
452
+ }), [isRealtime, isDaily, props.tooltipOutSide, legendColors]);
453
+ return /*#__PURE__*/React.createElement(Chart, _extends({
454
+ type: "areaspline",
455
+ categorize: true,
456
+ xAxisType: "datetime",
457
+ xAxisLabelFormat: isDaily ? '%m-%d' : '%H:%M',
458
+ colors: colors,
459
+ tooltip: tooltip
460
+ }, props));
461
+ }
462
+
463
+ function StackedAreaTrendChart({
464
+ metrics,
465
+ records,
466
+ isDaily = false,
467
+ xPlotIndex,
468
+ xAxisTickInterval,
469
+ yAxisLabelEnabled = false,
470
+ legendLimit = 3,
471
+ colors = defaultColors,
472
+ ...props
473
+ }) {
474
+ const _metrics = useMemo(() => {
475
+ return metrics.map(m => ({
476
+ metric: m.id,
477
+ chartType: 'area'
478
+ }));
479
+ }, [metrics]);
480
+ const metas = useMemo(() => {
481
+ const result = {
482
+ time: {
483
+ name: "일시",
484
+ type: "DATE"
485
+ }
486
+ };
487
+ metrics.forEach(m => {
488
+ result[m.id] = {
489
+ name: m.name,
490
+ type: "NUMBER"
491
+ };
492
+ });
493
+ return result;
494
+ }, [metrics]);
495
+ return /*#__PURE__*/React.createElement(TrendChart, _extends({
496
+ dimension: "time",
497
+ stacking: "normal",
498
+ metrics: _metrics,
499
+ metas: metas,
500
+ records: records,
501
+ isRealtime: xPlotIndex !== undefined,
502
+ isDaily: isDaily,
503
+ xPlotIndex: xPlotIndex,
504
+ xAxisTickInterval: xAxisTickInterval,
505
+ yAxisLabelEnabled: yAxisLabelEnabled,
506
+ legendLimit: legendLimit,
507
+ colors: colors
508
+ }, props));
509
+ }
510
+ const defaultColors = [["rgba(54,115,237,0.8)", "#3673ed"], ["rgba(149,77,241,0.8)", "#954df1"], ["#4dc391", "#20b476"], ["#fbba3d", "#faa90c"], ["#f2426d", "#ef1348"]];
37
511
 
38
- export { Button };
512
+ export { StackedAreaTrendChart };
package/dist/index.js CHANGED
@@ -1,40 +1,514 @@
1
1
  'use strict';
2
2
 
3
- var React = require('react');
4
-
5
- const Button = ({
6
- children,
7
- onClick,
8
- variant = 'primary',
9
- disabled = false
10
- }) => {
11
- const baseStyles = {
12
- padding: '10px 20px',
13
- fontSize: '16px',
14
- border: 'none',
15
- borderRadius: '4px',
16
- cursor: disabled ? 'not-allowed' : 'pointer',
17
- opacity: disabled ? 0.6 : 1,
18
- transition: 'all 0.2s ease'
3
+ var react = require('react');
4
+ var Highcharts = require('highcharts');
5
+ var HighchartsReact = require('highcharts-react-official');
6
+ var highchartsAccessibility = require('highcharts/modules/accessibility');
7
+ var highchartsAnnotations = require('highcharts/modules/annotations');
8
+ var colors = require('@nethru/ui/base/colors');
9
+ var material = require('@mui/material');
10
+
11
+ function _extends() {
12
+ return _extends = Object.assign ? Object.assign.bind() : function (n) {
13
+ for (var e = 1; e < arguments.length; e++) {
14
+ var t = arguments[e];
15
+ for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
16
+ }
17
+ return n;
18
+ }, _extends.apply(null, arguments);
19
+ }
20
+
21
+ function toValue(value) {
22
+ return value && typeof value === 'object' ? value.value : value;
23
+ }
24
+ function toPercent(value) {
25
+ return Math.round(value * 100 * 100) / 100;
26
+ }
27
+ function formatNumber(value) {
28
+ return value !== undefined ? value.toLocaleString() : '-';
29
+ }
30
+ function formatToPercent(value) {
31
+ return formatPercent(toPercent(value));
32
+ }
33
+ function formatPercent(value) {
34
+ return value !== undefined ? `${value.toLocaleString()}%` : '-';
35
+ }
36
+ function formatDuration(value) {
37
+ value = toValue(value);
38
+ if (!value) return '-';
39
+ let day = Math.floor(value / (60 * 60 * 24));
40
+ let hour = Math.floor(value / (60 * 60)) - day * 24;
41
+ let minute = Math.floor(value / 60) - (day * 24 + hour) * 60;
42
+ let second = Math.round(value % 60);
43
+ day = day > 0 ? `${day.toLocaleString()}일 ` : '';
44
+ hour = hour > 0 ? `${hour >= 10 ? hour : '0' + hour}시간 ` : '';
45
+ minute = `${minute >= 10 ? minute : '0' + minute}분 `;
46
+ second = `${second >= 10 ? second : '0' + second}초`;
47
+ return `${day}${hour}${minute}${second}`;
48
+ }
49
+ function formatBytes(value) {
50
+ return value !== undefined ? `${value.toLocaleString()} bytes` : '-';
51
+ }
52
+ function format(value, type) {
53
+ switch (type) {
54
+ case 'TO_PERCENT':
55
+ return formatToPercent(value);
56
+ case 'DURATION':
57
+ return formatDuration(value);
58
+ case 'BYTES':
59
+ return formatBytes(value);
60
+ case 'PERCENT':
61
+ return formatPercent(value);
62
+ default:
63
+ return formatNumber(value);
64
+ }
65
+ }
66
+
67
+ const all = [[colors.blue[500]], [colors.purple[500], '#4168a6', '#81a7e1'], [colors.green[500], '#debb7f', '#ecdbbe'], [colors.yellow[500], '#1b8286', '#4ab3b6'], [colors.red[500]], [colors.orange[500]], [colors.lime[500]], ['#54AAF9'], ['#BB4ECD'], ['#6DDBBA'], ['#FFE81C'], ['#FA7EBA'], ['#F55713'], ['#51B304'], ['#1559B2'], ['#733FAB'], ['#1C7B5F'], ['#D87500'], ['#C91E48'], ['#2F862F']];
68
+ [[colors.blue[400], colors.blue[500]], [colors.purple[400], colors.purple[500]], [colors.green[400], colors.green[500]], [colors.yellow[400], colors.yellow[500]], [colors.red[400], colors.red[500]], [colors.orange[400], colors.orange[500]], [colors.lime[400], colors.lime[500]]];
69
+ [[material.alpha(colors.blue[500], 0.2), 'transparent', colors.blue[300]], [material.alpha(colors.purple[500], 0.2), 'transparent', colors.purple[500]], [material.alpha(colors.green[500], 0.2), 'transparent', colors.green[500]], [material.alpha(colors.yellow[500], 0.2), 'transparent', colors.yellow[500]], [material.alpha(colors.red[500], 0.2), 'transparent', colors.red[500]], [material.alpha(colors.orange[500], 0.2), 'transparent', colors.orange[500]], [material.alpha(colors.lime[500], 0.2), 'transparent', colors.lime[500]]];
70
+ const preset = ['#003f5c', '#a05195', '#665191', '#2f4b7c', '#d45087', '#f95d6a', '#ff7c43', '#ffa600'];
71
+ all.map(color => color[0]);
72
+
73
+ highchartsAccessibility(Highcharts);
74
+ highchartsAnnotations(Highcharts);
75
+ function Chart(props) {
76
+ const [legendLimitEnabled, setLegendLimitEnabled] = react.useState(props.legendLimit > 0);
77
+ const [options, setOptions] = react.useState(toOptions(props, legendLimitEnabled));
78
+ react.useEffect(() => {
79
+ if (props.delay) setTimeout(() => setOptions(toOptions(props, legendLimitEnabled, handleLegendItemClick)), props.delay);else setOptions(toOptions(props, legendLimitEnabled, handleLegendItemClick));
80
+ // eslint-disable-next-line
81
+ }, [props]);
82
+ const handleLegendItemClick = react.useCallback(() => setLegendLimitEnabled(false), []);
83
+ return /*#__PURE__*/React.createElement(React.Fragment, null, options && /*#__PURE__*/React.createElement(HighchartsReact, {
84
+ highcharts: Highcharts,
85
+ options: options
86
+ }));
87
+ }
88
+ function toOptions(props, legendLimitEnabled, handleLegendItemClick) {
89
+ const {
90
+ title = '',
91
+ type,
92
+ categorize,
93
+ stacking,
94
+ showLegend = true,
95
+ showDataLabel,
96
+ xAxisType,
97
+ xAxisLabel = true,
98
+ xAxisLabelFormat,
99
+ xAxisTickInterval,
100
+ yAxisLabelEnabled = true,
101
+ yAxisLabelFormat,
102
+ yAxisTickInterval = null,
103
+ yAxisPlotLines = [],
104
+ opacity = 1,
105
+ innerSize = 0,
106
+ colors: customColors,
107
+ backgroundColor = undefined,
108
+ xPlotIndex,
109
+ dimension,
110
+ metrics,
111
+ records = [],
112
+ metas,
113
+ height = 200,
114
+ legendLimit,
115
+ legendAlign = 'center',
116
+ tooltip = {},
117
+ secondaryXAxis = []
118
+ } = props;
119
+ const {
120
+ outside
121
+ } = tooltip;
122
+ const pie = type === 'pie';
123
+ const centers = pieCenters(metrics.length);
124
+ const size = pieSize(metrics.length);
125
+ const colors$1 = customColors ? customColors : preset;
126
+ const categories = records.map(record => record[dimension]);
127
+ const mainXAxis = records.map(r => r.time);
128
+ const chartColorHandlers = {
129
+ line: (colors, index) => ({
130
+ color: colors[index],
131
+ mainColor: colors[index]
132
+ }),
133
+ area: (colors, index) => ({
134
+ color: colors[index][0],
135
+ mainColor: colors[index][1]
136
+ }),
137
+ gradient: (colors, index) => ({
138
+ color: {
139
+ linearGradient: {
140
+ x1: 0,
141
+ y1: 0,
142
+ x2: 0,
143
+ y2: 1
144
+ },
145
+ stops: [[0, `${colors[index][0]}`], [0.95, `${colors[index][1]}`]]
146
+ },
147
+ mainColor: colors[index][2]
148
+ })
19
149
  };
20
- const variantStyles = {
21
- primary: {
22
- backgroundColor: '#007bff',
23
- color: 'white'
24
- },
25
- secondary: {
26
- backgroundColor: '#6c757d',
27
- color: 'white'
150
+ const series = metrics.map(({
151
+ chartType,
152
+ metric
153
+ }, index) => {
154
+ const type = chartType === 'gradient' ? 'area' : chartType;
155
+ const {
156
+ color,
157
+ mainColor
158
+ } = chartColorHandlers[chartType](colors$1, index);
159
+ const meta = metas[metric];
160
+ let option = {
161
+ type: type,
162
+ name: meta ? meta.name : '',
163
+ data: records.map(record => {
164
+ const data = record.metric[metric] ?? null;
165
+ return pie ? {
166
+ name: record[dimension],
167
+ y: data
168
+ } : data;
169
+ }),
170
+ custom: {
171
+ type: meta ? meta.type : '',
172
+ pointerNames: meta?.pointerNames ?? null
173
+ },
174
+ center: centers[index] || [null, null],
175
+ size: size,
176
+ point: {
177
+ events: {
178
+ render: function () {
179
+ this.graphic.attr({
180
+ zIndex: this.y === 0 ? -1 : ZIndex.COMPARE_CHART_BASE - index
181
+ });
182
+ }
183
+ }
184
+ },
185
+ showInLegend: pie && index > 0 ? false : true,
186
+ color: color,
187
+ lineColor: mainColor,
188
+ lineWidth: 2,
189
+ marker: {
190
+ fillColor: mainColor
191
+ },
192
+ metric: metric
193
+ };
194
+ if (legendLimitEnabled) option = {
195
+ ...option,
196
+ visible: index < legendLimit
197
+ };
198
+ return option;
199
+ });
200
+ const yPlotLines = yAxisPlotLines.map((line, index) => {
201
+ return {
202
+ value: line.value,
203
+ width: 1,
204
+ color: line.color,
205
+ dashStyle: "Dash",
206
+ zIndex: ZIndex.AVERAGE_LINE_BASE + index,
207
+ label: {
208
+ text: line.label,
209
+ align: line.labelAlign,
210
+ style: {
211
+ color: line.labelColor,
212
+ opacity: 0.7,
213
+ fontSize: "12px",
214
+ padding: '1px 5px'
215
+ },
216
+ y: -8,
217
+ useHTML: true
218
+ }
219
+ };
220
+ });
221
+ const xPlotBands = [{
222
+ from: xPlotIndex,
223
+ to: categories.length - 1,
224
+ color: 'rgba(0, 0, 0, 0.03)',
225
+ zIndex: ZIndex.NOW_BAND
226
+ }];
227
+ const xPlotLines = [{
228
+ color: "#aaa",
229
+ width: 1,
230
+ value: xPlotIndex,
231
+ zIndex: ZIndex.NOW_BORDER,
232
+ label: {
233
+ text: "현재",
234
+ rotation: 0,
235
+ x: 5,
236
+ y: 13,
237
+ style: {
238
+ fontWeight: 500,
239
+ lineHeight: "16.8px",
240
+ color: "#333",
241
+ fontSize: "12px"
242
+ }
243
+ }
244
+ }];
245
+ return {
246
+ chart: {
247
+ type: type.toLowerCase(),
248
+ height: height,
249
+ backgroundColor,
250
+ events: {
251
+ render: secondaryXAxis.length === 0 ? undefined : function () {
252
+ const chart = this;
253
+ const visibleSeries = chart.series.filter(s => s.visible);
254
+ let newCategories = null;
255
+ if (visibleSeries.length === 1 && visibleSeries[0].options.metric === 'prev') {
256
+ newCategories = secondaryXAxis;
257
+ } else {
258
+ newCategories = mainXAxis;
259
+ }
260
+ const isSame = JSON.stringify(chart.xAxis[0].categories) === JSON.stringify(newCategories);
261
+ if (!isSame) {
262
+ chart.xAxis[0].setCategories(newCategories);
263
+ }
264
+ }
265
+ }
266
+ },
267
+ title: {
268
+ text: ''
269
+ },
270
+ subtitle: {
271
+ text: title
272
+ },
273
+ colors: colors$1,
274
+ xAxis: {
275
+ type: xAxisType,
276
+ labels: {
277
+ enabled: xAxisLabel,
278
+ autoRotation: false,
279
+ format: xAxisLabelFormat ? `{value:${xAxisLabelFormat}}` : undefined,
280
+ style: {
281
+ color: colors.grey[600]
282
+ }
283
+ },
284
+ categories: categorize ? categories : undefined,
285
+ tickWidth: 1,
286
+ tickInterval: xAxisTickInterval ?? (xAxisType === 'datetime' ? records.length / 20 : 1),
287
+ lineColor: colors.grey[900],
288
+ tickColor: colors.grey[900],
289
+ crosshair: true,
290
+ startOnTick: true,
291
+ plotBands: xPlotIndex !== undefined ? xPlotBands : undefined,
292
+ plotLines: xPlotIndex !== undefined ? xPlotLines : undefined
293
+ },
294
+ yAxis: {
295
+ title: {
296
+ text: ''
297
+ },
298
+ labels: {
299
+ enabled: yAxisLabelEnabled,
300
+ formatter: yAxisLabelFormat,
301
+ style: {
302
+ color: colors.grey[600]
303
+ }
304
+ },
305
+ tickInterval: yAxisTickInterval,
306
+ gridLineColor: '#f8f8f8',
307
+ plotLines: yPlotLines
308
+ },
309
+ plotOptions: {
310
+ series: {
311
+ opacity: opacity,
312
+ stacking: stackingType(stacking),
313
+ showInLegend: showLegend,
314
+ dataLabels: {
315
+ enabled: showDataLabel
316
+ },
317
+ colorByPoint: pie,
318
+ lineWidth: 2,
319
+ marker: {
320
+ enabled: false,
321
+ symbol: 'circle'
322
+ },
323
+ cursor: 'pointer',
324
+ point: {
325
+ events: {}
326
+ },
327
+ events: {
328
+ legendItemClick: handleLegendItemClick
329
+ }
330
+ },
331
+ pie: {
332
+ allowPointSelect: true,
333
+ innerSize: `${innerSize}%`
334
+ }
335
+ },
336
+ legend: {
337
+ align: legendAlign,
338
+ verticalAlign: 'top',
339
+ enabled: showLegend
340
+ },
341
+ tooltip: {
342
+ followPointer: false,
343
+ shared: true,
344
+ shadow: false,
345
+ useHTML: true,
346
+ formatter: function () {
347
+ return tooltipFormatter(this, tooltip);
348
+ },
349
+ outside: outside,
350
+ style: {
351
+ zIndex: 2000
352
+ }
353
+ },
354
+ series: series,
355
+ time: {
356
+ useUTC: false,
357
+ getTimezoneOffset: function () {
358
+ return new Date().getTimezoneOffset();
359
+ }
360
+ },
361
+ credits: {
362
+ enabled: false
28
363
  }
29
364
  };
30
- return /*#__PURE__*/React.createElement("button", {
31
- onClick: onClick,
32
- disabled: disabled,
33
- style: {
34
- ...baseStyles,
35
- ...variantStyles[variant]
365
+ }
366
+ const tooltipFormatter = (_this, props) => {
367
+ const {
368
+ headerVisible = true,
369
+ dateFormat,
370
+ headerTime,
371
+ legendColors = []
372
+ } = props;
373
+ let tooltip = '<div style="font-size: 14px;padding: 12px; min-width: 200px;">';
374
+ if (headerVisible) {
375
+ const xDate = new Date(_this.x);
376
+ const date = formatDate(xDate, dateFormat);
377
+ tooltip += `<div style="display: flex; justify-content: space-between; font-weight: bold; font-size: 12px;">
378
+ <span style="font-size: 12px; color: #333;">${date}</span>`;
379
+ if (headerTime) {
380
+ const time = formatDate(xDate, '%H:%M');
381
+ tooltip += `<span style="font-size: 12px; color: #333;">${time}</span>`;
36
382
  }
37
- }, children);
383
+ tooltip += '</div>';
384
+ }
385
+ _this.points.forEach(point => {
386
+ const type = point.series.options.custom.type;
387
+ const value = type !== 'TO_PERCENT' ? format(point.y, type) : formatToPercent(_this.percentage / 100);
388
+ const name = point.series.name;
389
+ let color = type !== 'TO_PERCENT' ? point.series.color : point.color;
390
+ if (legendColors.length !== 0) {
391
+ color = legendColors[point.series.index];
392
+ }
393
+ tooltip += `<div style="display: flex; flex-direction: column; gap: 8px;">
394
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 15px;">
395
+ <span style="width: 10px; height: 10px; border-radius: 2px; margin-right: 8px; background-color: ${color}"></span>
396
+ <span style="flex-grow: 1; font-size: 14px; padding-right: 20px;">${name}</span>
397
+ <span style="font-weight: bold; font-size: 14px;">${value}</span>
398
+ </div>
399
+ </div>`;
400
+ });
401
+ tooltip += '</div>';
402
+ return tooltip;
403
+ };
404
+ function formatDate(data, format) {
405
+ return Highcharts.dateFormat(format, data.getTime() - data.getTimezoneOffset() * 60000);
406
+ }
407
+ const ZIndex = {
408
+ AVERAGE_LINE_BASE: 10,
409
+ COMPARE_CHART_BASE: 5,
410
+ NOW_BORDER: 2,
411
+ NOW_BAND: 1
38
412
  };
413
+ function stackingType(stacking) {
414
+ return stacking ? stacking.toLowerCase() : '';
415
+ }
416
+ function pieCenters(count) {
417
+ switch (count) {
418
+ case 2:
419
+ return [['25%', '50%'], ['75%', '50%']];
420
+ case 3:
421
+ return [['25%', '25%'], ['75%', '25%'], ['50%', '75%']];
422
+ case 4:
423
+ return [['25%', '25%'], ['75%', '25%'], ['25%', '75%'], ['75%', '75%']];
424
+ default:
425
+ return [];
426
+ }
427
+ }
428
+ function pieSize(count) {
429
+ switch (count) {
430
+ case 3:
431
+ return '50%';
432
+ case 4:
433
+ return '25%';
434
+ default:
435
+ return null;
436
+ }
437
+ }
438
+
439
+ function TrendChart({
440
+ colorIndex = 0,
441
+ legendColors,
442
+ isDaily,
443
+ isRealtime,
444
+ ...props
445
+ }) {
446
+ const colors = react.useMemo(() => {
447
+ return all[colorIndex % all.length];
448
+ }, [colorIndex]);
449
+ const tooltip = react.useMemo(() => ({
450
+ dateFormat: isRealtime ? '%H:%M' : '%Y-%m-%d',
451
+ headerTime: !isDaily && !isRealtime,
452
+ outside: props.tooltipOutSide,
453
+ legendColors
454
+ }), [isRealtime, isDaily, props.tooltipOutSide, legendColors]);
455
+ return /*#__PURE__*/React.createElement(Chart, _extends({
456
+ type: "areaspline",
457
+ categorize: true,
458
+ xAxisType: "datetime",
459
+ xAxisLabelFormat: isDaily ? '%m-%d' : '%H:%M',
460
+ colors: colors,
461
+ tooltip: tooltip
462
+ }, props));
463
+ }
464
+
465
+ function StackedAreaTrendChart({
466
+ metrics,
467
+ records,
468
+ isDaily = false,
469
+ xPlotIndex,
470
+ xAxisTickInterval,
471
+ yAxisLabelEnabled = false,
472
+ legendLimit = 3,
473
+ colors = defaultColors,
474
+ ...props
475
+ }) {
476
+ const _metrics = react.useMemo(() => {
477
+ return metrics.map(m => ({
478
+ metric: m.id,
479
+ chartType: 'area'
480
+ }));
481
+ }, [metrics]);
482
+ const metas = react.useMemo(() => {
483
+ const result = {
484
+ time: {
485
+ name: "일시",
486
+ type: "DATE"
487
+ }
488
+ };
489
+ metrics.forEach(m => {
490
+ result[m.id] = {
491
+ name: m.name,
492
+ type: "NUMBER"
493
+ };
494
+ });
495
+ return result;
496
+ }, [metrics]);
497
+ return /*#__PURE__*/React.createElement(TrendChart, _extends({
498
+ dimension: "time",
499
+ stacking: "normal",
500
+ metrics: _metrics,
501
+ metas: metas,
502
+ records: records,
503
+ isRealtime: xPlotIndex !== undefined,
504
+ isDaily: isDaily,
505
+ xPlotIndex: xPlotIndex,
506
+ xAxisTickInterval: xAxisTickInterval,
507
+ yAxisLabelEnabled: yAxisLabelEnabled,
508
+ legendLimit: legendLimit,
509
+ colors: colors
510
+ }, props));
511
+ }
512
+ const defaultColors = [["rgba(54,115,237,0.8)", "#3673ed"], ["rgba(149,77,241,0.8)", "#954df1"], ["#4dc391", "#20b476"], ["#fbba3d", "#faa90c"], ["#f2426d", "#ef1348"]];
39
513
 
40
- exports.Button = Button;
514
+ exports.StackedAreaTrendChart = StackedAreaTrendChart;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nethru/kit",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A React component library by Nethru",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,11 +35,18 @@
35
35
  "@babel/core": "^7.23.0",
36
36
  "@babel/preset-react": "^7.23.0",
37
37
  "@rollup/plugin-babel": "^6.0.4",
38
+ "@rollup/plugin-commonjs": "^29.0.0",
38
39
  "@rollup/plugin-node-resolve": "^15.2.3",
39
40
  "@vitejs/plugin-react": "^5.1.1",
40
41
  "react": "^18.2.0",
41
42
  "react-dom": "^18.2.0",
42
43
  "rollup": "^4.0.0",
43
44
  "vite": "^7.2.6"
45
+ },
46
+ "dependencies": {
47
+ "@mui/material": "^6.0.1",
48
+ "@nethru/ui": "^2.1.45",
49
+ "highcharts": "^11.3.0",
50
+ "highcharts-react-official": "^3.2.1"
44
51
  }
45
52
  }