poe-svelte-ui-lib 1.31.0 → 1.31.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.
|
@@ -37,7 +37,30 @@
|
|
|
37
37
|
|
|
38
38
|
const currentLevel = $derived<IPeriodChartLevel | undefined>(levels[currentLevelIndex])
|
|
39
39
|
const values = $derived(currentLevel?.data ?? [])
|
|
40
|
-
|
|
40
|
+
|
|
41
|
+
/* Шкала Y: шаг 1/2/2.5/5·10ⁿ, ~4 деления, верх округляется вверх до шага, низ всегда 0 */
|
|
42
|
+
const AXIS_DIVISIONS = 4
|
|
43
|
+
const axis = $derived.by(() => {
|
|
44
|
+
const max = values.reduce((m, v) => (Number.isFinite(v) && v > m ? v : m), 0)
|
|
45
|
+
if (max <= 0) return { step: 0.25, top: 1, ticks: [1, 0.75, 0.5, 0.25, 0] }
|
|
46
|
+
const raw = max / AXIS_DIVISIONS
|
|
47
|
+
const mag = 10 ** Math.floor(Math.log10(raw))
|
|
48
|
+
const step = ([1, 2, 2.5, 5, 10].find((m) => m * mag >= raw * (1 - 1e-9)) ?? 10) * mag
|
|
49
|
+
const count = Math.max(1, Math.ceil(max / step - 1e-9))
|
|
50
|
+
return { step, top: step * count, ticks: Array.from({ length: count + 1 }, (_, i) => step * (count - i)) }
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
/* Подпись деления: k/M для больших значений, число знаков после запятой — по величине шага */
|
|
54
|
+
const formatTick = (v: number) => {
|
|
55
|
+
const [div, suffix] = axis.top >= 1e6 ? [1e6, "M"] : axis.top >= 1e4 ? [1e3, "k"] : [1, ""]
|
|
56
|
+
const step = axis.step / div
|
|
57
|
+
let decimals = 0
|
|
58
|
+
while (decimals < 10 && Math.abs(Math.round(step * 10 ** decimals) - step * 10 ** decimals) > 1e-6) decimals++
|
|
59
|
+
return (v / div).toFixed(decimals) + suffix
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Значение во всплывающей подсказке: 4 значащие цифры, без потери малых величин вроде 0.0734 */
|
|
63
|
+
const formatValue = (v: number) => (Number.isInteger(v) ? String(v) : String(Number(v.toPrecision(4))))
|
|
41
64
|
|
|
42
65
|
const selectLevel = (index: number) => {
|
|
43
66
|
if (index === currentLevelIndex) return
|
|
@@ -46,7 +69,7 @@
|
|
|
46
69
|
}
|
|
47
70
|
|
|
48
71
|
const barLabel = (i: number) => currentLevel?.labels?.[i] ?? String(i + 1)
|
|
49
|
-
const barHeightPercent = (value: number) => Math.max((value /
|
|
72
|
+
const barHeightPercent = (value: number) => Math.max((value / axis.top) * 100, value > 0 ? 2 : 0)
|
|
50
73
|
|
|
51
74
|
/* Подписи по оси прореживаются: при 31 значении все они не читаются */
|
|
52
75
|
const labelStep = $derived(Math.ceil(values.length / 12) || 1)
|
|
@@ -80,38 +103,60 @@
|
|
|
80
103
|
{#if values.length === 0}
|
|
81
104
|
<div class="flex h-40 w-full items-center justify-center text-[13px] text-(--faint-color)">Нет данных</div>
|
|
82
105
|
{:else}
|
|
83
|
-
<div class="flex
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
<div class="flex w-full gap-2">
|
|
107
|
+
<!-- Шкала Y: подписи нулевой высоты, центр каждой ровно на своей линии сетки -->
|
|
108
|
+
<div class="flex h-40 shrink-0 flex-col justify-between">
|
|
109
|
+
{#each axis.ticks as tick}
|
|
110
|
+
<div class="flex h-0 items-center justify-end text-[10px] font-medium text-(--faint-color) tabular-nums">
|
|
111
|
+
{formatTick(tick)}
|
|
112
|
+
</div>
|
|
113
|
+
{/each}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
117
|
+
<div class="relative flex h-40 w-full items-end gap-1">
|
|
118
|
+
{#each axis.ticks as tick}
|
|
119
|
+
<div
|
|
120
|
+
class="pointer-events-none absolute inset-x-0 border-t {tick === 0
|
|
121
|
+
? 'border-(--hairline-color)'
|
|
122
|
+
: 'border-dashed border-(--hairline-color)/60'}"
|
|
123
|
+
style="bottom: {(tick / axis.top) * 100}%"
|
|
124
|
+
></div>
|
|
125
|
+
{/each}
|
|
126
|
+
|
|
127
|
+
{#each values as value, i}
|
|
128
|
+
<div
|
|
129
|
+
class="relative flex h-full flex-1 flex-col items-center justify-end"
|
|
130
|
+
role="img"
|
|
131
|
+
aria-label={`${barLabel(i)}: ${value}${unit}`}
|
|
132
|
+
onmouseenter={() => (hoveredIndex = i)}
|
|
133
|
+
onmouseleave={() => (hoveredIndex = null)}
|
|
95
134
|
>
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
{#if hoveredIndex === i}
|
|
136
|
+
<span
|
|
137
|
+
class="pointer-events-none absolute -top-1 z-10 -translate-y-full rounded-lg border border-(--hairline-color) bg-(--back-color) px-2 py-1 text-[12px] font-semibold tabular-nums whitespace-nowrap shadow-(--elevation-2)"
|
|
138
|
+
>
|
|
139
|
+
{barLabel(i)}: {formatValue(value)}{unit}
|
|
140
|
+
</span>
|
|
141
|
+
{/if}
|
|
142
|
+
<div
|
|
143
|
+
class="w-full rounded-t-[3px] transition-[height,background-color] duration-150 {hoveredIndex === i
|
|
144
|
+
? 'bg-(--accent-color)'
|
|
145
|
+
: 'bg-(--accent-color)/35'}"
|
|
146
|
+
style="height: {barHeightPercent(value)}%"
|
|
147
|
+
></div>
|
|
148
|
+
</div>
|
|
149
|
+
{/each}
|
|
105
150
|
</div>
|
|
106
|
-
{/each}
|
|
107
|
-
</div>
|
|
108
151
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
152
|
+
<div class="flex w-full gap-1">
|
|
153
|
+
{#each values as _, i}
|
|
154
|
+
<div class="flex-1 truncate text-center text-[10px] font-bold text-(--faint-color)">
|
|
155
|
+
{i % labelStep === 0 ? barLabel(i) : ""}
|
|
156
|
+
</div>
|
|
157
|
+
{/each}
|
|
113
158
|
</div>
|
|
114
|
-
|
|
159
|
+
</div>
|
|
115
160
|
</div>
|
|
116
161
|
{/if}
|
|
117
162
|
</div>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
| `levels[].variable` | `string` | `undefined` | Имя переменной устройства, откуда берутся `data` этого уровня, напр. `"PWR.EHour"` — используется конструктором для подписки, сам компонент по нему ничего не запрашивает |
|
|
21
21
|
| `levels[].data` | `number[]` | `-` | Значения — длина массива = число столбиков, ничем не ограничена |
|
|
22
22
|
| `levels[].labels` | `string[]` | `undefined` | Подписи столбиков по X; если не заданы — используется индекс+1 |
|
|
23
|
-
| `unit` | `string` | `""` | Единицы измерения, добавляются к значению в `aria-label`, напр. `"Вт·ч"`
|
|
23
|
+
| `unit` | `string` | `""` | Единицы измерения, добавляются к значению в подсказке и `aria-label`, напр. `" Вт·ч"` |
|
|
24
24
|
| `onLevelChange` | `(index: number, level: IPeriodChartLevel) => void` | `() => {}` | Вызывается при переключении вкладки — только уведомление, без фетча внутри |
|
|
25
25
|
|
|
26
26
|
## События
|
|
@@ -72,7 +72,9 @@
|
|
|
72
72
|
|
|
73
73
|
- `currentLevelIndex` (`$state`) — индекс активной вкладки, всегда инициализируется `0` (первый уровень)
|
|
74
74
|
- `currentLevel`/`values` (`$derived`) — текущий уровень и его массив значений
|
|
75
|
-
- `
|
|
75
|
+
- `axis` (`$derived`) — шкала Y по максимуму текущего массива: шаг из ряда 1 / 2 / 2.5 / 5 × 10ⁿ
|
|
76
|
+
(~4 деления), верх шкалы = максимум, округлённый вверх до шага, низ всегда 0; высота столбиков
|
|
77
|
+
считается от верха шкалы
|
|
76
78
|
|
|
77
79
|
### Сторы и зависимости
|
|
78
80
|
|
|
@@ -92,23 +94,24 @@
|
|
|
92
94
|
### Адаптивность
|
|
93
95
|
|
|
94
96
|
- Столбики растягиваются на всю ширину контейнера (`flex-1` на каждый), высота — в процентах от
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
-
|
|
103
|
-
|
|
97
|
+
верха шкалы в зоне фиксированной высоты (`h-40`); ненулевое значение всегда видно минимум на 2 %
|
|
98
|
+
- Шкала Y слева: подписи делений + горизонтальная сетка за столбиками (пунктир, базовая линия 0 —
|
|
99
|
+
сплошная). Шкала пересчитывается автоматически при смене уровня или данных: `0…100`,
|
|
100
|
+
`0…0.08` с шагом `0.02`, `0…3M`. Число знаков после запятой — по величине шага, для больших
|
|
101
|
+
значений — суффиксы `k` (верх ≥ 10 000) и `M` (≥ 1 000 000)
|
|
102
|
+
- Точное значение — в подсказке при наведении на столбик: 4 значащие цифры + `unit`
|
|
103
|
+
(`910: 0.0734 мкВт/см²`), малые величины не теряются
|
|
104
|
+
- Подписи по X (`labels[i]`/индекс+1) — отдельной строкой под столбиками, при большом числе
|
|
105
|
+
столбиков прореживаются (не больше ~12 подписей)
|
|
106
|
+
- Полностью на CSS-переменных темы (`--back-color`, `--accent-color`, `--hairline-color`,
|
|
107
|
+
`--faint-color`) — тёмная/светлая тема не требует отдельной настройки
|
|
104
108
|
|
|
105
109
|
### Ограничения
|
|
106
110
|
|
|
107
111
|
- Компонент презентационный — не хранит историю, не опрашивает устройство сам; при смене
|
|
108
112
|
`levels` извне (например после ответа на `onLevelChange`) график перерисовывается автоматически
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
места), только в `aria-label` для доступности
|
|
113
|
+
- Отрицательные значения не отображаются — шкала всегда начинается с 0
|
|
114
|
+
- Округление только для отображения — исходные данные в `levels[].data` не изменяются
|
|
112
115
|
|
|
113
116
|
### Производительность
|
|
114
117
|
|