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