poe-svelte-ui-lib 1.31.0 → 1.31.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.
|
@@ -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)
|
|
@@ -57,61 +80,85 @@
|
|
|
57
80
|
<h5 class={twMerge("w-full text-[13px] font-semibold text-(--muted-color)", label.class)}>{label.name}</h5>
|
|
58
81
|
{/if}
|
|
59
82
|
|
|
60
|
-
<!-- Сегментированный переключатель
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
<!-- Сегментированный переключатель уровней: при одном уровне переключать нечего -->
|
|
84
|
+
{#if levels.length > 1}
|
|
85
|
+
<div class="flex w-full items-center gap-0.5 rounded-[10px] bg-(--container-color) p-[3px]">
|
|
86
|
+
{#each levels as level, index (level.name)}
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
class={twMerge(
|
|
90
|
+
"flex-1 cursor-pointer rounded-[7px] px-3 py-1.5 text-[13px] transition-colors duration-150",
|
|
91
|
+
index === currentLevelIndex
|
|
92
|
+
? "bg-(--back-color) font-semibold text-(--font-color) shadow-(--elevation-1)"
|
|
93
|
+
: "font-medium text-(--muted-color) hover:text-(--font-color)",
|
|
94
|
+
)}
|
|
95
|
+
onclick={() => selectLevel(index)}
|
|
96
|
+
>
|
|
97
|
+
{level.name}
|
|
98
|
+
</button>
|
|
99
|
+
{/each}
|
|
100
|
+
</div>
|
|
101
|
+
{/if}
|
|
77
102
|
|
|
78
103
|
<!-- Столбчатый график -->
|
|
79
104
|
<div class="relative flex w-full flex-col gap-1.5 rounded-[12px] border border-(--hairline-color) bg-(--back-color) p-3.5">
|
|
80
105
|
{#if values.length === 0}
|
|
81
106
|
<div class="flex h-40 w-full items-center justify-center text-[13px] text-(--faint-color)">Нет данных</div>
|
|
82
107
|
{:else}
|
|
83
|
-
<div class="flex
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
<div class="flex w-full gap-2">
|
|
109
|
+
<!-- Шкала Y: подписи нулевой высоты, центр каждой ровно на своей линии сетки -->
|
|
110
|
+
<div class="flex h-40 shrink-0 flex-col justify-between">
|
|
111
|
+
{#each axis.ticks as tick}
|
|
112
|
+
<div class="flex h-0 items-center justify-end text-[10px] font-medium text-(--faint-color) tabular-nums">
|
|
113
|
+
{formatTick(tick)}
|
|
114
|
+
</div>
|
|
115
|
+
{/each}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
119
|
+
<div class="relative flex h-40 w-full items-end gap-1">
|
|
120
|
+
{#each axis.ticks as tick}
|
|
121
|
+
<div
|
|
122
|
+
class="pointer-events-none absolute inset-x-0 border-t {tick === 0
|
|
123
|
+
? 'border-(--hairline-color)'
|
|
124
|
+
: 'border-dashed border-(--hairline-color)/60'}"
|
|
125
|
+
style="bottom: {(tick / axis.top) * 100}%"
|
|
126
|
+
></div>
|
|
127
|
+
{/each}
|
|
128
|
+
|
|
129
|
+
{#each values as value, i}
|
|
130
|
+
<div
|
|
131
|
+
class="relative flex h-full flex-1 flex-col items-center justify-end"
|
|
132
|
+
role="img"
|
|
133
|
+
aria-label={`${barLabel(i)}: ${value}${unit}`}
|
|
134
|
+
onmouseenter={() => (hoveredIndex = i)}
|
|
135
|
+
onmouseleave={() => (hoveredIndex = null)}
|
|
95
136
|
>
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
137
|
+
{#if hoveredIndex === i}
|
|
138
|
+
<span
|
|
139
|
+
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)"
|
|
140
|
+
>
|
|
141
|
+
{barLabel(i)}: {formatValue(value)}{unit}
|
|
142
|
+
</span>
|
|
143
|
+
{/if}
|
|
144
|
+
<div
|
|
145
|
+
class="w-full rounded-t-[3px] transition-[height,background-color] duration-150 {hoveredIndex === i
|
|
146
|
+
? 'bg-(--accent-color)'
|
|
147
|
+
: 'bg-(--accent-color)/35'}"
|
|
148
|
+
style="height: {barHeightPercent(value)}%"
|
|
149
|
+
></div>
|
|
150
|
+
</div>
|
|
151
|
+
{/each}
|
|
105
152
|
</div>
|
|
106
|
-
{/each}
|
|
107
|
-
</div>
|
|
108
153
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
154
|
+
<div class="flex w-full gap-1">
|
|
155
|
+
{#each values as _, i}
|
|
156
|
+
<div class="flex-1 truncate text-center text-[10px] font-bold text-(--faint-color)">
|
|
157
|
+
{i % labelStep === 0 ? barLabel(i) : ""}
|
|
158
|
+
</div>
|
|
159
|
+
{/each}
|
|
113
160
|
</div>
|
|
114
|
-
|
|
161
|
+
</div>
|
|
115
162
|
</div>
|
|
116
163
|
{/if}
|
|
117
164
|
</div>
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
| Название | Тип | По умолчанию | Описание |
|
|
14
14
|
| ------------------- | --------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
15
15
|
| `id` | `string` | `crypto.randomUUID()` | Уникальный идентификатор компонента |
|
|
16
|
-
| `wrapperClass` | `string` | `""`
|
|
16
|
+
| `wrapperClass` | `string` | `""` | CSS-классы обёртки — через `bg-*` задаёт цвет переключателя и столбиков (по умолчанию нейтрально, без декоративной заливки) |
|
|
17
17
|
| `label` | `{ name?: string; class?: string }` | `{ name: "", class: "" }` | Заголовок над графиком |
|
|
18
|
-
| `levels` | `IPeriodChartLevel[]` | 4 демо-уровня со случайными данными | Уровни детализации, порядок = порядок вкладок переключателя; открывается всегда на первом (индекс 0)
|
|
18
|
+
| `levels` | `IPeriodChartLevel[]` | 4 демо-уровня со случайными данными | Уровни детализации, порядок = порядок вкладок переключателя; открывается всегда на первом (индекс 0); при одном уровне переключатель не показывается |
|
|
19
19
|
| `levels[].name` | `string` | `-` | Подпись вкладки, напр. `"Час"` |
|
|
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
|
|