poe-svelte-ui-lib 1.10.6 → 1.10.8

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.
@@ -5,8 +5,6 @@
5
5
  /* Константы и настройки — экспортируются для переиспользования в GraphProps.svelte (редактор конструктора) */
6
6
  export const REFRESH_OPTIONS: IOption[] = [
7
7
  { id: "RefreshOption-AUTO", name: "AUTO", value: 0, class: "" },
8
- { id: "RefreshOption-10", name: "10", value: 10, class: "" },
9
- { id: "RefreshOption-25", name: "25", value: 25, class: "" },
10
8
  { id: "RefreshOption-50", name: "50", value: 50, class: "" },
11
9
  { id: "RefreshOption-100", name: "100", value: 100, class: "" },
12
10
  { id: "RefreshOption-250", name: "250", value: 250, class: "" },
@@ -14,19 +12,12 @@
14
12
  { id: "RefreshOption-1000", name: "1000", value: 1000, class: "" },
15
13
  { id: "RefreshOption-5000", name: "5000", value: 5000, class: "" },
16
14
  ]
17
- export const SCALE_OPTIONS: IOption[] = [
18
- { id: "ScaleOption-50", name: "50", value: 50, class: "" },
19
- { id: "ScaleOption-100", name: "100", value: 100, class: "" },
20
- { id: "ScaleOption-500", name: "500", value: 500, class: "" },
21
- { id: "ScaleOption-1000", name: "1000", value: 1000, class: "" },
22
- { id: "ScaleOption-2000", name: "2000", value: 2000, class: "" },
23
- ]
24
15
  </script>
25
16
 
26
17
  <script lang="ts">
27
18
  import { onMount } from "svelte"
28
19
  import Select from "../Select/Select.svelte"
29
- import type { IGraphDataObject, IGraphProps, IOption } from "../types"
20
+ import type { IGraphDataObject, IGraphProps } from "../types"
30
21
 
31
22
  /* Инициализация пропсов с дефолтными значениями */
32
23
  let {
@@ -36,9 +27,13 @@
36
27
  streamingData = { data: [] },
37
28
  isTest = false,
38
29
  refreshRate = 0,
39
- scale = 100,
30
+ historyData = [],
40
31
  }: IGraphProps = $props()
41
32
 
33
+ /* Статический режим: historyData передан целиком с реальными timestamp (Unix-эпоха в мс) —
34
+ компонент рисует весь массив разом, без интервалов/AUTO; streamingData/refreshRate игнорируются */
35
+ const isHistoryMode = $derived(!!historyData && historyData.length > 0)
36
+
42
37
  /* Состояние компонента */
43
38
  let graphData = $state<{ id: string; points: { x: number; y: number }[]; color: string; name: string }[]>([])
44
39
  let currentValues = $state<number[]>([])
@@ -48,18 +43,53 @@
48
43
  let width = $state(600)
49
44
  let height = $state(125)
50
45
 
51
- /* selectedRefreshRate/selectedScale изначально берутся из пропов (сохраняются вместе с компонентом в конструкторе),
52
- но остаются локально переключаемыми через Select ниже — присвоение этим $derived временно
53
- переопределяет значение, пока не изменится refreshRate/scale/isTest (проп) */
46
+ /* selectedRefreshRate изначально берётся из пропа (сохраняется вместе с компонентом в конструкторе),
47
+ но остаётся локально переключаемым через Select ниже — присвоение этому $derived временно
48
+ переопределяет значение, пока не изменится refreshRate/isTest (проп) */
54
49
  let selectedRefreshRate = $derived(isTest ? 50 : (refreshRate ?? 0))
55
- let selectedScale = $derived(scale ?? 100)
56
50
  const maxDataPoints = $derived(selectedRefreshRate == 0 ? 20 : 100)
57
51
  const defaultColors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#ec4899"]
58
52
 
59
53
  let previousValues: number[] = $state([])
60
54
 
61
- /* Инициализация данных графиков */
55
+ /* Автомасштаб Y для живого режима: диапазон плавно "подъезжает" к фактическому min/max буфера
56
+ (±10% отступ), пересчитывается один раз за тик (не за кадр/mousemove) и сглаживается —
57
+ иначе ось дёргалась бы на каждый новый семпл */
58
+ let liveYRange = $state<{ min: number; max: number } | null>(null)
59
+
60
+ const computeAutoYRange = (values: number[]): { min: number; max: number } => {
61
+ if (values.length === 0) return { min: -1, max: 1 }
62
+ const rawMin = Math.min(...values)
63
+ const rawMax = Math.max(...values)
64
+ const pad = (rawMax - rawMin) * 0.1 || Math.abs(rawMax) * 0.1 || 1
65
+ return { min: rawMin - pad, max: rawMax + pad }
66
+ }
67
+
68
+ const updateLiveYRange = () => {
69
+ const target = computeAutoYRange(graphData.flatMap((g) => g.points.map((p) => p.y)))
70
+ if (!liveYRange) {
71
+ liveYRange = target
72
+ return
73
+ }
74
+ const smoothing = 0.15
75
+ liveYRange = {
76
+ min: liveYRange.min + (target.min - liveYRange.min) * smoothing,
77
+ max: liveYRange.max + (target.max - liveYRange.max) * smoothing,
78
+ }
79
+ }
80
+
81
+ /* Тултип при наведении — время и значения всех серий в ближайшей к курсору точке */
82
+ let tooltip = $state<{ visible: boolean; x: number; y: number; time: number; items: { name: string; value: number; color: string }[] }>({
83
+ visible: false,
84
+ x: 0,
85
+ y: 0,
86
+ time: 0,
87
+ items: [],
88
+ })
89
+
90
+ /* Инициализация данных графиков (потоковый режим) */
62
91
  const initializeGraphData = () => {
92
+ if (isHistoryMode) return
63
93
  if (typeof streamingData === "string") {
64
94
  streamingData = JSON.parse(streamingData)
65
95
  }
@@ -90,7 +120,25 @@
90
120
  initializeGraphData()
91
121
  })
92
122
 
93
- /* Обработка входящих данных.
123
+ /* Статический режим: строим graphData один раз (и заново при каждой смене historyData) —
124
+ x = реальный timestamp точки, "текущее" значение в панели Values — последняя (по времени) точка серии */
125
+ $effect(() => {
126
+ if (!isHistoryMode) return
127
+ const series = historyData.slice(0, 6).map((s, i) => {
128
+ const sortedPoints = [...s.points].sort((a, b) => a.timestamp - b.timestamp).map((p) => ({ x: p.timestamp, y: p.value }))
129
+ return {
130
+ id: crypto.randomUUID(),
131
+ points: sortedPoints,
132
+ color: defaultColors[i % defaultColors.length],
133
+ name: s.name || `Value ${i}`,
134
+ }
135
+ })
136
+ graphData = series
137
+ currentValues = series.map((s) => (s.points.length ? s.points[s.points.length - 1].y : 0))
138
+ drawAllGraphs()
139
+ })
140
+
141
+ /* Обработка входящих потоковых данных.
94
142
  ВАЖНО: streamingData читается только внутри колбэков setInterval (асинхронный контекст),
95
143
  а не в теле $effect — иначе каждое новое сообщение (у нас до 10 Гц с устройства) делает
96
144
  streamingData зависимостью эффекта, и он пересоздаёт таймер на каждый пакет: если Refresh
@@ -99,6 +147,7 @@
99
147
  let intervalId: ReturnType<typeof setInterval>
100
148
  $effect(() => {
101
149
  clearInterval(intervalId)
150
+ if (isHistoryMode) return
102
151
  if (selectedRefreshRate > 0) {
103
152
  intervalId = setInterval(() => {
104
153
  if (!isTest && (!streamingData.data || streamingData.data.length === 0)) return
@@ -114,10 +163,11 @@
114
163
  }
115
164
  currentValues[i] = value
116
165
  })
166
+ updateLiveYRange()
117
167
  drawAllGraphs()
118
168
  }, selectedRefreshRate)
119
169
  } else if (selectedRefreshRate == 0 && !isTest) {
120
- /* AUTO: streamingData не несёт признак "это новый пакет" (timestamp никем не передаётся —
170
+ /* AUTO: streamingData не несёт признак "это новый пакет" (метки времени тут нет —
121
171
  DeviceStore хранит только последнее значение), поэтому опрашиваем каждые 10мс и рисуем
122
172
  точку только когда сами значения реально изменились, а не на каждый тик поллинга. */
123
173
  intervalId = setInterval(() => {
@@ -135,6 +185,7 @@
135
185
  }
136
186
  currentValues[i] = value
137
187
  })
188
+ updateLiveYRange()
138
189
  drawAllGraphs()
139
190
  }, 10)
140
191
  }
@@ -161,28 +212,117 @@
161
212
  return () => resizeObserver.disconnect()
162
213
  })
163
214
 
164
- const drawAllGraphs = () => {
165
- if (!ctx) return
166
- ctx.clearRect(0, 0, width, height)
167
-
168
- const padding = {
169
- top: 10,
170
- right: 10,
171
- bottom: 20,
172
- left: 35,
173
- }
174
-
215
+ /* Общая геометрия осей — переиспользуется отрисовкой и обработчиком наведения мыши, чтобы не дублировать формулы */
216
+ const computeGeometry = () => {
217
+ const padding = { top: 10, right: 10, bottom: 20, left: 35 }
175
218
  const graphWidth = width - padding.left - padding.right
176
219
  const graphHeight = height - padding.top - padding.bottom
177
220
 
178
221
  const allPoints = graphData.flatMap((g) => g.points)
179
- const minX = Math.min(...allPoints.map((p) => p.x))
180
- const maxX = Math.max(...allPoints.map((p) => p.x))
222
+ const minX = allPoints.length ? Math.min(...allPoints.map((p) => p.x)) : 0
223
+ const maxX = allPoints.length ? Math.max(...allPoints.map((p) => p.x)) : 1
181
224
  const timeSpan = maxX - minX || 1
182
225
 
183
- // Функции преобразования координат с учетом отступов
226
+ /* Оба режима — авто-масштаб по фактическим данным (±10% отступ, не обязательно симметрично относительно нуля):
227
+ в статическом считается разово по всему массиву, в живом — используется сглаженный liveYRange
228
+ (обновляется отдельно, в updateLiveYRange, чтобы не дёргаться на каждый кадр/mousemove) */
229
+ let yMin: number
230
+ let yMax: number
231
+ if (isHistoryMode) {
232
+ const values = allPoints.map((p) => p.y)
233
+ const range = computeAutoYRange(values)
234
+ yMin = range.min
235
+ yMax = range.max
236
+ } else {
237
+ const range = liveYRange ?? computeAutoYRange(allPoints.map((p) => p.y))
238
+ yMin = range.min
239
+ yMax = range.max
240
+ }
241
+ const ySpan = yMax - yMin || 1
242
+
184
243
  const getX = (x: number) => padding.left + ((x - minX) / timeSpan) * graphWidth
185
- const getY = (y: number) => padding.top + graphHeight - (((y / selectedScale) * graphHeight) / 2 + graphHeight / 2)
244
+ const getY = (y: number) => padding.top + graphHeight - ((y - yMin) / ySpan) * graphHeight
245
+
246
+ return { padding, graphWidth, graphHeight, minX, maxX, timeSpan, yMin, yMax, ySpan, getX, getY }
247
+ }
248
+
249
+ /* Бинарный поиск ближайшей по времени точки — точки в graphData всегда отсортированы по x (по построению) */
250
+ const findNearestPoint = (points: { x: number; y: number }[], t: number): { x: number; y: number } | null => {
251
+ if (points.length === 0) return null
252
+ let lo = 0
253
+ let hi = points.length - 1
254
+ while (lo < hi) {
255
+ const mid = (lo + hi) >> 1
256
+ if (points[mid].x < t) lo = mid + 1
257
+ else hi = mid
258
+ }
259
+ if (lo > 0 && Math.abs(points[lo - 1].x - t) < Math.abs(points[lo].x - t)) return points[lo - 1]
260
+ return points[lo]
261
+ }
262
+
263
+ const pad2 = (n: number) => String(n).padStart(2, "0")
264
+
265
+ /* Компактная адаптивная подпись сетки по оси X: относительное время для живого режима, дата/время — для статического */
266
+ const formatAxisLabel = (t: number, spanMs: number): string => {
267
+ if (!isHistoryMode) return `${((t - Date.now()) / 1000).toFixed(0)}s`
268
+ const d = new Date(t)
269
+ const hhmm = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`
270
+ if (spanMs < 24 * 60 * 60 * 1000) return hhmm
271
+ const ddmm = `${pad2(d.getDate())}.${pad2(d.getMonth() + 1)}`
272
+ if (spanMs < 365 * 24 * 60 * 60 * 1000) return `${ddmm} ${hhmm}`
273
+ return `${ddmm}.${d.getFullYear()}`
274
+ }
275
+
276
+ /* Полная подпись времени в тултипе */
277
+ const formatTooltipTime = (t: number): string => {
278
+ if (!isHistoryMode) return `${((t - Date.now()) / 1000).toFixed(1)}s`
279
+ const d = new Date(t)
280
+ return `${pad2(d.getDate())}.${pad2(d.getMonth() + 1)}.${d.getFullYear()} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
281
+ }
282
+
283
+ const handleMouseMove = (e: MouseEvent) => {
284
+ if (!canvas || graphData.length === 0) return
285
+ const rect = canvas.getBoundingClientRect()
286
+ const mouseX = e.clientX - rect.left
287
+ const mouseY = e.clientY - rect.top
288
+ const { padding, graphWidth, minX, timeSpan } = computeGeometry()
289
+
290
+ if (mouseX < padding.left || mouseX > width - padding.right) {
291
+ if (tooltip.visible) {
292
+ tooltip = { ...tooltip, visible: false }
293
+ drawAllGraphs()
294
+ }
295
+ return
296
+ }
297
+
298
+ const t = minX + ((mouseX - padding.left) / graphWidth) * timeSpan
299
+ const items = graphData
300
+ .map((g) => {
301
+ const nearest = findNearestPoint(g.points, t)
302
+ return nearest ? { name: g.name, value: nearest.y, color: g.color, time: nearest.x } : null
303
+ })
304
+ .filter((v): v is { name: string; value: number; color: string; time: number } => v !== null)
305
+
306
+ if (items.length === 0) {
307
+ tooltip = { ...tooltip, visible: false }
308
+ return
309
+ }
310
+
311
+ tooltip = { visible: true, x: mouseX, y: mouseY, time: items[0].time, items }
312
+ drawAllGraphs()
313
+ }
314
+
315
+ const handleMouseLeave = () => {
316
+ if (!tooltip.visible) return
317
+ tooltip = { ...tooltip, visible: false }
318
+ drawAllGraphs()
319
+ }
320
+
321
+ const drawAllGraphs = () => {
322
+ if (!ctx) return
323
+ ctx.clearRect(0, 0, width, height)
324
+
325
+ const { padding, graphWidth, graphHeight, minX, timeSpan, yMin, yMax, getX, getY } = computeGeometry()
186
326
 
187
327
  /* Сетка X */
188
328
  ctx.strokeStyle = "#777"
@@ -191,17 +331,15 @@
191
331
  ctx.font = "10px monospace"
192
332
  ctx.textAlign = "center"
193
333
 
194
- const now = Date.now()
195
334
  for (let i = 0; i <= 10; i++) {
196
335
  const t = minX + (i / 10) * timeSpan
197
336
  const x = getX(t)
198
- const secondsAgo = ((t - now) / 1000).toFixed(0)
199
337
  ctx.beginPath()
200
338
  ctx.moveTo(x, padding.top)
201
339
  ctx.lineTo(x, height - padding.bottom)
202
340
  ctx.stroke()
203
341
  ctx.textBaseline = "top"
204
- ctx.fillText(`${secondsAgo}s`, x, height - padding.bottom + 2)
342
+ ctx.fillText(formatAxisLabel(t, timeSpan), x, height - padding.bottom + 2)
205
343
  }
206
344
 
207
345
  /* Сетка Y */
@@ -209,13 +347,13 @@
209
347
  ctx.textBaseline = "middle"
210
348
  const ySteps = 8
211
349
  for (let i = 0; i <= ySteps; i++) {
212
- const yVal = selectedScale - (i * 2 * selectedScale) / ySteps
350
+ const yVal = yMax - (i * (yMax - yMin)) / ySteps
213
351
  const y = getY(yVal)
214
352
  ctx.beginPath()
215
353
  ctx.moveTo(padding.left, y)
216
354
  ctx.lineTo(width - padding.right, y)
217
355
  ctx.stroke()
218
- ctx.fillText(`${yVal.toFixed(0)}`, padding.left - 5, y)
356
+ ctx.fillText(yVal.toFixed(1), padding.left - 5, y)
219
357
  }
220
358
 
221
359
  /* Отрисовка графиков */
@@ -236,6 +374,17 @@
236
374
  ctx.lineTo(getX(last.x), getY(last.y))
237
375
  ctx.stroke()
238
376
  })
377
+
378
+ /* Вертикальная линия-указатель под тултипом */
379
+ if (tooltip.visible) {
380
+ const x = getX(tooltip.time)
381
+ ctx.strokeStyle = "rgba(120, 120, 120, 0.6)"
382
+ ctx.lineWidth = 1
383
+ ctx.beginPath()
384
+ ctx.moveTo(x, padding.top)
385
+ ctx.lineTo(x, height - padding.bottom)
386
+ ctx.stroke()
387
+ }
239
388
  }
240
389
  </script>
241
390
 
@@ -246,38 +395,53 @@
246
395
 
247
396
  <div class="flex w-full flex-row gap-4">
248
397
  <!-- График -->
249
- <div bind:this={container} class="h-64 grow overflow-hidden rounded-xl border border-gray-200 shadow-sm">
250
- <canvas class="h-full w-full bg-(--back-color)" bind:this={canvas}></canvas>
398
+ <div bind:this={container} class="relative h-64 grow overflow-hidden rounded-xl border border-gray-200 shadow-sm">
399
+ <canvas
400
+ class="h-full w-full bg-(--back-color)"
401
+ bind:this={canvas}
402
+ onmousemove={handleMouseMove}
403
+ onmouseleave={handleMouseLeave}
404
+ ></canvas>
405
+
406
+ {#if tooltip.visible}
407
+ <div
408
+ class="pointer-events-none absolute z-10 rounded-lg border border-(--border-color) bg-(--back-color) px-2 py-1 text-xs whitespace-nowrap shadow-lg"
409
+ style="left: {Math.min(tooltip.x + 12, width - 150)}px; top: {Math.min(tooltip.y + 12, height - 16 - tooltip.items.length * 16)}px;"
410
+ >
411
+ <div class="mb-1 font-semibold">{formatTooltipTime(tooltip.time)}</div>
412
+ {#each tooltip.items as item (item.name)}
413
+ <div class="flex items-center gap-1.5">
414
+ <span class="h-2 w-2 shrink-0 rounded-full" style="background-color: {item.color}"></span>
415
+ <span>{item.name}:</span>
416
+ <span class="font-semibold">{item.value.toFixed(2)}</span>
417
+ </div>
418
+ {/each}
419
+ </div>
420
+ {/if}
251
421
  </div>
252
422
 
253
423
  <!-- Панель настроек -->
254
424
  <div class="flex w-48 flex-col gap-2">
255
- <!-- Развертка по горизонтали -->
256
- <Select
257
- label={{ name: "Refresh rate", class: "" }}
258
- options={REFRESH_OPTIONS}
259
- value={REFRESH_OPTIONS.find((o) => o.value == selectedRefreshRate)}
260
- onUpdate={(value) => (selectedRefreshRate = (value as IOption).value as number)}
261
- />
262
-
263
- <!-- Масштаб по вертикали -->
264
- <Select
265
- label={{ name: "Scale" }}
266
- options={SCALE_OPTIONS}
267
- value={SCALE_OPTIONS.find((o) => o.value == selectedScale)}
268
- onUpdate={(value) => (selectedScale = (value as IOption).value as number)}
269
- />
425
+ {#if !isHistoryMode}
426
+ <!-- Развертка по горизонтали -->
427
+ <Select
428
+ label={{ name: "Refresh Rate", class: "" }}
429
+ options={REFRESH_OPTIONS}
430
+ value={REFRESH_OPTIONS.find((o) => o.value == selectedRefreshRate)}
431
+ onUpdate={(value) => (selectedRefreshRate = (value as IOption).value as number)}
432
+ />
433
+ {/if}
270
434
 
271
435
  <!-- Переменные и их значение -->
272
436
  <div>
273
437
  <h5 class="px-4">Values</h5>
274
438
  <table class="w-full font-mono text-sm">
275
439
  <tbody>
276
- {#each graphData as data, i (i)}
440
+ {#each graphData as data, i (data.id)}
277
441
  <tr>
278
442
  <td><div class="mr-2 h-4 w-4 rounded-full" style="background-color: {data.color}"></div></td>
279
- <td class="w-24 truncate text-left font-semibold">{(streamingData.data as IGraphDataObject[])?.[i]?.name}</td>
280
- <td class="w-16 text-right">{currentValues[i].toFixed(2)}</td>
443
+ <td class="w-24 truncate text-left font-semibold">{data.name}</td>
444
+ <td class="w-16 text-right">{currentValues[i]?.toFixed(2)}</td>
281
445
  </tr>
282
446
  {/each}
283
447
  </tbody>
@@ -1,6 +1,5 @@
1
1
  import type { IOption } from "../types";
2
2
  export declare const REFRESH_OPTIONS: IOption[];
3
- export declare const SCALE_OPTIONS: IOption[];
4
3
  import type { IGraphProps } from "../types";
5
4
  declare const Graph: import("svelte").Component<IGraphProps, {}, "">;
6
5
  type Graph = ReturnType<typeof Graph>;
@@ -6,7 +6,7 @@
6
6
  import { optionsStore } from "../options"
7
7
  import { twMerge } from "tailwind-merge"
8
8
  import CommonSnippets from "../CommonSnippets.svelte"
9
- import { REFRESH_OPTIONS, SCALE_OPTIONS } from "./Graph.svelte"
9
+ import { REFRESH_OPTIONS } from "./Graph.svelte"
10
10
 
11
11
  const {
12
12
  component,
@@ -45,12 +45,6 @@
45
45
  value={REFRESH_OPTIONS.find((o) => o.value == (component.properties.refreshRate ?? 0))}
46
46
  onUpdate={(value) => updateProperty("refreshRate", (value as IOption).value as number, component, onPropertyChange)}
47
47
  />
48
- <UI.Select
49
- label={{ name: $T("constructor.props.scale") }}
50
- options={SCALE_OPTIONS}
51
- value={SCALE_OPTIONS.find((o) => o.value == (component.properties.scale ?? 100))}
52
- onUpdate={(value) => updateProperty("scale", (value as IOption).value as number, component, onPropertyChange)}
53
- />
54
48
  {/snippet}
55
49
 
56
50
  {#if forConstructor}
@@ -2,17 +2,28 @@
2
2
 
3
3
  ## Описание
4
4
 
5
- График - это UI-компонент, который позволяет визуализировать данные в виде динамических графиков. Компонент поддерживает потоковую передачу данных, настройку частоты обновления, масштаба отображения и другие параметры через систему пропсов.
5
+ График - это UI-компонент, который позволяет визуализировать данные в виде динамических графиков. Компонент поддерживает два режима работы:
6
+
7
+ - **Живой (потоковый) режим** — точки добавляются по одной по мере поступления `streamingData`, X-ось показывает "N секунд назад" относительно текущего момента.
8
+ - **Статический (исторический) режим** — компонент получает уже готовый массив точек через `historyData` (каждая со своим реальным `timestamp`) и рисует весь ряд разом, используя настоящие дату/время на оси X.
9
+
10
+ Режим определяется автоматически: если проп `historyData` задан и непустой, компонент переходит в статический режим (`streamingData`/`refreshRate` в этом случае игнорируются).
11
+
12
+ Отдельного пропа для масштаба вертикальной оси нет — в обоих режимах Y-ось масштабируется автоматически по фактическим данным (±10% отступ). В живом режиме диапазон плавно "подъезжает" к новому min/max буфера (сглаживание), а не пересчитывается резко на каждый семпл, чтобы сетка не дёргалась.
6
13
 
7
14
  ## Пропсы
8
15
 
9
- | Название | Тип | По умолчанию | Описание |
10
- | ------------- | --------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------ |
11
- | id | string | crypto.randomUUID() | Уникальный идентификатор компонента |
12
- | wrapperClass | string | "" | Дополнительные CSS-классы для обертки компонента |
13
- | label | { name?: string; class?: string } | { name: "", class: "" } | Настройки заголовка компонента |
14
- | streamingData | { data: { name: string; value: number }[]; timestamp?: number } | { data: [], timestamp: Date.now() } | Потоковые данные для отображения на графике |
15
- | isTest | boolean | false | Режим тестирования (генерация случайных данных) |
16
+ | Название | Тип | По умолчанию | Описание |
17
+ | ------------- | ------------------------------------------------------------------ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
18
+ | id | string | crypto.randomUUID() | Уникальный идентификатор компонента |
19
+ | wrapperClass | string | "" | Дополнительные CSS-классы для обертки компонента |
20
+ | label | { name?: string; class?: string } | { name: "", class: "" } | Настройки заголовка компонента |
21
+ | streamingData | { data: { name: string; value: number }[] \| null } | { data: [] } | Потоковые данные для живого режима |
22
+ | isTest | boolean | false | Режим тестирования (генерация случайных данных); также форсирует `refreshRate` = 50мс |
23
+ | refreshRate | number | 0 | Интервал отрисовки точки в живом режиме, мс. `0` = **AUTO** — точка рисуется, когда значения реально изменились (без таймера) |
24
+ | historyData | { name: string; points: { value: number; timestamp: number }[] }[] | [] | Готовый исторический ряд — если задан (непустой), включает статический режим (см. ниже) |
25
+
26
+ `refreshRate` относится только к живому режиму и сохраняется вместе с компонентом при сборке интерфейса в конструкторе (редактируется через `GraphProps.svelte`, см. ниже) — это стартовое значение при монтировании; остаётся переключаемым "на лету" прямо на самом графике через встроенный `Select`. В статическом режиме (`historyData`) этот селект скрыт, так как не имеет эффекта.
16
27
 
17
28
  ### Вложенные типы
18
29
 
@@ -25,10 +36,20 @@
25
36
 
26
37
  #### StreamingData
27
38
 
28
- | Название | Тип | По умолчанию | Описание |
29
- | --------- | --------------------------------- | ------------ | -------------------------------------------- |
30
- | data | { name: string; value: number }[] | [] | Массив данных для отображения |
31
- | timestamp | number | Date.now() | Временная метка последнего обновления данных |
39
+ | Название | Тип | По умолчанию | Описание |
40
+ | -------- | ----------------------------------------- | ------------ | ----------------------------- |
41
+ | data | { name: string; value: number }[] \| null | [] | Массив данных для отображения |
42
+
43
+ Режим AUTO (`refreshRate: 0`) определяет появление новых данных сравнением самих значений с предыдущим пакетом, а не по временной метке.
44
+
45
+ #### HistoryData
46
+
47
+ | Название | Тип | По умолчанию | Описание |
48
+ | -------- | -------------------- | ------------ | ------------------------------------------------------------ |
49
+ | name | string | - | Имя серии (отображается в подписи и в таблице значений) |
50
+ | points | IGraphHistoryPoint[] | - | Точки серии, каждая — `{ value: number; timestamp: number }` |
51
+
52
+ `timestamp` — Unix-эпоха в миллисекундах (как `Date.now()`). Точки сортируются компонентом по `timestamp` при построении графика — исходный порядок в массиве не важен.
32
53
 
33
54
  ## События
34
55
 
@@ -64,10 +85,10 @@
64
85
  { name: "Humidity", value: 60 },
65
86
  { name: "Pressure", value: 1013 },
66
87
  ],
67
- timestamp: Date.now(),
68
88
  })
69
89
 
70
- // Обновление данных в реальном времени
90
+ // Обновление данных в реальном времени — при refreshRate: 0 (AUTO) новая точка
91
+ // рисуется на каждое изменение значений, без привязки к частоте setInterval здесь
71
92
  setInterval(() => {
72
93
  data = {
73
94
  data: [
@@ -75,22 +96,51 @@
75
96
  { name: "Humidity", value: Math.random() * 100 },
76
97
  { name: "Pressure", value: 1000 + Math.random() * 50 },
77
98
  ],
78
- timestamp: Date.now(),
79
99
  }
80
100
  }, 1000)
81
101
  </script>
82
102
 
83
- <UI.Graph label={{ name: "График с потоковыми данными" }} {streamingData} />
103
+ <UI.Graph label={{ name: "График с потоковыми данными" }} streamingData={data} refreshRate={0} />
84
104
  ```
85
105
 
106
+ ### Статический режим (historyData)
107
+
108
+ ```svelte
109
+ <script>
110
+ import * as UI from "poe-svelte-ui-lib"
111
+
112
+ const now = Date.now()
113
+ const hourMs = 60 * 60 * 1000
114
+
115
+ // Готовый исторический ряд, например потребление электроэнергии за сутки с шагом в час
116
+ const historyExample = [
117
+ {
118
+ name: "P, кВт·ч",
119
+ points: Array.from({ length: 24 }, (_, i) => ({
120
+ timestamp: now - (23 - i) * hourMs,
121
+ value: 30 + Math.round(Math.sin(i / 3) * 20),
122
+ })),
123
+ },
124
+ ]
125
+ </script>
126
+
127
+ <UI.Graph label={{ name: "Потребление за сутки" }} historyData={historyExample} />
128
+ ```
129
+
130
+ При наведении курсора на график появляется всплывающая подсказка с точным временем и значениями всех серий в ближайшей к курсору временной точке.
131
+
86
132
  ## Внутренняя архитектура
87
133
 
88
134
  ### Реактивность
89
135
 
90
- - Переменная `graphData` хранит точки графиков для отображения
91
- - Переменная `currentValues` хранит текущие значения переменных
92
- - Переменные `selectedRefreshRate` и `selectedScale` управляют частотой обновления и масштабом
93
- - Объекты `REFRESH_OPTIONS` и `SCALE_OPTIONS` определяют доступные настройки
136
+ - Переменная `graphData` хранит точки графиков для отображения (в обоих режимах)
137
+ - Переменная `currentValues` хранит текущие значения переменных — в живом режиме это последний полученный пакет, в статическом — последняя точка каждой серии из `historyData`
138
+ - Переменная `previousValues` хранит значения предыдущего пакета — используется в режиме AUTO, чтобы отличить "пришли новые данные" от повторного опроса
139
+ - Переменная `isHistoryMode` — `$derived` от наличия непустого `historyData`; определяет режим работы компонента целиком (без отдельного булевого пропа)
140
+ - Переменная `selectedRefreshRate` — `$derived` от пропа `refreshRate` (и `isTest`); присвоение через встроенный `Select` временно переопределяет значение, пока не изменится сам проп; в статическом режиме `Select` скрыт
141
+ - Переменная `liveYRange` хранит текущий (сглаженный) диапазон Y-оси живого режима; обновляется функцией `updateLiveYRange()`, вызываемой один раз за тик (после добавления новой точки в интервале), а не за кадр — целевой диапазон берётся из фактических min/max буфера (±10% отступ, `computeAutoYRange()`) и приближается к нему экспоненциальным сглаживанием (коэффициент 0.15), чтобы сетка не дёргалась на каждый семпл. В статическом режиме диапазон считается разово по всему массиву той же `computeAutoYRange()`
142
+ - Объект `REFRESH_OPTIONS` экспортируется из `<script module>` компонента — переиспользуется в `GraphProps.svelte`, чтобы не дублировать список вариантов
143
+ - Переменная `tooltip` хранит состояние всплывающей подсказки (позиция, время, значения серий); обновляется по `onmousemove`/`onmouseleave` канваса, поиск ближайшей точки — бинарным поиском по отсортированному массиву точек серии
94
144
 
95
145
  ### Сторы и зависимости
96
146
 
@@ -124,7 +174,7 @@
124
174
 
125
175
  ### Особенности конструктора
126
176
 
127
- - Предоставляет визуальные элементы управления для всех доступных свойств
177
+ - Предоставляет визуальные элементы управления для всех доступных свойств, включая `refreshRate` (`constructor.props.refreshrate`) через `Select` с теми же вариантами, что и на самом графике (`REFRESH_OPTIONS` из `Graph.svelte`)
128
178
  - Поддерживает настройку переменной для отображения
129
179
  - Включает переключатель для режима тестирования
130
180
  - Имеет различные наборы полей в зависимости от режима (`forConstructor`)
@@ -142,7 +192,8 @@
142
192
 
143
193
  - Компонент ограничен максимальным количеством отображаемых графиков (6)
144
194
  - При отсутствии данных график остается пустым
145
- - Частота обновления влияет на производительность при большом объеме данных
195
+ - Частота обновления влияет на производительность при большом объеме данных (актуально только для живого режима)
196
+ - В статическом режиме (`historyData`) данные рисуются один раз при получении массива — последующие изменения `historyData` (например, замена на новый массив) перестраивают график заново, инкрементального добавления точек нет
146
197
 
147
198
  ### Производительность
148
199
 
@@ -141,7 +141,6 @@ const translations = {
141
141
  "constructor.props.readonly": "Только чтение",
142
142
  "constructor.props.istest": "Тестовые данные",
143
143
  "constructor.props.refreshrate": "Скорость обновления",
144
- "constructor.props.scale": "Масштаб",
145
144
  "constructor.props.variable": "Имя переменной",
146
145
  "constructor.props.widthMode": "Ширина кнопок",
147
146
  "constructor.props.graphdata.title": "Начальные данные ",
@@ -306,7 +305,6 @@ const translations = {
306
305
  "constructor.props.file.select": "Select a file",
307
306
  "constructor.props.file.notselected": "File is not selected",
308
307
  "constructor.props.refreshrate": "Refresh rate",
309
- "constructor.props.scale": "Scale",
310
308
  },
311
309
  zh: {},
312
310
  };
package/dist/types.d.ts CHANGED
@@ -133,7 +133,14 @@ export interface IFileAttachProps {
133
133
  export interface IGraphDataObject {
134
134
  name: string;
135
135
  value: number;
136
- timestamp?: Date;
136
+ }
137
+ export interface IGraphHistoryPoint {
138
+ value: number;
139
+ timestamp: number;
140
+ }
141
+ export interface IGraphHistorySeries {
142
+ name: string;
143
+ points: IGraphHistoryPoint[];
137
144
  }
138
145
  export interface IGraphProps {
139
146
  id?: string;
@@ -147,7 +154,7 @@ export interface IGraphProps {
147
154
  };
148
155
  isTest?: boolean;
149
156
  refreshRate?: number;
150
- scale?: number;
157
+ historyData?: IGraphHistorySeries[];
151
158
  }
152
159
  export interface IInputProps {
153
160
  id?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.10.6",
3
+ "version": "1.10.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {