chuijs 4.0.2 → 4.0.4

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 (292) hide show
  1. package/CHANGELOG.md +597 -0
  2. package/README.md +43 -644
  3. package/framework/appLayout/default/chui_app_layout.js +602 -496
  4. package/framework/appLayout/default/global_style.css +288 -97
  5. package/framework/appLayout/default/main_theme_style.css +188 -101
  6. package/framework/components/chui_accordion/accordion.js +38 -35
  7. package/framework/components/chui_accordion/styles.css +13 -12
  8. package/framework/components/chui_action_sheet/action_sheet.js +93 -64
  9. package/framework/components/chui_action_sheet/styles.css +12 -5
  10. package/framework/components/chui_actions_bar/actions_bar.js +338 -0
  11. package/framework/components/chui_actions_bar/styles.css +139 -0
  12. package/framework/components/chui_affix/affix.js +16 -11
  13. package/framework/components/chui_affix/styles.css +5 -0
  14. package/framework/components/chui_alert/alert.js +34 -19
  15. package/framework/components/chui_alert/styles.css +7 -0
  16. package/framework/components/chui_avatar/avatar.js +36 -35
  17. package/framework/components/chui_avatar/styles.css +1 -1
  18. package/framework/components/chui_avatar_group/avatar_group.js +25 -23
  19. package/framework/components/chui_avatar_group/styles.css +1 -1
  20. package/framework/components/chui_backtop/backtop.js +60 -21
  21. package/framework/components/chui_backtop/styles.css +21 -0
  22. package/framework/components/chui_badge/badge.js +30 -28
  23. package/framework/components/chui_badge/styles.css +6 -6
  24. package/framework/components/chui_banner/banner.js +222 -0
  25. package/framework/components/chui_banner/styles.css +170 -0
  26. package/framework/components/chui_breadcrumb/breadcrumb.js +15 -13
  27. package/framework/components/chui_breadcrumb/styles.css +1 -1
  28. package/framework/components/chui_button/button.js +52 -36
  29. package/framework/components/chui_button/styles.css +7 -7
  30. package/framework/components/chui_calendar/calendar.js +18 -16
  31. package/framework/components/chui_card/card.js +52 -32
  32. package/framework/components/chui_card_scroll/card_scroll.js +169 -0
  33. package/framework/components/chui_card_scroll/styles.css +86 -0
  34. package/framework/components/chui_chat/chat.js +140 -79
  35. package/framework/components/chui_chat/styles.css +18 -7
  36. package/framework/components/chui_clipboard_history/clipboard_history.js +53 -39
  37. package/framework/components/chui_clipboard_history/styles.css +13 -9
  38. package/framework/components/chui_code/code.js +10 -10
  39. package/framework/components/chui_code/styles.css +4 -4
  40. package/framework/components/chui_code_editor/code_editor.js +73 -57
  41. package/framework/components/chui_code_editor/styles.css +6 -5
  42. package/framework/components/chui_command_palette/command_palette.js +87 -64
  43. package/framework/components/chui_command_palette/styles.css +5 -5
  44. package/framework/components/chui_console/console.js +13 -13
  45. package/framework/components/chui_console/styles.css +6 -3
  46. package/framework/components/chui_content_block/content_block.js +35 -21
  47. package/framework/components/chui_content_block/styles.css +2 -2
  48. package/framework/components/chui_context_menu/context_menu.js +171 -58
  49. package/framework/components/chui_context_menu/styles.css +82 -8
  50. package/framework/components/chui_custom_element/custom_element.js +25 -18
  51. package/framework/components/chui_data_grid/data_grid.js +633 -471
  52. package/framework/components/chui_data_grid/styles.css +24 -22
  53. package/framework/components/chui_descriptions/descriptions.js +34 -17
  54. package/framework/components/chui_descriptions/styles.css +1 -1
  55. package/framework/components/chui_details/details.js +42 -36
  56. package/framework/components/chui_details/styles.css +12 -11
  57. package/framework/components/chui_diff_viewer/diff_viewer.js +172 -147
  58. package/framework/components/chui_diff_viewer/styles.css +10 -10
  59. package/framework/components/chui_divider/divider.js +19 -17
  60. package/framework/components/chui_divider/styles.css +2 -2
  61. package/framework/components/chui_drawer/drawer.js +63 -38
  62. package/framework/components/chui_drawer/styles.css +18 -4
  63. package/framework/components/chui_drop_zone/drop_zone.js +37 -27
  64. package/framework/components/chui_drop_zone/styles.css +10 -6
  65. package/framework/components/chui_empty_state/empty_state.js +20 -15
  66. package/framework/components/chui_fieldset/fieldset.js +40 -23
  67. package/framework/components/chui_fieldset/styles.css +6 -6
  68. package/framework/components/chui_file_explorer/file_explorer.js +260 -189
  69. package/framework/components/chui_file_explorer/styles.css +8 -7
  70. package/framework/components/chui_form/form.js +40 -28
  71. package/framework/components/chui_form/styles.css +1 -1
  72. package/framework/components/chui_gantt/gantt.js +102 -78
  73. package/framework/components/chui_gantt/styles.css +5 -5
  74. package/framework/components/chui_graphs/graphs.js +376 -239
  75. package/framework/components/chui_graphs/styles.css +27 -6
  76. package/framework/components/chui_hero/hero.js +98 -37
  77. package/framework/components/chui_hero/styles.css +176 -7
  78. package/framework/components/chui_html_block/html_block.js +8 -8
  79. package/framework/components/chui_html_block/styles.css +3 -3
  80. package/framework/components/chui_hx/hx.js +3 -3
  81. package/framework/components/chui_hx/styles.css +13 -6
  82. package/framework/components/chui_icons/icons.js +2115 -2115
  83. package/framework/components/chui_icons/styles.css +4 -3
  84. package/framework/components/chui_inputs/chui_check_box/check_box.js +31 -19
  85. package/framework/components/chui_inputs/chui_check_box/styles.css +30 -21
  86. package/framework/components/chui_inputs/chui_chips/chips.js +141 -70
  87. package/framework/components/chui_inputs/chui_chips/styles.css +32 -7
  88. package/framework/components/chui_inputs/chui_color_picker/color_picker.js +55 -38
  89. package/framework/components/chui_inputs/chui_color_picker/styles.css +7 -7
  90. package/framework/components/chui_inputs/chui_combo_box/combo_box.js +127 -109
  91. package/framework/components/chui_inputs/chui_combo_box/styles.css +23 -21
  92. package/framework/components/chui_inputs/chui_date/date.js +161 -127
  93. package/framework/components/chui_inputs/chui_date/styles.css +20 -19
  94. package/framework/components/chui_inputs/chui_date_range/date_range.js +61 -50
  95. package/framework/components/chui_inputs/chui_date_range/styles.css +3 -3
  96. package/framework/components/chui_inputs/chui_datetime/datetime.js +47 -39
  97. package/framework/components/chui_inputs/chui_datetime/styles.css +3 -3
  98. package/framework/components/chui_inputs/chui_email/email.js +48 -36
  99. package/framework/components/chui_inputs/chui_email/styles.css +11 -11
  100. package/framework/components/chui_inputs/chui_file/file.js +71 -44
  101. package/framework/components/chui_inputs/chui_file/styles.css +23 -19
  102. package/framework/components/chui_inputs/chui_hotkey/hotkey.js +101 -76
  103. package/framework/components/chui_inputs/chui_hotkey/styles.css +6 -6
  104. package/framework/components/chui_inputs/chui_masked/masked.js +83 -72
  105. package/framework/components/chui_inputs/chui_masked/styles.css +5 -5
  106. package/framework/components/chui_inputs/chui_mention/mention.js +87 -40
  107. package/framework/components/chui_inputs/chui_mention/styles.css +14 -8
  108. package/framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box.js +242 -212
  109. package/framework/components/chui_inputs/chui_multi_combo_box/styles.css +23 -15
  110. package/framework/components/chui_inputs/chui_number/number.js +84 -70
  111. package/framework/components/chui_inputs/chui_number/styles.css +13 -13
  112. package/framework/components/chui_inputs/chui_otp/otp.js +71 -62
  113. package/framework/components/chui_inputs/chui_otp/styles.css +5 -5
  114. package/framework/components/chui_inputs/chui_password/password.js +72 -56
  115. package/framework/components/chui_inputs/chui_password/styles.css +15 -15
  116. package/framework/components/chui_inputs/chui_radio_button/radio_button.js +37 -25
  117. package/framework/components/chui_inputs/chui_radio_button/styles.css +22 -22
  118. package/framework/components/chui_inputs/chui_rating/rating.js +54 -50
  119. package/framework/components/chui_inputs/chui_rating/styles.css +3 -3
  120. package/framework/components/chui_inputs/chui_select_box/select_box.js +132 -108
  121. package/framework/components/chui_inputs/chui_select_box/styles.css +21 -21
  122. package/framework/components/chui_inputs/chui_slider/slider.js +41 -38
  123. package/framework/components/chui_inputs/chui_slider/styles.css +7 -5
  124. package/framework/components/chui_inputs/chui_text/styles.css +14 -14
  125. package/framework/components/chui_inputs/chui_text/text.js +69 -52
  126. package/framework/components/chui_inputs/chui_text_area/styles.css +14 -14
  127. package/framework/components/chui_inputs/chui_text_area/text_area.js +59 -36
  128. package/framework/components/chui_inputs/chui_time/styles.css +18 -20
  129. package/framework/components/chui_inputs/chui_time/time.js +71 -49
  130. package/framework/components/chui_inputs/chui_toggle/styles.css +13 -6
  131. package/framework/components/chui_inputs/chui_toggle/toggle.js +27 -27
  132. package/framework/components/chui_json_editor/json_editor.js +137 -127
  133. package/framework/components/chui_json_editor/styles.css +5 -5
  134. package/framework/components/chui_json_viewer/json_viewer.js +133 -114
  135. package/framework/components/chui_json_viewer/styles.css +9 -8
  136. package/framework/components/chui_kanban/kanban.js +78 -60
  137. package/framework/components/chui_kanban/styles.css +8 -4
  138. package/framework/components/chui_kbd/kbd.js +12 -10
  139. package/framework/components/chui_kbd/styles.css +1 -1
  140. package/framework/components/chui_label/label.js +31 -29
  141. package/framework/components/chui_label/styles.css +3 -3
  142. package/framework/components/chui_link/link.js +36 -29
  143. package/framework/components/chui_link/styles.css +11 -4
  144. package/framework/components/chui_list/list.js +42 -38
  145. package/framework/components/chui_list/styles.css +4 -3
  146. package/framework/components/chui_markdown_editor/markdown_editor.js +125 -109
  147. package/framework/components/chui_markdown_editor/styles.css +5 -5
  148. package/framework/components/chui_media/audio.js +457 -364
  149. package/framework/components/chui_media/audio_styles.css +74 -37
  150. package/framework/components/chui_media/image.js +99 -64
  151. package/framework/components/chui_media/image_compare.js +86 -55
  152. package/framework/components/chui_media/image_compare_styles.css +5 -5
  153. package/framework/components/chui_media/image_styles.css +4 -4
  154. package/framework/components/chui_media/video.js +264 -171
  155. package/framework/components/chui_media/video_styles.css +57 -24
  156. package/framework/components/chui_menu_bar/menu_bar.js +67 -65
  157. package/framework/components/chui_menu_bar/styles.css +14 -12
  158. package/framework/components/chui_message_bar/message_bar.js +551 -0
  159. package/framework/components/chui_message_bar/styles.css +248 -0
  160. package/framework/components/chui_modal/modal.js +81 -56
  161. package/framework/components/chui_modal/styles.css +3 -2
  162. package/framework/components/chui_notification/notification.js +107 -69
  163. package/framework/components/chui_notification/notification_download.js +28 -77
  164. package/framework/components/chui_notification/notification_download_card.js +115 -0
  165. package/framework/components/chui_notification/notification_download_progress.css +5 -4
  166. package/framework/components/chui_notification/notification_download_progress.js +73 -105
  167. package/framework/components/chui_notification/notification_update.js +44 -37
  168. package/framework/components/chui_notification/status_plate.css +3 -3
  169. package/framework/components/chui_notification/status_plate.js +2 -2
  170. package/framework/components/chui_notification/styles.css +6 -1
  171. package/framework/components/chui_notification/styles_download.css +2 -2
  172. package/framework/components/chui_notification/styles_update.css +3 -3
  173. package/framework/components/chui_org_chart/org_chart.js +101 -62
  174. package/framework/components/chui_org_chart/styles.css +2 -2
  175. package/framework/components/chui_pagination/pagination.js +65 -30
  176. package/framework/components/chui_pagination/styles.css +1 -1
  177. package/framework/components/chui_paragraph/paragraph.js +2 -2
  178. package/framework/components/chui_paragraph/styles.css +3 -3
  179. package/framework/components/chui_popover/popover.js +77 -50
  180. package/framework/components/chui_popover/styles.css +1 -1
  181. package/framework/components/chui_popups/popups.js +210 -181
  182. package/framework/components/chui_popups/styles.css +7 -7
  183. package/framework/components/chui_print_button/print_button.js +48 -33
  184. package/framework/components/chui_progress_bar/progress_bar.js +49 -19
  185. package/framework/components/chui_progress_bar/styles.css +6 -6
  186. package/framework/components/chui_progress_ring/progress_ring.js +32 -30
  187. package/framework/components/chui_progress_ring/styles.css +1 -1
  188. package/framework/components/chui_qrcode/qrcode.js +85 -41
  189. package/framework/components/chui_query_builder/query_builder.js +263 -170
  190. package/framework/components/chui_query_builder/styles.css +4 -4
  191. package/framework/components/chui_radio_group/radio_group.js +36 -26
  192. package/framework/components/chui_radio_group/styles.css +2 -2
  193. package/framework/components/chui_result/result.js +41 -31
  194. package/framework/components/chui_result/styles.css +3 -3
  195. package/framework/components/chui_scheduler/scheduler.js +126 -104
  196. package/framework/components/chui_scheduler/styles.css +11 -7
  197. package/framework/components/chui_screen_spinner/screen_spinner.js +167 -0
  198. package/framework/components/chui_screen_spinner/styles.css +49 -0
  199. package/framework/components/chui_segmented_control/segmented_control.js +90 -27
  200. package/framework/components/chui_segmented_control/styles.css +32 -6
  201. package/framework/components/chui_settings/settings.js +160 -87
  202. package/framework/components/chui_settings/styles.css +4 -4
  203. package/framework/components/chui_shortcut_help/shortcut_help.js +82 -61
  204. package/framework/components/chui_sidebar_nav/sidebar_nav.js +132 -53
  205. package/framework/components/chui_sidebar_nav/styles.css +9 -3
  206. package/framework/components/chui_skeleton/skeleton.js +29 -24
  207. package/framework/components/chui_slideshow/slideshow.js +122 -103
  208. package/framework/components/chui_slideshow/styles.css +22 -16
  209. package/framework/components/chui_spinner/spinner.js +17 -17
  210. package/framework/components/chui_spinner/styles.css +0 -1
  211. package/framework/components/chui_split_button/split_button.js +53 -30
  212. package/framework/components/chui_split_button/styles.css +22 -6
  213. package/framework/components/chui_splitter/splitter.js +46 -39
  214. package/framework/components/chui_splitter/styles.css +2 -2
  215. package/framework/components/chui_statistic/statistic.js +111 -84
  216. package/framework/components/chui_status_bar/status_bar.js +11 -9
  217. package/framework/components/chui_status_bar/styles.css +2 -2
  218. package/framework/components/chui_stepper/stepper.js +230 -54
  219. package/framework/components/chui_stepper/styles.css +79 -14
  220. package/framework/components/chui_sticky_bar/sticky_bar.js +53 -17
  221. package/framework/components/chui_sticky_bar/styles.css +11 -7
  222. package/framework/components/chui_tabs/chui_tabs.js +100 -46
  223. package/framework/components/chui_tabs/styles.css +43 -15
  224. package/framework/components/chui_text_editor/styles.css +24 -23
  225. package/framework/components/chui_text_editor/text_editor.js +103 -79
  226. package/framework/components/chui_text_editor/text_editor_panel.js +428 -367
  227. package/framework/components/chui_theme_toggle/theme_toggle.js +46 -25
  228. package/framework/components/chui_timeline/styles.css +3 -1
  229. package/framework/components/chui_timeline/timeline.js +14 -12
  230. package/framework/components/chui_toast_queue/styles.css +11 -11
  231. package/framework/components/chui_toast_queue/toast_queue.js +82 -73
  232. package/framework/components/chui_toolbar/styles.css +2 -2
  233. package/framework/components/chui_toolbar/toolbar.js +20 -13
  234. package/framework/components/chui_tooltip/styles.css +2 -2
  235. package/framework/components/chui_tooltip/tooltip.js +40 -27
  236. package/framework/components/chui_tour/styles.css +12 -7
  237. package/framework/components/chui_tour/tour.js +132 -98
  238. package/framework/components/chui_transfer/styles.css +8 -7
  239. package/framework/components/chui_transfer/transfer.js +143 -123
  240. package/framework/components/chui_tree_table/styles.css +4 -4
  241. package/framework/components/chui_tree_table/tree_table.js +88 -74
  242. package/framework/components/chui_tree_view/styles.css +14 -10
  243. package/framework/components/chui_tree_view/tree_view.js +298 -253
  244. package/framework/components/chui_user_bar/styles.css +145 -0
  245. package/framework/components/chui_user_bar/user_bar.js +402 -0
  246. package/framework/components/chui_user_list_bar/styles.css +173 -0
  247. package/framework/components/chui_user_list_bar/user_list_bar.js +485 -0
  248. package/framework/components/chui_watermark/watermark.js +41 -33
  249. package/framework/components/chui_webview/styles.css +2 -2
  250. package/framework/components/chui_webview/webview.js +64 -52
  251. package/framework/components/chui_write_bar/styles.css +172 -0
  252. package/framework/components/chui_write_bar/write_bar.js +320 -0
  253. package/framework/main_window.js +1234 -0
  254. package/framework/modules/chui_a11y/chui_a11y.js +153 -123
  255. package/framework/modules/chui_animations/animations.js +105 -43
  256. package/framework/modules/chui_animations/motion.js +120 -0
  257. package/framework/modules/chui_animations/styles.css +37 -4
  258. package/framework/modules/chui_bridge/preload.js +36 -25
  259. package/framework/modules/chui_functions.js +69 -63
  260. package/framework/modules/chui_highlight/chui_highlight.js +25 -22
  261. package/framework/modules/chui_highlight/styles.css +13 -10
  262. package/framework/modules/chui_i18n/chui_i18n.js +12 -10
  263. package/framework/modules/chui_i18n/locales/a11y.js +5 -5
  264. package/framework/modules/chui_i18n/locales/app_layout.js +5 -5
  265. package/framework/modules/chui_i18n/locales/calendar.js +3 -3
  266. package/framework/modules/chui_i18n/locales/common.js +3 -3
  267. package/framework/modules/chui_i18n/locales/data.js +36 -26
  268. package/framework/modules/chui_i18n/locales/editors.js +40 -13
  269. package/framework/modules/chui_i18n/locales/feedback.js +65 -3
  270. package/framework/modules/chui_i18n/locales/forms.js +8 -8
  271. package/framework/modules/chui_i18n/locales/graphics.js +8 -8
  272. package/framework/modules/chui_i18n/locales/media.js +4 -4
  273. package/framework/modules/chui_i18n/locales/navigation.js +12 -8
  274. package/framework/modules/chui_i18n/locales/validate.js +3 -3
  275. package/framework/modules/chui_json/chui_json.js +11 -11
  276. package/framework/modules/chui_layout/chui_layout.js +12 -10
  277. package/framework/modules/chui_logger/chui_logger.js +47 -28
  278. package/framework/modules/chui_page/page.js +39 -33
  279. package/framework/modules/chui_palette/palette.js +61 -0
  280. package/framework/modules/chui_qr/chui_qr.js +179 -88
  281. package/framework/modules/chui_scrollbar/chui_scrollbar.js +79 -68
  282. package/framework/modules/chui_scrollbar/styles.css +4 -2
  283. package/framework/modules/chui_services/chui_channels.js +27 -13
  284. package/framework/modules/chui_services/chui_services.js +47 -39
  285. package/framework/modules/chui_services/desktop.js +108 -101
  286. package/framework/modules/chui_sortable/chui_sortable.js +33 -28
  287. package/framework/modules/chui_validate/chui_validate.js +10 -7
  288. package/framework/tray_bar/chui_menu_items.js +136 -46
  289. package/index.d.ts +5046 -0
  290. package/index.js +199 -1254
  291. package/package.json +81 -68
  292. package/framework/components/telegram_bot/chui_telegram_bot.js +0 -184
@@ -1,19 +1,21 @@
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
-
5
- const STYLES_ID = 'chUiJS_Canvas';
6
- const AXIS_COLOR = '#8e8e93';
7
- const LABEL_FONT = 'bold 14px Arial';
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
+ const { animate } = require("../../modules/chui_animations/motion");
5
+ const { paletteColor } = require("../../modules/chui_palette/palette");
6
+
7
+ const STYLES_ID = "chUiJS_Canvas";
8
+ const AXIS_COLOR = paletteColor("gray");
9
+ const LABEL_FONT = "bold 14px Arial";
8
10
  const Y_AXIS_STEPS = 4;
9
11
  const PLOT_HEIGHT = 240;
10
12
  const CATEGORY_WIDTH = 150;
11
13
  const BAR_WIDTH = 75;
12
- const BAR_FALLBACK_COLOR = '#1676f3';
14
+ const BAR_FALLBACK_COLOR = paletteColor("blue");
13
15
  const PADDING = { top: 20, right: 20, bottom: 50, left: 50 };
14
16
  const PIE_SIZE = 400;
15
- const GRID_COLOR = "rgba(142, 142, 147, 0.25)";
16
- const LINE_FALLBACK_COLOR = "#1676f3";
17
+ const GRID_COLOR = paletteColor("gray", { alpha: 0.25 });
18
+ const LINE_FALLBACK_COLOR = paletteColor("blue");
17
19
  const LINE_POINT_RADIUS = 4;
18
20
  const POINT_HOVER_DISTANCE = 30;
19
21
  const SCATTER_PLOT_WIDTH = 460;
@@ -24,9 +26,9 @@ const GAUGE_THICKNESS = 20;
24
26
  const GAUGE_START_ANGLE = 135;
25
27
  const GAUGE_SWEEP = 270;
26
28
  const GAUGE_TICKS = 6;
27
- const GAUGE_FALLBACK_COLOR = "#1676f3";
28
- const GAUGE_TRACK_COLOR = "rgba(142, 142, 147, 0.25)";
29
- const GAUGE_THRESHOLD_COLORS = ["#34c759", "#ff9f0a", "#ff3b30"];
29
+ const GAUGE_FALLBACK_COLOR = paletteColor("blue");
30
+ const GAUGE_TRACK_COLOR = paletteColor("gray", { alpha: 0.25 });
31
+ const GAUGE_THRESHOLD_COLORS = [paletteColor("green"), paletteColor("orange"), paletteColor("red")];
30
32
  const SPARKLINE_WIDTH = 160;
31
33
  const SPARKLINE_HEIGHT = 40;
32
34
  const SPARKLINE_INSET = 3;
@@ -42,7 +44,7 @@ const HEATMAP_GAP = 4;
42
44
  const HEATMAP_LABEL_WIDTH = 70;
43
45
  const HEATMAP_LABEL_HEIGHT = 24;
44
46
  const HEATMAP_PADDING = 10;
45
- const HEATMAP_EMPTY_COLOR = "rgba(142, 142, 147, 0.18)";
47
+ const HEATMAP_EMPTY_COLOR = paletteColor("gray", { alpha: 0.18 });
46
48
  // Самая бледная клетка с данными: пустая заливка остаётся светлее
47
49
  const HEATMAP_ALPHA_MIN = 0.2;
48
50
 
@@ -82,9 +84,9 @@ function prepareCanvas(canvas, cssWidth, cssHeight) {
82
84
  const ratio = window.devicePixelRatio || 1;
83
85
  canvas.width = cssWidth * ratio;
84
86
  canvas.height = cssHeight * ratio;
85
- canvas.style.width = cssWidth + 'px';
86
- canvas.style.height = cssHeight + 'px';
87
- const ctx = canvas.getContext('2d');
87
+ canvas.style.width = cssWidth + "px";
88
+ canvas.style.height = cssHeight + "px";
89
+ const ctx = canvas.getContext("2d");
88
90
  ctx.scale(ratio, ratio);
89
91
  return ctx;
90
92
  }
@@ -104,8 +106,8 @@ function drawAxesGrid(ctx, plotWidth, scale) {
104
106
  ctx.stroke();
105
107
 
106
108
  ctx.font = LABEL_FONT;
107
- ctx.textAlign = 'right';
108
- ctx.textBaseline = 'middle';
109
+ ctx.textAlign = "right";
110
+ ctx.textBaseline = "middle";
109
111
  for (let i = 0; i <= Y_AXIS_STEPS; i++) {
110
112
  const y = bottomY - (i / Y_AXIS_STEPS) * PLOT_HEIGHT;
111
113
  ctx.strokeStyle = AXIS_COLOR;
@@ -139,36 +141,51 @@ function createTooltip(container) {
139
141
  element: tooltip,
140
142
  show(text, x, y) {
141
143
  tooltip.innerText = text;
144
+ // Незавершённый уход отменяется: подсказка снова на месте
145
+ tooltip.classList.remove("chui_chart_tooltip_leaving");
142
146
  tooltip.style.display = "block";
143
147
  tooltip.style.left = Math.round(x) + "px";
144
148
  tooltip.style.top = Math.round(y) + "px";
145
149
  },
146
150
  hide() {
147
- tooltip.style.display = "none";
151
+ // Уход — движением: подсказка гаснет, а узел прячется по концу кадра
152
+ if (getComputedStyle(tooltip).display === "none") return;
153
+ if (tooltip.classList.contains("chui_chart_tooltip_leaving")) return;
154
+ tooltip.classList.add("chui_chart_tooltip_leaving");
155
+ const done = (event) => {
156
+ if (event.target !== tooltip) return;
157
+ tooltip.removeEventListener("animationend", done);
158
+ // Показ за время ухода — узел остаётся на месте
159
+ if (tooltip.classList.contains("chui_chart_tooltip_leaving") === false) return;
160
+ tooltip.classList.remove("chui_chart_tooltip_leaving");
161
+ tooltip.style.display = "none";
162
+ };
163
+ tooltip.addEventListener("animationend", done);
148
164
  },
149
165
  };
150
166
  }
151
167
 
168
+ /** Уменьшенное движение: диаграмма строится сразу, без роста */
152
169
  class BarGraph {
153
- #canvas = document.createElement('canvas');
154
- #main = document.createElement('chui_bar_graph');
170
+ #canvas = document.createElement("canvas");
171
+ #main = document.createElement("chui_bar_graph");
155
172
  #options = undefined;
156
173
  #bars = [];
157
174
  #active = undefined;
158
175
  #tooltip = undefined;
159
176
 
160
177
  constructor(options = {}) {
161
- setStyles(__dirname + '/styles.css', STYLES_ID);
178
+ setStyles(__dirname + "/styles.css", STYLES_ID);
162
179
  this.#options = options;
163
180
  this.#main.appendChild(this.#canvas);
164
181
  this.#tooltip = createTooltip(this.#main);
165
- this.#draw();
182
+ animate((growth) => this.#draw(growth));
166
183
  this.#bindPointer();
167
184
  // Доступная альтернатива канвасу: та же таблица данных рядом с графиком
168
185
  this.#main.appendChild(buildDataTable(this.#options.data || {}));
169
186
  }
170
187
 
171
- #draw() {
188
+ #draw(growth = 1) {
172
189
  const data = this.#options.data || {};
173
190
  const colors = this.#options.colors || [];
174
191
  const labels = Object.keys(data);
@@ -181,18 +198,18 @@ class BarGraph {
181
198
  const ctx = prepareCanvas(this.#canvas, cssWidth, cssHeight);
182
199
 
183
200
  drawAxesGrid(ctx, plotWidth, scale);
184
- this.#drawBars(ctx, labels, values, scale, colors);
201
+ this.#drawBars(ctx, labels, values, scale, colors, growth);
185
202
  }
186
203
 
187
- #drawBars(ctx, labels, values, scale, colors) {
204
+ #drawBars(ctx, labels, values, scale, colors, growth = 1) {
188
205
  const bottomY = PADDING.top + PLOT_HEIGHT;
189
206
  this.#bars = [];
190
- ctx.textAlign = 'center';
191
- ctx.textBaseline = 'top';
207
+ ctx.textAlign = "center";
208
+ ctx.textBaseline = "top";
192
209
  for (let i = 0; i < labels.length; i++) {
193
210
  // Отрицательное значение — это ноль на оси, которая начинается с нуля:
194
211
  // иначе столбец уходил бы вниз, на подписи категорий (как у RadarGraph и HeatmapGraph)
195
- const barHeight = Math.max(0, (values[i] / scale.max) * PLOT_HEIGHT);
212
+ const barHeight = Math.max(0, (values[i] / scale.max) * PLOT_HEIGHT * growth);
196
213
  const x = PADDING.left + i * CATEGORY_WIDTH + (CATEGORY_WIDTH - BAR_WIDTH) / 2;
197
214
  const bar = { index: i, x: x, y: bottomY - barHeight, w: BAR_WIDTH, h: barHeight };
198
215
  this.#bars.push(bar);
@@ -217,18 +234,21 @@ class BarGraph {
217
234
  this.#canvas.setAttribute("role", "img");
218
235
  this.#canvas.setAttribute("tabindex", "0");
219
236
  this.#canvas.setAttribute("aria-label", describeData(I18n.t("graphics.barChart"), this.#options.data || {}));
220
- this.#canvas.addEventListener('click', (event) => {
237
+ this.#canvas.addEventListener("click", (event) => {
221
238
  const bar = this.#findBar(event);
222
239
  if (bar === null) return;
223
240
  this.#announce(bar.index);
224
241
  });
225
242
  // Наведение: подсказка с категорией и значением
226
- this.#canvas.addEventListener('mousemove', (event) => {
243
+ this.#canvas.addEventListener("mousemove", (event) => {
227
244
  const bar = this.#findBar(event);
228
- if (bar === null) { this.#tooltip.hide(); return; }
245
+ if (bar === null) {
246
+ this.#tooltip.hide();
247
+ return;
248
+ }
229
249
  this.#showTooltip(bar.index);
230
250
  });
231
- this.#canvas.addEventListener('mouseleave', () => {
251
+ this.#canvas.addEventListener("mouseleave", () => {
232
252
  if (this.#active === undefined) this.#tooltip.hide();
233
253
  });
234
254
  // Клавиатура: ←/→ ходят по столбцам, Enter показывает значение (как клик мышью)
@@ -241,10 +261,13 @@ class BarGraph {
241
261
  if (next > this.#bars.length - 1) next = 0;
242
262
  this.#active = next;
243
263
  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
- }));
264
+ this.#canvas.setAttribute(
265
+ "aria-label",
266
+ I18n.t("graphics.selected", {
267
+ summary: describeData(I18n.t("graphics.barChart"), this.#options.data || {}),
268
+ value: this.#label(this.#active),
269
+ })
270
+ );
248
271
  this.#showTooltip(this.#active);
249
272
  } else if (event.key === "Enter" || event.key === " ") {
250
273
  if (this.#active === undefined) return;
@@ -276,7 +299,7 @@ class BarGraph {
276
299
  const rect = this.#canvas.getBoundingClientRect();
277
300
  const x = event.clientX - rect.left;
278
301
  const y = event.clientY - rect.top;
279
- for (let bar of this.#bars) {
302
+ for (const bar of this.#bars) {
280
303
  if (x >= bar.x && x <= bar.x + bar.w && y >= bar.y && y <= bar.y + bar.h) return bar;
281
304
  }
282
305
  return null;
@@ -288,24 +311,28 @@ class BarGraph {
288
311
  }
289
312
 
290
313
  class PieGraph {
291
- #chui_piechart = document.createElement('chui_piechart');
292
- #canvas = document.createElement('canvas');
314
+ #chuiPiechart = document.createElement("chui_piechart");
315
+ #canvas = document.createElement("canvas");
293
316
  #options = undefined;
294
317
 
295
318
  constructor(options = {}) {
296
- setStyles(__dirname + '/styles.css', STYLES_ID);
319
+ setStyles(__dirname + "/styles.css", STYLES_ID);
297
320
  this.#options = options;
298
- const ctx = prepareCanvas(this.#canvas, PIE_SIZE, PIE_SIZE);
299
- this.#drawSlices(ctx);
321
+ animate((growth) => this.#draw(growth));
300
322
  this.#canvas.setAttribute("role", "img");
301
323
  this.#canvas.setAttribute("aria-label", describeData(I18n.t("graphics.pieChart"), options.data || {}));
302
- this.#chui_piechart.appendChild(this.#canvas);
303
- if (options.legend) this.#chui_piechart.appendChild(this.#buildLegend());
324
+ this.#chuiPiechart.appendChild(this.#canvas);
325
+ if (options.legend) this.#chuiPiechart.appendChild(this.#buildLegend());
304
326
  // Доступная альтернатива канвасу: та же таблица данных рядом с графиком
305
- this.#chui_piechart.appendChild(buildDataTable(options.data || {}));
327
+ this.#chuiPiechart.appendChild(buildDataTable(options.data || {}));
328
+ }
329
+
330
+ #draw(growth = 1) {
331
+ const ctx = prepareCanvas(this.#canvas, PIE_SIZE, PIE_SIZE);
332
+ this.#drawSlices(ctx, growth);
306
333
  }
307
334
 
308
- #drawSlices(ctx) {
335
+ #drawSlices(ctx, growth = 1) {
309
336
  const data = this.#options.data || {};
310
337
  const colors = this.#options.colors || [];
311
338
  const labels = Object.keys(data);
@@ -316,12 +343,17 @@ class PieGraph {
316
343
  if (total <= 0) return;
317
344
 
318
345
  const center = PIE_SIZE / 2;
346
+ // Построение: секторы прочерчиваются по кругу. Подписи процентов встают на место,
347
+ // когда круг замкнулся, — иначе они ехали бы вслед за незакрытым сектором
348
+ const limit = 2 * Math.PI * growth;
319
349
  let startAngle = 0;
320
350
  for (let i = 0; i < labels.length; i++) {
321
351
  const value = values[i];
322
- const sliceAngle = 2 * Math.PI * value / total;
352
+ const remainder = limit - startAngle;
353
+ if (remainder <= 0) break;
354
+ const sliceAngle = Math.min((2 * Math.PI * value) / total, remainder);
323
355
  drawPieSlice(ctx, center, center, center, startAngle, startAngle + sliceAngle, colors[i % colors.length]);
324
- this.#drawPercentage(ctx, center, startAngle, sliceAngle, value, total);
356
+ if (growth >= 1) this.#drawPercentage(ctx, center, startAngle, sliceAngle, value, total);
325
357
  startAngle += sliceAngle;
326
358
  }
327
359
  this.#drawDoughnutHole(ctx, center);
@@ -330,18 +362,22 @@ class PieGraph {
330
362
  #drawPercentage(ctx, center, startAngle, sliceAngle, value, total) {
331
363
  const labelRadius = center * 0.65;
332
364
  const angle = startAngle + sliceAngle / 2;
333
- ctx.fillStyle = 'white';
365
+ ctx.fillStyle = "white";
334
366
  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));
367
+ ctx.textAlign = "center";
368
+ ctx.textBaseline = "middle";
369
+ ctx.fillText(
370
+ `${Math.round((100 * value) / total)}%`,
371
+ center + labelRadius * Math.cos(angle),
372
+ center + labelRadius * Math.sin(angle)
373
+ );
338
374
  }
339
375
 
340
376
  #drawDoughnutHole(ctx, center) {
341
377
  const holeSize = this.#options.doughnutHoleSize;
342
378
  if (!(holeSize > 0) || holeSize >= 1) return;
343
379
  ctx.save();
344
- ctx.globalCompositeOperation = 'destination-out';
380
+ ctx.globalCompositeOperation = "destination-out";
345
381
  ctx.beginPath();
346
382
  ctx.arc(center, center, center * holeSize, 0, 2 * Math.PI);
347
383
  ctx.closePath();
@@ -352,28 +388,30 @@ class PieGraph {
352
388
  #buildLegend() {
353
389
  const data = this.#options.data || {};
354
390
  const colors = this.#options.colors || [];
355
- return buildLegend(Object.keys(data).map((label, index) => ({ title: label, color: colors[index % colors.length] })));
391
+ return buildLegend(
392
+ Object.keys(data).map((label, index) => ({ title: label, color: colors[index % colors.length] }))
393
+ );
356
394
  }
357
395
 
358
396
  set() {
359
- return this.#chui_piechart;
397
+ return this.#chuiPiechart;
360
398
  }
361
399
  }
362
400
 
363
401
  class LineGraph {
364
- #canvas = document.createElement('canvas');
365
- #main = document.createElement('chui_line_graph');
402
+ #canvas = document.createElement("canvas");
403
+ #main = document.createElement("chui_line_graph");
366
404
  #options = undefined;
367
405
  #points = [];
368
406
  #active = undefined;
369
407
  #tooltip = undefined;
370
408
 
371
409
  constructor(options = {}) {
372
- setStyles(__dirname + '/styles.css', STYLES_ID);
410
+ setStyles(__dirname + "/styles.css", STYLES_ID);
373
411
  this.#options = options;
374
412
  this.#main.appendChild(this.#canvas);
375
413
  this.#tooltip = createTooltip(this.#main);
376
- this.#draw();
414
+ animate((growth) => this.#draw(growth));
377
415
  this.#bindPointer();
378
416
  // Доступная альтернатива канвасу
379
417
  this.#main.appendChild(buildDataTable(options.data || {}));
@@ -383,7 +421,7 @@ class LineGraph {
383
421
  return (this.#options.colors || [])[0] || LINE_FALLBACK_COLOR;
384
422
  }
385
423
 
386
- #draw() {
424
+ #draw(growth = 1) {
387
425
  const data = this.#options.data || {};
388
426
  const labels = Object.keys(data);
389
427
  const values = labels.map((label) => Number(data[label]) || 0);
@@ -404,12 +442,17 @@ class LineGraph {
404
442
  }));
405
443
 
406
444
  // Заливка под линией (AreaGraph): градиент только для hex-цветов
445
+ // Построение: линия прочерчивается слева направо — поле приоткрывается по мере роста
446
+ ctx.save();
447
+ ctx.beginPath();
448
+ ctx.rect(0, 0, PADDING.left + plotWidth * growth, cssHeight);
449
+ ctx.clip();
407
450
  if (this.#options.area === true && this.#points.length > 0) {
408
451
  ctx.fillStyle = this.#color;
409
- if (this.#color.startsWith('#')) {
452
+ if (this.#color.startsWith("#")) {
410
453
  const gradient = ctx.createLinearGradient(0, PADDING.top, 0, bottomY);
411
- gradient.addColorStop(0, this.#color + '66');
412
- gradient.addColorStop(1, this.#color + '00');
454
+ gradient.addColorStop(0, this.#color + "66");
455
+ gradient.addColorStop(1, this.#color + "00");
413
456
  ctx.fillStyle = gradient;
414
457
  }
415
458
  ctx.beginPath();
@@ -445,10 +488,11 @@ class LineGraph {
445
488
  ctx.stroke();
446
489
  }
447
490
  ctx.fillStyle = AXIS_COLOR;
448
- ctx.textAlign = 'center';
449
- ctx.textBaseline = 'top';
491
+ ctx.textAlign = "center";
492
+ ctx.textBaseline = "top";
450
493
  ctx.fillText(labels[point.index], point.x, bottomY + 10);
451
494
  }
495
+ ctx.restore();
452
496
  }
453
497
 
454
498
  #bindPointer() {
@@ -456,17 +500,20 @@ class LineGraph {
456
500
  this.#canvas.setAttribute("role", "img");
457
501
  this.#canvas.setAttribute("tabindex", "0");
458
502
  this.#canvas.setAttribute("aria-label", describeData(I18n.t("graphics.lineChart"), this.#options.data || {}));
459
- this.#canvas.addEventListener('click', (event) => {
503
+ this.#canvas.addEventListener("click", (event) => {
460
504
  const point = this.#findPoint(event);
461
505
  if (point !== null) this.#announce(point.index);
462
506
  });
463
507
  // Наведение: подсказка у ближайшей точки
464
- this.#canvas.addEventListener('mousemove', (event) => {
508
+ this.#canvas.addEventListener("mousemove", (event) => {
465
509
  const point = this.#findPoint(event);
466
- if (point === null) { this.#tooltip.hide(); return; }
510
+ if (point === null) {
511
+ this.#tooltip.hide();
512
+ return;
513
+ }
467
514
  this.#select(point.index);
468
515
  });
469
- this.#canvas.addEventListener('mouseleave', () => {
516
+ this.#canvas.addEventListener("mouseleave", () => {
470
517
  if (this.#active === undefined) this.#tooltip.hide();
471
518
  });
472
519
  // Клавиатура: ←/→ ходят по точкам, Enter показывает значение
@@ -504,10 +551,13 @@ class LineGraph {
504
551
  const label = labels[index];
505
552
  const point = this.#points[index];
506
553
  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
- }));
554
+ this.#canvas.setAttribute(
555
+ "aria-label",
556
+ I18n.t("graphics.selected", {
557
+ summary: describeData(I18n.t("graphics.lineChart"), this.#options.data || {}),
558
+ value: `${label}: ${this.#options.data[label]}`,
559
+ })
560
+ );
511
561
  this.#draw();
512
562
  }
513
563
 
@@ -570,8 +620,8 @@ function drawPieSlice(ctx, centerX, centerY, radius, startAngle, endAngle, color
570
620
  * ```
571
621
  */
572
622
  class ScatterGraph {
573
- #canvas = document.createElement('canvas');
574
- #main = document.createElement('chui_scatter_graph');
623
+ #canvas = document.createElement("canvas");
624
+ #main = document.createElement("chui_scatter_graph");
575
625
  #options = undefined;
576
626
  #series = [];
577
627
  /** Плоский список точек с экранными координатами — общий для мыши и клавиатуры */
@@ -580,17 +630,19 @@ class ScatterGraph {
580
630
  #tooltip = undefined;
581
631
 
582
632
  constructor(options = {}) {
583
- setStyles(__dirname + '/styles.css', STYLES_ID);
633
+ setStyles(__dirname + "/styles.css", STYLES_ID);
584
634
  this.#options = options;
585
635
  this.#series = ScatterGraph.#normalize(options.series, options.colors);
586
636
  this.#main.appendChild(this.#canvas);
587
637
  this.#tooltip = createTooltip(this.#main);
588
- this.#draw();
638
+ animate((growth) => this.#draw(growth));
589
639
  // Легенда нужна только когда серий больше одной: у одной подпись есть в подсказке
590
640
  if (options.legend !== false && this.#series.length > 1) {
591
- this.#main.appendChild(buildLegend(this.#series.map((serie) => ({ title: serie.title, color: serie.color }))));
641
+ this.#main.appendChild(
642
+ buildLegend(this.#series.map((serie) => ({ title: serie.title, color: serie.color })))
643
+ );
592
644
  }
593
- this.#main.appendChild(buildTable([I18n.t("graphics.seriesColumn"), 'X', 'Y'], this.#tableRows()));
645
+ this.#main.appendChild(buildTable([I18n.t("graphics.seriesColumn"), "X", "Y"], this.#tableRows()));
594
646
  this.#bindPointer();
595
647
  }
596
648
 
@@ -623,15 +675,19 @@ class ScatterGraph {
623
675
  const list = Array.isArray(series) ? series : [];
624
676
  const palette = Array.isArray(colors) ? colors : [];
625
677
  return list.map((item, index) => {
626
- const entry = item === null || typeof item !== 'object' ? {} : item;
678
+ const entry = item === null || typeof item !== "object" ? {} : item;
627
679
  const points = (Array.isArray(entry.points) ? entry.points : []).map((point) => {
628
680
  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 };
681
+ if (point === null || typeof point !== "object") return { x: 0, y: 0 };
630
682
  return { x: Number(point.x) || 0, y: Number(point.y) || 0 };
631
683
  });
632
684
  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),
685
+ title:
686
+ entry.title === undefined ? I18n.t("graphics.series", { index: index + 1 }) : String(entry.title),
687
+ color:
688
+ entry.color === undefined
689
+ ? palette[index % palette.length] || LINE_FALLBACK_COLOR
690
+ : String(entry.color),
635
691
  points: points,
636
692
  };
637
693
  });
@@ -645,12 +701,18 @@ class ScatterGraph {
645
701
  return rows;
646
702
  }
647
703
 
648
- #draw() {
704
+ #draw(growth = 1) {
649
705
  const points = this.#series.reduce((list, serie) => list.concat(serie.points), []);
650
706
  const xs = points.map((point) => point.x);
651
707
  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);
708
+ const xScale = getRangeScale(
709
+ xs.length ? Math.min.apply(null, xs) : 0,
710
+ xs.length ? Math.max.apply(null, xs) : 0
711
+ );
712
+ const yScale = getRangeScale(
713
+ ys.length ? Math.min.apply(null, ys) : 0,
714
+ ys.length ? Math.max.apply(null, ys) : 0
715
+ );
654
716
  const xTitle = this.#options.xTitle;
655
717
  const yTitle = this.#options.yTitle;
656
718
  // Подписи делений оси Y и её название стоят слева: под них и расширяем отступ
@@ -684,8 +746,8 @@ class ScatterGraph {
684
746
  ctx.lineTo(x, bottom + 5);
685
747
  ctx.stroke();
686
748
  ctx.fillStyle = AXIS_COLOR;
687
- ctx.textAlign = 'center';
688
- ctx.textBaseline = 'top';
749
+ ctx.textAlign = "center";
750
+ ctx.textBaseline = "top";
689
751
  ctx.fillText(formatAxisLabel(xScale.min + i * xScale.step), x, bottom + 8);
690
752
  }
691
753
  for (let i = 0; i <= yScale.count; i++) {
@@ -705,8 +767,8 @@ class ScatterGraph {
705
767
  ctx.lineTo(left - 5, y);
706
768
  ctx.stroke();
707
769
  ctx.fillStyle = AXIS_COLOR;
708
- ctx.textAlign = 'right';
709
- ctx.textBaseline = 'middle';
770
+ ctx.textAlign = "right";
771
+ ctx.textBaseline = "middle";
710
772
  ctx.fillText(formatAxisLabel(yScale.min + i * yScale.step), left - 6, y);
711
773
  }
712
774
  // Оси рисуются по краям поля данных
@@ -724,15 +786,15 @@ class ScatterGraph {
724
786
  ctx.translate(14, PADDING.top + PLOT_HEIGHT / 2);
725
787
  ctx.rotate(-Math.PI / 2);
726
788
  ctx.fillStyle = AXIS_COLOR;
727
- ctx.textAlign = 'center';
728
- ctx.textBaseline = 'middle';
789
+ ctx.textAlign = "center";
790
+ ctx.textBaseline = "middle";
729
791
  ctx.fillText(String(yTitle), 0, 0);
730
792
  ctx.restore();
731
793
  }
732
794
  if (xTitle !== undefined) {
733
795
  ctx.fillStyle = AXIS_COLOR;
734
- ctx.textAlign = 'center';
735
- ctx.textBaseline = 'top';
796
+ ctx.textAlign = "center";
797
+ ctx.textBaseline = "top";
736
798
  ctx.fillText(String(xTitle), left + plotWidth / 2, bottom + 30);
737
799
  }
738
800
 
@@ -743,10 +805,19 @@ class ScatterGraph {
743
805
  const x = toX(point.x);
744
806
  const y = toY(point.y);
745
807
  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 });
808
+ this.#points.push({
809
+ index: index,
810
+ x: x,
811
+ y: y,
812
+ serie: serie.title,
813
+ color: serie.color,
814
+ xValue: point.x,
815
+ yValue: point.y,
816
+ });
747
817
  ctx.fillStyle = serie.color;
748
818
  ctx.beginPath();
749
- ctx.arc(x, y, SCATTER_POINT_RADIUS, 0, 2 * Math.PI);
819
+ // Точки растут из нуля: появление серии читается, как её набор
820
+ ctx.arc(x, y, Math.max(0.5, SCATTER_POINT_RADIUS * growth), 0, 2 * Math.PI);
750
821
  ctx.fill();
751
822
  if (index === this.#active) {
752
823
  ctx.strokeStyle = serie.color;
@@ -761,15 +832,15 @@ class ScatterGraph {
761
832
 
762
833
  #bindPointer() {
763
834
  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) => {
835
+ this.#canvas.setAttribute("role", "img");
836
+ this.#canvas.setAttribute("tabindex", "0");
837
+ this.#canvas.setAttribute("aria-label", this.#describe());
838
+ this.#canvas.addEventListener("click", (event) => {
768
839
  const point = this.#findPoint(event);
769
840
  if (point !== null) this.#announce(point.index);
770
841
  });
771
842
  // Наведение: подсказка у ближайшей точки
772
- this.#canvas.addEventListener('mousemove', (event) => {
843
+ this.#canvas.addEventListener("mousemove", (event) => {
773
844
  const point = this.#findPoint(event);
774
845
  if (point === null) {
775
846
  this.#tooltip.hide();
@@ -777,20 +848,20 @@ class ScatterGraph {
777
848
  }
778
849
  this.#select(point.index);
779
850
  });
780
- this.#canvas.addEventListener('mouseleave', () => {
851
+ this.#canvas.addEventListener("mouseleave", () => {
781
852
  if (this.#active === undefined) this.#tooltip.hide();
782
853
  });
783
854
  // Клавиатура: ←/→ водят по точкам, Enter показывает значение (как клик мышью)
784
- this.#canvas.addEventListener('keydown', (event) => {
855
+ this.#canvas.addEventListener("keydown", (event) => {
785
856
  if (this.#points.length === 0) return;
786
- if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
857
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
787
858
  event.preventDefault();
788
- let next = this.#active === undefined ? 0 : this.#active + (event.key === 'ArrowRight' ? 1 : -1);
859
+ let next = this.#active === undefined ? 0 : this.#active + (event.key === "ArrowRight" ? 1 : -1);
789
860
  if (next < 0) next = this.#points.length - 1;
790
861
  if (next > this.#points.length - 1) next = 0;
791
862
  this.#draw();
792
863
  this.#select(next);
793
- } else if (event.key === 'Enter' || event.key === ' ') {
864
+ } else if (event.key === "Enter" || event.key === " ") {
794
865
  if (this.#active === undefined) return;
795
866
  event.preventDefault();
796
867
  this.#announce(this.#active);
@@ -815,7 +886,10 @@ class ScatterGraph {
815
886
  const point = this.#points[index];
816
887
  if (point === undefined) return;
817
888
  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)}));
889
+ this.#canvas.setAttribute(
890
+ "aria-label",
891
+ I18n.t("graphics.selected", { summary: this.#describe(), value: this.#pointText(point) })
892
+ );
819
893
  this.#draw();
820
894
  }
821
895
 
@@ -837,7 +911,7 @@ class ScatterGraph {
837
911
  if (parts.length === 0) return I18n.t("graphics.scatterChart");
838
912
  const total = this.#series.reduce((sum, serie) => sum + serie.points.length, 0);
839
913
  const rest = total - parts.length;
840
- return `${I18n.t("graphics.scatterChart")}: ${parts.join(', ')}${rest > 0 ? `, и ещё ${rest}` : ""}`;
914
+ return `${I18n.t("graphics.scatterChart")}: ${parts.join(", ")}${rest > 0 ? `, и ещё ${rest}` : ""}`;
841
915
  }
842
916
 
843
917
  #announce(index) {
@@ -879,40 +953,40 @@ class Gauge {
879
953
  static DEFAULT_THICKNESS = GAUGE_THICKNESS;
880
954
  static #counter = 0;
881
955
 
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');
956
+ #root = document.createElement("chui_gauge");
957
+ #canvas = document.createElement("canvas");
958
+ #valueText = document.createElement("gauge_value");
959
+ #status = document.createElement("chui_gauge_status");
886
960
  #id = undefined;
887
961
  #value = 0;
888
962
  #min = 0;
889
963
  #max = 100;
890
- #unit = '';
964
+ #unit = "";
891
965
  #label = I18n.t("graphics.gauge");
892
966
  #format = undefined;
893
- #show_value = true;
967
+ #showValue = true;
894
968
  #size = GAUGE_SIZE;
895
969
  #thickness = GAUGE_THICKNESS;
896
- #start_angle = GAUGE_START_ANGLE;
970
+ #startAngle = GAUGE_START_ANGLE;
897
971
  #sweep = GAUGE_SWEEP;
898
972
  #ticks = GAUGE_TICKS;
899
973
  #color = undefined;
900
974
  #thresholds = [];
901
975
 
902
976
  constructor(options = {}) {
903
- setStyles(__dirname + '/styles.css', STYLES_ID);
977
+ setStyles(__dirname + "/styles.css", STYLES_ID);
904
978
  Gauge.#counter += 1;
905
979
  this.#id = options.id === undefined ? `chui_gauge_${Gauge.#counter}` : options.id;
906
980
  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');
981
+ this.#root.setAttribute("role", "meter");
982
+ this.#canvas.setAttribute("aria-hidden", "true");
983
+ this.#valueText.className = "gauge_value";
984
+ this.#valueText.setAttribute("aria-hidden", "true");
985
+ this.#status.className = "gauge_status chui_visually_hidden";
986
+ this.#status.setAttribute("role", "status");
987
+ this.#status.setAttribute("aria-live", "polite");
914
988
  this.#root.appendChild(this.#canvas);
915
- this.#root.appendChild(this.#value_text);
989
+ this.#root.appendChild(this.#valueText);
916
990
  this.#root.appendChild(this.#status);
917
991
  if (options.min !== undefined) this.#min = Number(options.min) || 0;
918
992
  if (options.max !== undefined) this.#max = Number(options.max) || 0;
@@ -920,17 +994,20 @@ class Gauge {
920
994
  if (options.unit !== undefined) this.#unit = String(options.unit);
921
995
  if (options.size !== undefined) this.#size = Number(options.size) || GAUGE_SIZE;
922
996
  if (options.thickness !== undefined) this.#thickness = Number(options.thickness) || GAUGE_THICKNESS;
923
- if (options.startAngle !== undefined) this.#start_angle = Number(options.startAngle) || 0;
997
+ if (options.startAngle !== undefined) this.#startAngle = Number(options.startAngle) || 0;
924
998
  if (options.sweep !== undefined) this.#sweep = Number(options.sweep) || GAUGE_SWEEP;
925
999
  if (options.ticks !== undefined) this.#ticks = Math.max(1, Number(options.ticks) || GAUGE_TICKS);
926
1000
  if (options.color !== undefined) this.#color = String(options.color);
927
1001
  this.#format = options.format;
928
- 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);
1002
+ this.#showValue = options.showValue !== false;
1003
+ this.setLabel(optionOr(options.label, I18n.t("graphics.gauge")));
1004
+ if (options.thresholds !== undefined)
1005
+ this.#thresholds = Gauge.#normalizeThresholds(options.thresholds, this.#max);
931
1006
  // Значение ставим последним: оно уже знает про min/max и пороги
932
1007
  this.#value = this.#min;
933
1008
  this.setValue(options.value === undefined ? this.#min : options.value, true);
1009
+ // Построение: дуга идёт от начала шкалы к значению
1010
+ animate((growth) => this.#render(growth));
934
1011
  }
935
1012
 
936
1013
  // GET
@@ -959,7 +1036,7 @@ class Gauge {
959
1036
  getValueText() {
960
1037
  if (this.#format !== undefined) return String(this.#format(this.#value, this.#max));
961
1038
  const text = formatAxisLabel(this.#value);
962
- return this.#unit === '' ? text : `${text} ${this.#unit}`;
1039
+ return this.#unit === "" ? text : `${text} ${this.#unit}`;
963
1040
  }
964
1041
 
965
1042
  getUnit() {
@@ -989,7 +1066,7 @@ class Gauge {
989
1066
  }
990
1067
 
991
1068
  isValueVisible() {
992
- return this.#show_value;
1069
+ return this.#showValue;
993
1070
  }
994
1071
 
995
1072
  // SET
@@ -1025,14 +1102,14 @@ class Gauge {
1025
1102
  }
1026
1103
 
1027
1104
  setUnit(unit = String()) {
1028
- this.#unit = unit === undefined ? '' : String(unit);
1105
+ this.#unit = unit === undefined ? "" : String(unit);
1029
1106
  return this.#render();
1030
1107
  }
1031
1108
 
1032
1109
  /** Доступное имя индикатора */
1033
1110
  setLabel(text = String()) {
1034
1111
  this.#label = text;
1035
- this.#root.setAttribute('aria-label', text === undefined ? I18n.t('graphics.gauge') : String(text));
1112
+ this.#root.setAttribute("aria-label", text === undefined ? I18n.t("graphics.gauge") : String(text));
1036
1113
  return this;
1037
1114
  }
1038
1115
 
@@ -1059,7 +1136,7 @@ class Gauge {
1059
1136
 
1060
1137
  /** Показывать ли значение в центре шкалы */
1061
1138
  setShowValue(visible = Boolean()) {
1062
- this.#show_value = visible === true;
1139
+ this.#showValue = visible === true;
1063
1140
  return this.#render();
1064
1141
  }
1065
1142
 
@@ -1081,13 +1158,14 @@ class Gauge {
1081
1158
  static #normalizeThresholds(list = Array(), max = Number()) {
1082
1159
  const source = Array.isArray(list) ? list : [];
1083
1160
  return source.map((item, index) => {
1084
- const entry = item === null || typeof item !== 'object' ? {} : item;
1161
+ const entry = item === null || typeof item !== "object" ? {} : item;
1085
1162
  const to = Number(entry.to);
1086
1163
  return {
1087
1164
  to: Number.isFinite(to) ? to : max,
1088
- color: entry.color === undefined
1089
- ? GAUGE_THRESHOLD_COLORS[index % GAUGE_THRESHOLD_COLORS.length]
1090
- : String(entry.color),
1165
+ color:
1166
+ entry.color === undefined
1167
+ ? GAUGE_THRESHOLD_COLORS[index % GAUGE_THRESHOLD_COLORS.length]
1168
+ : String(entry.color),
1091
1169
  };
1092
1170
  });
1093
1171
  }
@@ -1100,18 +1178,19 @@ class Gauge {
1100
1178
  return this.#color === undefined ? GAUGE_FALLBACK_COLOR : this.#color;
1101
1179
  }
1102
1180
 
1103
- #render() {
1104
- const start = (this.#start_angle * Math.PI) / 180;
1105
- const end = ((this.#start_angle + this.#sweep) * Math.PI) / 180;
1106
- const ratio = Math.max(0, Math.min(1, this.getRatio()));
1181
+ #render(growth = 1) {
1182
+ const start = (this.#startAngle * Math.PI) / 180;
1183
+ const end = ((this.#startAngle + this.#sweep) * Math.PI) / 180;
1184
+ // Рост — только у построения: смена значения и настроек рисуется полной дугой
1185
+ const ratio = Math.max(0, Math.min(1, this.getRatio())) * growth;
1107
1186
  const color = this.#zoneColor();
1108
1187
  // Размер шкалы виден и подписи значения: её кегль считается от него в CSS
1109
- this.#root.style.setProperty('--gauge_size', `${this.#size}px`);
1188
+ this.#root.style.setProperty("--gauge_size", `${this.#size}px`);
1110
1189
  const ctx = prepareCanvas(this.#canvas, this.#size, this.#size);
1111
1190
  const center = this.#size / 2;
1112
1191
  // Запас по краю — под деления и их подписи
1113
1192
  const radius = Math.max(10, center - this.#thickness / 2 - 12);
1114
- ctx.lineCap = 'round';
1193
+ ctx.lineCap = "round";
1115
1194
 
1116
1195
  // Дорожка шкалы: видно, куда значение может дойти
1117
1196
  ctx.strokeStyle = GAUGE_TRACK_COLOR;
@@ -1141,9 +1220,13 @@ class Gauge {
1141
1220
  ctx.lineTo(center + cos * (outer + 5), center + sin * (outer + 5));
1142
1221
  ctx.stroke();
1143
1222
  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));
1223
+ ctx.textAlign = "center";
1224
+ ctx.textBaseline = "middle";
1225
+ ctx.fillText(
1226
+ formatAxisLabel(this.#min + ((this.#max - this.#min) * i) / this.#ticks),
1227
+ center + cos * (outer + 18),
1228
+ center + sin * (outer + 18)
1229
+ );
1147
1230
  }
1148
1231
 
1149
1232
  // Стрелка с осью в центре шкалы
@@ -1160,12 +1243,12 @@ class Gauge {
1160
1243
  ctx.arc(center, center, 5, 0, 2 * Math.PI);
1161
1244
  ctx.fill();
1162
1245
 
1163
- 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());
1246
+ this.#valueText.hidden = !this.#showValue;
1247
+ this.#valueText.innerText = this.#showValue ? this.getValueText() : "";
1248
+ this.#root.setAttribute("aria-valuemin", String(this.#min));
1249
+ this.#root.setAttribute("aria-valuemax", String(this.#max));
1250
+ this.#root.setAttribute("aria-valuenow", String(this.#value));
1251
+ this.#root.setAttribute("aria-valuetext", this.getValueText());
1169
1252
  return this;
1170
1253
  }
1171
1254
 
@@ -1190,13 +1273,13 @@ class Gauge {
1190
1273
  * ```
1191
1274
  */
1192
1275
  class Sparkline {
1193
- #canvas = document.createElement('canvas');
1194
- #main = document.createElement('chui_sparkline');
1276
+ #canvas = document.createElement("canvas");
1277
+ #main = document.createElement("chui_sparkline");
1195
1278
  #options = undefined;
1196
1279
  #points = [];
1197
1280
 
1198
1281
  constructor(options = {}) {
1199
- setStyles(__dirname + '/styles.css', STYLES_ID);
1282
+ setStyles(__dirname + "/styles.css", STYLES_ID);
1200
1283
  this.#options = options;
1201
1284
  this.#main.appendChild(this.#canvas);
1202
1285
  this.#draw();
@@ -1214,7 +1297,7 @@ class Sparkline {
1214
1297
 
1215
1298
  /** Экранные точки — по ним видно, что именно нарисовано */
1216
1299
  getPoints() {
1217
- return this.#points.map((point) => ({label: point.label, value: point.value, x: point.x, y: point.y}));
1300
+ return this.#points.map((point) => ({ label: point.label, value: point.value, x: point.x, y: point.y }));
1218
1301
  }
1219
1302
 
1220
1303
  set() {
@@ -1235,9 +1318,10 @@ class Sparkline {
1235
1318
  const max = values.length ? Math.max.apply(null, values) : 0;
1236
1319
  const span = max - min;
1237
1320
  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);
1321
+ const toY = (value) =>
1322
+ span === 0
1323
+ ? height / 2
1324
+ : height - SPARKLINE_INSET - ((value - min) / span) * (height - 2 * SPARKLINE_INSET);
1241
1325
  this.#points = labels.map((label, index) => ({
1242
1326
  label: label,
1243
1327
  value: values[index],
@@ -1248,10 +1332,10 @@ class Sparkline {
1248
1332
  // Заливка под линией: градиент только для hex-цвета — как у AreaGraph
1249
1333
  if (this.#options.area === true && this.#points.length > 0) {
1250
1334
  ctx.fillStyle = color;
1251
- if (color.startsWith('#')) {
1335
+ if (color.startsWith("#")) {
1252
1336
  const gradient = ctx.createLinearGradient(0, 0, 0, height);
1253
- gradient.addColorStop(0, color + '66');
1254
- gradient.addColorStop(1, color + '00');
1337
+ gradient.addColorStop(0, color + "66");
1338
+ gradient.addColorStop(1, color + "00");
1255
1339
  ctx.fillStyle = gradient;
1256
1340
  }
1257
1341
  ctx.beginPath();
@@ -1265,7 +1349,7 @@ class Sparkline {
1265
1349
  if (this.#points.length > 1) {
1266
1350
  ctx.strokeStyle = color;
1267
1351
  ctx.lineWidth = 2;
1268
- ctx.lineJoin = 'round';
1352
+ ctx.lineJoin = "round";
1269
1353
  ctx.beginPath();
1270
1354
  ctx.moveTo(this.#points[0].x, this.#points[0].y);
1271
1355
  for (const point of this.#points.slice(1)) ctx.lineTo(point.x, point.y);
@@ -1281,8 +1365,8 @@ class Sparkline {
1281
1365
  ctx.fill();
1282
1366
  }
1283
1367
 
1284
- this.#canvas.setAttribute('role', 'img');
1285
- this.#canvas.setAttribute('aria-label', describeData(I18n.t('graphics.sparkline'), data));
1368
+ this.#canvas.setAttribute("role", "img");
1369
+ this.#canvas.setAttribute("aria-label", describeData(I18n.t("graphics.sparkline"), data));
1286
1370
  }
1287
1371
  }
1288
1372
 
@@ -1307,8 +1391,8 @@ class Sparkline {
1307
1391
  * ```
1308
1392
  */
1309
1393
  class RadarGraph {
1310
- #canvas = document.createElement('canvas');
1311
- #main = document.createElement('chui_radar_graph');
1394
+ #canvas = document.createElement("canvas");
1395
+ #main = document.createElement("chui_radar_graph");
1312
1396
  #axes = [];
1313
1397
  #series = [];
1314
1398
  /** Плоский список точек с экранными координатами — общий для мыши и клавиатуры */
@@ -1317,7 +1401,7 @@ class RadarGraph {
1317
1401
  #tooltip = undefined;
1318
1402
 
1319
1403
  constructor(options = {}) {
1320
- setStyles(__dirname + '/styles.css', STYLES_ID);
1404
+ setStyles(__dirname + "/styles.css", STYLES_ID);
1321
1405
  this.#axes = RadarGraph.#normalizeAxes(options.axes);
1322
1406
  this.#series = RadarGraph.#normalizeSeries(options.series, options.colors, this.#axes.length);
1323
1407
  this.#main.appendChild(this.#canvas);
@@ -1325,14 +1409,21 @@ class RadarGraph {
1325
1409
  this.#draw();
1326
1410
  // Легенда нужна только когда серий больше одной: у одной подпись есть в подсказке
1327
1411
  if (options.legend !== false && this.#series.length > 1) {
1328
- this.#main.appendChild(buildLegend(this.#series.map((serie) => ({title: serie.title, color: serie.color}))));
1412
+ this.#main.appendChild(
1413
+ buildLegend(this.#series.map((serie) => ({ title: serie.title, color: serie.color })))
1414
+ );
1329
1415
  }
1330
- this.#main.appendChild(buildTable([I18n.t('graphics.seriesColumn'), I18n.t('graphics.axisColumn'), I18n.t('graphics.valueColumn')], this.#tableRows()));
1416
+ this.#main.appendChild(
1417
+ buildTable(
1418
+ [I18n.t("graphics.seriesColumn"), I18n.t("graphics.axisColumn"), I18n.t("graphics.valueColumn")],
1419
+ this.#tableRows()
1420
+ )
1421
+ );
1331
1422
  this.#bindPointer();
1332
1423
  }
1333
1424
 
1334
1425
  getAxes() {
1335
- return this.#axes.map((axis) => ({key: axis.key, title: axis.title}));
1426
+ return this.#axes.map((axis) => ({ key: axis.key, title: axis.title }));
1336
1427
  }
1337
1428
 
1338
1429
  /** Серии с их цветами и значениями (копии — наружу отдаём данные, а не состояние) */
@@ -1363,14 +1454,18 @@ class RadarGraph {
1363
1454
  static #normalizeAxes(axes = Array()) {
1364
1455
  const list = Array.isArray(axes) ? axes : [];
1365
1456
  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};
1457
+ if (item === null || typeof item !== "object") {
1458
+ const title =
1459
+ item === undefined || item === null ? I18n.t("graphics.axis", { index: index + 1 }) : String(item);
1460
+ return { key: title, title: title };
1369
1461
  }
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};
1462
+ const title =
1463
+ item.title === undefined
1464
+ ? item.key === undefined
1465
+ ? I18n.t("graphics.axis", { index: index + 1 })
1466
+ : String(item.key)
1467
+ : String(item.title);
1468
+ return { key: item.key === undefined ? title : String(item.key), title: title };
1374
1469
  });
1375
1470
  }
1376
1471
 
@@ -1378,20 +1473,26 @@ class RadarGraph {
1378
1473
  const list = Array.isArray(series) ? series : [];
1379
1474
  const palette = Array.isArray(colors) ? colors : [];
1380
1475
  return list.map((item, index) => {
1381
- const entry = item === null || typeof item !== 'object' ? {} : item;
1476
+ const entry = item === null || typeof item !== "object" ? {} : item;
1382
1477
  const source = Array.isArray(entry.values) ? entry.values : [];
1383
1478
  const values = [];
1384
1479
  for (let i = 0; i < axesCount; i++) values.push(Number(source[i]) || 0);
1385
1480
  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),
1481
+ title:
1482
+ entry.title === undefined ? I18n.t("graphics.series", { index: index + 1 }) : String(entry.title),
1483
+ color:
1484
+ entry.color === undefined
1485
+ ? palette[index % palette.length] || LINE_FALLBACK_COLOR
1486
+ : String(entry.color),
1388
1487
  values: values,
1389
1488
  };
1390
1489
  });
1391
1490
  }
1392
1491
 
1393
1492
  #axisTitle(index) {
1394
- return this.#axes[index] === undefined ? I18n.t('graphics.axis', {index: index + 1}) : this.#axes[index].title;
1493
+ return this.#axes[index] === undefined
1494
+ ? I18n.t("graphics.axis", { index: index + 1 })
1495
+ : this.#axes[index].title;
1395
1496
  }
1396
1497
 
1397
1498
  #tableRows() {
@@ -1413,10 +1514,10 @@ class RadarGraph {
1413
1514
  }
1414
1515
  if (parts.length === SUMMARY_LIMIT) break;
1415
1516
  }
1416
- if (parts.length === 0) return I18n.t('graphics.radarChart');
1517
+ if (parts.length === 0) return I18n.t("graphics.radarChart");
1417
1518
  const total = this.#series.reduce((sum, serie) => sum + serie.values.length, 0);
1418
1519
  const rest = total - parts.length;
1419
- return `${I18n.t('graphics.radarChart')}: ${parts.join(', ')}${rest > 0 ? `, и ещё ${rest}` : ""}`;
1520
+ return `${I18n.t("graphics.radarChart")}: ${parts.join(", ")}${rest > 0 ? `, и ещё ${rest}` : ""}`;
1420
1521
  }
1421
1522
 
1422
1523
  #draw() {
@@ -1462,8 +1563,8 @@ class RadarGraph {
1462
1563
  // Значения уровней — вдоль верхней оси, названия осей — снаружи вершин
1463
1564
  const top = pointAt(0, 1);
1464
1565
  ctx.fillStyle = AXIS_COLOR;
1465
- ctx.textAlign = 'left';
1466
- ctx.textBaseline = 'middle';
1566
+ ctx.textAlign = "left";
1567
+ ctx.textBaseline = "middle";
1467
1568
  for (let level = 1; level <= Y_AXIS_STEPS; level++) {
1468
1569
  ctx.fillText(formatAxisLabel(scale.step * level), top.x + 4, center - radius * (level / Y_AXIS_STEPS));
1469
1570
  }
@@ -1471,8 +1572,8 @@ class RadarGraph {
1471
1572
  const cos = Math.cos(angleOf(index));
1472
1573
  const sin = Math.sin(angleOf(index));
1473
1574
  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';
1575
+ ctx.textAlign = cos > 0.2 ? "left" : cos < -0.2 ? "right" : "center";
1576
+ ctx.textBaseline = sin > 0.2 ? "top" : sin < -0.2 ? "bottom" : "middle";
1476
1577
  // Подпись оси не выходит за канву: длинное имя слева обрезалось её краем
1477
1578
  // («Анимация» читалась как «ментация») — якорь поджимается по ширине текста
1478
1579
  const title = String(axis.title);
@@ -1480,28 +1581,38 @@ class RadarGraph {
1480
1581
  const anchor = center + cos * (radius + 14);
1481
1582
  const align = ctx.textAlign;
1482
1583
  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);
1584
+ if (align === "left") labelX = Math.min(anchor, cssSize - RADAR_LABEL_INSET - titleWidth);
1585
+ else if (align === "right") labelX = Math.max(anchor, RADAR_LABEL_INSET + titleWidth);
1586
+ else
1587
+ labelX = Math.min(
1588
+ Math.max(anchor, RADAR_LABEL_INSET + titleWidth / 2),
1589
+ cssSize - RADAR_LABEL_INSET - titleWidth / 2
1590
+ );
1486
1591
  ctx.fillText(title, labelX, center + sin * (radius + 10));
1487
1592
  });
1488
1593
 
1489
1594
  // Серии: полупрозрачная заливка, контур и точки
1490
1595
  this.#points = [];
1491
1596
  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));
1597
+ const coords = serie.values.map((value, index) =>
1598
+ pointAt(index, scale.max > 0 ? Math.max(0, Math.min(1, value / scale.max)) : 0)
1599
+ );
1493
1600
  if (coords.length > 1) {
1494
1601
  ctx.fillStyle = serie.color;
1495
1602
  ctx.globalAlpha = 0.18;
1496
1603
  ctx.beginPath();
1497
- coords.forEach((point, index) => (index === 0 ? ctx.moveTo(point.x, point.y) : ctx.lineTo(point.x, point.y)));
1604
+ coords.forEach((point, index) =>
1605
+ index === 0 ? ctx.moveTo(point.x, point.y) : ctx.lineTo(point.x, point.y)
1606
+ );
1498
1607
  ctx.closePath();
1499
1608
  ctx.fill();
1500
1609
  ctx.globalAlpha = 1;
1501
1610
  ctx.strokeStyle = serie.color;
1502
1611
  ctx.lineWidth = 2.5;
1503
1612
  ctx.beginPath();
1504
- coords.forEach((point, index) => (index === 0 ? ctx.moveTo(point.x, point.y) : ctx.lineTo(point.x, point.y)));
1613
+ coords.forEach((point, index) =>
1614
+ index === 0 ? ctx.moveTo(point.x, point.y) : ctx.lineTo(point.x, point.y)
1615
+ );
1505
1616
  ctx.closePath();
1506
1617
  ctx.stroke();
1507
1618
  }
@@ -1533,15 +1644,15 @@ class RadarGraph {
1533
1644
 
1534
1645
  #bindPointer() {
1535
1646
  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) => {
1647
+ this.#canvas.setAttribute("role", "img");
1648
+ this.#canvas.setAttribute("tabindex", "0");
1649
+ this.#canvas.setAttribute("aria-label", this.#describe());
1650
+ this.#canvas.addEventListener("click", (event) => {
1540
1651
  const point = this.#findPoint(event);
1541
1652
  if (point !== null) this.#announce(point.index);
1542
1653
  });
1543
1654
  // Наведение: подсказка у ближайшей точки
1544
- this.#canvas.addEventListener('mousemove', (event) => {
1655
+ this.#canvas.addEventListener("mousemove", (event) => {
1545
1656
  const point = this.#findPoint(event);
1546
1657
  if (point === null) {
1547
1658
  this.#tooltip.hide();
@@ -1549,20 +1660,20 @@ class RadarGraph {
1549
1660
  }
1550
1661
  this.#select(point.index);
1551
1662
  });
1552
- this.#canvas.addEventListener('mouseleave', () => {
1663
+ this.#canvas.addEventListener("mouseleave", () => {
1553
1664
  if (this.#active === undefined) this.#tooltip.hide();
1554
1665
  });
1555
1666
  // Клавиатура: ←/→ водят по точкам, Enter показывает значение (как клик мышью)
1556
- this.#canvas.addEventListener('keydown', (event) => {
1667
+ this.#canvas.addEventListener("keydown", (event) => {
1557
1668
  if (this.#points.length === 0) return;
1558
- if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
1669
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
1559
1670
  event.preventDefault();
1560
- let next = this.#active === undefined ? 0 : this.#active + (event.key === 'ArrowRight' ? 1 : -1);
1671
+ let next = this.#active === undefined ? 0 : this.#active + (event.key === "ArrowRight" ? 1 : -1);
1561
1672
  if (next < 0) next = this.#points.length - 1;
1562
1673
  if (next > this.#points.length - 1) next = 0;
1563
1674
  this.#draw();
1564
1675
  this.#select(next);
1565
- } else if (event.key === 'Enter' || event.key === ' ') {
1676
+ } else if (event.key === "Enter" || event.key === " ") {
1566
1677
  if (this.#active === undefined) return;
1567
1678
  event.preventDefault();
1568
1679
  this.#announce(this.#active);
@@ -1577,7 +1688,7 @@ class RadarGraph {
1577
1688
  let nearest = null;
1578
1689
  for (const point of this.#points) {
1579
1690
  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};
1691
+ if (nearest === null || distance < nearest.distance) nearest = { point: point, distance: distance };
1581
1692
  }
1582
1693
  return nearest !== null && nearest.distance <= RADAR_HOVER_DISTANCE ? nearest.point : null;
1583
1694
  }
@@ -1591,7 +1702,10 @@ class RadarGraph {
1591
1702
  const point = this.#points[index];
1592
1703
  if (point === undefined) return;
1593
1704
  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)}));
1705
+ this.#canvas.setAttribute(
1706
+ "aria-label",
1707
+ I18n.t("graphics.selected", { summary: this.#describe(), value: this.#pointText(point) })
1708
+ );
1595
1709
  this.#draw();
1596
1710
  }
1597
1711
 
@@ -1631,8 +1745,8 @@ class RadarGraph {
1631
1745
  * ```
1632
1746
  */
1633
1747
  class HeatmapGraph {
1634
- #canvas = document.createElement('canvas');
1635
- #main = document.createElement('chui_heatmap_graph');
1748
+ #canvas = document.createElement("canvas");
1749
+ #main = document.createElement("chui_heatmap_graph");
1636
1750
  #options = undefined;
1637
1751
  #rows = [];
1638
1752
  #columns = [];
@@ -1644,17 +1758,22 @@ class HeatmapGraph {
1644
1758
  #tooltip = undefined;
1645
1759
 
1646
1760
  constructor(options = {}) {
1647
- setStyles(__dirname + '/styles.css', STYLES_ID);
1761
+ setStyles(__dirname + "/styles.css", STYLES_ID);
1648
1762
  this.#options = options;
1649
1763
  this.#values = HeatmapGraph.#normalizeValues(options.values);
1650
1764
  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'));
1765
+ this.#rows = HeatmapGraph.#normalizeLabels(options.rows, this.#values.length, I18n.t("graphics.rowColumn"));
1766
+ this.#columns = HeatmapGraph.#normalizeLabels(options.columns, columns, I18n.t("graphics.columnColumn"));
1653
1767
  this.#max = Number.isFinite(Number(options.max)) ? Number(options.max) : this.#maxOfValues();
1654
1768
  this.#main.appendChild(this.#canvas);
1655
1769
  this.#tooltip = createTooltip(this.#main);
1656
1770
  this.#draw();
1657
- this.#main.appendChild(buildTable([I18n.t('graphics.rowColumn'), I18n.t('graphics.columnColumn'), I18n.t('graphics.valueColumn')], this.#tableRows()));
1771
+ this.#main.appendChild(
1772
+ buildTable(
1773
+ [I18n.t("graphics.rowColumn"), I18n.t("graphics.columnColumn"), I18n.t("graphics.valueColumn")],
1774
+ this.#tableRows()
1775
+ )
1776
+ );
1658
1777
  this.#bindPointer();
1659
1778
  }
1660
1779
 
@@ -1670,7 +1789,7 @@ class HeatmapGraph {
1670
1789
  /** Выбранная клетка; `undefined` — ничего не выбрано */
1671
1790
  getActive() {
1672
1791
  const cell = this.#cells[this.#active];
1673
- return cell === undefined ? undefined : {row: cell.row, column: cell.column, value: cell.value};
1792
+ return cell === undefined ? undefined : { row: cell.row, column: cell.column, value: cell.value };
1674
1793
  }
1675
1794
 
1676
1795
  /** Фокус на канвас: с него работают стрелки и Enter */
@@ -1693,7 +1812,9 @@ class HeatmapGraph {
1693
1812
  for (let index = 0; index < width; index++) {
1694
1813
  const value = Number(cells[index]);
1695
1814
  // Пропуск и нечисло — пустая клетка: карта честно показывает «данных нет»
1696
- result.push(cells[index] === undefined || cells[index] === null || Number.isNaN(value) ? undefined : value);
1815
+ result.push(
1816
+ cells[index] === undefined || cells[index] === null || Number.isNaN(value) ? undefined : value
1817
+ );
1697
1818
  }
1698
1819
  return result;
1699
1820
  });
@@ -1742,7 +1863,7 @@ class HeatmapGraph {
1742
1863
  #tableRows() {
1743
1864
  const rows = [];
1744
1865
  for (const cell of this.#renderableCells()) {
1745
- rows.push([this.#rows[cell.row], this.#columns[cell.column], cell.value === undefined ? '' : cell.value]);
1866
+ rows.push([this.#rows[cell.row], this.#columns[cell.column], cell.value === undefined ? "" : cell.value]);
1746
1867
  }
1747
1868
  return rows;
1748
1869
  }
@@ -1751,21 +1872,27 @@ class HeatmapGraph {
1751
1872
  const cells = [];
1752
1873
  for (let row = 0; row < this.#values.length; row++) {
1753
1874
  for (let column = 0; column < this.#columns.length; column++) {
1754
- cells.push({row: row, column: column, value: this.#values[row][column]});
1875
+ cells.push({ row: row, column: column, value: this.#values[row][column] });
1755
1876
  }
1756
1877
  }
1757
1878
  return cells;
1758
1879
  }
1759
1880
 
1760
1881
  #cellText(cell) {
1761
- const value = cell.value === undefined ? I18n.t('graphics.noData') : String(cell.value);
1882
+ const value = cell.value === undefined ? I18n.t("graphics.noData") : String(cell.value);
1762
1883
  return `${this.#rows[cell.row]} · ${this.#columns[cell.column]}: ${value}`;
1763
1884
  }
1764
1885
 
1765
1886
  #describe() {
1766
- const base = I18n.t("graphics.heatmapSummary", {rows: this.#rows.length, columns: this.#columns.length, max: formatAxisLabel(this.#max)});
1887
+ const base = I18n.t("graphics.heatmapSummary", {
1888
+ rows: this.#rows.length,
1889
+ columns: this.#columns.length,
1890
+ max: formatAxisLabel(this.#max),
1891
+ });
1767
1892
  const cell = this.#cells[this.#active];
1768
- return cell === undefined ? base : I18n.t("graphics.selectedCell", {summary: base, value: this.#cellText(cell)});
1893
+ return cell === undefined
1894
+ ? base
1895
+ : I18n.t("graphics.selectedCell", { summary: base, value: this.#cellText(cell) });
1769
1896
  }
1770
1897
 
1771
1898
  #draw() {
@@ -1775,14 +1902,18 @@ class HeatmapGraph {
1775
1902
  // Слева место под подписи строк, сверху — под подписи столбцов
1776
1903
  const left = columns === 0 ? HEATMAP_PADDING : HEATMAP_LABEL_WIDTH;
1777
1904
  const top = HEATMAP_LABEL_HEIGHT;
1778
- const ctx = prepareCanvas(this.#canvas, left + columns * step + HEATMAP_PADDING, top + rows * step + HEATMAP_PADDING);
1905
+ const ctx = prepareCanvas(
1906
+ this.#canvas,
1907
+ left + columns * step + HEATMAP_PADDING,
1908
+ top + rows * step + HEATMAP_PADDING
1909
+ );
1779
1910
  ctx.font = LABEL_FONT;
1780
1911
  this.#cells = [];
1781
1912
 
1782
1913
  for (let row = 0; row < rows; row++) {
1783
1914
  ctx.fillStyle = AXIS_COLOR;
1784
- ctx.textAlign = 'right';
1785
- ctx.textBaseline = 'middle';
1915
+ ctx.textAlign = "right";
1916
+ ctx.textBaseline = "middle";
1786
1917
  ctx.fillText(this.#rows[row], left - HEATMAP_GAP - 4, top + row * step + HEATMAP_CELL / 2);
1787
1918
  for (let column = 0; column < columns; column++) {
1788
1919
  const value = this.#values[row][column];
@@ -1791,7 +1922,7 @@ class HeatmapGraph {
1791
1922
  ctx.fillStyle = this.#cellColor(value);
1792
1923
  ctx.fillRect(x, y, HEATMAP_CELL, HEATMAP_CELL);
1793
1924
  const index = this.#cells.length;
1794
- this.#cells.push({index: index, x: x, y: y, row: row, column: column, value: value});
1925
+ this.#cells.push({ index: index, x: x, y: y, row: row, column: column, value: value });
1795
1926
  if (index === this.#active) {
1796
1927
  ctx.strokeStyle = AXIS_COLOR;
1797
1928
  ctx.lineWidth = 2;
@@ -1802,25 +1933,25 @@ class HeatmapGraph {
1802
1933
 
1803
1934
  // Подписи столбцов — над сеткой
1804
1935
  ctx.fillStyle = AXIS_COLOR;
1805
- ctx.textAlign = 'center';
1806
- ctx.textBaseline = 'bottom';
1936
+ ctx.textAlign = "center";
1937
+ ctx.textBaseline = "bottom";
1807
1938
  for (let column = 0; column < columns; column++) {
1808
1939
  ctx.fillText(this.#columns[column], left + column * step + HEATMAP_CELL / 2, top - HEATMAP_GAP);
1809
1940
  }
1810
1941
 
1811
- this.#canvas.setAttribute('role', 'img');
1812
- this.#canvas.setAttribute('tabindex', '0');
1813
- this.#canvas.setAttribute('aria-label', this.#describe());
1942
+ this.#canvas.setAttribute("role", "img");
1943
+ this.#canvas.setAttribute("tabindex", "0");
1944
+ this.#canvas.setAttribute("aria-label", this.#describe());
1814
1945
  }
1815
1946
 
1816
1947
  #bindPointer() {
1817
1948
  this.#canvas.classList.add("graph_interactive");
1818
- this.#canvas.addEventListener('click', (event) => {
1949
+ this.#canvas.addEventListener("click", (event) => {
1819
1950
  const cell = this.#findCell(event);
1820
1951
  if (cell !== null) this.#announce(cell.index);
1821
1952
  });
1822
1953
  // Наведение: подсказка над клеткой
1823
- this.#canvas.addEventListener('mousemove', (event) => {
1954
+ this.#canvas.addEventListener("mousemove", (event) => {
1824
1955
  const cell = this.#findCell(event);
1825
1956
  if (cell === null) {
1826
1957
  this.#tooltip.hide();
@@ -1828,21 +1959,27 @@ class HeatmapGraph {
1828
1959
  }
1829
1960
  this.#select(cell.index);
1830
1961
  });
1831
- this.#canvas.addEventListener('mouseleave', () => {
1962
+ this.#canvas.addEventListener("mouseleave", () => {
1832
1963
  if (this.#active === undefined) this.#tooltip.hide();
1833
1964
  });
1834
1965
  // Клавиатура: стрелки водят по клеткам и на краю останавливаются, Enter показывает значение
1835
- this.#canvas.addEventListener('keydown', (event) => {
1966
+ this.#canvas.addEventListener("keydown", (event) => {
1836
1967
  if (this.#cells.length === 0) return;
1837
- const moves = {ArrowRight: [0, 1], ArrowLeft: [0, -1], ArrowDown: [1, 0], ArrowUp: [-1, 0]};
1968
+ const moves = { ArrowRight: [0, 1], ArrowLeft: [0, -1], ArrowDown: [1, 0], ArrowUp: [-1, 0] };
1838
1969
  const move = moves[event.key];
1839
1970
  if (move !== undefined) {
1840
1971
  event.preventDefault();
1841
1972
  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]));
1973
+ const row = Math.max(
1974
+ 0,
1975
+ Math.min(this.#rows.length - 1, (current === undefined ? 0 : current.row) + move[0])
1976
+ );
1977
+ const column = Math.max(
1978
+ 0,
1979
+ Math.min(this.#columns.length - 1, (current === undefined ? 0 : current.column) + move[1])
1980
+ );
1844
1981
  this.#select(row * this.#columns.length + column);
1845
- } else if (event.key === 'Enter' || event.key === ' ') {
1982
+ } else if (event.key === "Enter" || event.key === " ") {
1846
1983
  if (this.#active === undefined) return;
1847
1984
  event.preventDefault();
1848
1985
  this.#announce(this.#active);
@@ -1873,7 +2010,7 @@ class HeatmapGraph {
1873
2010
  if (cell === undefined) return;
1874
2011
  new Notification({
1875
2012
  title: `${this.#rows[cell.row]} · ${this.#columns[cell.column]}`,
1876
- text: cell.value === undefined ? I18n.t('graphics.noData') : String(cell.value),
2013
+ text: cell.value === undefined ? I18n.t("graphics.noData") : String(cell.value),
1877
2014
  showTime: 3000,
1878
2015
  }).show();
1879
2016
  }