@operato/chart 7.0.1 → 7.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.
@@ -0,0 +1,178 @@
1
+ import { BasePointMarker, TSciChart, IRenderableSeries, EPointMarkerType } from 'scichart'
2
+
3
+ interface PointMarkerOptions {
4
+ width?: number
5
+ height?: number
6
+ strokeThickness?: number
7
+ fill?: string
8
+ stroke?: string
9
+ }
10
+
11
+ abstract class CustomPointMarker extends BasePointMarker {
12
+ private _width: number
13
+ private _height: number
14
+ private _strokeThickness: number
15
+ private _fill: string
16
+ private _stroke: string
17
+
18
+ constructor(wasmContext: TSciChart, options: PointMarkerOptions = {}) {
19
+ super(wasmContext)
20
+ this._width = options.width ?? 10
21
+ this._height = options.height ?? 10
22
+ this._strokeThickness = options.strokeThickness ?? 2
23
+ this._fill = options.fill ?? '#FF6600'
24
+ this._stroke = options.stroke ?? '#000000'
25
+ }
26
+
27
+ get width(): number {
28
+ return this._width
29
+ }
30
+ set width(value: number) {
31
+ this._width = value
32
+ }
33
+
34
+ get height(): number {
35
+ return this._height
36
+ }
37
+ set height(value: number) {
38
+ this._height = value
39
+ }
40
+
41
+ get strokeThickness(): number {
42
+ return this._strokeThickness
43
+ }
44
+ set strokeThickness(value: number) {
45
+ this._strokeThickness = value
46
+ }
47
+
48
+ get fill(): string {
49
+ return this._fill
50
+ }
51
+ set fill(value: string) {
52
+ this._fill = value
53
+ }
54
+
55
+ get stroke(): string {
56
+ return this._stroke
57
+ }
58
+ set stroke(value: string) {
59
+ this._stroke = value
60
+ }
61
+
62
+ get type(): EPointMarkerType {
63
+ return EPointMarkerType.Custom
64
+ }
65
+
66
+ drawSprite(
67
+ context: CanvasRenderingContext2D,
68
+ x: number,
69
+ y: number,
70
+ stroke: string,
71
+ dpiAdjustedStrokeThickness: number,
72
+ fill: string
73
+ ): void {
74
+ this.draw(context, null as any, x, y)
75
+ }
76
+
77
+ abstract draw(
78
+ context: CanvasRenderingContext2D,
79
+ renderableSeries: IRenderableSeries,
80
+ xCoord: number,
81
+ yCoord: number
82
+ ): void
83
+ }
84
+
85
+ // Rotated Square Point Marker
86
+ export class RotatedSquarePointMarker extends CustomPointMarker {
87
+ draw(context: CanvasRenderingContext2D, renderableSeries: IRenderableSeries, xCoord: number, yCoord: number): void {
88
+ context.save()
89
+ context.translate(xCoord, yCoord)
90
+ context.rotate(Math.PI / 4) // 45 degrees rotation
91
+ context.fillStyle = this.fill
92
+ context.strokeStyle = this.stroke
93
+ context.lineWidth = this.strokeThickness
94
+ context.beginPath()
95
+ context.rect(-this.width / 2, -this.height / 2, this.width, this.height)
96
+ context.fill()
97
+ context.stroke()
98
+ context.restore()
99
+ }
100
+ }
101
+
102
+ // Cross Rot Point Marker
103
+ export class CrossRotPointMarker extends CustomPointMarker {
104
+ draw(context: CanvasRenderingContext2D, renderableSeries: IRenderableSeries, xCoord: number, yCoord: number): void {
105
+ context.save()
106
+ context.translate(xCoord, yCoord)
107
+ context.rotate(Math.PI / 4) // 45 degrees rotation
108
+ context.strokeStyle = this.stroke
109
+ context.lineWidth = this.strokeThickness
110
+ context.beginPath()
111
+ context.moveTo(-this.width / 2, 0)
112
+ context.lineTo(this.width / 2, 0)
113
+ context.moveTo(0, -this.height / 2)
114
+ context.lineTo(0, this.height / 2)
115
+ context.stroke()
116
+ context.restore()
117
+ }
118
+ }
119
+
120
+ // Star Point Marker
121
+ export class StarPointMarker extends CustomPointMarker {
122
+ draw(context: CanvasRenderingContext2D, renderableSeries: IRenderableSeries, xCoord: number, yCoord: number): void {
123
+ const spikes = 5
124
+ const outerRadius = this.width / 2
125
+ const innerRadius = outerRadius / 2
126
+ context.save()
127
+ context.translate(xCoord, yCoord)
128
+ context.fillStyle = this.fill
129
+ context.strokeStyle = this.stroke
130
+ context.lineWidth = this.strokeThickness
131
+ context.beginPath()
132
+ for (let i = 0; i < spikes; i++) {
133
+ context.lineTo(
134
+ Math.cos((i * 2 * Math.PI) / spikes) * outerRadius,
135
+ Math.sin((i * 2 * Math.PI) / spikes) * outerRadius
136
+ )
137
+ context.lineTo(
138
+ Math.cos(((i * 2 + 1) * Math.PI) / spikes) * innerRadius,
139
+ Math.sin(((i * 2 + 1) * Math.PI) / spikes) * innerRadius
140
+ )
141
+ }
142
+ context.closePath()
143
+ context.fill()
144
+ context.stroke()
145
+ context.restore()
146
+ }
147
+ }
148
+
149
+ // Line Point Marker
150
+ export class LinePointMarker extends CustomPointMarker {
151
+ draw(context: CanvasRenderingContext2D, renderableSeries: IRenderableSeries, xCoord: number, yCoord: number): void {
152
+ context.save()
153
+ context.translate(xCoord, yCoord)
154
+ context.strokeStyle = this.stroke
155
+ context.lineWidth = this.strokeThickness
156
+ context.beginPath()
157
+ context.moveTo(-this.width / 2, 0)
158
+ context.lineTo(this.width / 2, 0)
159
+ context.stroke()
160
+ context.restore()
161
+ }
162
+ }
163
+
164
+ // Dash Point Marker
165
+ export class DashPointMarker extends CustomPointMarker {
166
+ draw(context: CanvasRenderingContext2D, renderableSeries: IRenderableSeries, xCoord: number, yCoord: number): void {
167
+ context.save()
168
+ context.translate(xCoord, yCoord)
169
+ context.strokeStyle = this.stroke
170
+ context.lineWidth = this.strokeThickness
171
+ context.setLineDash([this.width / 4, this.width / 4])
172
+ context.beginPath()
173
+ context.moveTo(-this.width / 2, 0)
174
+ context.lineTo(this.width / 2, 0)
175
+ context.stroke()
176
+ context.restore()
177
+ }
178
+ }
@@ -20,16 +20,43 @@ class OxSciChart extends LitElement {
20
20
  private dataSeries: any[] = []
21
21
 
22
22
  @query('div#container') container!: HTMLDivElement
23
+ @query('div#legend') legendContainer!: HTMLDivElement
23
24
 
24
25
  static styles = css`
25
26
  :host {
26
27
  display: block;
28
+ width: 100%;
29
+ height: 100%;
27
30
  }
28
31
 
29
- div {
32
+ .chart-container {
33
+ display: flex;
30
34
  width: 100%;
31
35
  height: 100%;
32
36
  }
37
+
38
+ .chart-content {
39
+ flex: 1;
40
+ position: relative;
41
+ }
42
+
43
+ .legend {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ }
48
+
49
+ .legend-top,
50
+ .legend-bottom {
51
+ width: 100%;
52
+ height: 50px;
53
+ }
54
+
55
+ .legend-right,
56
+ .legend-left {
57
+ width: 150px;
58
+ height: 100%;
59
+ }
33
60
  `
34
61
 
35
62
  firstUpdated() {
@@ -70,8 +97,9 @@ class OxSciChart extends LitElement {
70
97
  }
71
98
 
72
99
  async updated(changedProperties: Map<string | number | symbol, unknown>) {
73
- if (changedProperties.has('config')) {
100
+ if (changedProperties.has('config') && this.config) {
74
101
  await this.initializeSciChart()
102
+ this.updateLegend()
75
103
  }
76
104
 
77
105
  if (changedProperties.has('data')) {
@@ -127,8 +155,27 @@ class OxSciChart extends LitElement {
127
155
  })
128
156
  }
129
157
 
158
+ updateLegend() {
159
+ const legendPosition = this.config?.options?.legend?.position || 'right'
160
+ const legendElement = this.shadowRoot?.getElementById('legend')
161
+ if (legendElement) {
162
+ legendElement.className = `legend legend-${legendPosition}`
163
+ }
164
+ }
165
+
130
166
  render() {
131
- return html`<div id="container"></div>`
167
+ const legendPosition = this.config?.options?.legend?.position || 'right'
168
+ return html`
169
+ <div class="chart-container">
170
+ ${legendPosition === 'left' ? html`<div id="legend" class="legend legend-left"></div>` : ''}
171
+ <div class="chart-content">
172
+ ${legendPosition === 'top' ? html`<div id="legend" class="legend legend-top"></div>` : ''}
173
+ <div id="container"></div>
174
+ ${legendPosition === 'bottom' ? html`<div id="legend" class="legend legend-bottom"></div>` : ''}
175
+ </div>
176
+ ${legendPosition === 'right' ? html`<div id="legend" class="legend legend-right"></div>` : ''}
177
+ </div>
178
+ `
132
179
  }
133
180
  }
134
181
 
@@ -1,10 +1,18 @@
1
1
  import { TinyColor } from '@ctrl/tinycolor'
2
2
  import { format as formatText } from '../utils/text-formatter'
3
3
 
4
+ function getBaseColorFromTheme(theme?: 'light' | 'dark' | 'auto') {
5
+ return new TinyColor(theme == 'dark' ? '#fff' : '#000')
6
+ }
7
+
8
+ function getThemeFromBrowser() {
9
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
10
+ }
11
+
4
12
  export async function buildSciChart(
5
13
  config: OperatoChart.ChartConfig | undefined | null,
6
14
  container: any,
7
- { fontSize, fontFamily, fontColor = 'red' }: { fontSize?: number; fontFamily?: string; fontColor?: string }
15
+ { fontSize, fontFamily, fontColor }: { fontSize?: number; fontFamily?: string; fontColor?: string }
8
16
  ): Promise<{ chart: any; dataSeries: any[] } | undefined> {
9
17
  if (!config) {
10
18
  return
@@ -12,10 +20,16 @@ export async function buildSciChart(
12
20
 
13
21
  const {
14
22
  SciChartSurface,
15
- SciChartJsNavyTheme,
23
+ SciChartJSLightTheme,
24
+ SciChartJSDarkv2Theme,
16
25
  XyDataSeries,
17
26
  FastLineRenderableSeries,
27
+ SplineLineRenderableSeries,
18
28
  FastColumnRenderableSeries,
29
+ StackedColumnRenderableSeries,
30
+ StackedMountainRenderableSeries,
31
+ StackedColumnCollection,
32
+ StackedMountainCollection,
19
33
  NumericAxis,
20
34
  DateTimeNumericAxis,
21
35
  EAutoRange,
@@ -24,25 +38,60 @@ export async function buildSciChart(
24
38
  MouseWheelZoomModifier,
25
39
  RubberBandXyZoomModifier,
26
40
  ZoomExtentsModifier,
27
- RolloverModifier
41
+ RolloverModifier,
42
+ SmartDateLabelProvider,
43
+ EllipsePointMarker,
44
+ SquarePointMarker,
45
+ TrianglePointMarker,
46
+ CrossPointMarker,
47
+ XPointMarker,
48
+ WaveAnimation,
49
+ LegendModifier
28
50
  } = SciChart
29
51
 
30
52
  const { type: chartType, options, data: fromData } = config
31
- const { theme, legend, scales: fromScales, xGridLine, yGridLine, y2ndGridLine } = options || {}
32
53
  const { datasets = [] } = fromData || {}
54
+ var {
55
+ theme,
56
+ tooltip,
57
+ animation,
58
+ legend,
59
+ scales: fromScales,
60
+ xGridLine,
61
+ yGridLine,
62
+ y2ndGridLine,
63
+ stacked
64
+ } = options || {}
65
+
66
+ var baseColor = getBaseColorFromTheme(theme)
67
+
68
+ if (theme === 'auto') {
69
+ theme = getThemeFromBrowser()
70
+ }
71
+
72
+ fontColor = fontColor || baseColor.clone().toString()
33
73
 
34
74
  const { xAxes = [], yAxes = [] } = fromScales || {}
35
75
 
36
76
  const chart = await SciChartSurface.create(container, {
37
- theme: new SciChartJsNavyTheme()
77
+ theme: theme == 'dark' ? new SciChartJSDarkv2Theme() : new SciChartJSLightTheme()
38
78
  })
39
79
  const { sciChartSurface, wasmContext } = chart
40
80
 
41
81
  // X 축 설정
42
82
  xAxes.forEach((axis, index) => {
43
83
  const { axisTitle, ticks } = axis
44
- const { autoMax, autoMin, min, max, stepSize, beginAtZero } = ticks || {}
45
- const id = xAxes.length > 1 ? `x${index + 1}` : 'x'
84
+ const {
85
+ autoMax,
86
+ autoMin,
87
+ min,
88
+ max,
89
+ stepSize,
90
+ beginAtZero,
91
+ color = fontColor,
92
+ textStrokeColor = fontColor,
93
+ display = !!axisTitle
94
+ } = ticks || {}
46
95
 
47
96
  const xAxis = new DateTimeNumericAxis(wasmContext, {
48
97
  axisTitle,
@@ -54,13 +103,14 @@ export async function buildSciChart(
54
103
  labelStyle: {
55
104
  fontFamily,
56
105
  fontSize,
57
- color: fontColor
106
+ color
58
107
  },
59
108
  axisTitleStyle: {
60
109
  fontFamily,
61
110
  fontSize,
62
- color: fontColor
63
- }
111
+ color: textStrokeColor
112
+ },
113
+ labelProvider: new SmartDateLabelProvider()
64
114
  })
65
115
 
66
116
  sciChartSurface.xAxes.add(xAxis)
@@ -70,12 +120,12 @@ export async function buildSciChart(
70
120
  yAxes.forEach((axis, index) => {
71
121
  const { axisTitle, ticks } = axis
72
122
  const { autoMax, autoMin, min, max, stepSize, beginAtZero } = ticks || {}
73
- const id = yAxes.length > 1 ? `right` : 'left'
74
123
 
75
124
  const yAxis = new NumericAxis(wasmContext, {
125
+ id: `yAxis${index}`,
76
126
  axisTitle,
77
127
  autoRange: autoMin || autoMax ? EAutoRange.Always : undefined,
78
- axisAlignment: yAxes.length > 1 ? EAxisAlignment.Right : EAxisAlignment.Left,
128
+ axisAlignment: index === 0 ? EAxisAlignment.Left : EAxisAlignment.Right,
79
129
  visibleRange: min !== undefined && max !== undefined ? new NumberRange(min, max) : undefined,
80
130
  majorDelta: stepSize,
81
131
  growBy: beginAtZero ? new NumberRange(0.1, 0.1) : undefined,
@@ -90,6 +140,7 @@ export async function buildSciChart(
90
140
  color: fontColor
91
141
  }
92
142
  })
143
+
93
144
  sciChartSurface.yAxes.add(yAxis)
94
145
  })
95
146
 
@@ -100,36 +151,158 @@ export async function buildSciChart(
100
151
  containsNaN: false
101
152
  })
102
153
 
154
+ const yAxisId = dataset.yAxisID || 'yAxis0'
155
+ const stackGroupId = dataset.stack || `__stack${index}__`
156
+
103
157
  let series: any
104
158
  if (dataset.type === 'bar') {
105
- series = new FastColumnRenderableSeries(wasmContext, {
106
- dataSeries,
107
- strokeThickness: dataset.borderWidth || 2,
108
- fill: dataset.backgroundColor || '#FF6600'
109
- })
159
+ if (stacked) {
160
+ series = new StackedColumnRenderableSeries(wasmContext, {
161
+ dataSeries,
162
+ strokeThickness: dataset.borderWidth || 2,
163
+ fill: dataset.backgroundColor || '#FF6600',
164
+ yAxisId,
165
+ stackedGroupId: stackGroupId
166
+ })
167
+ } else {
168
+ series = new FastColumnRenderableSeries(wasmContext, {
169
+ dataSeries,
170
+ strokeThickness: dataset.borderWidth || 2,
171
+ fill: dataset.backgroundColor || '#FF6600',
172
+ animation: animation && new WaveAnimation({ duration: 1000, fadeEffect: true }),
173
+ yAxisId
174
+ })
175
+ }
110
176
  } else {
111
- series = new FastLineRenderableSeries(wasmContext, {
112
- dataSeries,
113
- strokeThickness: dataset.borderWidth || 2,
114
- stroke: dataset.color || '#FF6600'
115
- })
177
+ const { pointStyle, lineTension } = dataset
178
+ let pointMarker
179
+
180
+ switch (pointStyle) {
181
+ case 'circle':
182
+ pointMarker = new EllipsePointMarker(wasmContext, {
183
+ width: 10,
184
+ height: 10,
185
+ strokeThickness: 2,
186
+ fill: dataset.color || '#FF6600',
187
+ stroke: '#000000'
188
+ })
189
+ break
190
+ case 'triangle':
191
+ pointMarker = new TrianglePointMarker(wasmContext, {
192
+ width: 10,
193
+ height: 10,
194
+ strokeThickness: 2,
195
+ fill: dataset.color || '#FF6600',
196
+ stroke: '#000000'
197
+ })
198
+ break
199
+ case 'rect':
200
+ pointMarker = new SquarePointMarker(wasmContext, {
201
+ width: 10,
202
+ height: 10,
203
+ strokeThickness: 2,
204
+ fill: dataset.color || '#FF6600',
205
+ stroke: '#000000'
206
+ })
207
+ break
208
+ case 'cross':
209
+ pointMarker = new CrossPointMarker(wasmContext, {
210
+ width: 10,
211
+ height: 10,
212
+ strokeThickness: 2,
213
+ fill: dataset.color || '#FF6600',
214
+ stroke: '#000000'
215
+ })
216
+ break
217
+ case 'crossRot':
218
+ pointMarker = new XPointMarker(wasmContext, {
219
+ width: 10,
220
+ height: 10,
221
+ strokeThickness: 2,
222
+ fill: dataset.color || '#FF6600',
223
+ stroke: '#000000'
224
+ })
225
+ break
226
+ default:
227
+ pointMarker = new EllipsePointMarker(wasmContext, {
228
+ width: 10,
229
+ height: 10,
230
+ strokeThickness: 2,
231
+ fill: dataset.color || '#FF6600',
232
+ stroke: '#000000'
233
+ })
234
+ }
235
+
236
+ if (stacked) {
237
+ series = new StackedMountainRenderableSeries(wasmContext, {
238
+ dataSeries,
239
+ strokeThickness: dataset.borderWidth || 2,
240
+ stroke: dataset.color || '#FF6600',
241
+ fill: dataset.backgroundColor || '#FF6600',
242
+ yAxisId,
243
+ stackedGroupId: stackGroupId
244
+ })
245
+ } else {
246
+ series =
247
+ !!lineTension && lineTension > 0
248
+ ? new SplineLineRenderableSeries(wasmContext, {
249
+ dataSeries,
250
+ strokeThickness: dataset.borderWidth || 2,
251
+ stroke: dataset.color || '#FF6600',
252
+ pointMarker,
253
+ animation: animation && new WaveAnimation({ duration: 1000, fadeEffect: true }),
254
+ yAxisId
255
+ })
256
+ : new FastLineRenderableSeries(wasmContext, {
257
+ dataSeries,
258
+ strokeThickness: dataset.borderWidth || 2,
259
+ stroke: dataset.color || '#FF6600',
260
+ pointMarker,
261
+ animation: animation && new WaveAnimation({ duration: 1000, fadeEffect: true }),
262
+ yAxisId
263
+ })
264
+ }
116
265
  }
117
266
 
118
267
  sciChartSurface.renderableSeries.add(series)
119
268
 
120
- const rolloverModifier = new RolloverModifier({
121
- showTooltip: true,
122
- showAxisLabels: true,
123
- tooltipColor: 'white',
124
- tooltipBackgroundColor: 'rgba(0, 0, 0, 0.7)',
125
- rollOverDataSeries: dataSeries
126
- })
269
+ if (tooltip) {
270
+ const rolloverModifier = new RolloverModifier({
271
+ showTooltip: true,
272
+ showAxisLabels: true,
273
+ tooltipColor: 'white',
274
+ tooltipBackgroundColor: 'rgba(0, 0, 0, 0.7)',
275
+ rollOverDataSeries: dataSeries
276
+ })
127
277
 
128
- sciChartSurface.chartModifiers.add(rolloverModifier)
278
+ sciChartSurface.chartModifiers.add(rolloverModifier)
279
+ }
129
280
 
130
281
  return dataSeries
131
282
  })
132
283
 
284
+ // Stacked collections 추가
285
+ if (stacked) {
286
+ const stackedColumnCollection = new StackedColumnCollection(wasmContext)
287
+ const stackedMountainCollection = new StackedMountainCollection(wasmContext)
288
+
289
+ sciChartSurface.renderableSeries.asArray().forEach((series: any) => {
290
+ if (series instanceof StackedColumnRenderableSeries) {
291
+ stackedColumnCollection.add(series)
292
+ } else if (series instanceof StackedMountainRenderableSeries) {
293
+ stackedMountainCollection.add(series)
294
+ }
295
+ })
296
+
297
+ if (stackedColumnCollection.size() > 0) {
298
+ sciChartSurface.renderableSeries.add(stackedColumnCollection)
299
+ }
300
+
301
+ if (stackedMountainCollection.size() > 0) {
302
+ sciChartSurface.renderableSeries.add(stackedMountainCollection)
303
+ }
304
+ }
305
+
133
306
  // 줌인/줌아웃 모디파이어 추가
134
307
  sciChartSurface.chartModifiers.add(
135
308
  new RubberBandXyZoomModifier(),
@@ -137,5 +310,16 @@ export async function buildSciChart(
137
310
  new ZoomExtentsModifier()
138
311
  )
139
312
 
313
+ // Legend 설정
314
+ if (legend?.display) {
315
+ const legendModifier = new LegendModifier({
316
+ showCheckboxes: true,
317
+ showSeriesMarkers: true,
318
+ showLegend: true,
319
+ placement: legend.position || 'bottom-right'
320
+ })
321
+ sciChartSurface.chartModifiers.add(legendModifier)
322
+ }
323
+
140
324
  return { chart, dataSeries: dataSeriesArray }
141
325
  }
package/src/types.d.ts CHANGED
@@ -24,7 +24,17 @@ declare namespace OperatoChart {
24
24
  stack?: string
25
25
  fill?: boolean
26
26
  lineTension?: number
27
- pointStyle?: string
27
+ pointStyle?:
28
+ | 'circle'
29
+ | 'triangle'
30
+ | 'rect'
31
+ | 'rectRot'
32
+ | 'cross'
33
+ | 'crossRot'
34
+ | 'star'
35
+ | 'line'
36
+ | 'dash'
37
+ | undefined
28
38
  pointRadius?: number
29
39
  valuePrefix?: string
30
40
  valueSuffix?: string
@@ -34,7 +44,7 @@ declare namespace OperatoChart {
34
44
  }
35
45
 
36
46
  export interface ChartOptions {
37
- theme?: 'dark' | 'light'
47
+ theme?: 'dark' | 'light' | 'auto'
38
48
  tooltip?: boolean
39
49
  animation?: boolean
40
50
  legend?: LegendOptions
@@ -74,5 +84,7 @@ declare namespace OperatoChart {
74
84
  max?: number
75
85
  stepSize?: number
76
86
  beginAtZero?: boolean
87
+ color?: string
88
+ textStrokeColor?: string
77
89
  }
78
90
  }
package/stories/common.ts CHANGED
@@ -63,10 +63,10 @@ function getRandomInRange(min: number, max: number) {
63
63
  // 랜덤 데이터를 생성하는 함수
64
64
  function generateRandomData(count: number) {
65
65
  const randomData = []
66
- const startTimestamp = Math.floor(Date.now() / 1000) // 현재 시간을 Unix 타임스탬프로 설정
66
+ const startTimestamp = Math.floor(Date.now()) // 현재 시간을 Unix 타임스탬프로 설정
67
67
 
68
68
  for (let i = 0; i < count; i++) {
69
- const timestamp = startTimestamp + i * 3 // 3초씩 증가하는 타임스탬프 설정
69
+ const timestamp = startTimestamp + i * 360 * 30 * 1000 // 3초씩 증가하는 타임스탬프 설정
70
70
  const randomCount = getRandomInRange(5, 35) // count 값을 5에서 35 사이로 랜덤 생성
71
71
  const randomAverage = getRandomInRange(50, 150) // average 값을 50에서 150 사이로 랜덤 생성
72
72