chuijs 4.0.2 → 4.0.3

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.
Files changed (221) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +43 -644
  3. package/framework/appLayout/default/chui_app_layout.js +308 -260
  4. package/framework/appLayout/default/global_style.css +85 -48
  5. package/framework/components/chui_accordion/accordion.js +29 -26
  6. package/framework/components/chui_accordion/styles.css +6 -5
  7. package/framework/components/chui_action_sheet/action_sheet.js +59 -33
  8. package/framework/components/chui_action_sheet/styles.css +2 -1
  9. package/framework/components/chui_affix/affix.js +11 -6
  10. package/framework/components/chui_alert/alert.js +23 -14
  11. package/framework/components/chui_avatar/avatar.js +23 -22
  12. package/framework/components/chui_avatar_group/avatar_group.js +21 -19
  13. package/framework/components/chui_backtop/backtop.js +35 -19
  14. package/framework/components/chui_badge/badge.js +15 -13
  15. package/framework/components/chui_breadcrumb/breadcrumb.js +15 -13
  16. package/framework/components/chui_button/button.js +42 -22
  17. package/framework/components/chui_button/styles.css +2 -2
  18. package/framework/components/chui_calendar/calendar.js +12 -10
  19. package/framework/components/chui_card/card.js +43 -23
  20. package/framework/components/chui_chat/chat.js +72 -33
  21. package/framework/components/chui_clipboard_history/clipboard_history.js +30 -21
  22. package/framework/components/chui_clipboard_history/styles.css +4 -2
  23. package/framework/components/chui_code/code.js +2 -2
  24. package/framework/components/chui_code/styles.css +2 -2
  25. package/framework/components/chui_code_editor/code_editor.js +47 -31
  26. package/framework/components/chui_code_editor/styles.css +3 -2
  27. package/framework/components/chui_command_palette/command_palette.js +60 -37
  28. package/framework/components/chui_console/console.js +5 -5
  29. package/framework/components/chui_console/styles.css +1 -1
  30. package/framework/components/chui_content_block/content_block.js +33 -19
  31. package/framework/components/chui_content_block/styles.css +2 -2
  32. package/framework/components/chui_context_menu/context_menu.js +9 -4
  33. package/framework/components/chui_custom_element/custom_element.js +22 -15
  34. package/framework/components/chui_data_grid/data_grid.js +396 -234
  35. package/framework/components/chui_data_grid/styles.css +10 -8
  36. package/framework/components/chui_descriptions/descriptions.js +34 -17
  37. package/framework/components/chui_details/details.js +32 -26
  38. package/framework/components/chui_details/styles.css +5 -4
  39. package/framework/components/chui_diff_viewer/diff_viewer.js +91 -66
  40. package/framework/components/chui_diff_viewer/styles.css +1 -1
  41. package/framework/components/chui_divider/divider.js +13 -11
  42. package/framework/components/chui_drawer/drawer.js +27 -25
  43. package/framework/components/chui_drop_zone/drop_zone.js +31 -21
  44. package/framework/components/chui_empty_state/empty_state.js +20 -15
  45. package/framework/components/chui_fieldset/fieldset.js +37 -20
  46. package/framework/components/chui_fieldset/styles.css +1 -1
  47. package/framework/components/chui_file_explorer/file_explorer.js +157 -86
  48. package/framework/components/chui_file_explorer/styles.css +2 -1
  49. package/framework/components/chui_form/form.js +31 -19
  50. package/framework/components/chui_form/styles.css +1 -1
  51. package/framework/components/chui_gantt/gantt.js +64 -40
  52. package/framework/components/chui_graphs/graphs.js +301 -200
  53. package/framework/components/chui_graphs/styles.css +1 -1
  54. package/framework/components/chui_hero/hero.js +40 -22
  55. package/framework/components/chui_html_block/html_block.js +6 -6
  56. package/framework/components/chui_html_block/styles.css +2 -2
  57. package/framework/components/chui_hx/hx.js +3 -3
  58. package/framework/components/chui_hx/styles.css +11 -4
  59. package/framework/components/chui_icons/icons.js +2109 -2109
  60. package/framework/components/chui_icons/styles.css +2 -2
  61. package/framework/components/chui_inputs/chui_check_box/check_box.js +31 -19
  62. package/framework/components/chui_inputs/chui_check_box/styles.css +18 -18
  63. package/framework/components/chui_inputs/chui_chips/chips.js +50 -40
  64. package/framework/components/chui_inputs/chui_chips/styles.css +4 -2
  65. package/framework/components/chui_inputs/chui_color_picker/color_picker.js +36 -24
  66. package/framework/components/chui_inputs/chui_combo_box/combo_box.js +100 -82
  67. package/framework/components/chui_inputs/chui_combo_box/styles.css +14 -12
  68. package/framework/components/chui_inputs/chui_date/date.js +102 -66
  69. package/framework/components/chui_inputs/chui_date/styles.css +12 -11
  70. package/framework/components/chui_inputs/chui_date_range/date_range.js +40 -29
  71. package/framework/components/chui_inputs/chui_datetime/datetime.js +35 -27
  72. package/framework/components/chui_inputs/chui_email/email.js +43 -31
  73. package/framework/components/chui_inputs/chui_email/styles.css +5 -5
  74. package/framework/components/chui_inputs/chui_file/file.js +68 -41
  75. package/framework/components/chui_inputs/chui_file/styles.css +13 -12
  76. package/framework/components/chui_inputs/chui_hotkey/hotkey.js +61 -36
  77. package/framework/components/chui_inputs/chui_masked/masked.js +39 -28
  78. package/framework/components/chui_inputs/chui_mention/mention.js +77 -30
  79. package/framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box.js +173 -153
  80. package/framework/components/chui_inputs/chui_multi_combo_box/styles.css +9 -7
  81. package/framework/components/chui_inputs/chui_number/number.js +58 -41
  82. package/framework/components/chui_inputs/chui_number/styles.css +7 -7
  83. package/framework/components/chui_inputs/chui_otp/otp.js +38 -29
  84. package/framework/components/chui_inputs/chui_password/password.js +58 -36
  85. package/framework/components/chui_inputs/chui_password/styles.css +8 -8
  86. package/framework/components/chui_inputs/chui_radio_button/radio_button.js +37 -25
  87. package/framework/components/chui_inputs/chui_radio_button/styles.css +20 -20
  88. package/framework/components/chui_inputs/chui_rating/rating.js +26 -22
  89. package/framework/components/chui_inputs/chui_select_box/select_box.js +102 -78
  90. package/framework/components/chui_inputs/chui_select_box/styles.css +11 -11
  91. package/framework/components/chui_inputs/chui_slider/slider.js +29 -26
  92. package/framework/components/chui_inputs/chui_slider/styles.css +2 -1
  93. package/framework/components/chui_inputs/chui_text/styles.css +8 -8
  94. package/framework/components/chui_inputs/chui_text/text.js +65 -44
  95. package/framework/components/chui_inputs/chui_text_area/styles.css +6 -6
  96. package/framework/components/chui_inputs/chui_text_area/text_area.js +51 -28
  97. package/framework/components/chui_inputs/chui_time/styles.css +5 -3
  98. package/framework/components/chui_inputs/chui_time/time.js +38 -27
  99. package/framework/components/chui_inputs/chui_toggle/styles.css +3 -3
  100. package/framework/components/chui_inputs/chui_toggle/toggle.js +16 -16
  101. package/framework/components/chui_json_editor/json_editor.js +53 -43
  102. package/framework/components/chui_json_viewer/json_viewer.js +52 -33
  103. package/framework/components/chui_json_viewer/styles.css +3 -2
  104. package/framework/components/chui_kanban/kanban.js +52 -34
  105. package/framework/components/chui_kbd/kbd.js +12 -10
  106. package/framework/components/chui_label/label.js +14 -12
  107. package/framework/components/chui_label/styles.css +2 -2
  108. package/framework/components/chui_link/link.js +27 -20
  109. package/framework/components/chui_list/list.js +31 -27
  110. package/framework/components/chui_list/styles.css +2 -1
  111. package/framework/components/chui_markdown_editor/markdown_editor.js +60 -44
  112. package/framework/components/chui_media/audio.js +326 -227
  113. package/framework/components/chui_media/audio_styles.css +40 -21
  114. package/framework/components/chui_media/image.js +70 -38
  115. package/framework/components/chui_media/image_compare.js +52 -21
  116. package/framework/components/chui_media/image_styles.css +3 -3
  117. package/framework/components/chui_media/video.js +155 -62
  118. package/framework/components/chui_media/video_styles.css +33 -16
  119. package/framework/components/chui_menu_bar/menu_bar.js +26 -24
  120. package/framework/components/chui_menu_bar/styles.css +4 -2
  121. package/framework/components/chui_modal/modal.js +44 -43
  122. package/framework/components/chui_modal/styles.css +3 -2
  123. package/framework/components/chui_notification/notification.js +50 -34
  124. package/framework/components/chui_notification/notification_download.js +31 -25
  125. package/framework/components/chui_notification/notification_download_progress.css +3 -2
  126. package/framework/components/chui_notification/notification_download_progress.js +47 -41
  127. package/framework/components/chui_notification/notification_update.js +23 -16
  128. package/framework/components/chui_notification/status_plate.js +2 -2
  129. package/framework/components/chui_notification/styles_download.css +1 -1
  130. package/framework/components/chui_notification/styles_update.css +1 -1
  131. package/framework/components/chui_org_chart/org_chart.js +74 -35
  132. package/framework/components/chui_pagination/pagination.js +62 -27
  133. package/framework/components/chui_paragraph/paragraph.js +2 -2
  134. package/framework/components/chui_paragraph/styles.css +1 -1
  135. package/framework/components/chui_popover/popover.js +49 -28
  136. package/framework/components/chui_popups/popups.js +137 -108
  137. package/framework/components/chui_popups/styles.css +2 -2
  138. package/framework/components/chui_print_button/print_button.js +33 -23
  139. package/framework/components/chui_progress_bar/progress_bar.js +23 -15
  140. package/framework/components/chui_progress_bar/styles.css +2 -2
  141. package/framework/components/chui_progress_ring/progress_ring.js +17 -15
  142. package/framework/components/chui_qrcode/qrcode.js +75 -31
  143. package/framework/components/chui_query_builder/query_builder.js +216 -123
  144. package/framework/components/chui_radio_group/radio_group.js +36 -26
  145. package/framework/components/chui_radio_group/styles.css +1 -1
  146. package/framework/components/chui_result/result.js +26 -16
  147. package/framework/components/chui_scheduler/scheduler.js +57 -35
  148. package/framework/components/chui_scheduler/styles.css +6 -2
  149. package/framework/components/chui_segmented_control/segmented_control.js +27 -21
  150. package/framework/components/chui_settings/settings.js +122 -48
  151. package/framework/components/chui_shortcut_help/shortcut_help.js +55 -32
  152. package/framework/components/chui_sidebar_nav/sidebar_nav.js +33 -23
  153. package/framework/components/chui_skeleton/skeleton.js +29 -24
  154. package/framework/components/chui_slideshow/slideshow.js +60 -41
  155. package/framework/components/chui_slideshow/styles.css +18 -12
  156. package/framework/components/chui_spinner/spinner.js +8 -8
  157. package/framework/components/chui_spinner/styles.css +0 -1
  158. package/framework/components/chui_split_button/split_button.js +23 -18
  159. package/framework/components/chui_split_button/styles.css +1 -1
  160. package/framework/components/chui_splitter/splitter.js +35 -28
  161. package/framework/components/chui_statistic/statistic.js +65 -38
  162. package/framework/components/chui_status_bar/status_bar.js +11 -9
  163. package/framework/components/chui_stepper/stepper.js +27 -22
  164. package/framework/components/chui_sticky_bar/sticky_bar.js +9 -4
  165. package/framework/components/chui_sticky_bar/styles.css +3 -1
  166. package/framework/components/chui_tabs/chui_tabs.js +28 -22
  167. package/framework/components/chui_tabs/styles.css +3 -3
  168. package/framework/components/chui_text_editor/styles.css +20 -19
  169. package/framework/components/chui_text_editor/text_editor.js +85 -61
  170. package/framework/components/chui_text_editor/text_editor_panel.js +346 -284
  171. package/framework/components/chui_theme_toggle/theme_toggle.js +27 -22
  172. package/framework/components/chui_timeline/timeline.js +14 -12
  173. package/framework/components/chui_toast_queue/toast_queue.js +52 -46
  174. package/framework/components/chui_toolbar/toolbar.js +20 -13
  175. package/framework/components/chui_tooltip/tooltip.js +37 -24
  176. package/framework/components/chui_tour/styles.css +8 -3
  177. package/framework/components/chui_tour/tour.js +61 -27
  178. package/framework/components/chui_transfer/styles.css +2 -1
  179. package/framework/components/chui_transfer/transfer.js +75 -42
  180. package/framework/components/chui_tree_table/tree_table.js +43 -29
  181. package/framework/components/chui_tree_view/styles.css +4 -2
  182. package/framework/components/chui_tree_view/tree_view.js +142 -99
  183. package/framework/components/chui_watermark/watermark.js +24 -16
  184. package/framework/components/chui_webview/styles.css +2 -2
  185. package/framework/components/chui_webview/webview.js +62 -50
  186. package/framework/components/telegram_bot/chui_telegram_bot.js +71 -64
  187. package/framework/modules/chui_a11y/chui_a11y.js +77 -46
  188. package/framework/modules/chui_animations/animations.js +42 -42
  189. package/framework/modules/chui_animations/styles.css +1 -1
  190. package/framework/modules/chui_bridge/preload.js +36 -25
  191. package/framework/modules/chui_functions.js +52 -50
  192. package/framework/modules/chui_highlight/chui_highlight.js +25 -22
  193. package/framework/modules/chui_i18n/chui_i18n.js +9 -7
  194. package/framework/modules/chui_i18n/locales/a11y.js +5 -5
  195. package/framework/modules/chui_i18n/locales/app_layout.js +5 -5
  196. package/framework/modules/chui_i18n/locales/calendar.js +3 -3
  197. package/framework/modules/chui_i18n/locales/common.js +3 -3
  198. package/framework/modules/chui_i18n/locales/data.js +36 -26
  199. package/framework/modules/chui_i18n/locales/editors.js +40 -13
  200. package/framework/modules/chui_i18n/locales/feedback.js +3 -3
  201. package/framework/modules/chui_i18n/locales/forms.js +8 -8
  202. package/framework/modules/chui_i18n/locales/graphics.js +8 -8
  203. package/framework/modules/chui_i18n/locales/media.js +4 -4
  204. package/framework/modules/chui_i18n/locales/navigation.js +8 -8
  205. package/framework/modules/chui_i18n/locales/validate.js +3 -3
  206. package/framework/modules/chui_json/chui_json.js +9 -9
  207. package/framework/modules/chui_layout/chui_layout.js +12 -10
  208. package/framework/modules/chui_logger/chui_logger.js +29 -25
  209. package/framework/modules/chui_page/page.js +33 -27
  210. package/framework/modules/chui_qr/chui_qr.js +158 -67
  211. package/framework/modules/chui_scrollbar/chui_scrollbar.js +35 -24
  212. package/framework/modules/chui_scrollbar/styles.css +3 -1
  213. package/framework/modules/chui_services/chui_channels.js +27 -13
  214. package/framework/modules/chui_services/chui_services.js +39 -31
  215. package/framework/modules/chui_services/desktop.js +32 -31
  216. package/framework/modules/chui_sortable/chui_sortable.js +17 -12
  217. package/framework/modules/chui_validate/chui_validate.js +7 -4
  218. package/framework/tray_bar/chui_menu_items.js +136 -46
  219. package/index.d.ts +4814 -0
  220. package/index.js +385 -292
  221. package/package.json +79 -68
@@ -1,15 +1,15 @@
1
- const { Notification } = require('../chui_notification/notification');
2
- const { setStyles, optionOr } = require('../../modules/chui_functions');
3
- const { I18n } = require('../../modules/chui_i18n/chui_i18n');
1
+ const { Notification } = require("../chui_notification/notification");
2
+ const { setStyles, optionOr } = require("../../modules/chui_functions");
3
+ const { I18n } = require("../../modules/chui_i18n/chui_i18n");
4
4
 
5
- const STYLES_ID = 'chUiJS_Canvas';
6
- const AXIS_COLOR = '#8e8e93';
7
- const LABEL_FONT = 'bold 14px Arial';
5
+ const STYLES_ID = "chUiJS_Canvas";
6
+ const AXIS_COLOR = "#8e8e93";
7
+ const LABEL_FONT = "bold 14px Arial";
8
8
  const Y_AXIS_STEPS = 4;
9
9
  const PLOT_HEIGHT = 240;
10
10
  const CATEGORY_WIDTH = 150;
11
11
  const BAR_WIDTH = 75;
12
- const BAR_FALLBACK_COLOR = '#1676f3';
12
+ const BAR_FALLBACK_COLOR = "#1676f3";
13
13
  const PADDING = { top: 20, right: 20, bottom: 50, left: 50 };
14
14
  const PIE_SIZE = 400;
15
15
  const GRID_COLOR = "rgba(142, 142, 147, 0.25)";
@@ -82,9 +82,9 @@ function prepareCanvas(canvas, cssWidth, cssHeight) {
82
82
  const ratio = window.devicePixelRatio || 1;
83
83
  canvas.width = cssWidth * ratio;
84
84
  canvas.height = cssHeight * ratio;
85
- canvas.style.width = cssWidth + 'px';
86
- canvas.style.height = cssHeight + 'px';
87
- const ctx = canvas.getContext('2d');
85
+ canvas.style.width = cssWidth + "px";
86
+ canvas.style.height = cssHeight + "px";
87
+ const ctx = canvas.getContext("2d");
88
88
  ctx.scale(ratio, ratio);
89
89
  return ctx;
90
90
  }
@@ -104,8 +104,8 @@ function drawAxesGrid(ctx, plotWidth, scale) {
104
104
  ctx.stroke();
105
105
 
106
106
  ctx.font = LABEL_FONT;
107
- ctx.textAlign = 'right';
108
- ctx.textBaseline = 'middle';
107
+ ctx.textAlign = "right";
108
+ ctx.textBaseline = "middle";
109
109
  for (let i = 0; i <= Y_AXIS_STEPS; i++) {
110
110
  const y = bottomY - (i / Y_AXIS_STEPS) * PLOT_HEIGHT;
111
111
  ctx.strokeStyle = AXIS_COLOR;
@@ -150,15 +150,15 @@ function createTooltip(container) {
150
150
  }
151
151
 
152
152
  class BarGraph {
153
- #canvas = document.createElement('canvas');
154
- #main = document.createElement('chui_bar_graph');
153
+ #canvas = document.createElement("canvas");
154
+ #main = document.createElement("chui_bar_graph");
155
155
  #options = undefined;
156
156
  #bars = [];
157
157
  #active = undefined;
158
158
  #tooltip = undefined;
159
159
 
160
160
  constructor(options = {}) {
161
- setStyles(__dirname + '/styles.css', STYLES_ID);
161
+ setStyles(__dirname + "/styles.css", STYLES_ID);
162
162
  this.#options = options;
163
163
  this.#main.appendChild(this.#canvas);
164
164
  this.#tooltip = createTooltip(this.#main);
@@ -187,8 +187,8 @@ class BarGraph {
187
187
  #drawBars(ctx, labels, values, scale, colors) {
188
188
  const bottomY = PADDING.top + PLOT_HEIGHT;
189
189
  this.#bars = [];
190
- ctx.textAlign = 'center';
191
- ctx.textBaseline = 'top';
190
+ ctx.textAlign = "center";
191
+ ctx.textBaseline = "top";
192
192
  for (let i = 0; i < labels.length; i++) {
193
193
  // Отрицательное значение — это ноль на оси, которая начинается с нуля:
194
194
  // иначе столбец уходил бы вниз, на подписи категорий (как у RadarGraph и HeatmapGraph)
@@ -217,18 +217,21 @@ class BarGraph {
217
217
  this.#canvas.setAttribute("role", "img");
218
218
  this.#canvas.setAttribute("tabindex", "0");
219
219
  this.#canvas.setAttribute("aria-label", describeData(I18n.t("graphics.barChart"), this.#options.data || {}));
220
- this.#canvas.addEventListener('click', (event) => {
220
+ this.#canvas.addEventListener("click", (event) => {
221
221
  const bar = this.#findBar(event);
222
222
  if (bar === null) return;
223
223
  this.#announce(bar.index);
224
224
  });
225
225
  // Наведение: подсказка с категорией и значением
226
- this.#canvas.addEventListener('mousemove', (event) => {
226
+ this.#canvas.addEventListener("mousemove", (event) => {
227
227
  const bar = this.#findBar(event);
228
- if (bar === null) { this.#tooltip.hide(); return; }
228
+ if (bar === null) {
229
+ this.#tooltip.hide();
230
+ return;
231
+ }
229
232
  this.#showTooltip(bar.index);
230
233
  });
231
- this.#canvas.addEventListener('mouseleave', () => {
234
+ this.#canvas.addEventListener("mouseleave", () => {
232
235
  if (this.#active === undefined) this.#tooltip.hide();
233
236
  });
234
237
  // Клавиатура: ←/→ ходят по столбцам, Enter показывает значение (как клик мышью)
@@ -241,10 +244,13 @@ class BarGraph {
241
244
  if (next > this.#bars.length - 1) next = 0;
242
245
  this.#active = next;
243
246
  this.#draw();
244
- this.#canvas.setAttribute("aria-label", I18n.t("graphics.selected", {
245
- summary: describeData(I18n.t("graphics.barChart"), this.#options.data || {}),
246
- value: this.#label(this.#active)
247
- }));
247
+ this.#canvas.setAttribute(
248
+ "aria-label",
249
+ I18n.t("graphics.selected", {
250
+ summary: describeData(I18n.t("graphics.barChart"), this.#options.data || {}),
251
+ value: this.#label(this.#active),
252
+ })
253
+ );
248
254
  this.#showTooltip(this.#active);
249
255
  } else if (event.key === "Enter" || event.key === " ") {
250
256
  if (this.#active === undefined) return;
@@ -288,12 +294,12 @@ class BarGraph {
288
294
  }
289
295
 
290
296
  class PieGraph {
291
- #chui_piechart = document.createElement('chui_piechart');
292
- #canvas = document.createElement('canvas');
297
+ #chui_piechart = document.createElement("chui_piechart");
298
+ #canvas = document.createElement("canvas");
293
299
  #options = undefined;
294
300
 
295
301
  constructor(options = {}) {
296
- setStyles(__dirname + '/styles.css', STYLES_ID);
302
+ setStyles(__dirname + "/styles.css", STYLES_ID);
297
303
  this.#options = options;
298
304
  const ctx = prepareCanvas(this.#canvas, PIE_SIZE, PIE_SIZE);
299
305
  this.#drawSlices(ctx);
@@ -319,7 +325,7 @@ class PieGraph {
319
325
  let startAngle = 0;
320
326
  for (let i = 0; i < labels.length; i++) {
321
327
  const value = values[i];
322
- const sliceAngle = 2 * Math.PI * value / total;
328
+ const sliceAngle = (2 * Math.PI * value) / total;
323
329
  drawPieSlice(ctx, center, center, center, startAngle, startAngle + sliceAngle, colors[i % colors.length]);
324
330
  this.#drawPercentage(ctx, center, startAngle, sliceAngle, value, total);
325
331
  startAngle += sliceAngle;
@@ -330,18 +336,22 @@ class PieGraph {
330
336
  #drawPercentage(ctx, center, startAngle, sliceAngle, value, total) {
331
337
  const labelRadius = center * 0.65;
332
338
  const angle = startAngle + sliceAngle / 2;
333
- ctx.fillStyle = 'white';
339
+ ctx.fillStyle = "white";
334
340
  ctx.font = LABEL_FONT;
335
- ctx.textAlign = 'center';
336
- ctx.textBaseline = 'middle';
337
- ctx.fillText(`${Math.round(100 * value / total)}%`, center + labelRadius * Math.cos(angle), center + labelRadius * Math.sin(angle));
341
+ ctx.textAlign = "center";
342
+ ctx.textBaseline = "middle";
343
+ ctx.fillText(
344
+ `${Math.round((100 * value) / total)}%`,
345
+ center + labelRadius * Math.cos(angle),
346
+ center + labelRadius * Math.sin(angle)
347
+ );
338
348
  }
339
349
 
340
350
  #drawDoughnutHole(ctx, center) {
341
351
  const holeSize = this.#options.doughnutHoleSize;
342
352
  if (!(holeSize > 0) || holeSize >= 1) return;
343
353
  ctx.save();
344
- ctx.globalCompositeOperation = 'destination-out';
354
+ ctx.globalCompositeOperation = "destination-out";
345
355
  ctx.beginPath();
346
356
  ctx.arc(center, center, center * holeSize, 0, 2 * Math.PI);
347
357
  ctx.closePath();
@@ -352,7 +362,9 @@ class PieGraph {
352
362
  #buildLegend() {
353
363
  const data = this.#options.data || {};
354
364
  const colors = this.#options.colors || [];
355
- return buildLegend(Object.keys(data).map((label, index) => ({ title: label, color: colors[index % colors.length] })));
365
+ return buildLegend(
366
+ Object.keys(data).map((label, index) => ({ title: label, color: colors[index % colors.length] }))
367
+ );
356
368
  }
357
369
 
358
370
  set() {
@@ -361,15 +373,15 @@ class PieGraph {
361
373
  }
362
374
 
363
375
  class LineGraph {
364
- #canvas = document.createElement('canvas');
365
- #main = document.createElement('chui_line_graph');
376
+ #canvas = document.createElement("canvas");
377
+ #main = document.createElement("chui_line_graph");
366
378
  #options = undefined;
367
379
  #points = [];
368
380
  #active = undefined;
369
381
  #tooltip = undefined;
370
382
 
371
383
  constructor(options = {}) {
372
- setStyles(__dirname + '/styles.css', STYLES_ID);
384
+ setStyles(__dirname + "/styles.css", STYLES_ID);
373
385
  this.#options = options;
374
386
  this.#main.appendChild(this.#canvas);
375
387
  this.#tooltip = createTooltip(this.#main);
@@ -406,10 +418,10 @@ class LineGraph {
406
418
  // Заливка под линией (AreaGraph): градиент только для hex-цветов
407
419
  if (this.#options.area === true && this.#points.length > 0) {
408
420
  ctx.fillStyle = this.#color;
409
- if (this.#color.startsWith('#')) {
421
+ if (this.#color.startsWith("#")) {
410
422
  const gradient = ctx.createLinearGradient(0, PADDING.top, 0, bottomY);
411
- gradient.addColorStop(0, this.#color + '66');
412
- gradient.addColorStop(1, this.#color + '00');
423
+ gradient.addColorStop(0, this.#color + "66");
424
+ gradient.addColorStop(1, this.#color + "00");
413
425
  ctx.fillStyle = gradient;
414
426
  }
415
427
  ctx.beginPath();
@@ -445,8 +457,8 @@ class LineGraph {
445
457
  ctx.stroke();
446
458
  }
447
459
  ctx.fillStyle = AXIS_COLOR;
448
- ctx.textAlign = 'center';
449
- ctx.textBaseline = 'top';
460
+ ctx.textAlign = "center";
461
+ ctx.textBaseline = "top";
450
462
  ctx.fillText(labels[point.index], point.x, bottomY + 10);
451
463
  }
452
464
  }
@@ -456,17 +468,20 @@ class LineGraph {
456
468
  this.#canvas.setAttribute("role", "img");
457
469
  this.#canvas.setAttribute("tabindex", "0");
458
470
  this.#canvas.setAttribute("aria-label", describeData(I18n.t("graphics.lineChart"), this.#options.data || {}));
459
- this.#canvas.addEventListener('click', (event) => {
471
+ this.#canvas.addEventListener("click", (event) => {
460
472
  const point = this.#findPoint(event);
461
473
  if (point !== null) this.#announce(point.index);
462
474
  });
463
475
  // Наведение: подсказка у ближайшей точки
464
- this.#canvas.addEventListener('mousemove', (event) => {
476
+ this.#canvas.addEventListener("mousemove", (event) => {
465
477
  const point = this.#findPoint(event);
466
- if (point === null) { this.#tooltip.hide(); return; }
478
+ if (point === null) {
479
+ this.#tooltip.hide();
480
+ return;
481
+ }
467
482
  this.#select(point.index);
468
483
  });
469
- this.#canvas.addEventListener('mouseleave', () => {
484
+ this.#canvas.addEventListener("mouseleave", () => {
470
485
  if (this.#active === undefined) this.#tooltip.hide();
471
486
  });
472
487
  // Клавиатура: ←/→ ходят по точкам, Enter показывает значение
@@ -504,10 +519,13 @@ class LineGraph {
504
519
  const label = labels[index];
505
520
  const point = this.#points[index];
506
521
  if (point !== undefined) this.#tooltip.show(`${label}: ${this.#options.data[label]}`, point.x, point.y);
507
- this.#canvas.setAttribute("aria-label", I18n.t("graphics.selected", {
508
- summary: describeData(I18n.t("graphics.lineChart"), this.#options.data || {}),
509
- value: `${label}: ${this.#options.data[label]}`
510
- }));
522
+ this.#canvas.setAttribute(
523
+ "aria-label",
524
+ I18n.t("graphics.selected", {
525
+ summary: describeData(I18n.t("graphics.lineChart"), this.#options.data || {}),
526
+ value: `${label}: ${this.#options.data[label]}`,
527
+ })
528
+ );
511
529
  this.#draw();
512
530
  }
513
531
 
@@ -570,8 +588,8 @@ function drawPieSlice(ctx, centerX, centerY, radius, startAngle, endAngle, color
570
588
  * ```
571
589
  */
572
590
  class ScatterGraph {
573
- #canvas = document.createElement('canvas');
574
- #main = document.createElement('chui_scatter_graph');
591
+ #canvas = document.createElement("canvas");
592
+ #main = document.createElement("chui_scatter_graph");
575
593
  #options = undefined;
576
594
  #series = [];
577
595
  /** Плоский список точек с экранными координатами — общий для мыши и клавиатуры */
@@ -580,7 +598,7 @@ class ScatterGraph {
580
598
  #tooltip = undefined;
581
599
 
582
600
  constructor(options = {}) {
583
- setStyles(__dirname + '/styles.css', STYLES_ID);
601
+ setStyles(__dirname + "/styles.css", STYLES_ID);
584
602
  this.#options = options;
585
603
  this.#series = ScatterGraph.#normalize(options.series, options.colors);
586
604
  this.#main.appendChild(this.#canvas);
@@ -588,9 +606,11 @@ class ScatterGraph {
588
606
  this.#draw();
589
607
  // Легенда нужна только когда серий больше одной: у одной подпись есть в подсказке
590
608
  if (options.legend !== false && this.#series.length > 1) {
591
- this.#main.appendChild(buildLegend(this.#series.map((serie) => ({ title: serie.title, color: serie.color }))));
609
+ this.#main.appendChild(
610
+ buildLegend(this.#series.map((serie) => ({ title: serie.title, color: serie.color })))
611
+ );
592
612
  }
593
- this.#main.appendChild(buildTable([I18n.t("graphics.seriesColumn"), 'X', 'Y'], this.#tableRows()));
613
+ this.#main.appendChild(buildTable([I18n.t("graphics.seriesColumn"), "X", "Y"], this.#tableRows()));
594
614
  this.#bindPointer();
595
615
  }
596
616
 
@@ -623,15 +643,19 @@ class ScatterGraph {
623
643
  const list = Array.isArray(series) ? series : [];
624
644
  const palette = Array.isArray(colors) ? colors : [];
625
645
  return list.map((item, index) => {
626
- const entry = item === null || typeof item !== 'object' ? {} : item;
646
+ const entry = item === null || typeof item !== "object" ? {} : item;
627
647
  const points = (Array.isArray(entry.points) ? entry.points : []).map((point) => {
628
648
  if (Array.isArray(point)) return { x: Number(point[0]) || 0, y: Number(point[1]) || 0 };
629
- if (point === null || typeof point !== 'object') return { x: 0, y: 0 };
649
+ if (point === null || typeof point !== "object") return { x: 0, y: 0 };
630
650
  return { x: Number(point.x) || 0, y: Number(point.y) || 0 };
631
651
  });
632
652
  return {
633
- title: entry.title === undefined ? I18n.t("graphics.series", {index: index + 1}) : String(entry.title),
634
- color: entry.color === undefined ? (palette[index % palette.length] || LINE_FALLBACK_COLOR) : String(entry.color),
653
+ title:
654
+ entry.title === undefined ? I18n.t("graphics.series", { index: index + 1 }) : String(entry.title),
655
+ color:
656
+ entry.color === undefined
657
+ ? palette[index % palette.length] || LINE_FALLBACK_COLOR
658
+ : String(entry.color),
635
659
  points: points,
636
660
  };
637
661
  });
@@ -649,8 +673,14 @@ class ScatterGraph {
649
673
  const points = this.#series.reduce((list, serie) => list.concat(serie.points), []);
650
674
  const xs = points.map((point) => point.x);
651
675
  const ys = points.map((point) => point.y);
652
- const xScale = getRangeScale(xs.length ? Math.min.apply(null, xs) : 0, xs.length ? Math.max.apply(null, xs) : 0);
653
- const yScale = getRangeScale(ys.length ? Math.min.apply(null, ys) : 0, ys.length ? Math.max.apply(null, ys) : 0);
676
+ const xScale = getRangeScale(
677
+ xs.length ? Math.min.apply(null, xs) : 0,
678
+ xs.length ? Math.max.apply(null, xs) : 0
679
+ );
680
+ const yScale = getRangeScale(
681
+ ys.length ? Math.min.apply(null, ys) : 0,
682
+ ys.length ? Math.max.apply(null, ys) : 0
683
+ );
654
684
  const xTitle = this.#options.xTitle;
655
685
  const yTitle = this.#options.yTitle;
656
686
  // Подписи делений оси Y и её название стоят слева: под них и расширяем отступ
@@ -684,8 +714,8 @@ class ScatterGraph {
684
714
  ctx.lineTo(x, bottom + 5);
685
715
  ctx.stroke();
686
716
  ctx.fillStyle = AXIS_COLOR;
687
- ctx.textAlign = 'center';
688
- ctx.textBaseline = 'top';
717
+ ctx.textAlign = "center";
718
+ ctx.textBaseline = "top";
689
719
  ctx.fillText(formatAxisLabel(xScale.min + i * xScale.step), x, bottom + 8);
690
720
  }
691
721
  for (let i = 0; i <= yScale.count; i++) {
@@ -705,8 +735,8 @@ class ScatterGraph {
705
735
  ctx.lineTo(left - 5, y);
706
736
  ctx.stroke();
707
737
  ctx.fillStyle = AXIS_COLOR;
708
- ctx.textAlign = 'right';
709
- ctx.textBaseline = 'middle';
738
+ ctx.textAlign = "right";
739
+ ctx.textBaseline = "middle";
710
740
  ctx.fillText(formatAxisLabel(yScale.min + i * yScale.step), left - 6, y);
711
741
  }
712
742
  // Оси рисуются по краям поля данных
@@ -724,15 +754,15 @@ class ScatterGraph {
724
754
  ctx.translate(14, PADDING.top + PLOT_HEIGHT / 2);
725
755
  ctx.rotate(-Math.PI / 2);
726
756
  ctx.fillStyle = AXIS_COLOR;
727
- ctx.textAlign = 'center';
728
- ctx.textBaseline = 'middle';
757
+ ctx.textAlign = "center";
758
+ ctx.textBaseline = "middle";
729
759
  ctx.fillText(String(yTitle), 0, 0);
730
760
  ctx.restore();
731
761
  }
732
762
  if (xTitle !== undefined) {
733
763
  ctx.fillStyle = AXIS_COLOR;
734
- ctx.textAlign = 'center';
735
- ctx.textBaseline = 'top';
764
+ ctx.textAlign = "center";
765
+ ctx.textBaseline = "top";
736
766
  ctx.fillText(String(xTitle), left + plotWidth / 2, bottom + 30);
737
767
  }
738
768
 
@@ -743,7 +773,15 @@ class ScatterGraph {
743
773
  const x = toX(point.x);
744
774
  const y = toY(point.y);
745
775
  const index = this.#points.length;
746
- this.#points.push({ index: index, x: x, y: y, serie: serie.title, color: serie.color, xValue: point.x, yValue: point.y });
776
+ this.#points.push({
777
+ index: index,
778
+ x: x,
779
+ y: y,
780
+ serie: serie.title,
781
+ color: serie.color,
782
+ xValue: point.x,
783
+ yValue: point.y,
784
+ });
747
785
  ctx.fillStyle = serie.color;
748
786
  ctx.beginPath();
749
787
  ctx.arc(x, y, SCATTER_POINT_RADIUS, 0, 2 * Math.PI);
@@ -761,15 +799,15 @@ class ScatterGraph {
761
799
 
762
800
  #bindPointer() {
763
801
  this.#canvas.classList.add("graph_interactive");
764
- this.#canvas.setAttribute('role', 'img');
765
- this.#canvas.setAttribute('tabindex', '0');
766
- this.#canvas.setAttribute('aria-label', this.#describe());
767
- this.#canvas.addEventListener('click', (event) => {
802
+ this.#canvas.setAttribute("role", "img");
803
+ this.#canvas.setAttribute("tabindex", "0");
804
+ this.#canvas.setAttribute("aria-label", this.#describe());
805
+ this.#canvas.addEventListener("click", (event) => {
768
806
  const point = this.#findPoint(event);
769
807
  if (point !== null) this.#announce(point.index);
770
808
  });
771
809
  // Наведение: подсказка у ближайшей точки
772
- this.#canvas.addEventListener('mousemove', (event) => {
810
+ this.#canvas.addEventListener("mousemove", (event) => {
773
811
  const point = this.#findPoint(event);
774
812
  if (point === null) {
775
813
  this.#tooltip.hide();
@@ -777,20 +815,20 @@ class ScatterGraph {
777
815
  }
778
816
  this.#select(point.index);
779
817
  });
780
- this.#canvas.addEventListener('mouseleave', () => {
818
+ this.#canvas.addEventListener("mouseleave", () => {
781
819
  if (this.#active === undefined) this.#tooltip.hide();
782
820
  });
783
821
  // Клавиатура: ←/→ водят по точкам, Enter показывает значение (как клик мышью)
784
- this.#canvas.addEventListener('keydown', (event) => {
822
+ this.#canvas.addEventListener("keydown", (event) => {
785
823
  if (this.#points.length === 0) return;
786
- if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
824
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
787
825
  event.preventDefault();
788
- let next = this.#active === undefined ? 0 : this.#active + (event.key === 'ArrowRight' ? 1 : -1);
826
+ let next = this.#active === undefined ? 0 : this.#active + (event.key === "ArrowRight" ? 1 : -1);
789
827
  if (next < 0) next = this.#points.length - 1;
790
828
  if (next > this.#points.length - 1) next = 0;
791
829
  this.#draw();
792
830
  this.#select(next);
793
- } else if (event.key === 'Enter' || event.key === ' ') {
831
+ } else if (event.key === "Enter" || event.key === " ") {
794
832
  if (this.#active === undefined) return;
795
833
  event.preventDefault();
796
834
  this.#announce(this.#active);
@@ -815,7 +853,10 @@ class ScatterGraph {
815
853
  const point = this.#points[index];
816
854
  if (point === undefined) return;
817
855
  this.#tooltip.show(this.#pointText(point), point.x, point.y);
818
- this.#canvas.setAttribute('aria-label', I18n.t("graphics.selected", {summary: this.#describe(), value: this.#pointText(point)}));
856
+ this.#canvas.setAttribute(
857
+ "aria-label",
858
+ I18n.t("graphics.selected", { summary: this.#describe(), value: this.#pointText(point) })
859
+ );
819
860
  this.#draw();
820
861
  }
821
862
 
@@ -837,7 +878,7 @@ class ScatterGraph {
837
878
  if (parts.length === 0) return I18n.t("graphics.scatterChart");
838
879
  const total = this.#series.reduce((sum, serie) => sum + serie.points.length, 0);
839
880
  const rest = total - parts.length;
840
- return `${I18n.t("graphics.scatterChart")}: ${parts.join(', ')}${rest > 0 ? `, и ещё ${rest}` : ""}`;
881
+ return `${I18n.t("graphics.scatterChart")}: ${parts.join(", ")}${rest > 0 ? `, и ещё ${rest}` : ""}`;
841
882
  }
842
883
 
843
884
  #announce(index) {
@@ -879,15 +920,15 @@ class Gauge {
879
920
  static DEFAULT_THICKNESS = GAUGE_THICKNESS;
880
921
  static #counter = 0;
881
922
 
882
- #root = document.createElement('chui_gauge');
883
- #canvas = document.createElement('canvas');
884
- #value_text = document.createElement('gauge_value');
885
- #status = document.createElement('chui_gauge_status');
923
+ #root = document.createElement("chui_gauge");
924
+ #canvas = document.createElement("canvas");
925
+ #value_text = document.createElement("gauge_value");
926
+ #status = document.createElement("chui_gauge_status");
886
927
  #id = undefined;
887
928
  #value = 0;
888
929
  #min = 0;
889
930
  #max = 100;
890
- #unit = '';
931
+ #unit = "";
891
932
  #label = I18n.t("graphics.gauge");
892
933
  #format = undefined;
893
934
  #show_value = true;
@@ -900,17 +941,17 @@ class Gauge {
900
941
  #thresholds = [];
901
942
 
902
943
  constructor(options = {}) {
903
- setStyles(__dirname + '/styles.css', STYLES_ID);
944
+ setStyles(__dirname + "/styles.css", STYLES_ID);
904
945
  Gauge.#counter += 1;
905
946
  this.#id = options.id === undefined ? `chui_gauge_${Gauge.#counter}` : options.id;
906
947
  this.#root.id = this.#id;
907
- this.#root.setAttribute('role', 'meter');
908
- this.#canvas.setAttribute('aria-hidden', 'true');
909
- this.#value_text.className = 'gauge_value';
910
- this.#value_text.setAttribute('aria-hidden', 'true');
911
- this.#status.className = 'gauge_status chui_visually_hidden';
912
- this.#status.setAttribute('role', 'status');
913
- this.#status.setAttribute('aria-live', 'polite');
948
+ this.#root.setAttribute("role", "meter");
949
+ this.#canvas.setAttribute("aria-hidden", "true");
950
+ this.#value_text.className = "gauge_value";
951
+ this.#value_text.setAttribute("aria-hidden", "true");
952
+ this.#status.className = "gauge_status chui_visually_hidden";
953
+ this.#status.setAttribute("role", "status");
954
+ this.#status.setAttribute("aria-live", "polite");
914
955
  this.#root.appendChild(this.#canvas);
915
956
  this.#root.appendChild(this.#value_text);
916
957
  this.#root.appendChild(this.#status);
@@ -926,8 +967,9 @@ class Gauge {
926
967
  if (options.color !== undefined) this.#color = String(options.color);
927
968
  this.#format = options.format;
928
969
  this.#show_value = options.showValue !== false;
929
- this.setLabel(optionOr(options.label, I18n.t('graphics.gauge')));
930
- if (options.thresholds !== undefined) this.#thresholds = Gauge.#normalizeThresholds(options.thresholds, this.#max);
970
+ this.setLabel(optionOr(options.label, I18n.t("graphics.gauge")));
971
+ if (options.thresholds !== undefined)
972
+ this.#thresholds = Gauge.#normalizeThresholds(options.thresholds, this.#max);
931
973
  // Значение ставим последним: оно уже знает про min/max и пороги
932
974
  this.#value = this.#min;
933
975
  this.setValue(options.value === undefined ? this.#min : options.value, true);
@@ -959,7 +1001,7 @@ class Gauge {
959
1001
  getValueText() {
960
1002
  if (this.#format !== undefined) return String(this.#format(this.#value, this.#max));
961
1003
  const text = formatAxisLabel(this.#value);
962
- return this.#unit === '' ? text : `${text} ${this.#unit}`;
1004
+ return this.#unit === "" ? text : `${text} ${this.#unit}`;
963
1005
  }
964
1006
 
965
1007
  getUnit() {
@@ -1025,14 +1067,14 @@ class Gauge {
1025
1067
  }
1026
1068
 
1027
1069
  setUnit(unit = String()) {
1028
- this.#unit = unit === undefined ? '' : String(unit);
1070
+ this.#unit = unit === undefined ? "" : String(unit);
1029
1071
  return this.#render();
1030
1072
  }
1031
1073
 
1032
1074
  /** Доступное имя индикатора */
1033
1075
  setLabel(text = String()) {
1034
1076
  this.#label = text;
1035
- this.#root.setAttribute('aria-label', text === undefined ? I18n.t('graphics.gauge') : String(text));
1077
+ this.#root.setAttribute("aria-label", text === undefined ? I18n.t("graphics.gauge") : String(text));
1036
1078
  return this;
1037
1079
  }
1038
1080
 
@@ -1081,13 +1123,14 @@ class Gauge {
1081
1123
  static #normalizeThresholds(list = Array(), max = Number()) {
1082
1124
  const source = Array.isArray(list) ? list : [];
1083
1125
  return source.map((item, index) => {
1084
- const entry = item === null || typeof item !== 'object' ? {} : item;
1126
+ const entry = item === null || typeof item !== "object" ? {} : item;
1085
1127
  const to = Number(entry.to);
1086
1128
  return {
1087
1129
  to: Number.isFinite(to) ? to : max,
1088
- color: entry.color === undefined
1089
- ? GAUGE_THRESHOLD_COLORS[index % GAUGE_THRESHOLD_COLORS.length]
1090
- : String(entry.color),
1130
+ color:
1131
+ entry.color === undefined
1132
+ ? GAUGE_THRESHOLD_COLORS[index % GAUGE_THRESHOLD_COLORS.length]
1133
+ : String(entry.color),
1091
1134
  };
1092
1135
  });
1093
1136
  }
@@ -1106,12 +1149,12 @@ class Gauge {
1106
1149
  const ratio = Math.max(0, Math.min(1, this.getRatio()));
1107
1150
  const color = this.#zoneColor();
1108
1151
  // Размер шкалы виден и подписи значения: её кегль считается от него в CSS
1109
- this.#root.style.setProperty('--gauge_size', `${this.#size}px`);
1152
+ this.#root.style.setProperty("--gauge_size", `${this.#size}px`);
1110
1153
  const ctx = prepareCanvas(this.#canvas, this.#size, this.#size);
1111
1154
  const center = this.#size / 2;
1112
1155
  // Запас по краю — под деления и их подписи
1113
1156
  const radius = Math.max(10, center - this.#thickness / 2 - 12);
1114
- ctx.lineCap = 'round';
1157
+ ctx.lineCap = "round";
1115
1158
 
1116
1159
  // Дорожка шкалы: видно, куда значение может дойти
1117
1160
  ctx.strokeStyle = GAUGE_TRACK_COLOR;
@@ -1141,9 +1184,13 @@ class Gauge {
1141
1184
  ctx.lineTo(center + cos * (outer + 5), center + sin * (outer + 5));
1142
1185
  ctx.stroke();
1143
1186
  ctx.fillStyle = AXIS_COLOR;
1144
- ctx.textAlign = 'center';
1145
- ctx.textBaseline = 'middle';
1146
- ctx.fillText(formatAxisLabel(this.#min + ((this.#max - this.#min) * i) / this.#ticks), center + cos * (outer + 18), center + sin * (outer + 18));
1187
+ ctx.textAlign = "center";
1188
+ ctx.textBaseline = "middle";
1189
+ ctx.fillText(
1190
+ formatAxisLabel(this.#min + ((this.#max - this.#min) * i) / this.#ticks),
1191
+ center + cos * (outer + 18),
1192
+ center + sin * (outer + 18)
1193
+ );
1147
1194
  }
1148
1195
 
1149
1196
  // Стрелка с осью в центре шкалы
@@ -1161,11 +1208,11 @@ class Gauge {
1161
1208
  ctx.fill();
1162
1209
 
1163
1210
  this.#value_text.hidden = !this.#show_value;
1164
- this.#value_text.innerText = this.#show_value ? this.getValueText() : '';
1165
- this.#root.setAttribute('aria-valuemin', String(this.#min));
1166
- this.#root.setAttribute('aria-valuemax', String(this.#max));
1167
- this.#root.setAttribute('aria-valuenow', String(this.#value));
1168
- this.#root.setAttribute('aria-valuetext', this.getValueText());
1211
+ this.#value_text.innerText = this.#show_value ? this.getValueText() : "";
1212
+ this.#root.setAttribute("aria-valuemin", String(this.#min));
1213
+ this.#root.setAttribute("aria-valuemax", String(this.#max));
1214
+ this.#root.setAttribute("aria-valuenow", String(this.#value));
1215
+ this.#root.setAttribute("aria-valuetext", this.getValueText());
1169
1216
  return this;
1170
1217
  }
1171
1218
 
@@ -1190,13 +1237,13 @@ class Gauge {
1190
1237
  * ```
1191
1238
  */
1192
1239
  class Sparkline {
1193
- #canvas = document.createElement('canvas');
1194
- #main = document.createElement('chui_sparkline');
1240
+ #canvas = document.createElement("canvas");
1241
+ #main = document.createElement("chui_sparkline");
1195
1242
  #options = undefined;
1196
1243
  #points = [];
1197
1244
 
1198
1245
  constructor(options = {}) {
1199
- setStyles(__dirname + '/styles.css', STYLES_ID);
1246
+ setStyles(__dirname + "/styles.css", STYLES_ID);
1200
1247
  this.#options = options;
1201
1248
  this.#main.appendChild(this.#canvas);
1202
1249
  this.#draw();
@@ -1214,7 +1261,7 @@ class Sparkline {
1214
1261
 
1215
1262
  /** Экранные точки — по ним видно, что именно нарисовано */
1216
1263
  getPoints() {
1217
- return this.#points.map((point) => ({label: point.label, value: point.value, x: point.x, y: point.y}));
1264
+ return this.#points.map((point) => ({ label: point.label, value: point.value, x: point.x, y: point.y }));
1218
1265
  }
1219
1266
 
1220
1267
  set() {
@@ -1235,9 +1282,10 @@ class Sparkline {
1235
1282
  const max = values.length ? Math.max.apply(null, values) : 0;
1236
1283
  const span = max - min;
1237
1284
  const stepX = labels.length > 1 ? (width - 2 * SPARKLINE_INSET) / (labels.length - 1) : 0;
1238
- const toY = (value) => span === 0
1239
- ? height / 2
1240
- : height - SPARKLINE_INSET - ((value - min) / span) * (height - 2 * SPARKLINE_INSET);
1285
+ const toY = (value) =>
1286
+ span === 0
1287
+ ? height / 2
1288
+ : height - SPARKLINE_INSET - ((value - min) / span) * (height - 2 * SPARKLINE_INSET);
1241
1289
  this.#points = labels.map((label, index) => ({
1242
1290
  label: label,
1243
1291
  value: values[index],
@@ -1248,10 +1296,10 @@ class Sparkline {
1248
1296
  // Заливка под линией: градиент только для hex-цвета — как у AreaGraph
1249
1297
  if (this.#options.area === true && this.#points.length > 0) {
1250
1298
  ctx.fillStyle = color;
1251
- if (color.startsWith('#')) {
1299
+ if (color.startsWith("#")) {
1252
1300
  const gradient = ctx.createLinearGradient(0, 0, 0, height);
1253
- gradient.addColorStop(0, color + '66');
1254
- gradient.addColorStop(1, color + '00');
1301
+ gradient.addColorStop(0, color + "66");
1302
+ gradient.addColorStop(1, color + "00");
1255
1303
  ctx.fillStyle = gradient;
1256
1304
  }
1257
1305
  ctx.beginPath();
@@ -1265,7 +1313,7 @@ class Sparkline {
1265
1313
  if (this.#points.length > 1) {
1266
1314
  ctx.strokeStyle = color;
1267
1315
  ctx.lineWidth = 2;
1268
- ctx.lineJoin = 'round';
1316
+ ctx.lineJoin = "round";
1269
1317
  ctx.beginPath();
1270
1318
  ctx.moveTo(this.#points[0].x, this.#points[0].y);
1271
1319
  for (const point of this.#points.slice(1)) ctx.lineTo(point.x, point.y);
@@ -1281,8 +1329,8 @@ class Sparkline {
1281
1329
  ctx.fill();
1282
1330
  }
1283
1331
 
1284
- this.#canvas.setAttribute('role', 'img');
1285
- this.#canvas.setAttribute('aria-label', describeData(I18n.t('graphics.sparkline'), data));
1332
+ this.#canvas.setAttribute("role", "img");
1333
+ this.#canvas.setAttribute("aria-label", describeData(I18n.t("graphics.sparkline"), data));
1286
1334
  }
1287
1335
  }
1288
1336
 
@@ -1307,8 +1355,8 @@ class Sparkline {
1307
1355
  * ```
1308
1356
  */
1309
1357
  class RadarGraph {
1310
- #canvas = document.createElement('canvas');
1311
- #main = document.createElement('chui_radar_graph');
1358
+ #canvas = document.createElement("canvas");
1359
+ #main = document.createElement("chui_radar_graph");
1312
1360
  #axes = [];
1313
1361
  #series = [];
1314
1362
  /** Плоский список точек с экранными координатами — общий для мыши и клавиатуры */
@@ -1317,7 +1365,7 @@ class RadarGraph {
1317
1365
  #tooltip = undefined;
1318
1366
 
1319
1367
  constructor(options = {}) {
1320
- setStyles(__dirname + '/styles.css', STYLES_ID);
1368
+ setStyles(__dirname + "/styles.css", STYLES_ID);
1321
1369
  this.#axes = RadarGraph.#normalizeAxes(options.axes);
1322
1370
  this.#series = RadarGraph.#normalizeSeries(options.series, options.colors, this.#axes.length);
1323
1371
  this.#main.appendChild(this.#canvas);
@@ -1325,14 +1373,21 @@ class RadarGraph {
1325
1373
  this.#draw();
1326
1374
  // Легенда нужна только когда серий больше одной: у одной подпись есть в подсказке
1327
1375
  if (options.legend !== false && this.#series.length > 1) {
1328
- this.#main.appendChild(buildLegend(this.#series.map((serie) => ({title: serie.title, color: serie.color}))));
1376
+ this.#main.appendChild(
1377
+ buildLegend(this.#series.map((serie) => ({ title: serie.title, color: serie.color })))
1378
+ );
1329
1379
  }
1330
- this.#main.appendChild(buildTable([I18n.t('graphics.seriesColumn'), I18n.t('graphics.axisColumn'), I18n.t('graphics.valueColumn')], this.#tableRows()));
1380
+ this.#main.appendChild(
1381
+ buildTable(
1382
+ [I18n.t("graphics.seriesColumn"), I18n.t("graphics.axisColumn"), I18n.t("graphics.valueColumn")],
1383
+ this.#tableRows()
1384
+ )
1385
+ );
1331
1386
  this.#bindPointer();
1332
1387
  }
1333
1388
 
1334
1389
  getAxes() {
1335
- return this.#axes.map((axis) => ({key: axis.key, title: axis.title}));
1390
+ return this.#axes.map((axis) => ({ key: axis.key, title: axis.title }));
1336
1391
  }
1337
1392
 
1338
1393
  /** Серии с их цветами и значениями (копии — наружу отдаём данные, а не состояние) */
@@ -1363,14 +1418,18 @@ class RadarGraph {
1363
1418
  static #normalizeAxes(axes = Array()) {
1364
1419
  const list = Array.isArray(axes) ? axes : [];
1365
1420
  return list.map((item, index) => {
1366
- if (item === null || typeof item !== 'object') {
1367
- const title = item === undefined || item === null ? I18n.t('graphics.axis', {index: index + 1}) : String(item);
1368
- return {key: title, title: title};
1421
+ if (item === null || typeof item !== "object") {
1422
+ const title =
1423
+ item === undefined || item === null ? I18n.t("graphics.axis", { index: index + 1 }) : String(item);
1424
+ return { key: title, title: title };
1369
1425
  }
1370
- const title = item.title === undefined
1371
- ? (item.key === undefined ? I18n.t('graphics.axis', {index: index + 1}) : String(item.key))
1372
- : String(item.title);
1373
- return {key: item.key === undefined ? title : String(item.key), title: title};
1426
+ const title =
1427
+ item.title === undefined
1428
+ ? item.key === undefined
1429
+ ? I18n.t("graphics.axis", { index: index + 1 })
1430
+ : String(item.key)
1431
+ : String(item.title);
1432
+ return { key: item.key === undefined ? title : String(item.key), title: title };
1374
1433
  });
1375
1434
  }
1376
1435
 
@@ -1378,20 +1437,26 @@ class RadarGraph {
1378
1437
  const list = Array.isArray(series) ? series : [];
1379
1438
  const palette = Array.isArray(colors) ? colors : [];
1380
1439
  return list.map((item, index) => {
1381
- const entry = item === null || typeof item !== 'object' ? {} : item;
1440
+ const entry = item === null || typeof item !== "object" ? {} : item;
1382
1441
  const source = Array.isArray(entry.values) ? entry.values : [];
1383
1442
  const values = [];
1384
1443
  for (let i = 0; i < axesCount; i++) values.push(Number(source[i]) || 0);
1385
1444
  return {
1386
- title: entry.title === undefined ? I18n.t('graphics.series', {index: index + 1}) : String(entry.title),
1387
- color: entry.color === undefined ? (palette[index % palette.length] || LINE_FALLBACK_COLOR) : String(entry.color),
1445
+ title:
1446
+ entry.title === undefined ? I18n.t("graphics.series", { index: index + 1 }) : String(entry.title),
1447
+ color:
1448
+ entry.color === undefined
1449
+ ? palette[index % palette.length] || LINE_FALLBACK_COLOR
1450
+ : String(entry.color),
1388
1451
  values: values,
1389
1452
  };
1390
1453
  });
1391
1454
  }
1392
1455
 
1393
1456
  #axisTitle(index) {
1394
- return this.#axes[index] === undefined ? I18n.t('graphics.axis', {index: index + 1}) : this.#axes[index].title;
1457
+ return this.#axes[index] === undefined
1458
+ ? I18n.t("graphics.axis", { index: index + 1 })
1459
+ : this.#axes[index].title;
1395
1460
  }
1396
1461
 
1397
1462
  #tableRows() {
@@ -1413,10 +1478,10 @@ class RadarGraph {
1413
1478
  }
1414
1479
  if (parts.length === SUMMARY_LIMIT) break;
1415
1480
  }
1416
- if (parts.length === 0) return I18n.t('graphics.radarChart');
1481
+ if (parts.length === 0) return I18n.t("graphics.radarChart");
1417
1482
  const total = this.#series.reduce((sum, serie) => sum + serie.values.length, 0);
1418
1483
  const rest = total - parts.length;
1419
- return `${I18n.t('graphics.radarChart')}: ${parts.join(', ')}${rest > 0 ? `, и ещё ${rest}` : ""}`;
1484
+ return `${I18n.t("graphics.radarChart")}: ${parts.join(", ")}${rest > 0 ? `, и ещё ${rest}` : ""}`;
1420
1485
  }
1421
1486
 
1422
1487
  #draw() {
@@ -1462,8 +1527,8 @@ class RadarGraph {
1462
1527
  // Значения уровней — вдоль верхней оси, названия осей — снаружи вершин
1463
1528
  const top = pointAt(0, 1);
1464
1529
  ctx.fillStyle = AXIS_COLOR;
1465
- ctx.textAlign = 'left';
1466
- ctx.textBaseline = 'middle';
1530
+ ctx.textAlign = "left";
1531
+ ctx.textBaseline = "middle";
1467
1532
  for (let level = 1; level <= Y_AXIS_STEPS; level++) {
1468
1533
  ctx.fillText(formatAxisLabel(scale.step * level), top.x + 4, center - radius * (level / Y_AXIS_STEPS));
1469
1534
  }
@@ -1471,8 +1536,8 @@ class RadarGraph {
1471
1536
  const cos = Math.cos(angleOf(index));
1472
1537
  const sin = Math.sin(angleOf(index));
1473
1538
  ctx.fillStyle = AXIS_COLOR;
1474
- ctx.textAlign = cos > 0.2 ? 'left' : cos < -0.2 ? 'right' : 'center';
1475
- ctx.textBaseline = sin > 0.2 ? 'top' : sin < -0.2 ? 'bottom' : 'middle';
1539
+ ctx.textAlign = cos > 0.2 ? "left" : cos < -0.2 ? "right" : "center";
1540
+ ctx.textBaseline = sin > 0.2 ? "top" : sin < -0.2 ? "bottom" : "middle";
1476
1541
  // Подпись оси не выходит за канву: длинное имя слева обрезалось её краем
1477
1542
  // («Анимация» читалась как «ментация») — якорь поджимается по ширине текста
1478
1543
  const title = String(axis.title);
@@ -1480,28 +1545,38 @@ class RadarGraph {
1480
1545
  const anchor = center + cos * (radius + 14);
1481
1546
  const align = ctx.textAlign;
1482
1547
  let labelX;
1483
- if (align === 'left') labelX = Math.min(anchor, cssSize - RADAR_LABEL_INSET - titleWidth);
1484
- else if (align === 'right') labelX = Math.max(anchor, RADAR_LABEL_INSET + titleWidth);
1485
- else labelX = Math.min(Math.max(anchor, RADAR_LABEL_INSET + titleWidth / 2), cssSize - RADAR_LABEL_INSET - titleWidth / 2);
1548
+ if (align === "left") labelX = Math.min(anchor, cssSize - RADAR_LABEL_INSET - titleWidth);
1549
+ else if (align === "right") labelX = Math.max(anchor, RADAR_LABEL_INSET + titleWidth);
1550
+ else
1551
+ labelX = Math.min(
1552
+ Math.max(anchor, RADAR_LABEL_INSET + titleWidth / 2),
1553
+ cssSize - RADAR_LABEL_INSET - titleWidth / 2
1554
+ );
1486
1555
  ctx.fillText(title, labelX, center + sin * (radius + 10));
1487
1556
  });
1488
1557
 
1489
1558
  // Серии: полупрозрачная заливка, контур и точки
1490
1559
  this.#points = [];
1491
1560
  for (const serie of this.#series) {
1492
- const coords = serie.values.map((value, index) => pointAt(index, scale.max > 0 ? Math.max(0, Math.min(1, value / scale.max)) : 0));
1561
+ const coords = serie.values.map((value, index) =>
1562
+ pointAt(index, scale.max > 0 ? Math.max(0, Math.min(1, value / scale.max)) : 0)
1563
+ );
1493
1564
  if (coords.length > 1) {
1494
1565
  ctx.fillStyle = serie.color;
1495
1566
  ctx.globalAlpha = 0.18;
1496
1567
  ctx.beginPath();
1497
- coords.forEach((point, index) => (index === 0 ? ctx.moveTo(point.x, point.y) : ctx.lineTo(point.x, point.y)));
1568
+ coords.forEach((point, index) =>
1569
+ index === 0 ? ctx.moveTo(point.x, point.y) : ctx.lineTo(point.x, point.y)
1570
+ );
1498
1571
  ctx.closePath();
1499
1572
  ctx.fill();
1500
1573
  ctx.globalAlpha = 1;
1501
1574
  ctx.strokeStyle = serie.color;
1502
1575
  ctx.lineWidth = 2.5;
1503
1576
  ctx.beginPath();
1504
- coords.forEach((point, index) => (index === 0 ? ctx.moveTo(point.x, point.y) : ctx.lineTo(point.x, point.y)));
1577
+ coords.forEach((point, index) =>
1578
+ index === 0 ? ctx.moveTo(point.x, point.y) : ctx.lineTo(point.x, point.y)
1579
+ );
1505
1580
  ctx.closePath();
1506
1581
  ctx.stroke();
1507
1582
  }
@@ -1533,15 +1608,15 @@ class RadarGraph {
1533
1608
 
1534
1609
  #bindPointer() {
1535
1610
  this.#canvas.classList.add("graph_interactive");
1536
- this.#canvas.setAttribute('role', 'img');
1537
- this.#canvas.setAttribute('tabindex', '0');
1538
- this.#canvas.setAttribute('aria-label', this.#describe());
1539
- this.#canvas.addEventListener('click', (event) => {
1611
+ this.#canvas.setAttribute("role", "img");
1612
+ this.#canvas.setAttribute("tabindex", "0");
1613
+ this.#canvas.setAttribute("aria-label", this.#describe());
1614
+ this.#canvas.addEventListener("click", (event) => {
1540
1615
  const point = this.#findPoint(event);
1541
1616
  if (point !== null) this.#announce(point.index);
1542
1617
  });
1543
1618
  // Наведение: подсказка у ближайшей точки
1544
- this.#canvas.addEventListener('mousemove', (event) => {
1619
+ this.#canvas.addEventListener("mousemove", (event) => {
1545
1620
  const point = this.#findPoint(event);
1546
1621
  if (point === null) {
1547
1622
  this.#tooltip.hide();
@@ -1549,20 +1624,20 @@ class RadarGraph {
1549
1624
  }
1550
1625
  this.#select(point.index);
1551
1626
  });
1552
- this.#canvas.addEventListener('mouseleave', () => {
1627
+ this.#canvas.addEventListener("mouseleave", () => {
1553
1628
  if (this.#active === undefined) this.#tooltip.hide();
1554
1629
  });
1555
1630
  // Клавиатура: ←/→ водят по точкам, Enter показывает значение (как клик мышью)
1556
- this.#canvas.addEventListener('keydown', (event) => {
1631
+ this.#canvas.addEventListener("keydown", (event) => {
1557
1632
  if (this.#points.length === 0) return;
1558
- if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
1633
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
1559
1634
  event.preventDefault();
1560
- let next = this.#active === undefined ? 0 : this.#active + (event.key === 'ArrowRight' ? 1 : -1);
1635
+ let next = this.#active === undefined ? 0 : this.#active + (event.key === "ArrowRight" ? 1 : -1);
1561
1636
  if (next < 0) next = this.#points.length - 1;
1562
1637
  if (next > this.#points.length - 1) next = 0;
1563
1638
  this.#draw();
1564
1639
  this.#select(next);
1565
- } else if (event.key === 'Enter' || event.key === ' ') {
1640
+ } else if (event.key === "Enter" || event.key === " ") {
1566
1641
  if (this.#active === undefined) return;
1567
1642
  event.preventDefault();
1568
1643
  this.#announce(this.#active);
@@ -1577,7 +1652,7 @@ class RadarGraph {
1577
1652
  let nearest = null;
1578
1653
  for (const point of this.#points) {
1579
1654
  const distance = Math.sqrt(Math.pow(point.x - x, 2) + Math.pow(point.y - y, 2));
1580
- if (nearest === null || distance < nearest.distance) nearest = {point: point, distance: distance};
1655
+ if (nearest === null || distance < nearest.distance) nearest = { point: point, distance: distance };
1581
1656
  }
1582
1657
  return nearest !== null && nearest.distance <= RADAR_HOVER_DISTANCE ? nearest.point : null;
1583
1658
  }
@@ -1591,7 +1666,10 @@ class RadarGraph {
1591
1666
  const point = this.#points[index];
1592
1667
  if (point === undefined) return;
1593
1668
  this.#tooltip.show(this.#pointText(point), point.x, point.y);
1594
- this.#canvas.setAttribute('aria-label', I18n.t("graphics.selected", {summary: this.#describe(), value: this.#pointText(point)}));
1669
+ this.#canvas.setAttribute(
1670
+ "aria-label",
1671
+ I18n.t("graphics.selected", { summary: this.#describe(), value: this.#pointText(point) })
1672
+ );
1595
1673
  this.#draw();
1596
1674
  }
1597
1675
 
@@ -1631,8 +1709,8 @@ class RadarGraph {
1631
1709
  * ```
1632
1710
  */
1633
1711
  class HeatmapGraph {
1634
- #canvas = document.createElement('canvas');
1635
- #main = document.createElement('chui_heatmap_graph');
1712
+ #canvas = document.createElement("canvas");
1713
+ #main = document.createElement("chui_heatmap_graph");
1636
1714
  #options = undefined;
1637
1715
  #rows = [];
1638
1716
  #columns = [];
@@ -1644,17 +1722,22 @@ class HeatmapGraph {
1644
1722
  #tooltip = undefined;
1645
1723
 
1646
1724
  constructor(options = {}) {
1647
- setStyles(__dirname + '/styles.css', STYLES_ID);
1725
+ setStyles(__dirname + "/styles.css", STYLES_ID);
1648
1726
  this.#options = options;
1649
1727
  this.#values = HeatmapGraph.#normalizeValues(options.values);
1650
1728
  const columns = this.#values.length === 0 ? 0 : this.#values[0].length;
1651
- this.#rows = HeatmapGraph.#normalizeLabels(options.rows, this.#values.length, I18n.t('graphics.rowColumn'));
1652
- this.#columns = HeatmapGraph.#normalizeLabels(options.columns, columns, I18n.t('graphics.columnColumn'));
1729
+ this.#rows = HeatmapGraph.#normalizeLabels(options.rows, this.#values.length, I18n.t("graphics.rowColumn"));
1730
+ this.#columns = HeatmapGraph.#normalizeLabels(options.columns, columns, I18n.t("graphics.columnColumn"));
1653
1731
  this.#max = Number.isFinite(Number(options.max)) ? Number(options.max) : this.#maxOfValues();
1654
1732
  this.#main.appendChild(this.#canvas);
1655
1733
  this.#tooltip = createTooltip(this.#main);
1656
1734
  this.#draw();
1657
- this.#main.appendChild(buildTable([I18n.t('graphics.rowColumn'), I18n.t('graphics.columnColumn'), I18n.t('graphics.valueColumn')], this.#tableRows()));
1735
+ this.#main.appendChild(
1736
+ buildTable(
1737
+ [I18n.t("graphics.rowColumn"), I18n.t("graphics.columnColumn"), I18n.t("graphics.valueColumn")],
1738
+ this.#tableRows()
1739
+ )
1740
+ );
1658
1741
  this.#bindPointer();
1659
1742
  }
1660
1743
 
@@ -1670,7 +1753,7 @@ class HeatmapGraph {
1670
1753
  /** Выбранная клетка; `undefined` — ничего не выбрано */
1671
1754
  getActive() {
1672
1755
  const cell = this.#cells[this.#active];
1673
- return cell === undefined ? undefined : {row: cell.row, column: cell.column, value: cell.value};
1756
+ return cell === undefined ? undefined : { row: cell.row, column: cell.column, value: cell.value };
1674
1757
  }
1675
1758
 
1676
1759
  /** Фокус на канвас: с него работают стрелки и Enter */
@@ -1693,7 +1776,9 @@ class HeatmapGraph {
1693
1776
  for (let index = 0; index < width; index++) {
1694
1777
  const value = Number(cells[index]);
1695
1778
  // Пропуск и нечисло — пустая клетка: карта честно показывает «данных нет»
1696
- result.push(cells[index] === undefined || cells[index] === null || Number.isNaN(value) ? undefined : value);
1779
+ result.push(
1780
+ cells[index] === undefined || cells[index] === null || Number.isNaN(value) ? undefined : value
1781
+ );
1697
1782
  }
1698
1783
  return result;
1699
1784
  });
@@ -1742,7 +1827,7 @@ class HeatmapGraph {
1742
1827
  #tableRows() {
1743
1828
  const rows = [];
1744
1829
  for (const cell of this.#renderableCells()) {
1745
- rows.push([this.#rows[cell.row], this.#columns[cell.column], cell.value === undefined ? '' : cell.value]);
1830
+ rows.push([this.#rows[cell.row], this.#columns[cell.column], cell.value === undefined ? "" : cell.value]);
1746
1831
  }
1747
1832
  return rows;
1748
1833
  }
@@ -1751,21 +1836,27 @@ class HeatmapGraph {
1751
1836
  const cells = [];
1752
1837
  for (let row = 0; row < this.#values.length; row++) {
1753
1838
  for (let column = 0; column < this.#columns.length; column++) {
1754
- cells.push({row: row, column: column, value: this.#values[row][column]});
1839
+ cells.push({ row: row, column: column, value: this.#values[row][column] });
1755
1840
  }
1756
1841
  }
1757
1842
  return cells;
1758
1843
  }
1759
1844
 
1760
1845
  #cellText(cell) {
1761
- const value = cell.value === undefined ? I18n.t('graphics.noData') : String(cell.value);
1846
+ const value = cell.value === undefined ? I18n.t("graphics.noData") : String(cell.value);
1762
1847
  return `${this.#rows[cell.row]} · ${this.#columns[cell.column]}: ${value}`;
1763
1848
  }
1764
1849
 
1765
1850
  #describe() {
1766
- const base = I18n.t("graphics.heatmapSummary", {rows: this.#rows.length, columns: this.#columns.length, max: formatAxisLabel(this.#max)});
1851
+ const base = I18n.t("graphics.heatmapSummary", {
1852
+ rows: this.#rows.length,
1853
+ columns: this.#columns.length,
1854
+ max: formatAxisLabel(this.#max),
1855
+ });
1767
1856
  const cell = this.#cells[this.#active];
1768
- return cell === undefined ? base : I18n.t("graphics.selectedCell", {summary: base, value: this.#cellText(cell)});
1857
+ return cell === undefined
1858
+ ? base
1859
+ : I18n.t("graphics.selectedCell", { summary: base, value: this.#cellText(cell) });
1769
1860
  }
1770
1861
 
1771
1862
  #draw() {
@@ -1775,14 +1866,18 @@ class HeatmapGraph {
1775
1866
  // Слева место под подписи строк, сверху — под подписи столбцов
1776
1867
  const left = columns === 0 ? HEATMAP_PADDING : HEATMAP_LABEL_WIDTH;
1777
1868
  const top = HEATMAP_LABEL_HEIGHT;
1778
- const ctx = prepareCanvas(this.#canvas, left + columns * step + HEATMAP_PADDING, top + rows * step + HEATMAP_PADDING);
1869
+ const ctx = prepareCanvas(
1870
+ this.#canvas,
1871
+ left + columns * step + HEATMAP_PADDING,
1872
+ top + rows * step + HEATMAP_PADDING
1873
+ );
1779
1874
  ctx.font = LABEL_FONT;
1780
1875
  this.#cells = [];
1781
1876
 
1782
1877
  for (let row = 0; row < rows; row++) {
1783
1878
  ctx.fillStyle = AXIS_COLOR;
1784
- ctx.textAlign = 'right';
1785
- ctx.textBaseline = 'middle';
1879
+ ctx.textAlign = "right";
1880
+ ctx.textBaseline = "middle";
1786
1881
  ctx.fillText(this.#rows[row], left - HEATMAP_GAP - 4, top + row * step + HEATMAP_CELL / 2);
1787
1882
  for (let column = 0; column < columns; column++) {
1788
1883
  const value = this.#values[row][column];
@@ -1791,7 +1886,7 @@ class HeatmapGraph {
1791
1886
  ctx.fillStyle = this.#cellColor(value);
1792
1887
  ctx.fillRect(x, y, HEATMAP_CELL, HEATMAP_CELL);
1793
1888
  const index = this.#cells.length;
1794
- this.#cells.push({index: index, x: x, y: y, row: row, column: column, value: value});
1889
+ this.#cells.push({ index: index, x: x, y: y, row: row, column: column, value: value });
1795
1890
  if (index === this.#active) {
1796
1891
  ctx.strokeStyle = AXIS_COLOR;
1797
1892
  ctx.lineWidth = 2;
@@ -1802,25 +1897,25 @@ class HeatmapGraph {
1802
1897
 
1803
1898
  // Подписи столбцов — над сеткой
1804
1899
  ctx.fillStyle = AXIS_COLOR;
1805
- ctx.textAlign = 'center';
1806
- ctx.textBaseline = 'bottom';
1900
+ ctx.textAlign = "center";
1901
+ ctx.textBaseline = "bottom";
1807
1902
  for (let column = 0; column < columns; column++) {
1808
1903
  ctx.fillText(this.#columns[column], left + column * step + HEATMAP_CELL / 2, top - HEATMAP_GAP);
1809
1904
  }
1810
1905
 
1811
- this.#canvas.setAttribute('role', 'img');
1812
- this.#canvas.setAttribute('tabindex', '0');
1813
- this.#canvas.setAttribute('aria-label', this.#describe());
1906
+ this.#canvas.setAttribute("role", "img");
1907
+ this.#canvas.setAttribute("tabindex", "0");
1908
+ this.#canvas.setAttribute("aria-label", this.#describe());
1814
1909
  }
1815
1910
 
1816
1911
  #bindPointer() {
1817
1912
  this.#canvas.classList.add("graph_interactive");
1818
- this.#canvas.addEventListener('click', (event) => {
1913
+ this.#canvas.addEventListener("click", (event) => {
1819
1914
  const cell = this.#findCell(event);
1820
1915
  if (cell !== null) this.#announce(cell.index);
1821
1916
  });
1822
1917
  // Наведение: подсказка над клеткой
1823
- this.#canvas.addEventListener('mousemove', (event) => {
1918
+ this.#canvas.addEventListener("mousemove", (event) => {
1824
1919
  const cell = this.#findCell(event);
1825
1920
  if (cell === null) {
1826
1921
  this.#tooltip.hide();
@@ -1828,21 +1923,27 @@ class HeatmapGraph {
1828
1923
  }
1829
1924
  this.#select(cell.index);
1830
1925
  });
1831
- this.#canvas.addEventListener('mouseleave', () => {
1926
+ this.#canvas.addEventListener("mouseleave", () => {
1832
1927
  if (this.#active === undefined) this.#tooltip.hide();
1833
1928
  });
1834
1929
  // Клавиатура: стрелки водят по клеткам и на краю останавливаются, Enter показывает значение
1835
- this.#canvas.addEventListener('keydown', (event) => {
1930
+ this.#canvas.addEventListener("keydown", (event) => {
1836
1931
  if (this.#cells.length === 0) return;
1837
- const moves = {ArrowRight: [0, 1], ArrowLeft: [0, -1], ArrowDown: [1, 0], ArrowUp: [-1, 0]};
1932
+ const moves = { ArrowRight: [0, 1], ArrowLeft: [0, -1], ArrowDown: [1, 0], ArrowUp: [-1, 0] };
1838
1933
  const move = moves[event.key];
1839
1934
  if (move !== undefined) {
1840
1935
  event.preventDefault();
1841
1936
  const current = this.#cells[this.#active];
1842
- const row = Math.max(0, Math.min(this.#rows.length - 1, (current === undefined ? 0 : current.row) + move[0]));
1843
- const column = Math.max(0, Math.min(this.#columns.length - 1, (current === undefined ? 0 : current.column) + move[1]));
1937
+ const row = Math.max(
1938
+ 0,
1939
+ Math.min(this.#rows.length - 1, (current === undefined ? 0 : current.row) + move[0])
1940
+ );
1941
+ const column = Math.max(
1942
+ 0,
1943
+ Math.min(this.#columns.length - 1, (current === undefined ? 0 : current.column) + move[1])
1944
+ );
1844
1945
  this.#select(row * this.#columns.length + column);
1845
- } else if (event.key === 'Enter' || event.key === ' ') {
1946
+ } else if (event.key === "Enter" || event.key === " ") {
1846
1947
  if (this.#active === undefined) return;
1847
1948
  event.preventDefault();
1848
1949
  this.#announce(this.#active);
@@ -1873,7 +1974,7 @@ class HeatmapGraph {
1873
1974
  if (cell === undefined) return;
1874
1975
  new Notification({
1875
1976
  title: `${this.#rows[cell.row]} · ${this.#columns[cell.column]}`,
1876
- text: cell.value === undefined ? I18n.t('graphics.noData') : String(cell.value),
1977
+ text: cell.value === undefined ? I18n.t("graphics.noData") : String(cell.value),
1877
1978
  showTime: 3000,
1878
1979
  }).show();
1879
1980
  }