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.
- package/CHANGELOG.md +597 -0
- package/README.md +43 -644
- package/framework/appLayout/default/chui_app_layout.js +602 -496
- package/framework/appLayout/default/global_style.css +288 -97
- package/framework/appLayout/default/main_theme_style.css +188 -101
- package/framework/components/chui_accordion/accordion.js +38 -35
- package/framework/components/chui_accordion/styles.css +13 -12
- package/framework/components/chui_action_sheet/action_sheet.js +93 -64
- package/framework/components/chui_action_sheet/styles.css +12 -5
- package/framework/components/chui_actions_bar/actions_bar.js +338 -0
- package/framework/components/chui_actions_bar/styles.css +139 -0
- package/framework/components/chui_affix/affix.js +16 -11
- package/framework/components/chui_affix/styles.css +5 -0
- package/framework/components/chui_alert/alert.js +34 -19
- package/framework/components/chui_alert/styles.css +7 -0
- package/framework/components/chui_avatar/avatar.js +36 -35
- package/framework/components/chui_avatar/styles.css +1 -1
- package/framework/components/chui_avatar_group/avatar_group.js +25 -23
- package/framework/components/chui_avatar_group/styles.css +1 -1
- package/framework/components/chui_backtop/backtop.js +60 -21
- package/framework/components/chui_backtop/styles.css +21 -0
- package/framework/components/chui_badge/badge.js +30 -28
- package/framework/components/chui_badge/styles.css +6 -6
- package/framework/components/chui_banner/banner.js +222 -0
- package/framework/components/chui_banner/styles.css +170 -0
- package/framework/components/chui_breadcrumb/breadcrumb.js +15 -13
- package/framework/components/chui_breadcrumb/styles.css +1 -1
- package/framework/components/chui_button/button.js +52 -36
- package/framework/components/chui_button/styles.css +7 -7
- package/framework/components/chui_calendar/calendar.js +18 -16
- package/framework/components/chui_card/card.js +52 -32
- package/framework/components/chui_card_scroll/card_scroll.js +169 -0
- package/framework/components/chui_card_scroll/styles.css +86 -0
- package/framework/components/chui_chat/chat.js +140 -79
- package/framework/components/chui_chat/styles.css +18 -7
- package/framework/components/chui_clipboard_history/clipboard_history.js +53 -39
- package/framework/components/chui_clipboard_history/styles.css +13 -9
- package/framework/components/chui_code/code.js +10 -10
- package/framework/components/chui_code/styles.css +4 -4
- package/framework/components/chui_code_editor/code_editor.js +73 -57
- package/framework/components/chui_code_editor/styles.css +6 -5
- package/framework/components/chui_command_palette/command_palette.js +87 -64
- package/framework/components/chui_command_palette/styles.css +5 -5
- package/framework/components/chui_console/console.js +13 -13
- package/framework/components/chui_console/styles.css +6 -3
- package/framework/components/chui_content_block/content_block.js +35 -21
- package/framework/components/chui_content_block/styles.css +2 -2
- package/framework/components/chui_context_menu/context_menu.js +171 -58
- package/framework/components/chui_context_menu/styles.css +82 -8
- package/framework/components/chui_custom_element/custom_element.js +25 -18
- package/framework/components/chui_data_grid/data_grid.js +633 -471
- package/framework/components/chui_data_grid/styles.css +24 -22
- package/framework/components/chui_descriptions/descriptions.js +34 -17
- package/framework/components/chui_descriptions/styles.css +1 -1
- package/framework/components/chui_details/details.js +42 -36
- package/framework/components/chui_details/styles.css +12 -11
- package/framework/components/chui_diff_viewer/diff_viewer.js +172 -147
- package/framework/components/chui_diff_viewer/styles.css +10 -10
- package/framework/components/chui_divider/divider.js +19 -17
- package/framework/components/chui_divider/styles.css +2 -2
- package/framework/components/chui_drawer/drawer.js +63 -38
- package/framework/components/chui_drawer/styles.css +18 -4
- package/framework/components/chui_drop_zone/drop_zone.js +37 -27
- package/framework/components/chui_drop_zone/styles.css +10 -6
- package/framework/components/chui_empty_state/empty_state.js +20 -15
- package/framework/components/chui_fieldset/fieldset.js +40 -23
- package/framework/components/chui_fieldset/styles.css +6 -6
- package/framework/components/chui_file_explorer/file_explorer.js +260 -189
- package/framework/components/chui_file_explorer/styles.css +8 -7
- package/framework/components/chui_form/form.js +40 -28
- package/framework/components/chui_form/styles.css +1 -1
- package/framework/components/chui_gantt/gantt.js +102 -78
- package/framework/components/chui_gantt/styles.css +5 -5
- package/framework/components/chui_graphs/graphs.js +376 -239
- package/framework/components/chui_graphs/styles.css +27 -6
- package/framework/components/chui_hero/hero.js +98 -37
- package/framework/components/chui_hero/styles.css +176 -7
- package/framework/components/chui_html_block/html_block.js +8 -8
- package/framework/components/chui_html_block/styles.css +3 -3
- package/framework/components/chui_hx/hx.js +3 -3
- package/framework/components/chui_hx/styles.css +13 -6
- package/framework/components/chui_icons/icons.js +2115 -2115
- package/framework/components/chui_icons/styles.css +4 -3
- package/framework/components/chui_inputs/chui_check_box/check_box.js +31 -19
- package/framework/components/chui_inputs/chui_check_box/styles.css +30 -21
- package/framework/components/chui_inputs/chui_chips/chips.js +141 -70
- package/framework/components/chui_inputs/chui_chips/styles.css +32 -7
- package/framework/components/chui_inputs/chui_color_picker/color_picker.js +55 -38
- package/framework/components/chui_inputs/chui_color_picker/styles.css +7 -7
- package/framework/components/chui_inputs/chui_combo_box/combo_box.js +127 -109
- package/framework/components/chui_inputs/chui_combo_box/styles.css +23 -21
- package/framework/components/chui_inputs/chui_date/date.js +161 -127
- package/framework/components/chui_inputs/chui_date/styles.css +20 -19
- package/framework/components/chui_inputs/chui_date_range/date_range.js +61 -50
- package/framework/components/chui_inputs/chui_date_range/styles.css +3 -3
- package/framework/components/chui_inputs/chui_datetime/datetime.js +47 -39
- package/framework/components/chui_inputs/chui_datetime/styles.css +3 -3
- package/framework/components/chui_inputs/chui_email/email.js +48 -36
- package/framework/components/chui_inputs/chui_email/styles.css +11 -11
- package/framework/components/chui_inputs/chui_file/file.js +71 -44
- package/framework/components/chui_inputs/chui_file/styles.css +23 -19
- package/framework/components/chui_inputs/chui_hotkey/hotkey.js +101 -76
- package/framework/components/chui_inputs/chui_hotkey/styles.css +6 -6
- package/framework/components/chui_inputs/chui_masked/masked.js +83 -72
- package/framework/components/chui_inputs/chui_masked/styles.css +5 -5
- package/framework/components/chui_inputs/chui_mention/mention.js +87 -40
- package/framework/components/chui_inputs/chui_mention/styles.css +14 -8
- package/framework/components/chui_inputs/chui_multi_combo_box/multi_combo_box.js +242 -212
- package/framework/components/chui_inputs/chui_multi_combo_box/styles.css +23 -15
- package/framework/components/chui_inputs/chui_number/number.js +84 -70
- package/framework/components/chui_inputs/chui_number/styles.css +13 -13
- package/framework/components/chui_inputs/chui_otp/otp.js +71 -62
- package/framework/components/chui_inputs/chui_otp/styles.css +5 -5
- package/framework/components/chui_inputs/chui_password/password.js +72 -56
- package/framework/components/chui_inputs/chui_password/styles.css +15 -15
- package/framework/components/chui_inputs/chui_radio_button/radio_button.js +37 -25
- package/framework/components/chui_inputs/chui_radio_button/styles.css +22 -22
- package/framework/components/chui_inputs/chui_rating/rating.js +54 -50
- package/framework/components/chui_inputs/chui_rating/styles.css +3 -3
- package/framework/components/chui_inputs/chui_select_box/select_box.js +132 -108
- package/framework/components/chui_inputs/chui_select_box/styles.css +21 -21
- package/framework/components/chui_inputs/chui_slider/slider.js +41 -38
- package/framework/components/chui_inputs/chui_slider/styles.css +7 -5
- package/framework/components/chui_inputs/chui_text/styles.css +14 -14
- package/framework/components/chui_inputs/chui_text/text.js +69 -52
- package/framework/components/chui_inputs/chui_text_area/styles.css +14 -14
- package/framework/components/chui_inputs/chui_text_area/text_area.js +59 -36
- package/framework/components/chui_inputs/chui_time/styles.css +18 -20
- package/framework/components/chui_inputs/chui_time/time.js +71 -49
- package/framework/components/chui_inputs/chui_toggle/styles.css +13 -6
- package/framework/components/chui_inputs/chui_toggle/toggle.js +27 -27
- package/framework/components/chui_json_editor/json_editor.js +137 -127
- package/framework/components/chui_json_editor/styles.css +5 -5
- package/framework/components/chui_json_viewer/json_viewer.js +133 -114
- package/framework/components/chui_json_viewer/styles.css +9 -8
- package/framework/components/chui_kanban/kanban.js +78 -60
- package/framework/components/chui_kanban/styles.css +8 -4
- package/framework/components/chui_kbd/kbd.js +12 -10
- package/framework/components/chui_kbd/styles.css +1 -1
- package/framework/components/chui_label/label.js +31 -29
- package/framework/components/chui_label/styles.css +3 -3
- package/framework/components/chui_link/link.js +36 -29
- package/framework/components/chui_link/styles.css +11 -4
- package/framework/components/chui_list/list.js +42 -38
- package/framework/components/chui_list/styles.css +4 -3
- package/framework/components/chui_markdown_editor/markdown_editor.js +125 -109
- package/framework/components/chui_markdown_editor/styles.css +5 -5
- package/framework/components/chui_media/audio.js +457 -364
- package/framework/components/chui_media/audio_styles.css +74 -37
- package/framework/components/chui_media/image.js +99 -64
- package/framework/components/chui_media/image_compare.js +86 -55
- package/framework/components/chui_media/image_compare_styles.css +5 -5
- package/framework/components/chui_media/image_styles.css +4 -4
- package/framework/components/chui_media/video.js +264 -171
- package/framework/components/chui_media/video_styles.css +57 -24
- package/framework/components/chui_menu_bar/menu_bar.js +67 -65
- package/framework/components/chui_menu_bar/styles.css +14 -12
- package/framework/components/chui_message_bar/message_bar.js +551 -0
- package/framework/components/chui_message_bar/styles.css +248 -0
- package/framework/components/chui_modal/modal.js +81 -56
- package/framework/components/chui_modal/styles.css +3 -2
- package/framework/components/chui_notification/notification.js +107 -69
- package/framework/components/chui_notification/notification_download.js +28 -77
- package/framework/components/chui_notification/notification_download_card.js +115 -0
- package/framework/components/chui_notification/notification_download_progress.css +5 -4
- package/framework/components/chui_notification/notification_download_progress.js +73 -105
- package/framework/components/chui_notification/notification_update.js +44 -37
- package/framework/components/chui_notification/status_plate.css +3 -3
- package/framework/components/chui_notification/status_plate.js +2 -2
- package/framework/components/chui_notification/styles.css +6 -1
- package/framework/components/chui_notification/styles_download.css +2 -2
- package/framework/components/chui_notification/styles_update.css +3 -3
- package/framework/components/chui_org_chart/org_chart.js +101 -62
- package/framework/components/chui_org_chart/styles.css +2 -2
- package/framework/components/chui_pagination/pagination.js +65 -30
- package/framework/components/chui_pagination/styles.css +1 -1
- package/framework/components/chui_paragraph/paragraph.js +2 -2
- package/framework/components/chui_paragraph/styles.css +3 -3
- package/framework/components/chui_popover/popover.js +77 -50
- package/framework/components/chui_popover/styles.css +1 -1
- package/framework/components/chui_popups/popups.js +210 -181
- package/framework/components/chui_popups/styles.css +7 -7
- package/framework/components/chui_print_button/print_button.js +48 -33
- package/framework/components/chui_progress_bar/progress_bar.js +49 -19
- package/framework/components/chui_progress_bar/styles.css +6 -6
- package/framework/components/chui_progress_ring/progress_ring.js +32 -30
- package/framework/components/chui_progress_ring/styles.css +1 -1
- package/framework/components/chui_qrcode/qrcode.js +85 -41
- package/framework/components/chui_query_builder/query_builder.js +263 -170
- package/framework/components/chui_query_builder/styles.css +4 -4
- package/framework/components/chui_radio_group/radio_group.js +36 -26
- package/framework/components/chui_radio_group/styles.css +2 -2
- package/framework/components/chui_result/result.js +41 -31
- package/framework/components/chui_result/styles.css +3 -3
- package/framework/components/chui_scheduler/scheduler.js +126 -104
- package/framework/components/chui_scheduler/styles.css +11 -7
- package/framework/components/chui_screen_spinner/screen_spinner.js +167 -0
- package/framework/components/chui_screen_spinner/styles.css +49 -0
- package/framework/components/chui_segmented_control/segmented_control.js +90 -27
- package/framework/components/chui_segmented_control/styles.css +32 -6
- package/framework/components/chui_settings/settings.js +160 -87
- package/framework/components/chui_settings/styles.css +4 -4
- package/framework/components/chui_shortcut_help/shortcut_help.js +82 -61
- package/framework/components/chui_sidebar_nav/sidebar_nav.js +132 -53
- package/framework/components/chui_sidebar_nav/styles.css +9 -3
- package/framework/components/chui_skeleton/skeleton.js +29 -24
- package/framework/components/chui_slideshow/slideshow.js +122 -103
- package/framework/components/chui_slideshow/styles.css +22 -16
- package/framework/components/chui_spinner/spinner.js +17 -17
- package/framework/components/chui_spinner/styles.css +0 -1
- package/framework/components/chui_split_button/split_button.js +53 -30
- package/framework/components/chui_split_button/styles.css +22 -6
- package/framework/components/chui_splitter/splitter.js +46 -39
- package/framework/components/chui_splitter/styles.css +2 -2
- package/framework/components/chui_statistic/statistic.js +111 -84
- package/framework/components/chui_status_bar/status_bar.js +11 -9
- package/framework/components/chui_status_bar/styles.css +2 -2
- package/framework/components/chui_stepper/stepper.js +230 -54
- package/framework/components/chui_stepper/styles.css +79 -14
- package/framework/components/chui_sticky_bar/sticky_bar.js +53 -17
- package/framework/components/chui_sticky_bar/styles.css +11 -7
- package/framework/components/chui_tabs/chui_tabs.js +100 -46
- package/framework/components/chui_tabs/styles.css +43 -15
- package/framework/components/chui_text_editor/styles.css +24 -23
- package/framework/components/chui_text_editor/text_editor.js +103 -79
- package/framework/components/chui_text_editor/text_editor_panel.js +428 -367
- package/framework/components/chui_theme_toggle/theme_toggle.js +46 -25
- package/framework/components/chui_timeline/styles.css +3 -1
- package/framework/components/chui_timeline/timeline.js +14 -12
- package/framework/components/chui_toast_queue/styles.css +11 -11
- package/framework/components/chui_toast_queue/toast_queue.js +82 -73
- package/framework/components/chui_toolbar/styles.css +2 -2
- package/framework/components/chui_toolbar/toolbar.js +20 -13
- package/framework/components/chui_tooltip/styles.css +2 -2
- package/framework/components/chui_tooltip/tooltip.js +40 -27
- package/framework/components/chui_tour/styles.css +12 -7
- package/framework/components/chui_tour/tour.js +132 -98
- package/framework/components/chui_transfer/styles.css +8 -7
- package/framework/components/chui_transfer/transfer.js +143 -123
- package/framework/components/chui_tree_table/styles.css +4 -4
- package/framework/components/chui_tree_table/tree_table.js +88 -74
- package/framework/components/chui_tree_view/styles.css +14 -10
- package/framework/components/chui_tree_view/tree_view.js +298 -253
- package/framework/components/chui_user_bar/styles.css +145 -0
- package/framework/components/chui_user_bar/user_bar.js +402 -0
- package/framework/components/chui_user_list_bar/styles.css +173 -0
- package/framework/components/chui_user_list_bar/user_list_bar.js +485 -0
- package/framework/components/chui_watermark/watermark.js +41 -33
- package/framework/components/chui_webview/styles.css +2 -2
- package/framework/components/chui_webview/webview.js +64 -52
- package/framework/components/chui_write_bar/styles.css +172 -0
- package/framework/components/chui_write_bar/write_bar.js +320 -0
- package/framework/main_window.js +1234 -0
- package/framework/modules/chui_a11y/chui_a11y.js +153 -123
- package/framework/modules/chui_animations/animations.js +105 -43
- package/framework/modules/chui_animations/motion.js +120 -0
- package/framework/modules/chui_animations/styles.css +37 -4
- package/framework/modules/chui_bridge/preload.js +36 -25
- package/framework/modules/chui_functions.js +69 -63
- package/framework/modules/chui_highlight/chui_highlight.js +25 -22
- package/framework/modules/chui_highlight/styles.css +13 -10
- package/framework/modules/chui_i18n/chui_i18n.js +12 -10
- package/framework/modules/chui_i18n/locales/a11y.js +5 -5
- package/framework/modules/chui_i18n/locales/app_layout.js +5 -5
- package/framework/modules/chui_i18n/locales/calendar.js +3 -3
- package/framework/modules/chui_i18n/locales/common.js +3 -3
- package/framework/modules/chui_i18n/locales/data.js +36 -26
- package/framework/modules/chui_i18n/locales/editors.js +40 -13
- package/framework/modules/chui_i18n/locales/feedback.js +65 -3
- package/framework/modules/chui_i18n/locales/forms.js +8 -8
- package/framework/modules/chui_i18n/locales/graphics.js +8 -8
- package/framework/modules/chui_i18n/locales/media.js +4 -4
- package/framework/modules/chui_i18n/locales/navigation.js +12 -8
- package/framework/modules/chui_i18n/locales/validate.js +3 -3
- package/framework/modules/chui_json/chui_json.js +11 -11
- package/framework/modules/chui_layout/chui_layout.js +12 -10
- package/framework/modules/chui_logger/chui_logger.js +47 -28
- package/framework/modules/chui_page/page.js +39 -33
- package/framework/modules/chui_palette/palette.js +61 -0
- package/framework/modules/chui_qr/chui_qr.js +179 -88
- package/framework/modules/chui_scrollbar/chui_scrollbar.js +79 -68
- package/framework/modules/chui_scrollbar/styles.css +4 -2
- package/framework/modules/chui_services/chui_channels.js +27 -13
- package/framework/modules/chui_services/chui_services.js +47 -39
- package/framework/modules/chui_services/desktop.js +108 -101
- package/framework/modules/chui_sortable/chui_sortable.js +33 -28
- package/framework/modules/chui_validate/chui_validate.js +10 -7
- package/framework/tray_bar/chui_menu_items.js +136 -46
- package/index.d.ts +5046 -0
- package/index.js +199 -1254
- package/package.json +81 -68
- package/framework/components/telegram_bot/chui_telegram_bot.js +0 -184
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
const { Notification } = require(
|
|
2
|
-
const { setStyles, optionOr } = require(
|
|
3
|
-
const { I18n } = require(
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
const
|
|
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 =
|
|
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 = "
|
|
16
|
-
const LINE_FALLBACK_COLOR = "
|
|
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 = "
|
|
28
|
-
const GAUGE_TRACK_COLOR = "
|
|
29
|
-
const GAUGE_THRESHOLD_COLORS = ["
|
|
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 = "
|
|
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 +
|
|
86
|
-
canvas.style.height = cssHeight +
|
|
87
|
-
const ctx = canvas.getContext(
|
|
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 =
|
|
108
|
-
ctx.textBaseline =
|
|
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
|
-
|
|
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(
|
|
154
|
-
#main = document.createElement(
|
|
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 +
|
|
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 =
|
|
191
|
-
ctx.textBaseline =
|
|
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(
|
|
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(
|
|
243
|
+
this.#canvas.addEventListener("mousemove", (event) => {
|
|
227
244
|
const bar = this.#findBar(event);
|
|
228
|
-
if (bar === null) {
|
|
245
|
+
if (bar === null) {
|
|
246
|
+
this.#tooltip.hide();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
229
249
|
this.#showTooltip(bar.index);
|
|
230
250
|
});
|
|
231
|
-
this.#canvas.addEventListener(
|
|
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(
|
|
245
|
-
|
|
246
|
-
|
|
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 (
|
|
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
|
-
#
|
|
292
|
-
#canvas = document.createElement(
|
|
314
|
+
#chuiPiechart = document.createElement("chui_piechart");
|
|
315
|
+
#canvas = document.createElement("canvas");
|
|
293
316
|
#options = undefined;
|
|
294
317
|
|
|
295
318
|
constructor(options = {}) {
|
|
296
|
-
setStyles(__dirname +
|
|
319
|
+
setStyles(__dirname + "/styles.css", STYLES_ID);
|
|
297
320
|
this.#options = options;
|
|
298
|
-
|
|
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.#
|
|
303
|
-
if (options.legend) this.#
|
|
324
|
+
this.#chuiPiechart.appendChild(this.#canvas);
|
|
325
|
+
if (options.legend) this.#chuiPiechart.appendChild(this.#buildLegend());
|
|
304
326
|
// Доступная альтернатива канвасу: та же таблица данных рядом с графиком
|
|
305
|
-
this.#
|
|
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
|
|
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 =
|
|
365
|
+
ctx.fillStyle = "white";
|
|
334
366
|
ctx.font = LABEL_FONT;
|
|
335
|
-
ctx.textAlign =
|
|
336
|
-
ctx.textBaseline =
|
|
337
|
-
ctx.fillText(
|
|
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 =
|
|
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(
|
|
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.#
|
|
397
|
+
return this.#chuiPiechart;
|
|
360
398
|
}
|
|
361
399
|
}
|
|
362
400
|
|
|
363
401
|
class LineGraph {
|
|
364
|
-
#canvas = document.createElement(
|
|
365
|
-
#main = document.createElement(
|
|
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 +
|
|
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 +
|
|
412
|
-
gradient.addColorStop(1, this.#color +
|
|
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 =
|
|
449
|
-
ctx.textBaseline =
|
|
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(
|
|
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(
|
|
508
|
+
this.#canvas.addEventListener("mousemove", (event) => {
|
|
465
509
|
const point = this.#findPoint(event);
|
|
466
|
-
if (point === null) {
|
|
510
|
+
if (point === null) {
|
|
511
|
+
this.#tooltip.hide();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
467
514
|
this.#select(point.index);
|
|
468
515
|
});
|
|
469
|
-
this.#canvas.addEventListener(
|
|
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(
|
|
508
|
-
|
|
509
|
-
|
|
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(
|
|
574
|
-
#main = document.createElement(
|
|
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 +
|
|
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(
|
|
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"),
|
|
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 !==
|
|
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 !==
|
|
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:
|
|
634
|
-
|
|
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(
|
|
653
|
-
|
|
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 =
|
|
688
|
-
ctx.textBaseline =
|
|
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 =
|
|
709
|
-
ctx.textBaseline =
|
|
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 =
|
|
728
|
-
ctx.textBaseline =
|
|
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 =
|
|
735
|
-
ctx.textBaseline =
|
|
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({
|
|
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
|
-
|
|
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(
|
|
765
|
-
this.#canvas.setAttribute(
|
|
766
|
-
this.#canvas.setAttribute(
|
|
767
|
-
this.#canvas.addEventListener(
|
|
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(
|
|
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(
|
|
851
|
+
this.#canvas.addEventListener("mouseleave", () => {
|
|
781
852
|
if (this.#active === undefined) this.#tooltip.hide();
|
|
782
853
|
});
|
|
783
854
|
// Клавиатура: ←/→ водят по точкам, Enter показывает значение (как клик мышью)
|
|
784
|
-
this.#canvas.addEventListener(
|
|
855
|
+
this.#canvas.addEventListener("keydown", (event) => {
|
|
785
856
|
if (this.#points.length === 0) return;
|
|
786
|
-
if (event.key ===
|
|
857
|
+
if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
|
|
787
858
|
event.preventDefault();
|
|
788
|
-
let next = this.#active === undefined ? 0 : this.#active + (event.key ===
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
883
|
-
#canvas = document.createElement(
|
|
884
|
-
#
|
|
885
|
-
#status = document.createElement(
|
|
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
|
-
#
|
|
967
|
+
#showValue = true;
|
|
894
968
|
#size = GAUGE_SIZE;
|
|
895
969
|
#thickness = GAUGE_THICKNESS;
|
|
896
|
-
#
|
|
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 +
|
|
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(
|
|
908
|
-
this.#canvas.setAttribute(
|
|
909
|
-
this.#
|
|
910
|
-
this.#
|
|
911
|
-
this.#status.className =
|
|
912
|
-
this.#status.setAttribute(
|
|
913
|
-
this.#status.setAttribute(
|
|
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.#
|
|
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.#
|
|
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.#
|
|
929
|
-
this.setLabel(optionOr(options.label, I18n.t(
|
|
930
|
-
if (options.thresholds !== undefined)
|
|
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 ===
|
|
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.#
|
|
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 ?
|
|
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(
|
|
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.#
|
|
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 !==
|
|
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:
|
|
1089
|
-
|
|
1090
|
-
|
|
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.#
|
|
1105
|
-
const end = ((this.#
|
|
1106
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
1145
|
-
ctx.textBaseline =
|
|
1146
|
-
ctx.fillText(
|
|
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.#
|
|
1164
|
-
this.#
|
|
1165
|
-
this.#root.setAttribute(
|
|
1166
|
-
this.#root.setAttribute(
|
|
1167
|
-
this.#root.setAttribute(
|
|
1168
|
-
this.#root.setAttribute(
|
|
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(
|
|
1194
|
-
#main = document.createElement(
|
|
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 +
|
|
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) =>
|
|
1239
|
-
|
|
1240
|
-
|
|
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 +
|
|
1254
|
-
gradient.addColorStop(1, color +
|
|
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 =
|
|
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(
|
|
1285
|
-
this.#canvas.setAttribute(
|
|
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(
|
|
1311
|
-
#main = document.createElement(
|
|
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 +
|
|
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(
|
|
1412
|
+
this.#main.appendChild(
|
|
1413
|
+
buildLegend(this.#series.map((serie) => ({ title: serie.title, color: serie.color })))
|
|
1414
|
+
);
|
|
1329
1415
|
}
|
|
1330
|
-
this.#main.appendChild(
|
|
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 !==
|
|
1367
|
-
const title =
|
|
1368
|
-
|
|
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 =
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
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 !==
|
|
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:
|
|
1387
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
1466
|
-
ctx.textBaseline =
|
|
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 ?
|
|
1475
|
-
ctx.textBaseline = sin > 0.2 ?
|
|
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 ===
|
|
1484
|
-
else if (align ===
|
|
1485
|
-
else
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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(
|
|
1537
|
-
this.#canvas.setAttribute(
|
|
1538
|
-
this.#canvas.setAttribute(
|
|
1539
|
-
this.#canvas.addEventListener(
|
|
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(
|
|
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(
|
|
1663
|
+
this.#canvas.addEventListener("mouseleave", () => {
|
|
1553
1664
|
if (this.#active === undefined) this.#tooltip.hide();
|
|
1554
1665
|
});
|
|
1555
1666
|
// Клавиатура: ←/→ водят по точкам, Enter показывает значение (как клик мышью)
|
|
1556
|
-
this.#canvas.addEventListener(
|
|
1667
|
+
this.#canvas.addEventListener("keydown", (event) => {
|
|
1557
1668
|
if (this.#points.length === 0) return;
|
|
1558
|
-
if (event.key ===
|
|
1669
|
+
if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
|
|
1559
1670
|
event.preventDefault();
|
|
1560
|
-
let next = this.#active === undefined ? 0 : this.#active + (event.key ===
|
|
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 ===
|
|
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(
|
|
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(
|
|
1635
|
-
#main = document.createElement(
|
|
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 +
|
|
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(
|
|
1652
|
-
this.#columns = HeatmapGraph.#normalizeLabels(options.columns, columns, I18n.t(
|
|
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(
|
|
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(
|
|
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 ?
|
|
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(
|
|
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", {
|
|
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
|
|
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(
|
|
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 =
|
|
1785
|
-
ctx.textBaseline =
|
|
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 =
|
|
1806
|
-
ctx.textBaseline =
|
|
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(
|
|
1812
|
-
this.#canvas.setAttribute(
|
|
1813
|
-
this.#canvas.setAttribute(
|
|
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(
|
|
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(
|
|
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(
|
|
1962
|
+
this.#canvas.addEventListener("mouseleave", () => {
|
|
1832
1963
|
if (this.#active === undefined) this.#tooltip.hide();
|
|
1833
1964
|
});
|
|
1834
1965
|
// Клавиатура: стрелки водят по клеткам и на краю останавливаются, Enter показывает значение
|
|
1835
|
-
this.#canvas.addEventListener(
|
|
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(
|
|
1843
|
-
|
|
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 ===
|
|
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(
|
|
2013
|
+
text: cell.value === undefined ? I18n.t("graphics.noData") : String(cell.value),
|
|
1877
2014
|
showTime: 3000,
|
|
1878
2015
|
}).show();
|
|
1879
2016
|
}
|